--- /dev/null
+<?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.');
+ }
+}
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;
}
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();
+ }
}
--- /dev/null
+<?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' => 'ИД редактировавшего',
+ ];
+ }
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
+
--- /dev/null
+<?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>
--- /dev/null
+<?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>