]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Актуализация номенклатуры
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Tue, 29 Jul 2025 15:53:19 +0000 (18:53 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Tue, 29 Jul 2025 15:53:19 +0000 (18:53 +0300)
erp24/controllers/Products1cNomenclatureActualityController.php [new file with mode: 0644]
erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php [new file with mode: 0644]
erp24/records/Products1cNomenclature.php
erp24/records/Products1cNomenclatureActuality.php [new file with mode: 0644]
erp24/records/Products1cNomenclatureActualitySearch.php [new file with mode: 0644]
erp24/views/products1c-nomenclature-actuality/index.php [new file with mode: 0644]

diff --git a/erp24/controllers/Products1cNomenclatureActualityController.php b/erp24/controllers/Products1cNomenclatureActualityController.php
new file mode 100644 (file)
index 0000000..b1af12b
--- /dev/null
@@ -0,0 +1,349 @@
+<?php
+
+namespace app\controllers;
+
+use Yii;
+use yii_app\records\Products1cAdditionalCharacteristics;
+use yii_app\records\Products1cNomenclature;
+use yii_app\records\Products1cNomenclatureActuality;
+use yii_app\records\Products1cNomenclatureActualitySearch;
+use yii\web\Controller;
+use yii\web\NotFoundHttpException;
+use yii\filters\VerbFilter;
+use yii_app\records\Products1cPropType;
+
+/**
+ * Products1cNomenclatureActualityController implements the CRUD actions for Products1cNomenclatureActuality model.
+ */
+class Products1cNomenclatureActualityController extends Controller
+{
+    /**
+     * @inheritDoc
+     */
+    public function behaviors()
+    {
+        return array_merge(
+            parent::behaviors(),
+            [
+                'verbs' => [
+                    'class' => VerbFilter::className(),
+                    'actions' => [
+                        'delete' => ['POST'],
+                    ],
+                ],
+            ]
+        );
+    }
+
+    /**
+     * Lists all Products1cNomenclatureActuality models.
+     *
+     * @return string
+     */
+    public function actionIndex()
+    {
+        $filter = new \yii\base\DynamicModel([
+            'category','subcategory','species',
+            'type','color','sort','size',
+            'date_from','date_to',
+        ]);
+        $filter->addRule([
+            'category','subcategory','species',
+            'type','color','sort','size',
+            'date_from','date_to',
+        ], 'safe');
+        $filter->load(Yii::$app->request->get());
+
+
+        if (Yii::$app->request->isPost && $post = Yii::$app->request->post('actuality', [])) {
+            $this->processBatchActuality($post);
+            Yii::$app->session->setFlash('success', 'Данные по актуальности успешно сохранены.');
+            return $this->refresh();
+        }
+
+
+        $emptyQuery = Products1cNomenclature::find()->where('0=1');
+        $dataProvider = new \yii\data\ActiveDataProvider([
+            'query' => $emptyQuery,
+            'pagination' => ['pageSize' => 50],
+            'sort' => ['defaultOrder'=>['name'=>SORT_ASC]],
+        ]);
+
+
+        $filtersUsed = array_filter([
+            $filter->category,
+            $filter->subcategory,
+            $filter->species,
+            $filter->type,
+            $filter->color,
+            $filter->sort,
+            $filter->size,
+            $filter->date_from,
+            $filter->date_to,
+        ]);
+        if ($filtersUsed) {
+            $query = Products1cNomenclature::find()->alias('n');
+
+
+            foreach (['category','subcategory','species'] as $attr) {
+                if ($filter->$attr) {
+                    $query->andWhere(["n.$attr" => $filter->$attr]);
+                }
+            }
+
+
+            if ($filter->date_from || $filter->date_to) {
+                $query->innerJoin(
+                    Products1cNomenclatureActuality::tableName() . ' a',
+                    'a.guid = n.id AND a.active = 1'
+                );
+                if ($filter->date_from) {
+                    $df = \DateTime::createFromFormat('Y-m', $filter->date_from)
+                        ->format('Y-m-01 00:00:00');
+                    $query->andWhere(['>=', 'a.date_from', $df]);
+                }
+                if ($filter->date_to) {
+                    $dt = \DateTime::createFromFormat('Y-m', $filter->date_to);
+                    $dt->modify('last day of this month')->setTime(23,59,59);
+                    $query->andWhere(['<=', 'a.date_end', $dt->format('Y-m-d H:i:s')]);
+                }
+            }
+
+
+            $attrMap = [
+                'type'  => ['type','тип'],
+                'color' => ['цвет','color'],
+                'sort'  => ['sort','сорт'],
+                'size'  => ['size','размер'],
+            ];
+            foreach (['type','color','sort','size'] as $attr) {
+                if ($filter->$attr) {
+
+                    $propIds = Products1cPropType::find()
+                        ->select('id')
+                        ->andWhere(['name' => $attrMap[$attr]])
+                        ->column();
+
+                    $productIds = Products1cAdditionalCharacteristics::find()
+                        ->select('product_id')
+                        ->distinct()
+                        ->andWhere(['property_id' => $propIds, 'value' => $filter->$attr])
+                        ->column();
+
+
+                    if (empty($productIds)) {
+                        $query->andWhere('0=1');
+                        break;
+                    }
+
+                    $query->andWhere(['n.id' => $productIds]);
+                }
+            }
+
+            // 4.4) Подставляем в провайдер
+            $dataProvider->query = $query;
+        }
+
+        $categories = Products1cNomenclature::find()->select('category')->distinct()->column();
+        $subcategories = Products1cNomenclature::find()->select('subcategory')->distinct();
+        $subcategories->andWhere(['not', ['subcategory' => null]]);
+        $subcategories->andWhere(['<>', 'subcategory', '']);
+        if ($filter->category) {
+            $subcategories->andWhere(['category' => $filter->category]);
+        }
+        $subcategories = $subcategories->column();
+
+        $species = Products1cNomenclature::find()
+            ->select('species')
+            ->distinct()
+            ->andWhere(['not', ['species' => null]])
+            ->andWhere(['<>', 'species', '']);
+
+        if ($filter->subcategory) {
+            $species->andWhere(['subcategory' => $filter->subcategory]);
+        }
+
+        $species = $species->column();
+
+        $lists = [];
+        $attrMap = [
+            'type'  => ['type','тип'],
+            'color' => ['цвет','color'],
+            'sort'  => ['sort','сорт'],
+            'size'  => ['size','размер'],
+        ];
+        foreach (['type','color','sort','size'] as $attr) {
+            $propIds = Products1cPropType::find()
+                ->select('id')
+                ->andWhere(['name'=>$attrMap[$attr]])
+                ->column();
+            $lists[$attr] = Products1cAdditionalCharacteristics::find()
+                ->select('value')->distinct()
+                ->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']),
+        ]);
+    }
+
+
+
+    /**
+     * Обработка массового сохранения диапазонов актуальности.
+     * Если из/до нет или невалидны — пропускаем.
+     * Закрываем старую запись и создаем новую при изменении диапазона.
+     * @param array $post
+     */
+    protected function processBatchActuality(array $post)
+    {
+        $userId = Yii::$app->user->id;
+        $now    = date('Y-m-d H:i:s');
+
+        foreach ($post as $row) {
+            if (empty($row['from']) || empty($row['to'])) {
+                continue;
+            }
+
+            $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) {
+                continue;
+            }
+            $toDt->modify('last day of this month')->setTime(23, 59, 59);
+            $newTo = $toDt->format('Y-m-d H:i:s');
+
+
+            $warehouseNN = !empty($row['warehouse_nn']);
+            $warehouseMS = !empty($row['warehouse_msk']);
+            $supplier    = $row['supplier'] ?? null;
+            $plantation  = $row['plantation'] ?? null;
+
+
+            $old = Products1cNomenclatureActuality::find()
+                ->andWhere(['guid' => $row['guid'], 'active' => 1])
+                ->one();
+
+            if ($old) {
+                if ($old->date_from === $newFrom && $old->date_end === $newTo) {
+                    continue;
+                }
+
+                $old->active     = 0;
+                $old->updated_by = $userId;
+                $old->updated_at = $now;
+                $old->save(false);
+            }
+
+
+            $new = new Products1cNomenclatureActuality([
+                'guid'        => $row['guid'],
+                'date_from'   => $newFrom,
+                'date_end'    => $newTo,
+                'active'      => 1,
+                'created_at'  => $now,
+                'created_by'  => $userId,
+            ]);
+            $new->save(false);
+        }
+    }
+
+    /**
+     * Displays a single Products1cNomenclatureActuality model.
+     * @param int $id ID
+     * @return string
+     * @throws NotFoundHttpException if the model cannot be found
+     */
+    public function actionView($id)
+    {
+        return $this->render('view', [
+            'model' => $this->findModel($id),
+        ]);
+    }
+
+    /**
+     * Creates a new Products1cNomenclatureActuality model.
+     * If creation is successful, the browser will be redirected to the 'view' page.
+     * @return string|\yii\web\Response
+     */
+    public function actionCreate()
+    {
+        $model = new Products1cNomenclatureActuality();
+
+        if ($this->request->isPost) {
+            if ($model->load($this->request->post()) && $model->save()) {
+                return $this->redirect(['view', 'id' => $model->id]);
+            }
+        } else {
+            $model->loadDefaultValues();
+        }
+
+        return $this->render('create', [
+            'model' => $model,
+        ]);
+    }
+
+    /**
+     * Updates an existing Products1cNomenclatureActuality model.
+     * If update is successful, the browser will be redirected to the 'view' page.
+     * @param int $id ID
+     * @return string|\yii\web\Response
+     * @throws NotFoundHttpException if the model cannot be found
+     */
+    public function actionUpdate($id)
+    {
+        $model = $this->findModel($id);
+
+        if ($this->request->isPost && $model->load($this->request->post()) && $model->save()) {
+            return $this->redirect(['view', 'id' => $model->id]);
+        }
+
+        return $this->render('update', [
+            'model' => $model,
+        ]);
+    }
+
+    /**
+     * Deletes an existing Products1cNomenclatureActuality model.
+     * If deletion is successful, the browser will be redirected to the 'index' page.
+     * @param int $id ID
+     * @return \yii\web\Response
+     * @throws NotFoundHttpException if the model cannot be found
+     */
+    public function actionDelete($id)
+    {
+        $this->findModel($id)->delete();
+
+        return $this->redirect(['index']);
+    }
+
+    /**
+     * Finds the Products1cNomenclatureActuality model based on its primary key value.
+     * If the model is not found, a 404 HTTP exception will be thrown.
+     * @param int $id ID
+     * @return Products1cNomenclatureActuality the loaded model
+     * @throws NotFoundHttpException if the model cannot be found
+     */
+    protected function findModel($id)
+    {
+        if (($model = Products1cNomenclatureActuality::findOne(['id' => $id])) !== null) {
+            return $model;
+        }
+
+        throw new NotFoundHttpException('The requested page does not exist.');
+    }
+}
diff --git a/erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php b/erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php
new file mode 100644 (file)
index 0000000..bb74e8c
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+use yii\db\Migration;
+
+/**
+ * Handles the creation of table `{{%products_1c_nomenclature_actuality}}`.
+ */
+class m250729_101715_create_products_1c_nomenclature_actuality_table extends Migration
+{
+    const TABLE_NAME = 'erp24.products_1c_nomenclature_actuality';
+    /**
+     * {@inheritdoc}
+     */
+    public function safeUp()
+    {
+        $tableSchema = $this->db->getTableSchema(self::TABLE_NAME);
+
+        if (!isset($tableSchema)) {
+            $this->createTable(self::TABLE_NAME, [
+                'id' => $this->primaryKey(),
+                'guid' => $this->string()->notNull(),
+                'date_from' => $this->dateTime()->notNull()->comment('Дата и время начала активности'),
+                'date_end' => $this->dateTime()->null()->comment('Дата и время окончания активности'),
+                'active' => $this->tinyInteger()->notNull()->defaultValue(1)->comment('Активность записи'),
+                'created_at' => $this->dateTime()->notNull()->comment('Дата создания'),
+                'updated_at' => $this->dateTime()->null()->comment('Дата обновления'),
+                'created_by' => $this->integer()->notNull()->comment('ИД создателя'),
+                'updated_by' => $this->integer()->null()->comment('ИД редактировавшего'),
+            ]);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function safeDown()
+    {
+        $tableSchema = $this->db->getTableSchema(self::TABLE_NAME);
+        if (isset($tableSchema)) {
+            $this->dropTable(self::TABLE_NAME);
+        }
+    }
+}
index 57f39e04d13076ae32afec4c138420a015a4a362..5342425f2484065c7860167a6ffd7808479b8e6c 100644 (file)
@@ -3,6 +3,7 @@
 namespace yii_app\records;
 
 use Yii;
+use yii\db\ActiveQuery;
 
 /**
  * This is the model class for table "products_1c_nomenclature".
@@ -65,4 +66,35 @@ class Products1cNomenclature extends \yii\db\ActiveRecord
             'color' => 'Цвет',
         ];
     }
+
+    /**
+     * Relation: все записи актуальности для этой номенклатуры
+     */
+    public function getActualities(): ActiveQuery
+    {
+        return $this->hasMany(
+            Products1cNomenclatureActuality::class,
+            ['guid' => 'id']
+        );
+    }
+
+    /**
+     * Relation: единственная (первыей) активная запись актуальности
+     */
+    public function getActiveActuality(): ActiveQuery
+    {
+        return $this->hasOne(
+            Products1cNomenclatureActuality::class,
+            ['guid' => 'id']
+        )->andWhere(['active' => 1]);
+    }
+
+    /**
+     * Проверяет, есть ли для этой номенклатуры активная запись
+     * @return bool
+     */
+    public function hasActiveActuality(): bool
+    {
+        return (bool)$this->getActiveActuality()->exists();
+    }
 }
diff --git a/erp24/records/Products1cNomenclatureActuality.php b/erp24/records/Products1cNomenclatureActuality.php
new file mode 100644 (file)
index 0000000..cce087e
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "products_1c_nomenclature_actuality".
+ *
+ * @property int $id
+ * @property string $guid
+ * @property string $date_from Дата и время начала активности
+ * @property string|null $date_end Дата и время окончания активности
+ * @property int $active Активность записи
+ * @property string $created_at Дата создания
+ * @property string|null $updated_at Дата обновления
+ * @property int $created_by ИД создателя
+ * @property int|null $updated_by ИД редактировавшего
+ */
+class Products1cNomenclatureActuality extends \yii\db\ActiveRecord
+{
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'products_1c_nomenclature_actuality';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['date_end', 'updated_at', 'updated_by'], 'default', 'value' => null],
+            [['active'], 'default', 'value' => 1],
+            [['guid', 'date_from', 'created_at', 'created_by'], 'required'],
+            [['date_from', 'date_end', 'created_at', 'updated_at'], 'safe'],
+            [['active', 'created_by', 'updated_by'], 'default', 'value' => null],
+            [['active', 'created_by', 'updated_by'], 'integer'],
+            [['guid'], 'string', 'max' => 255],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels()
+    {
+        return [
+            'id' => 'ID',
+            'guid' => 'Guid',
+            'date_from' => 'Date From',
+            'date_end' => 'Date End',
+            'active' => 'Active',
+            'created_at' => 'Created At',
+            'updated_at' => 'Updated At',
+            'created_by' => 'Created By',
+            'updated_by' => 'Updated By',
+        ];
+    }
+
+}
diff --git a/erp24/records/Products1cNomenclatureActualitySearch.php b/erp24/records/Products1cNomenclatureActualitySearch.php
new file mode 100644 (file)
index 0000000..bddbc6b
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+namespace yii_app\records;
+
+use yii\base\Model;
+use yii\data\ActiveDataProvider;
+use yii\helpers\ArrayHelper;
+use yii_app\records\Products1cNomenclatureActuality;
+
+/**
+ * Products1cNomenclatureActualitySearch represents the model behind the search form of `yii_app\records\Products1cNomenclatureActuality`.
+ */
+class Products1cNomenclatureActualitySearch extends Products1cNomenclatureActuality
+{
+    public $category;
+    public $type;
+    public $color;
+    public $subcategory;
+    public $sort;
+    public $species;
+    public $size;
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['id', 'active', 'created_by', 'updated_by'], 'integer'],
+            [['guid', 'date_from', 'date_end', 'created_at', 'updated_at'], 'safe'],
+            [['category','type','color','subcategory','sort','species','size'], 'safe'],
+
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function scenarios()
+    {
+        // bypass scenarios() implementation in the parent class
+        return Model::scenarios();
+    }
+
+    /**
+     * Creates data provider instance with search query applied
+     *
+     * @param array $params
+     * @param string|null $formName Form name to be used into `->load()` method.
+     *
+     * @return ActiveDataProvider
+     */
+    public function search($params, $formName = null)
+    {
+        $query = Products1cNomenclatureActuality::find();
+
+        // add conditions that should always apply here
+
+        $dataProvider = new ActiveDataProvider([
+            'query' => $query,
+        ]);
+
+        $this->load($params, $formName);
+
+        if (!$this->validate()) {
+            // uncomment the following line if you do not want to return any records when validation fails
+            // $query->where('0=1');
+            return $dataProvider;
+        }
+
+        // grid filtering conditions
+        $query->andFilterWhere([
+            'id' => $this->id,
+            'date_from' => $this->date_from,
+            'date_end' => $this->date_end,
+            'active' => $this->active,
+            'created_at' => $this->created_at,
+            'updated_at' => $this->updated_at,
+            'created_by' => $this->created_by,
+            'updated_by' => $this->updated_by,
+        ]);
+
+        $query->andFilterWhere(['ilike', 'guid', $this->guid]);
+
+        return $dataProvider;
+    }
+
+    public static function getCategoryList(): array
+    {
+        $rows = Products1cNomenclature::find()
+            ->select('category')
+            ->distinct()
+            ->where(['not', ['category'=>null]])
+            ->orderBy('category')
+            ->asArray()
+            ->all();
+
+        return ArrayHelper::map($rows, 'category', 'category');
+    }
+
+    public static function getTypeList(): array
+    {
+        $typeList = Products1cPropType::find()
+            ->select(['id'])
+            ->andWhere(['name' => ['type', 'тип']])
+            ->column();
+        $additionalCharacteristic = Products1cAdditionalCharacteristics::find()
+            ->select(['value'])->distinct()->where(['property_id' => $typeList ])->asArray()->all();
+
+        return ArrayHelper::map($additionalCharacteristic, 'value', 'value');
+    }
+
+    public static function getColorList(): array
+    {
+        $typeList = Products1cPropType::find()
+        ->select(['id'])
+            ->andWhere(['name' => ['цвет', 'color']])
+            ->column();
+        $additionalCharacteristic = Products1cAdditionalCharacteristics::find()
+            ->select(['value'])->distinct()->where(['property_id' => $typeList])->asArray()->all();
+
+        return ArrayHelper::map($additionalCharacteristic, 'value', 'value');
+    }
+
+    public static function getSubcategoryList(): array
+    {
+        $rows = Products1cNomenclature::find()
+            ->select('subcategory')
+            ->distinct()
+            ->where(['not', ['subcategory'=>null]])
+            ->orderBy('subcategory')
+            ->asArray()
+            ->all();
+
+        return ArrayHelper::map($rows, 'subcategory', 'subcategory');
+    }
+
+    public static function getSortList(): array
+    {
+        $typeList = Products1cPropType::find()
+            ->select(['id'])
+            ->andWhere(['name' => ['sort', 'сорт']])
+            ->column();
+        $additionalCharacteristic = Products1cAdditionalCharacteristics::find()
+            ->select(['value'])->distinct()->where(['property_id' => $typeList])->asArray()->all();
+
+        return ArrayHelper::map($additionalCharacteristic, 'value', 'value');
+    }
+
+    public static function getSpeciesList(): array
+    {
+        $rows = Products1cNomenclature::find()
+            ->select('species')
+            ->distinct()
+            ->where(['not', ['species'=>null]])
+            ->orderBy('species')
+            ->asArray()
+            ->all();
+
+        return ArrayHelper::map($rows, 'species', 'species');
+    }
+
+    public static function getSizeList(): array
+    {
+        $typeList = Products1cPropType::find()
+        ->select(['id'])
+        ->andWhere(['name' => ['size', 'размер']])
+        ->column();
+        $additionalCharacteristic = Products1cAdditionalCharacteristics::find()
+            ->select(['value'])->distinct()->where(['property_id' =>  $typeList])->asArray()->all();
+
+        return ArrayHelper::map($additionalCharacteristic, 'value', 'value');
+    }
+}
diff --git a/erp24/views/products1c-nomenclature-actuality/index.php b/erp24/views/products1c-nomenclature-actuality/index.php
new file mode 100644 (file)
index 0000000..270e4f2
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+
+use kartik\form\ActiveForm;
+use kartik\grid\GridView;
+use yii\helpers\Html;
+
+
+/* @var $this yii\web\View */
+/* @var $filter yii\base\DynamicModel */
+/* @var $dataProvider yii\data\ActiveDataProvider */
+/* @var array $categories */
+/* @var array $subcategories */
+/* @var array $species */
+/* @var array $types */
+/* @var array $colors */
+/* @var array $sorts */
+/* @var array $sizes */
+
+$this->title = 'Актуализация номенклатуры';
+$this->params['breadcrumbs'][] = $this->title;
+
+// Список месяцев-годов для выпадающих списков
+function monthList()
+{
+    $list = [];
+    $start = new DateTime('2021-01');
+    $end = new DateTime('2025-12');
+    while ($start <= $end) {
+        $key = $start->format('Y-m');
+        $list[$key] = $start->format('Y‑m');
+        $start->modify('+1 month');
+    }
+    return $list;
+}
+
+$months = monthList();
+?>
+
+<div class="products1c-nomenclature-actuality-index p-4">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <!-- Форма фильтров -->
+    <?php $formFilter = ActiveForm::begin([
+        'method' => 'get',
+        'action' => ['index'],
+        'options' => ['class' => 'mb-4'],
+    ]); ?>
+
+    <div class="row">
+
+        <!-- 1-я колонка: основные фильтры -->
+        <div class="col-6 ">
+            <div class="row mb-3">
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'category', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $categories,
+                            ['prompt' => 'Категория', 'id' => 'filter-category']
+                        )->label(false) ?>
+
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-category" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'type', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $types,
+                            ['prompt' => 'Тип', 'id' => 'filter-type']
+                        )->label(false) ?>
+
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-type" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'color', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $colors,
+                            ['prompt' => 'Цвет', 'id' => 'filter-color']
+                        )->label(false) ?>
+
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-color" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="row mb-3">
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'subcategory', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $subcategories,
+                            ['prompt' => 'Подкатегория', 'id' => 'filter-subcategory', 'class' => 'w-100']
+                        )->label(false) ?>
+
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-subcategory" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'sort', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $sorts,
+                            ['prompt' => 'Сорт', 'id' => 'filter-sort']
+                        )->label(false) ?>
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-sort" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col"></div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'species', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $species,
+                            ['prompt' => 'Вид', 'id' => 'filter-species']
+                        )->label(false) ?>
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-species" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col">
+                    <div class="d-flex justify-content-between">
+                        <?= $formFilter->field($filter, 'size', ['options' => ['class' => 'w-90']])->dropDownList(
+                            $sizes,
+                            ['prompt' => 'Размер', 'id' => 'filter-size']
+                        )->label(false) ?>
+                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-size" >
+                            <i class="fa fa-times"></i>
+                        </div>
+                    </div>
+                </div>
+                <div class="col"></div>
+            </div>
+        </div>
+
+        <!-- 2-я колонка: даты актуальности -->
+        <div class="col-3  ps-4" style="border-left: #ccc solid 1px">
+            <div class="mb-2 fw-bold">Актуальность ассортимента</div>
+            <div class="mb-3">
+                <div class="d-flex justify-content-between">
+                    <?= $formFilter->field($filter, 'date_from', ['options' => ['class' => 'w-100']])
+                        ->dropDownList($months, ['prompt' => 'Выбрать дату от', 'id' => 'filter-date-from'])
+                        ->label(false) ?>
+                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-date-from" >
+                        <i class="fa fa-times"></i>
+                    </div>
+                </div>
+            </div>
+            <div>
+                <div class="d-flex justify-content-between">
+                    <?= $formFilter->field($filter, 'date_to', ['options' => ['class' => 'w-100']])
+                        ->dropDownList($months, ['prompt' => 'Выбрать дату до', 'id' => 'filter-date-to'])
+                        ->label(false) ?>
+                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-date-to" >
+                        <i class="fa fa-times"></i>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 3-я колонка: поставщик/плантация + кнопка Применить -->
+        <div class="col-3 ps-4" style="border-left: #ccc solid 1px">
+            <div class="mb-3">
+
+                <div class="input-group">
+                    <?= Html::dropDownList('supplier', null,
+                        ['Астра','Бифлористика'],
+                        ['class' => 'form-select', 'id' => 'filter-supplier', 'prompt' => 'Поставщик']) ?>
+                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-supplier" >
+                        <i class="fa fa-times"></i>
+                    </div>
+                </div>
+            </div>
+            <div class="mb-4">
+
+                <div class="input-group">
+                    <?= Html::dropDownList('plantation', null,
+                        ['Плантация1','Плантация2'],
+                        ['class' => 'form-select', 'id' => 'filter-plantation', 'prompt' => 'Плантация']) ?>
+                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center  clear-btn" data-target="filter-plantation" >
+                        <i class="fa fa-times"></i>
+                    </div>
+                </div>
+            </div>
+            <div class="text-end">
+                <?= Html::submitButton('Применить', ['class' => 'btn btn-primary']) ?>
+            </div>
+        </div>
+
+    </div>
+
+    <?php ActiveForm::end(); ?>
+
+
+
+
+    <!-- Форма массового обновления актуальности -->
+    <?php $form = ActiveForm::begin(['id' => 'actuality-form']); ?>
+    <div class="form-group d-flex justify-content-end">
+        <?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
+    </div>
+    <?= GridView::widget([
+        'dataProvider' => $dataProvider,
+        'responsive' => false,
+        'hover' => true,
+        'floatHeader' => false,
+        'tableOptions' => ['class' => 'table table-bordered'],
+        'containerOptions' => ['style' => 'overflow:auto; max-height:500px;'],
+        'rowOptions' => function($model) {
+            return $model->hasActiveActuality() ? ['class' => 'table-success'] : [];
+        },
+        'columns' => [
+            [
+                'attribute' => 'name',
+                'label' => 'Наименование',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'min-width:200px;'],
+                'value' => function ($m) {
+                    return Html::encode($m->name);
+                }
+            ],
+            [
+                'label' => 'Актуальность ассортимента',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'white-space:nowrap; min-width:200px;'],
+                'value' => function ($m, $k, $i) use ($months) {
+                    $active = $m->getActiveActuality()->one();
+                    $from = $active ? (new \DateTime($active->date_from))->format('Y-m') : null;
+                    $to   = $active ? (new \DateTime($active->date_end))->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 form-select-sm me-1',
+                                'prompt'=>'от'
+                            ])
+                            . Html::dropDownList("actuality[$i][to]", $to, $months, [
+                                'class'=>'form-select form-select-sm',
+                                'prompt'=>'до'
+                            ]),
+                            ['class'=>'d-flex align-items-center']
+                        );
+                }
+            ],
+            [
+                'label' => 'Склад NN',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'width:60px; text-align:center;'],
+                'value' => function ($m, $k, $i) {
+                    return Html::checkbox("actuality[$i][warehouse_nn]", false);
+                }
+            ],
+            [
+                'label' => 'Склад MSK',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'width:60px; text-align:center;'],
+                'value' => function ($m, $k, $i) {
+                    return Html::checkbox("actuality[$i][warehouse_msk]", false);
+                }
+            ],
+            [
+                'label' => 'Поставщик/Плантация',
+                'format' => 'text',
+                'contentOptions' => ['style'=>'min-width:150px;'],
+                'value' => function ($m) {
+                    return '–';
+                }
+            ],
+        ],
+    ]); ?>
+
+
+
+    <?php ActiveForm::end(); ?>
+
+</div>
+
+<?php
+$js = <<<JS
+$('.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);
+    }
+});
+$('.clear-btn').on('click', function(){
+    var target = $(this).data('target');
+    $('#' + target).val(null).trigger('change');
+});
+JS;
+$this->registerJs($js);
+?>