]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Изменения интерфейса
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 8 Aug 2025 10:24:46 +0000 (13:24 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 8 Aug 2025 10:24:46 +0000 (13:24 +0300)
erp24/controllers/Products1cNomenclatureActualityController.php
erp24/records/Products1cNomenclature.php
erp24/records/Products1cNomenclatureActuality.php
erp24/views/products1c-nomenclature-actuality/index.php
erp24/web/js/products1cNomenclatureActuality/index.js

index 0b478c093146b0045b603ca4a69e84b0aeb8351f..6f645f01730d9f3f25cdd9a7224b3b5edead23eb 100644 (file)
@@ -45,16 +45,16 @@ class Products1cNomenclatureActualityController extends Controller
     public function actionIndex()
     {
         $filter = new \yii\base\DynamicModel([
-            'category','subcategory','species',
-            'type','color','sort','size',
-            'date_from','date_to',
+            'category', 'subcategory', 'species',
+            'type', 'color', 'sort', 'size',
+            'date_from', 'date_to',
         ]);
         $filter->addRule([
-            'category','subcategory','species',
-            'type','color','sort','size',
-            'date_from','date_to',
+            'category', 'subcategory', 'species',
+            'type', 'color', 'sort', 'size',
+            'date_from', 'date_to',
         ], 'safe');
-        $filter->addRule(['onlyActive','onlyInactive'], 'boolean');
+        $filter->addRule(['onlyActive', 'onlyInactive'], 'boolean');
         $filter->load(Yii::$app->request->get());
 
 
@@ -69,7 +69,7 @@ class Products1cNomenclatureActualityController extends Controller
         $dataProvider = new \yii\data\ActiveDataProvider([
             'query' => $emptyQuery,
             'pagination' => ['pageSize' => 50],
-            'sort' => ['defaultOrder'=>['name'=>SORT_ASC]],
+            'sort' => ['defaultOrder' => ['name' => SORT_ASC]],
         ]);
         $attrMap = [
             'type' => ['type', 'тип'],
@@ -118,54 +118,69 @@ class Products1cNomenclatureActualityController extends Controller
                 }
             }
 
-            $needJoin = $filter->onlyActive || $filter->onlyInactive || $filter->date_from || $filter->date_to;
-            if ($needJoin) {
-
-
-                if ($filter->onlyActive) {
-                    $query->innerJoin(
-                        Products1cNomenclatureActuality::tableName() . ' a',
-                        'a.guid = n.id'
-                    );
-                    $query->andWhere(['a.guid'  => 'n.id']);
-                } elseif ($filter->onlyInactive) {
-                   // $query->andWhere([ 'not', 'a.guid = n.id']);
-                }
-
-                if ($filter->date_from || $filter->date_to) {
-                    if (!$filter->onlyActive && !$filter->onlyInactive) {
-                        $query->innerJoin(
-                            Products1cNomenclatureActuality::tableName() . ' a',
-                            'a.guid = n.id'
-                        );
-                    }
+            if ($filter->onlyActive) {
+                $query->andWhere(['exists',
+                    Products1cNomenclatureActuality::find()
+                        ->where('guid = n.id')
+                        ->select(new \yii\db\Expression('1'))
+                ]);
+            } elseif ($filter->onlyInactive) {
+                $query->andWhere(['not exists',
+                    Products1cNomenclatureActuality::find()
+                        ->where('guid = n.id')
+                        ->select(new \yii\db\Expression('1'))
+                ]);
+            }
 
-                    if ($filter->date_from && $filter->date_to) {
+            if ($filter->date_from || $filter->date_to) {
+                $query->with(['actualities' => function ($q) use ($filter) {
+                    if ($filter->date_from) {
                         $df = (new \DateTime("{$filter->date_from}-01"))
                             ->setTime(0, 0, 0)->format('Y-m-d H:i:s');
+                        $q->andWhere(['>=', 'date_to', $df]);
+                    }
+                    if ($filter->date_to) {
                         $dt = (new \DateTime("{$filter->date_to}-01"))
                             ->modify('last day of this month')->setTime(23, 59, 59)
                             ->format('Y-m-d H:i:s');
-                        $query->andWhere(['<=', 'a.date_from', $dt])
-                            ->andWhere(['>=', 'a.date_to', $df]);
-                    } elseif ($filter->date_from) {
-                        $df = (new \DateTime("{$filter->date_from}-01"))
-                            ->setTime(0, 0, 0)->format('Y-m-d H:i:s');
-                        $query->andWhere(['>=', 'a.date_to', $df]);
-                    } elseif ($filter->date_to) {
-                        $dt = (new \DateTime("{$filter->date_to}-01"))
-                            ->modify('last day of this month')->setTime(23, 59, 59)
-                            ->format('Y-m-d H:i:s');
-                        $query->andWhere(['<=', 'a.date_from', $dt]);
+                        $q->andWhere(['<=', 'date_from', $dt]);
+                    }
+                }]);
+            } else {
+                $query->with('actualities');
+            }
+
+            $products = $query->orderBy(['n.name' => SORT_ASC])->all();
+
+            $rows = [];
+            foreach ($products as $product) {
+                $acts = $product->actualities;
+                if ($acts) {
+                    foreach ($acts as $act) {
+                        $rows[] = [
+                            'product' => $product,
+                            'actuality' => $act,
+                        ];
                     }
+                } else {
+                    $rows[] = [
+                        'product' => $product,
+                        'actuality' => null,
+                    ];
                 }
             }
-            
 
-            $dataProvider = new \yii\data\ActiveDataProvider([
-                'query' => $query,
+            $dataProvider = new \yii\data\ArrayDataProvider([
+                'allModels' => $rows,
                 'pagination' => ['pageSize' => 1000],
-                'sort' => ['defaultOrder' => ['name' => SORT_ASC]],
+                'sort' => [
+                    'attributes' => [
+                        'product.name',
+                        'actuality.date_from',
+                        'actuality.date_to'
+                    ],
+                    'defaultOrder' => ['product.name' => SORT_ASC],
+                ],
             ]);
         }
 
@@ -192,27 +207,27 @@ class Products1cNomenclatureActualityController extends Controller
 
         $lists = [];
 
-        foreach (['type','color','sort','size'] as $attr) {
+        foreach (['type', 'color', 'sort', 'size'] as $attr) {
             $propIds = Products1cPropType::find()
                 ->select('id')
-                ->andWhere(['name'=>$attrMap[$attr]])
+                ->andWhere(['name' => $attrMap[$attr]])
                 ->column();
             $lists[$attr] = Products1cAdditionalCharacteristics::find()
                 ->select('value')->distinct()
-                ->where(['property_id'=>$propIds])
+                ->where(['property_id' => $propIds])
                 ->column();
         }
 
         return $this->render('index', [
-            'filter'        => $filter,
-            'dataProvider'  => $dataProvider,
-            'categories'    => array_combine($categories,$categories),
-            'subcategories' => array_combine($subcategories,$subcategories),
-            'species'       => array_combine($species, $species),
-            'types'         => array_combine($lists['type'],  $lists['type']),
-            'colors'        => array_combine($lists['color'], $lists['color']),
-            'sorts'         => array_combine($lists['sort'],  $lists['sort']),
-            'sizes'         => array_combine($lists['size'],  $lists['size']),
+            'filter' => $filter,
+            'dataProvider' => $dataProvider,
+            'categories' => array_combine($categories, $categories),
+            'subcategories' => array_combine($subcategories, $subcategories),
+            'species' => array_combine($species, $species),
+            'types' => array_combine($lists['type'], $lists['type']),
+            'colors' => array_combine($lists['color'], $lists['color']),
+            'sorts' => array_combine($lists['sort'], $lists['sort']),
+            'sizes' => array_combine($lists['size'], $lists['size']),
         ]);
     }
 
@@ -220,127 +235,73 @@ class Products1cNomenclatureActualityController extends Controller
     {
         $request = Yii::$app->request;
 
-        $historyDays    = $request->get('historyDays');
+        $historyDays = $request->get('historyDays');
         $intervalMonths = $request->get('intervalMonths');
         $startFrom = $request->get('startFrom', date('Y-m-d'));
 
-
         if ($historyDays === null || $intervalMonths === null) {
             return $this->render('add-activity', [
-                'historyDays'    => $historyDays ?? 14,
+                'historyDays' => $historyDays ?? 14,
                 'intervalMonths' => $intervalMonths ?? 4,
+                'startFrom' => $startFrom,
             ]);
         }
 
-        $endDate   = date('Y-m-d', strtotime($startFrom));
+        $endDate = date('Y-m-d', strtotime($startFrom));
         $startDate = date('Y-m-d', strtotime("-{$historyDays} days", strtotime($endDate)));
 
         $productIds = (new Query())
             ->select('sp.product_id')
             ->from(['s' => 'sales'])
             ->innerJoin(['sp' => 'sales_products'], 's.id = sp.check_id')
-            //->andWhere(['s.store_id' => $storeIds])
             ->innerJoin(['p1c' => 'products_1c_nomenclature'], 'p1c.id = sp.product_id')
             ->andWhere(['between', 's.date', "{$startDate} 00:00:00", "{$endDate} 23:59:59"])
             ->groupBy('sp.product_id')
             ->column();
 
         if (empty($productIds)) {
-            Yii::$app->session->setFlash('info', 'Нет товаров, удовлетворяющих условиям.');
+            Yii::$app->session->setFlash('info', 'Нет товаров за указанный период.');
             return $this->render('add-activity', [
-                'historyDays'    => $historyDays,
+                'historyDays' => $historyDays,
                 'intervalMonths' => $intervalMonths,
-                'startFrom'      => $startFrom,
+                'startFrom' => $startFrom,
             ]);
         }
 
         $now = new \DateTime($endDate);
-        $from = (clone $now)->modify("-{$intervalMonths} months")
-                      ->modify('first day of this month')->setTime(0,0,0)
-        ->format('Y-m-d H:i:s');
-        $to   = (clone $now)->modify("+{$intervalMonths} months")
-                      ->modify('last day of this month')->setTime(23,59,59)
-        ->format('Y-m-d H:i:s');
-
-        $userId    = Yii::$app->user->id;
-        $createdAt = date('Y-m-d H:i:s');
-        $toInsert  = [];
-        $toDeactivate = [];
-
-        $existingActives = Products1cNomenclatureActuality::find()
-            ->where(['guid' => $productIds, 'active' => 1])
-            ->indexBy('guid')
-            ->all();
-
+        $fromStr = (clone $now)
+            ->modify("-{$intervalMonths} months")
+            ->modify('first day of this month')->setTime(0, 0, 0)
+            ->format('Y-m-d H:i:s');
+        $toStr = (clone $now)
+            ->modify("+{$intervalMonths} months")
+            ->modify('last day of this month')->setTime(23, 59, 59)
+            ->format('Y-m-d H:i:s');
+
+        $rows = [];
         foreach ($productIds as $pid) {
-            $needNewRecord = true;
-
-            if (isset($existingActives[$pid])) {
-                $existing = $existingActives[$pid];
-
-                if ($existing->date_from == $from && $existing->date_to == $to) {
-                    $needNewRecord = false;
-                } else {
-                    $toDeactivate[] = $existing->id;
-                }
-            }
-
-            if ($needNewRecord) {
-                $toInsert[] = [
-                    'guid'       => $pid,
-                    'date_from'  => $from,
-                    'date_to'   => $to,
-                    'active'     => 1,
-                    'created_at' => $createdAt,
-                    'created_by' => $userId,
-                ];
-            }
+            $rows[] = [
+                'guid' => $pid,
+                'from' => date('Y-m', strtotime($fromStr)),
+                'to' => date('Y-m', strtotime($toStr)),
+            ];
         }
 
-        $transaction = Yii::$app->db->beginTransaction();
-        try {
-            if (!empty($toDeactivate)) {
-                Products1cNomenclatureActuality::updateAll(
-                    [
-                        'active'     => 0,
-                        'updated_at' => $createdAt,
-                        'updated_by' => $userId,
-                    ],
-                    ['id' => $toDeactivate]
-                );
-            }
-
-            if (!empty($toInsert)) {
-                Yii::$app->db->createCommand()
-                    ->batchInsert(
-                        Products1cNomenclatureActuality::tableName(),
-                        ['guid','date_from','date_to','active','created_at','created_by'],
-                        $toInsert
-                    )
-                    ->execute();
-            }
-
-            $transaction->commit();
+        $this->processBatchActuality($rows);
 
-            $message = 'Таблица актуальности обновлена. ';
-            $message .= 'Добавлено: ' . count($toInsert) . '; ';
-            $message .= 'Деактивировано: ' . count($toDeactivate);
-
-            Yii::$app->session->setFlash('success', $message);
+        Yii::$app->session->setFlash(
+            'success',
+            "Обновлено актуальностей для " . count($rows) . " товаров."
+        );
 
-        } catch (\Exception $e) {
-            $transaction->rollBack();
-            Yii::$app->session->setFlash('error', 'Ошибка: ' . $e->getMessage());
-        }
         return $this->render('add-activity', [
-            'historyDays'    => $historyDays,
+            'historyDays' => $historyDays,
             'intervalMonths' => $intervalMonths,
             'startFrom' => $startFrom,
         ]);
     }
 
 
-
     /**
      * Обработка массового сохранения диапазонов актуальности.
      * Если из/до нет или невалидны — пропускаем.
@@ -350,7 +311,7 @@ class Products1cNomenclatureActualityController extends Controller
     protected function processBatchActuality(array $post)
     {
         $userId = Yii::$app->user->id;
-        $now    = date('Y-m-d H:i:s');
+        $now = date('Y-m-d H:i:s');
 
         foreach ($post as $row) {
             if (empty($row['from']) || empty($row['to'])) {
@@ -358,55 +319,65 @@ class Products1cNomenclatureActualityController extends Controller
             }
 
             $fromDt = \DateTime::createFromFormat('Y-m', $row['from']);
-            if (!$fromDt) {
-                continue;
-            }
-            $newFrom = $fromDt->format('Y-m-01 00:00:00');
-
             $toDt = \DateTime::createFromFormat('Y-m', $row['to']);
-            if (!$toDt) {
+            if (!$fromDt || !$toDt) {
                 continue;
             }
-            $toDt->modify('last day of this month')->setTime(23, 59, 59);
-            $newTo = $toDt->format('Y-m-d H:i:s');
-
+            $fromDt->setDate((int)$fromDt->format('Y'), (int)$fromDt->format('m'), 1)
+                ->setTime(0, 0, 0);
+            $toDt->modify('last day of this month')
+                ->setTime(23, 59, 59);
 
-            $warehouseNN = !empty($row['warehouse_nn']);
-            $warehouseMS = !empty($row['warehouse_msk']);
-            $supplier    = $row['supplier'] ?? null;
-            $plantation  = $row['plantation'] ?? null;
+            $from = $fromDt->format('Y-m-d H:i:s');
+            $to = $toDt->format('Y-m-d H:i:s');
 
+            if ($from > $to) {
+                Yii::warning("GUID {$row['guid']}: пропускаем — from > to");
+                continue;
+            }
 
-            $old = Products1cNomenclatureActuality::find()
-                ->andWhere(['guid' => $row['guid'], 'active' => 1])
-                ->one();
-
-            if ($old) {
-                if ($old->date_from === $newFrom && $old->date_to === $newTo) {
-                    continue;
+            $guid = $row['guid'];
+
+            /** @var Products1cNomenclatureActuality[] $hits */
+            $hits = Products1cNomenclatureActuality::find()
+                ->where(['guid' => $guid])
+                ->andWhere('date_to   >= :from', [':from' => $from])
+                ->andWhere('date_from <= :to', [':to' => $to])
+                ->orderBy(['date_from' => SORT_ASC])
+                ->all();
+
+            if (empty($hits)) {
+                $new = new Products1cNomenclatureActuality([
+                    'guid' => $guid,
+                    'date_from' => $from,
+                    'date_to' => $to,
+                    'created_at' => $now,
+                    'created_by' => $userId,
+                ]);
+                if (!$new->save()) {
+                    Yii::error("Ошибка создания GUID={$guid}: " . json_encode($new->getErrors(), JSON_UNESCAPED_UNICODE));
                 }
+                continue;
+            }
 
-                $old->active     = 0;
-                $old->updated_by = $userId;
-                $old->updated_at = $now;
+            $allFrom = array_map(fn($r) => $r->date_from, $hits);
+            $allTo = array_map(fn($r) => $r->date_to, $hits);
+            $minFrom = min($from, min($allFrom));
+            $maxTo = max($to, max($allTo));
 
-                if (!$old->save()) {
-                    Yii::error('Ошибка сохранения' . json_encode(
-                            $old->getErrors(), JSON_UNESCAPED_UNICODE
-                        ));
-                }
-            }
+            $master = array_shift($hits);
+            $master->date_from = $minFrom;
+            $master->date_to = $maxTo;
+            $master->updated_at = $now;
+            $master->updated_by = $userId;
 
+            if (!$master->save()) {
+                Yii::error("Ошибка обновления GUID={$guid}: " . json_encode($master->getErrors(), JSON_UNESCAPED_UNICODE));
+            }
 
-            $new = new Products1cNomenclatureActuality([
-                'guid'        => $row['guid'],
-                'date_from'   => $newFrom,
-                'date_to'    => $newTo,
-                'active'      => 1,
-                'created_at'  => $now,
-                'created_by'  => $userId,
-            ]);
-            $new->save(false);
+            foreach ($hits as $dup) {
+                $dup->delete();
+            }
         }
     }
 
index f48b07411b1ba5dedf0ea34fb8e61bd2f8c0a314..36c3985802264950e109f678372ab41ef2a4c5b5 100644 (file)
@@ -23,6 +23,8 @@ use yii\db\ActiveQuery;
  */
 class Products1cNomenclature extends \yii\db\ActiveRecord
 {
+    public $date_from;
+    public $date_to;
     /**
      * {@inheritdoc}
      */
@@ -31,6 +33,13 @@ class Products1cNomenclature extends \yii\db\ActiveRecord
         return 'products_1c_nomenclature';
     }
 
+    public function attributes(): array
+    {
+        return array_merge(parent::attributes(), [
+            'date_from',
+            'date_to',
+        ]);
+    }
     /**
      * {@inheritdoc}
      */
@@ -43,6 +52,7 @@ class Products1cNomenclature extends \yii\db\ActiveRecord
             [['id', 'location', 'name', 'type_num',
                 'category', 'subcategory', 'species', 'sort', 'measure', 'color', 'type'], 'string', 'max' => 255],
             [['id'], 'unique'],
+            [['date_from','date_to'], 'safe'],
         ];
     }
 
index 34b6318f4f2cbfa242a46d74aa33d4cfa72db22e..ba043e96b237a8611240be656208ce088ba15986 100644 (file)
@@ -60,4 +60,15 @@ class Products1cNomenclatureActuality extends \yii\db\ActiveRecord
         ];
     }
 
+    /**
+     * @return \yii\db\ActiveQuery
+     */
+    public function getProduct(): \yii\db\ActiveQuery
+    {
+        return $this->hasOne(
+            Products1cNomenclature::class,
+            ['id' => 'guid']
+        );
+    }
+
 }
index 130760a1bc7d7b16d355844aad01dd1f908dbba9..b3ed7faa037fe919f7481648b7038f45055f12a4 100644 (file)
@@ -36,6 +36,10 @@ function monthList()
 }
 
 $months = monthList();
+$monthOptions = '';
+foreach ($months as $k => $v) {
+    $monthOptions .= "<option value=\"$k\">$v</option>";
+}
 ?>
 
 <div class="products1c-nomenclature-actuality-index p-4">
@@ -244,45 +248,54 @@ $months = monthList();
         'floatHeader' => false,
         'tableOptions' => ['class' => 'table table-bordered'],
         'containerOptions' => ['style' => 'overflow:auto; max-height:500px;'],
-        'rowOptions' => function($model) use ($filter) {
-            if ($model->hasActuality()) {
-                return ['class' => 'table-success'];
-            }
-            return [];
+        'rowOptions' => function($row) {
+            return $row['actuality'] ? ['class'=>'table-success'] : [];
         },
         'columns' => [
             [
-                'attribute' => 'name',
                 'label' => 'Наименование',
                 'format' => 'raw',
-                'contentOptions' => ['style'=>'min-width:200px;'],
-                'value' => function ($m) {
-                    return Html::encode($m->name . ' (' . $m->id . ')');
+                'contentOptions' => ['style'=>'min-width:150px;'],
+                'value' => function ($row, $key, $index) {
+                    $product = $row['product'];
+                    $name = Html::encode($product->name . ' (' . $product->id . ')');
+                    $btn = Html::button('+ Добавить интервал', [
+                        'class' => 'btn btn-xs btn-outline-primary ms-2 add-actuality-row',
+                        'type' => 'button',
+                        'title' => 'Добавить интервал',
+                        'data-guid' => $product->id,
+                        'data-name' => $product->name,
+                    ]);
+                    return '<div class="d-flex justify-content-between">' . $name . $btn . '</div>';
                 }
             ],
             [
                 'label' => 'Актуальность ассортимента',
                 'format' => 'raw',
-                'contentOptions' => ['style'=>'white-space:nowrap; min-width:200px;'],
-                'value' => function ($m, $k, $i) use ($months, $filter) {
-                        $actuality = $m->getActualities()
-                            ->one();
-                        $from = $actuality ? (new \DateTime($actuality->date_from))->format('Y-m') : null;
-                        $to   = $actuality ? (new \DateTime($actuality->date_to))->format('Y-m') : null;
-                    return Html::hiddenInput("actuality[$i][guid]", $m->id)
-                        . Html::tag('div',
-                            Html::dropDownList("actuality[$i][from]", $from, $months, [
-                                'class'=>'form-select from-month form-select-sm me-1',
-                                'prompt'=>'от',
-
-                            ])
-                            . Html::dropDownList("actuality[$i][to]", $to, $months, [
-                                'class'=>'form-select to-month form-select-sm',
-                                'prompt'=>'до',
-
-                            ]),
-                            ['class'=>'d-flex align-items-center']
-                        );
+                'contentOptions' => ['style'=>'white-space:nowrap; min-width:100px;'],
+                'value' => function ($row, $k, $i) use ($months) {
+                    $product   = $row['product'];
+                    $actuality = $row['actuality'];
+                    $from = $actuality ? substr($actuality->date_from, 0, 7) : null;
+                    $to   = $actuality ? substr($actuality->date_to, 0, 7)   : null;
+                    $inputs = Html::hiddenInput("actuality[$i][guid]", $product->id);
+                    if ($actuality) {
+                        $inputs .= Html::hiddenInput("actuality[$i][id]", $actuality->id);
+                    }
+                    $inputs .= Html::tag('div',
+                        Html::dropDownList("actuality[$i][from]", $from, $months, [
+                            'class'=>'form-select from-month form-select-sm me-1',
+                            'prompt'=>'от',
+                            'style' => 'width:auto;display:inline-block'
+                        ]) .
+                        Html::dropDownList("actuality[$i][to]", $to, $months, [
+                            'class'=>'form-select to-month form-select-sm',
+                            'prompt'=>'до',
+                            'style' => 'width:auto;display:inline-block'
+                        ]),
+                        ['class'=>'d-flex align-items-center']
+                    );
+                    return $inputs;
                 }
             ],
             [
@@ -319,6 +332,11 @@ $months = monthList();
 
 
     <?php ActiveForm::end(); ?>
+    <script>
+        window.productActualityConfig = {
+            months: <?= json_encode($months, JSON_UNESCAPED_UNICODE) ?>
+        };
+    </script>
 
 </div>
 
index 1e4a04364d884c64ca249c8029e97b4f5b2dfbeb..88b227d5501a00a22744a978da3c45e345c36674 100644 (file)
@@ -1,34 +1,87 @@
 document.addEventListener("DOMContentLoaded", () => {
-$('.from-month').on('change', function(){
-    var from = $(this).val(),
-        to   = $(this).closest('td').find('.to-month');
-    to.find('option').each(function(){
-        $(this).toggle($(this).val() >= from);
-    });
-    if (to.val() < from) {
-        to.val(from);
-    }
-});
 
-$('#filter-date-from').on('change', function(){
-    var from = $(this).val();
-    var to = $('#filter-date-to');
-    to.find('option').each(function(){
-        var val = $(this).val();
-        if (val === '' || val >= from) {
-            $(this).show();
+    const monthOptions = Object.entries(window.productActualityConfig.months || {}).map(
+        ([k, v]) => `<option value="${k}">${v}</option>`
+    ).join('');
+
+    let actualIdx = $('#actuality-form table tbody tr').length || 0;
+
+    $(document).on('click', '.add-actuality-row', function(){
+        const btn   = $(this);
+        const guid  = btn.data('guid');
+        const name  = btn.data('name');
+        const table = $('#actuality-form table');
+        // Все строки для этого товара
+        const $rows = table.find('tbody tr').filter(function(){
+            return $(this).find('input[type=hidden][name*="[guid]"]').val() == guid;
+        });
+        const $lastRow = $rows.last();
+        actualIdx++;
+
+        const newRow = `
+            <tr>
+                <td>
+                    ${name} (${guid})
+                </td>
+                <td>
+                    <input type="hidden" name="actuality[${actualIdx}][guid]" value="${guid}">
+                    <div class="d-flex align-items-center">
+                        <select name="actuality[${actualIdx}][from]" class="form-select from-month form-select-sm me-1" style="width:auto;display:inline-block">
+                            <option value="">от</option>${monthOptions}
+                        </select>
+                        <select name="actuality[${actualIdx}][to]" class="form-select to-month form-select-sm" style="width:auto;display:inline-block">
+                            <option value="">до</option>${monthOptions}
+                        </select>
+                    </div>
+                </td>
+                <td style="width:60px;text-align:center;">
+                    <input type="checkbox" name="actuality[${actualIdx}][warehouse_nn]">
+                </td>
+                <td style="width:60px;text-align:center;">
+                    <input type="checkbox" name="actuality[${actualIdx}][warehouse_msk]">
+                </td>
+                <td style="min-width:150px;">–</td>
+            </tr>
+        `;
+
+        if ($lastRow.length) {
+            $lastRow.after(newRow);
         } else {
-            $(this).hide();
+            table.find('tbody').append(newRow);
         }
     });
-    if (to.val() && to.val() < from) {
-        to.val(from);
-    }
-});
-$('.clear-btn').on('click', function(){
-    var target = $(this).data('target');
-    $('#' + target).val(null).trigger('change');
-});
+
+    $(document).on('change', '.from-month', function(){
+        var from = $(this).val(),
+            to   = $(this).closest('td').find('.to-month');
+        to.find('option').each(function(){
+            $(this).toggle($(this).val() >= from || $(this).val() === "");
+        });
+        if (to.val() < from) {
+            to.val(from);
+        }
+    });
+
+    $('#filter-date-from').on('change', function(){
+        var from = $(this).val();
+        var to = $('#filter-date-to');
+        to.find('option').each(function(){
+            var val = $(this).val();
+            if (val === '' || val >= from) {
+                $(this).show();
+            } else {
+                $(this).hide();
+            }
+        });
+        if (to.val() && to.val() < from) {
+            to.val(from);
+        }
+    });
+
+    $('.clear-btn').on('click', function(){
+        var target = $(this).data('target');
+        $('#' + target).val(null).trigger('change');
+    });
 
     var $onlyActiveCheckbox = $('#onlyActiveCheckbox');
     var $onlyInactiveCheckbox = $('#onlyInactiveCheckbox');
@@ -51,11 +104,53 @@ $('.clear-btn').on('click', function(){
         }
     });
 
-
     if ($onlyActiveCheckbox.is(':checked')) {
         $onlyInactiveCheckbox.prop('disabled', true);
     } else if ($onlyInactiveCheckbox.is(':checked')) {
         $onlyActiveCheckbox.prop('disabled', true);
     }
 
-});
\ No newline at end of file
+    function checkIntervalsForGuid(guid) {
+        // Собираем все интервалы для товара (всех строк)
+        let intervals = [];
+        $('#actuality-form table tbody tr').each(function(){
+            let $row = $(this);
+            let rowGuid = $row.find('input[type=hidden][name*="[guid]"]').val();
+            if (rowGuid == guid) {
+                let from = $row.find('select.from-month').val();
+                let to   = $row.find('select.to-month').val();
+                if (from && to) intervals.push({from, to, $row});
+            }
+        });
+        intervals.sort((a,b) => a.from.localeCompare(b.from));
+
+        let hasOverlap = false;
+        for(let i=0; i<intervals.length; ++i) {
+            for(let j=i+1; j<intervals.length; ++j) {
+                if (intervals[i].to >= intervals[j].from) {
+                    // Пересечение!
+                    intervals[i].$row.addClass('table-danger');
+                    intervals[j].$row.addClass('table-danger');
+                    hasOverlap = true;
+                }
+            }
+        }
+        if (hasOverlap) {
+            if (!$('.interval-overlap-alert').length) {
+                $('<div class="alert alert-warning interval-overlap-alert mt-2">Пересекающиеся диапазоны по одному товару!</div>')
+                    .insertBefore('#actuality-form');
+            }
+        } else {
+            $('.interval-overlap-alert').remove();
+            $('#actuality-form table tbody tr').removeClass('table-danger');
+        }
+        return hasOverlap;
+    }
+
+    $(document).on('change', '.from-month, .to-month', function(){
+        let $row = $(this).closest('tr');
+        let guid = $row.find('input[type=hidden][name*="[guid]"]').val();
+        checkIntervalsForGuid(guid);
+    });
+
+});