]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Актуальность букетов
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Tue, 9 Sep 2025 12:12:23 +0000 (15:12 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Tue, 9 Sep 2025 12:12:23 +0000 (15:12 +0300)
erp24/controllers/MatrixBouquetActualityController.php [new file with mode: 0644]
erp24/records/BouquetComposition.php
erp24/records/MatrixBouquetActuality.php [new file with mode: 0644]
erp24/records/MatrixBouquetActualitySearch.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/_form.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/_search.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/create.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/index.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/update.php [new file with mode: 0644]
erp24/views/matrix-bouquet-actuality/view.php [new file with mode: 0644]

diff --git a/erp24/controllers/MatrixBouquetActualityController.php b/erp24/controllers/MatrixBouquetActualityController.php
new file mode 100644 (file)
index 0000000..4256f03
--- /dev/null
@@ -0,0 +1,480 @@
+<?php
+
+namespace app\controllers;
+
+use Yii;
+use yii\data\ActiveDataProvider;
+use yii\helpers\ArrayHelper;
+use yii_app\records\BouquetComposition;
+use yii_app\records\BouquetCompositionMatrixTypeHistory;
+use yii_app\records\MatrixBouquetActuality;
+use yii_app\records\MatrixBouquetActualitySearch;
+use yii\web\Controller;
+use yii\web\NotFoundHttpException;
+use yii\filters\VerbFilter;
+use yii_app\records\MatrixType;
+use yii_app\records\Products1cAdditionalCharacteristics;
+use yii_app\records\Products1cNomenclature;
+use yii_app\records\Products1cNomenclatureActuality;
+use yii_app\records\Products1cPropType;
+
+/**
+ * MatrixBouquetActualityController implements the CRUD actions for MatrixBouquetActuality model.
+ */
+class MatrixBouquetActualityController extends Controller
+{
+    /**
+     * @inheritDoc
+     */
+    public function behaviors()
+    {
+        return array_merge(
+            parent::behaviors(),
+            [
+                'verbs' => [
+                    'class' => VerbFilter::className(),
+                    'actions' => [
+                        'delete' => ['POST'],
+                    ],
+                ],
+            ]
+        );
+    }
+
+    /**
+     * Lists all MatrixBouquetActuality models.
+     *
+     * @return string
+     */
+    public function actionIndex()
+    {
+        $filter = new \yii\base\DynamicModel([
+            'group_id', 'subgroup_id', 'is_archive',
+            'date_from', 'date_to',
+            'onlyActive', 'onlyInactive',
+        ]);
+        $filter->addRule(['group_id', 'subgroup_id', 'is_archive', 'date_from', 'date_to'], 'safe');
+        $filter->addRule(['onlyActive', 'onlyInactive'], 'boolean');
+        $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();
+        }
+
+
+        $dataProvider = new ActiveDataProvider([
+            'query' => BouquetComposition::find()->where('0=1'),
+            'pagination' => ['pageSize' => 50],
+            'sort' => ['defaultOrder' => ['name' => SORT_ASC]],
+        ]);
+
+        $filtersUsed = array_filter([
+            $filter->group_id,
+            $filter->subgroup_id,
+            $filter->is_archive,
+            $filter->date_from,
+            $filter->date_to,
+            $filter->onlyActive,
+            $filter->onlyInactive
+        ], static fn($v) => $v !== null && $v !== '');
+
+
+        if ($filtersUsed) {
+            $query = BouquetComposition::find()->alias('bc');
+            $query->joinWith(['priceRel pr'])->addSelect(['bc.*', 'pr.price AS price']);
+
+            if (!empty($filter->group_id) || !empty($filter->subgroup_id)) {
+                $typeIds = [];
+
+                if (!empty($filter->group_id)) {
+                    $typeIds = $this->getMatrixTypeDescendantsIds((int)$filter->group_id);
+                }
+
+                if (!empty($filter->subgroup_id)) {
+                    if (!empty($typeIds)) {
+                        $typeIds =  array_values(array_intersect($typeIds, [(int)$filter->subgroup_id]));
+                    } else {
+                        $typeIds = [(int)$filter->subgroup_id];
+                    }
+                }
+
+                if (empty($typeIds)) {
+                    $query->andWhere('1=0');
+                } else {
+                    $bouquetIds = BouquetCompositionMatrixTypeHistory::find()
+                        ->select('bouquet_id')
+                        ->where(['matrix_type_id' => $typeIds, 'is_active' => true])
+                        ->andWhere(['is not', 'bouquet_id', null])
+                        ->distinct()->column();
+
+                    if (empty($bouquetIds)) {
+                        $query->andWhere('1=0');
+                    } else {
+                        $query->andWhere(['bc.id' => $bouquetIds]);
+                    }
+                }
+            }
+
+
+            if (!empty($filter->onlyActive)) {
+                    $query->andWhere(['exists',
+                        MatrixBouquetActuality::find()->alias('a1')
+                            ->where('a1.bouquet_id = bc.id')
+                            ->select(new \yii\db\Expression('1'))
+                    ]);
+            } elseif (!empty($filter->onlyInactive)) {
+                $query->andWhere(['not exists',
+                    MatrixBouquetActuality::find()->alias('a2')
+                        ->where('a2.bouquet_id = bc.id')
+                        ->select(new \yii\db\Expression('1'))
+                ]);
+            }
+
+            if ($filter->date_from || $filter->date_to || $filter->is_archive !== null && $filter->is_archive !== '') {
+                $hasDateFrom = !empty($filter->date_from);
+                $hasDateTo = !empty($filter->date_to);
+
+                $dateFrom = null;
+                $dateTo = null;
+
+                if ($hasDateFrom) {
+                    $dateFrom = (new \DateTime("{$filter->date_from}-01"))
+                        ->setTime(0, 0, 0)->format('Y-m-d H:i:s');
+                }
+                if ($hasDateTo) {
+                    $dateTo = (new \DateTime("{$filter->date_to}-01"))
+                        ->modify('last day of this month')->setTime(23, 59, 59)
+                        ->format('Y-m-d H:i:s');
+                }
+
+                $dateExists = MatrixBouquetActuality::find()
+                    ->alias('a')
+                    ->where('a.bouquet_id = bc.id');
+                if ($filter->is_archive !== null && $filter->is_archive !== '') {
+                    $dateExists->andWhere(['a.is_archive' => (int)$filter->is_archive]);
+                }
+
+                if ($hasDateFrom && !$hasDateTo) {
+                    $dateExists//->andWhere(['a.date_from' => $dateFrom])
+                    ->andWhere(['>=', 'a.date_to', $dateFrom]);;
+                } elseif (!$hasDateFrom && $hasDateTo) {
+                    $dateExists//->andWhere(['a.date_to' => $dateTo])
+                    ->andWhere(['<=', 'a.date_from', $dateTo]);
+                } else {
+                    $dateExists
+                        ->andWhere(['>=', 'a.date_to',   $dateFrom])
+                        ->andWhere(['<=', 'a.date_from', $dateTo]);
+                }
+
+                if (!empty($filter->onlyInactive)) {
+                    $query->andWhere(['not exists', $dateExists->select(new \yii\db\Expression('1'))]);
+                } else {
+                    $query->andWhere(['exists', $dateExists->select(new \yii\db\Expression('1'))]);
+                }
+
+                $query->with(['actualities' => function ($subQuery) use ($filter, $hasDateFrom, $hasDateTo, $dateFrom, $dateTo) {
+                    if ($filter->is_archive !== null && $filter->is_archive !== '') {
+                        $subQuery->andWhere(['is_archive' => (int)$filter->is_archive]);
+                    }
+
+                    if ($hasDateFrom && !$hasDateTo) {
+                        $subQuery//->andWhere(['date_from' => $dateFrom])
+                        ->andWhere(['>=', 'date_to', $dateFrom]);
+                    } elseif (!$hasDateFrom && $hasDateTo) {
+                        $subQuery//->andWhere(['date_to' => $dateTo])
+                        ->andWhere(['<=', 'date_from', $dateTo]);
+                    } else {
+                        $subQuery->andWhere(['>=', 'date_to',   $dateFrom])
+                            ->andWhere(['<=', 'date_from', $dateTo]);
+                    }
+                    $subQuery->orderBy(['date_from' => SORT_ASC]);
+                }]);
+            } else {
+                $query->with(['actualities' => function ($q) {
+                    $q->orderBy(['date_from' => SORT_ASC]);
+                }]);
+            }
+
+
+            $dataProvider = new ActiveDataProvider([
+                'query' => $query->orderBy(['bc.name' => SORT_ASC]),
+                'pagination' => ['pageSize' => 100],
+                'sort' => [
+                    'attributes' => [
+                        'name' => [
+                            'asc'  => ['bc.name' => SORT_ASC],
+                            'desc' => ['bc.name' => SORT_DESC],
+                        ],
+                        'price' => [
+                            'asc'  => ['price' => SORT_ASC],
+                            'desc' => ['price' => SORT_DESC],
+                        ],
+                    ],
+                    'defaultOrder' => ['name' => SORT_ASC],
+                ],
+            ]);
+        }
+
+        $groups = MatrixType::find()
+            ->select(['id','name'])
+            ->where(['parent_id' => null])
+            ->andWhere(['<>', 'deleted', 1])
+            ->orderBy(['name' => SORT_ASC])
+            ->asArray()->all();
+        $groupsList = ArrayHelper::map($groups, 'id', 'name');
+
+        $subgroups = MatrixType::find()
+            ->select(['id','name','parent_id'])
+            ->where(['not', ['parent_id' => null]])
+            ->andWhere(['<>', 'deleted', 1])
+            ->orderBy(['name' => SORT_ASC])
+            ->asArray()->all();
+        var_dump($dataProvider);die();
+        return $this->render('index', [
+            'filter'       => $filter,
+            'dataProvider' => $dataProvider,
+            'groups'       => $groupsList,
+            'subgroups'    => $subgroups,
+        ]);
+    }
+
+    private function getMatrixTypeDescendantsIds(int $rootId): array
+    {
+        $all = MatrixType::find()->select(['id','parent_id'])
+            ->andWhere(['<>', 'deleted', 1])
+            ->asArray()->all();
+
+        $byParent = [];
+        foreach ($all as $r) {
+            $byParent[(int)$r['parent_id']][] = (int)$r['id'];
+        }
+
+        $out = [];
+        $stack = [$rootId];
+        while ($stack) {
+            $id = array_pop($stack);
+            if (in_array($id, $out, true)) continue;
+            $out[] = $id;
+            if (!empty($byParent[$id])) {
+                foreach ($byParent[$id] as $cid) $stack[] = $cid;
+            }
+        }
+        return $out;
+    }
+
+
+    /**
+     * Обработка массового сохранения диапазонов актуальности.
+     * Если из/до нет или невалидны — пропускаем.
+     * Закрываем старую запись и создаем новую при изменении диапазона.
+     * @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;
+            }
+
+            $fromDate = \DateTime::createFromFormat('Y-m', $row['from']);
+            $toDate = \DateTime::createFromFormat('Y-m', $row['to']);
+            if (!$fromDate || !$toDate) {
+                continue;
+            }
+            $fromDate->setDate((int)$fromDate->format('Y'), (int)$fromDate->format('m'), 1)
+                ->setTime(0, 0, 0);
+            $toDate->modify('last day of this month')
+                ->setTime(23, 59, 59);
+
+            $from = $fromDate->format('Y-m-d H:i:s');
+            $to = $toDate->format('Y-m-d H:i:s');
+
+            if ($from > $to) {
+                Yii::warning("GUID {$row['guid']}: пропускаем — from > to");
+                continue;
+            }
+
+            $guid = $row['guid'];
+            $bouquetId = $row['bouquet_id'];
+
+
+            $fromAdj = (clone $fromDate)->modify('-1 second')->format('Y-m-d H:i:s');
+            $toAdj   = (clone $toDate)->modify('+1 second')->format('Y-m-d H:i:s');
+
+            /** @var MatrixBouquetActuality[] $hits */
+            $hits = MatrixBouquetActuality::find()
+                ->where(['guid' => $guid])
+                ->andWhere('date_to   >= :fromAdj', [':fromAdj' => $fromAdj])
+                ->andWhere('date_from <= :toAdj',   [':toAdj'   => $toAdj])
+                ->orderBy(['date_from' => SORT_ASC])
+                ->all();
+
+            if (empty($hits)) {
+                $new = new MatrixBouquetActuality([
+                    'guid' => $guid,
+                    'bouquet_id' => $bouquetId,
+                    'date_from' => $from,
+                    'date_to' => $to,
+                    'is_archive' => 0,
+                    'created_at' => $now,
+                    'created_by' => $userId,
+                ]);
+                if (!$new->save()) {
+                    Yii::error("Ошибка создания GUID={$guid}: " . json_encode($new->getErrors(), JSON_UNESCAPED_UNICODE));
+                }
+                continue;
+            }
+
+            $minFrom = $from;
+            $maxTo   = $to;
+            foreach ($hits as $h) {
+                if ($h->date_from < $minFrom) { $minFrom = $h->date_from; }
+                if ($h->date_to   > $maxTo)   { $maxTo   = $h->date_to;   }
+            }
+
+            $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));
+            }
+
+            foreach ($hits as $dup) {
+                $dup->delete();
+            }
+
+            while (true) {
+                $leftBound  = (new \DateTime($master->date_from))->modify('-1 second')->format('Y-m-d H:i:s');
+                $rightBound = (new \DateTime($master->date_to))->modify('+1 second')->format('Y-m-d H:i:s');
+
+                /** @var MatrixBouquetActuality[] $neighbors */
+                $neighbors = MatrixBouquetActuality::find()
+                    ->where(['guid' => $guid])
+                    ->andWhere(['<>', 'id', $master->id])
+                    ->andWhere('date_to   >= :leftBound',  [':leftBound'  => $leftBound])
+                    ->andWhere('date_from <= :rightBound', [':rightBound' => $rightBound])
+                    ->orderBy(['date_from' => SORT_ASC])
+                    ->all();
+
+                if (empty($neighbors)) {
+                    break;
+                }
+
+                foreach ($neighbors as $nei) {
+                    if ($nei->date_from < $master->date_from)
+                    {
+                        $master->date_from = $nei->date_from;
+                    }
+                    if ($nei->date_to   > $master->date_to)
+                    {
+                        $master->date_to   = $nei->date_to;
+                    }
+                }
+                $master->updated_at = $now;
+                $master->updated_by = $userId;
+
+                if (!$master->save()) {
+                    Yii::error("Ошибка повторного обновления GUID={$guid}: " . json_encode($master->getErrors(), JSON_UNESCAPED_UNICODE));
+                    break;
+                }
+
+                foreach ($neighbors as $nei) {
+                    $nei->delete();
+                }
+            }
+        }
+    }
+    /**
+     * Displays a single MatrixBouquetActuality 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 MatrixBouquetActuality model.
+     * If creation is successful, the browser will be redirected to the 'view' page.
+     * @return string|\yii\web\Response
+     */
+    public function actionCreate()
+    {
+        $model = new MatrixBouquetActuality();
+
+        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 MatrixBouquetActuality 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 MatrixBouquetActuality 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 MatrixBouquetActuality 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 MatrixBouquetActuality the loaded model
+     * @throws NotFoundHttpException if the model cannot be found
+     */
+    protected function findModel($id)
+    {
+        if (($model = MatrixBouquetActuality::findOne(['id' => $id])) !== null) {
+            return $model;
+        }
+
+        throw new NotFoundHttpException('The requested page does not exist.');
+    }
+}
index f0e1bc55c275c9c5ac9c92eb9526b46b813edd4b..05a82deefd0bb6efd34f4fc0ec34cc18fa6f12b3 100644 (file)
@@ -6,6 +6,7 @@ use Exception;
 use Yii;
 use yii\behaviors\BlameableBehavior;
 use yii\behaviors\TimestampBehavior;
+use yii\db\ActiveQuery;
 use yii\db\ActiveRecord;
 use yii\db\Expression;
 use yii\helpers\Json;
@@ -592,4 +593,20 @@ class BouquetComposition extends ActiveRecord
         }
         return (int)date('d') > 10;
     }
+
+    public function getPriceRel()
+    {
+        return $this->hasOne(Prices::class, ['product_id' => 'guid']);
+    }
+
+    public function getActualities()
+    {
+        return $this->hasMany(MatrixBouquetActuality::class, ['bouquet_id' => 'id'])
+            ->orderBy(['date_from' => SORT_ASC]);
+    }
+
+    public function hasActuality() : bool
+    {
+        return $this->getActualities()->exists();
+    }
 }
diff --git a/erp24/records/MatrixBouquetActuality.php b/erp24/records/MatrixBouquetActuality.php
new file mode 100644 (file)
index 0000000..a981680
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "matrix_bouquet_actuality".
+ *
+ * @property int $id
+ * @property string $guid GUID товара из 1С
+ * @property int $bouquet_id ID букета из bouquet_composition
+ * @property string $date_from Дата и время начала активности
+ * @property string|null $date_to Дата и время окончания активности
+ * @property int|null $is_archive Признак архивного товара
+ * @property string $created_at Дата создания
+ * @property string|null $updated_at Дата обновления
+ * @property int $created_by ИД создателя
+ * @property int|null $updated_by ИД редактировавшего
+ */
+class MatrixBouquetActuality extends \yii\db\ActiveRecord
+{
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName()
+    {
+        return 'matrix_bouquet_actuality';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['date_to', 'updated_at', 'updated_by'], 'default', 'value' => null],
+            [['is_archive'], 'default', 'value' => 0],
+            [['guid', 'bouquet_id', 'date_from', 'created_at', 'created_by'], 'required'],
+            [['bouquet_id', 'is_archive', 'created_by', 'updated_by'], 'default', 'value' => null],
+            [['bouquet_id', 'is_archive', 'created_by', 'updated_by'], 'integer'],
+            [['date_from', 'date_to', 'created_at', 'updated_at'], 'safe'],
+            [['guid'], 'string', 'max' => 255],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels()
+    {
+        return [
+            'id' => 'ID',
+            'guid' => 'GUID товара из 1С',
+            'bouquet_id' => 'ID букета из bouquet_composition',
+            'date_from' => 'Дата и время начала активности',
+            'date_to' => 'Дата и время окончания активности',
+            'is_archive' => 'Признак архивного товара',
+            'created_at' => 'Дата создания',
+            'updated_at' => 'Дата обновления',
+            'created_by' => 'ИД создателя',
+            'updated_by' => 'ИД редактировавшего',
+        ];
+    }
+
+}
diff --git a/erp24/records/MatrixBouquetActualitySearch.php b/erp24/records/MatrixBouquetActualitySearch.php
new file mode 100644 (file)
index 0000000..48bdcef
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace yii_app\records;
+
+use yii\base\Model;
+use yii\data\ActiveDataProvider;
+use yii_app\records\MatrixBouquetActuality;
+
+/**
+ * MatrixBouquetActualitySearch represents the model behind the search form of `yii_app\records\MatrixBouquetActuality`.
+ */
+class MatrixBouquetActualitySearch extends MatrixBouquetActuality
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [
+            [['id', 'bouquet_id', 'is_archive', 'created_by', 'updated_by'], 'integer'],
+            [['guid', 'date_from', 'date_to', 'created_at', 'updated_at'], '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 = MatrixBouquetActuality::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,
+            'bouquet_id' => $this->bouquet_id,
+            'date_from' => $this->date_from,
+            'date_to' => $this->date_to,
+            'is_archive' => $this->is_archive,
+            '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;
+    }
+}
diff --git a/erp24/views/matrix-bouquet-actuality/_form.php b/erp24/views/matrix-bouquet-actuality/_form.php
new file mode 100644 (file)
index 0000000..3f27f0d
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+
+/** @var yii\web\View $this */
+/** @var yii_app\records\MatrixBouquetActuality $model */
+/** @var yii\widgets\ActiveForm $form */
+?>
+
+<div class="matrix-bouquet-actuality-form">
+
+    <?php $form = ActiveForm::begin(); ?>
+
+    <?= $form->field($model, 'guid')->textInput(['maxlength' => true]) ?>
+
+    <?= $form->field($model, 'bouquet_id')->textInput() ?>
+
+    <?= $form->field($model, 'date_from')->textInput() ?>
+
+    <?= $form->field($model, 'date_to')->textInput() ?>
+
+    <?= $form->field($model, 'is_archive')->textInput() ?>
+
+    <?= $form->field($model, 'created_at')->textInput() ?>
+
+    <?= $form->field($model, 'updated_at')->textInput() ?>
+
+    <?= $form->field($model, 'created_by')->textInput() ?>
+
+    <?= $form->field($model, 'updated_by')->textInput() ?>
+
+    <div class="form-group">
+        <?= Html::submitButton('Save', ['class' => 'btn btn-success']) ?>
+    </div>
+
+    <?php ActiveForm::end(); ?>
+
+</div>
diff --git a/erp24/views/matrix-bouquet-actuality/_search.php b/erp24/views/matrix-bouquet-actuality/_search.php
new file mode 100644 (file)
index 0000000..84aec52
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+
+/** @var yii\web\View $this */
+/** @var yii_app\records\MatrixBouquetActualitySearch $model */
+/** @var yii\widgets\ActiveForm $form */
+?>
+
+<div class="matrix-bouquet-actuality-search">
+
+    <?php $form = ActiveForm::begin([
+        'action' => ['index'],
+        'method' => 'get',
+    ]); ?>
+
+    <?= $form->field($model, 'id') ?>
+
+    <?= $form->field($model, 'guid') ?>
+
+    <?= $form->field($model, 'bouquet_id') ?>
+
+    <?= $form->field($model, 'date_from') ?>
+
+    <?= $form->field($model, 'date_to') ?>
+
+    <?php // echo $form->field($model, 'is_archive') ?>
+
+    <?php // echo $form->field($model, 'created_at') ?>
+
+    <?php // echo $form->field($model, 'updated_at') ?>
+
+    <?php // echo $form->field($model, 'created_by') ?>
+
+    <?php // echo $form->field($model, 'updated_by') ?>
+
+    <div class="form-group">
+        <?= Html::submitButton('Search', ['class' => 'btn btn-primary']) ?>
+        <?= Html::resetButton('Reset', ['class' => 'btn btn-outline-secondary']) ?>
+    </div>
+
+    <?php ActiveForm::end(); ?>
+
+</div>
diff --git a/erp24/views/matrix-bouquet-actuality/create.php b/erp24/views/matrix-bouquet-actuality/create.php
new file mode 100644 (file)
index 0000000..c04854e
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+use yii\helpers\Html;
+
+/** @var yii\web\View $this */
+/** @var yii_app\records\MatrixBouquetActuality $model */
+
+$this->title = 'Create Matrix Bouquet Actuality';
+$this->params['breadcrumbs'][] = ['label' => 'Matrix Bouquet Actualities', 'url' => ['index']];
+$this->params['breadcrumbs'][] = $this->title;
+?>
+<div class="matrix-bouquet-actuality-create">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <?= $this->render('_form', [
+        'model' => $model,
+    ]) ?>
+
+</div>
diff --git a/erp24/views/matrix-bouquet-actuality/index.php b/erp24/views/matrix-bouquet-actuality/index.php
new file mode 100644 (file)
index 0000000..db2dfac
--- /dev/null
@@ -0,0 +1,349 @@
+<?php
+
+use kartik\form\ActiveForm;
+use kartik\grid\GridView;
+use yii\helpers\Html;
+use yii\web\View;
+use yii_app\records\Products1cNomenclatureActuality;
+
+
+/* @var $this yii\web\View */
+/* @var $filter yii\base\DynamicModel */
+/* @var $dataProvider yii\data\ActiveDataProvider */
+
+
+$this->title = 'Актуализация букетов';
+$this->params['breadcrumbs'][] = $this->title;
+$this->registerJsFile('/js/products1cNomenclatureActuality/index.js', ['position' => View::POS_END]);
+// Список месяцев-годов для выпадающих списков
+function monthList()
+{
+    $list = [];
+    $tz  = new DateTimeZone('Europe/Moscow');
+    $now = new DateTime('now', $tz);
+    $start = (clone $now)->modify('first day of january last year');
+    $end  = (clone $now)->modify('last day of december next year');
+    while ($start <= $end) {
+        $key = $start->format('Y-m');
+        $list[$key] = $start->format('Y‑m');
+        $start->modify('+1 month');
+    }
+    return $list;
+}
+
+$months = monthList();
+$monthOptions = '';
+foreach ($months as $k => $v) {
+    $monthOptions .= "<option value=\"$k\">$v</option>";
+}
+?>
+
+<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="mb-2 fw-bold">Номенклатура</div>
+            <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',
+                                'class' => ''
+                            ]
+                        )
+                        ->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',
+                                'class' => ''
+                            ]
+                        )
+                        ->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 class="mb-3">
+                <?= $formFilter->field($filter, 'onlyActive')->checkbox([
+                    'label' => 'Только активные',
+                    'uncheck' => 0,
+                    'checked' => (bool)$filter->onlyActive,
+                    'id' => 'onlyActiveCheckbox'
+                ])->label(false) ?>
+
+                <?= $formFilter->field($filter, 'onlyInactive')->checkbox([
+                    'label' => 'Только неактивные',
+                    'uncheck' => 0,
+                    'checked' => (bool)$filter->onlyInactive,
+                    'id' => 'onlyInactiveCheckbox'
+                ])->label(false) ?>
+            </div>
+        </div>
+
+        <!-- 3-я колонка: поставщик/плантация + кнопка Применить -->
+        <div class="col-2 ps-4" style="border-left: #ccc solid 1px">
+            <div class="mb-2 fw-bold">Поставщики</div>
+            <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>
+        <div class="col-1 ps-4 d-flex justify-content-end align-items-end" >
+
+            <?= Html::submitButton('Применить', ['class' => 'btn btn-primary w-100']) ?>
+
+        </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', 'id' => 'saveButton']) ?>
+    </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($row) {
+            return $row['actuality'] ? ['class'=>'table-success'] : [];
+        },
+        'columns' => [
+            [
+                'label' => 'Наименование',
+                'format' => 'raw',
+                '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: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;
+                    $clearBtn = $actuality ? '<div class="ms-2 d-flex justify-content-center align-items-center  clear-interval-btn" data-id="' . $actuality->id . '" >
+                        <i class="fa fa-times"></i>
+                    </div>' : '';
+                    $inputs = '<div class="d-flex justify-content-center align-items-center">';
+                    $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'=>'от',
+                                'data-actuality' => $actuality ? 1 : 0,
+                                'style' => 'width:auto;display:inline-block'
+                            ]) .
+                            Html::dropDownList("actuality[$i][to]", $to, $months, [
+                                'class'=>'form-select to-month form-select-sm',
+                                'prompt'=>'до',
+                                'data-actuality' => $actuality ? 1 : 0,
+                                'style' => 'width:auto;display:inline-block'
+                            ]),
+                            ['class'=>'d-flex align-items-center']
+                        ) . $clearBtn ;
+                    $inputs .= '</div>';
+                    return $inputs;
+                }
+            ],
+            [
+                'label' => 'Склад NN',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'width:60px; text-align:center;'],
+                'value' => function ($m, $k, $i) use ($filter){
+                    return Html::checkbox("actuality[$i][warehouse_nn]", false, [
+
+                    ]);
+                }
+            ],
+            [
+                'label' => 'Склад MSK',
+                'format' => 'raw',
+                'contentOptions' => ['style'=>'width:60px; text-align:center;'],
+                'value' => function ($m, $k, $i) use ($filter){
+                    return Html::checkbox("actuality[$i][warehouse_msk]", false, [
+
+                    ]);
+                }
+            ],
+            [
+                'label' => 'Поставщик/Плантация',
+                'format' => 'text',
+                'contentOptions' => ['style'=>'min-width:150px;'],
+                'value' => function ($m) {
+                    return '–';
+                }
+            ],
+        ],
+    ]); ?>
+
+
+
+    <?php ActiveForm::end(); ?>
+    <script>
+        window.productActualityConfig = {
+            months: <?= json_encode($months, JSON_UNESCAPED_UNICODE) ?>
+        };
+    </script>
+
+</div>
+
diff --git a/erp24/views/matrix-bouquet-actuality/update.php b/erp24/views/matrix-bouquet-actuality/update.php
new file mode 100644 (file)
index 0000000..f4fb8c3
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+use yii\helpers\Html;
+
+/** @var yii\web\View $this */
+/** @var yii_app\records\MatrixBouquetActuality $model */
+
+$this->title = 'Update Matrix Bouquet Actuality: ' . $model->id;
+$this->params['breadcrumbs'][] = ['label' => 'Matrix Bouquet Actualities', 'url' => ['index']];
+$this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]];
+$this->params['breadcrumbs'][] = 'Update';
+?>
+<div class="matrix-bouquet-actuality-update">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <?= $this->render('_form', [
+        'model' => $model,
+    ]) ?>
+
+</div>
diff --git a/erp24/views/matrix-bouquet-actuality/view.php b/erp24/views/matrix-bouquet-actuality/view.php
new file mode 100644 (file)
index 0000000..72107cb
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+use yii\helpers\Html;
+use yii\widgets\DetailView;
+
+/** @var yii\web\View $this */
+/** @var yii_app\records\MatrixBouquetActuality $model */
+
+$this->title = $model->id;
+$this->params['breadcrumbs'][] = ['label' => 'Matrix Bouquet Actualities', 'url' => ['index']];
+$this->params['breadcrumbs'][] = $this->title;
+\yii\web\YiiAsset::register($this);
+?>
+<div class="matrix-bouquet-actuality-view">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <p>
+        <?= Html::a('Update', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?>
+        <?= Html::a('Delete', ['delete', 'id' => $model->id], [
+            'class' => 'btn btn-danger',
+            'data' => [
+                'confirm' => 'Are you sure you want to delete this item?',
+                'method' => 'post',
+            ],
+        ]) ?>
+    </p>
+
+    <?= DetailView::widget([
+        'model' => $model,
+        'attributes' => [
+            'id',
+            'guid',
+            'bouquet_id',
+            'date_from',
+            'date_to',
+            'is_archive',
+            'created_at',
+            'updated_at',
+            'created_by',
+            'updated_by',
+        ],
+    ]) ?>
+
+</div>