]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
ERP-302 Редактирование букета
authormarina <m.zozirova@gmail.com>
Fri, 21 Feb 2025 13:37:07 +0000 (16:37 +0300)
committermarina <m.zozirova@gmail.com>
Fri, 21 Feb 2025 13:37:07 +0000 (16:37 +0300)
erp24/controllers/BouquetController.php
erp24/records/BouquetComposition.php
erp24/records/BouquetCompositionMatrixTypeHistory.php
erp24/records/BouquetCompositionProducts.php
erp24/records/BouquetForecast.php
erp24/views/bouquet/create.php
erp24/web/js/bouquet/bouquet.js
erp24/widgets/DualList.php

index 4a8ff53ba0f67f6eb5436cf2922ee49174e6a3e0..7a3d3b3b17d878ecb8ce2e40b9c488d0d29f9f04 100644 (file)
@@ -4,55 +4,29 @@ namespace app\controllers;
 
 use Exception;
 use Yii;
-use yii\helpers\ArrayHelper;
 use yii\helpers\Url;
 use yii\web\Controller;
 use yii\web\NotFoundHttpException;
 use yii\web\Response;
-use yii\web\UploadedFile;
 use yii_app\helpers\DataHelper;
-use yii_app\records\BouquetComposition;
-use yii_app\records\BouquetCompositionMatrixTypeHistory;
-use yii_app\records\BouquetCompositionProducts;
-use yii_app\records\BouquetForecast;
-use yii_app\records\CityStore;
-use yii_app\records\Files;
-use yii_app\records\MatrixType;
-use yii_app\records\Products1c;
-use yii_app\records\Products1cNomenclature;
-use yii_app\records\StoreType;
-use yii_app\services\FileService;
+use yii_app\records\{BouquetComposition,
+    BouquetCompositionProducts,
+    BouquetForecast,
+    CityStore,
+    Files,
+    MatrixType,
+    StoreType};
 
-/**
- * Контроллер для управления букетами и их составами.
- */
 class BouquetController extends Controller
 {
     public function actionIndex()
     {
-        $query = BouquetComposition::find()->orderBy('id desc');
         $request = Yii::$app->request;
-        $year = $request->get('year');
-        $month = $request->get('month');
-        $matrixTypeId = $request->get('matrix_type_id');
+        $query = BouquetComposition::find()
+            ->orderBy(['id' => SORT_DESC])
+            ->groupBy('bouquet_composition.id');
 
-        if ($year || $month) {
-            $query->joinWith('bouquetForecast as bf');
-        }
-        if ($year) {
-            $query->andWhere(['bf.year' => $year]);
-        }
-        if ($month) {
-            $query->andWhere(['bf.month' => $month]);
-        }
-
-        if ($matrixTypeId) {
-            $query->leftJoin('bouquet_composition_matrix_type_history mth', 'mth.bouquet_id = bouquet_composition.id');
-            $query->andWhere(['mth.matrix_type_id' => $matrixTypeId, 'mth.is_active' => BouquetCompositionMatrixTypeHistory::IS_ACTIVE]);
-        }
-
-        $query->groupBy('bouquet_composition.id')
-            ->orderBy('bouquet_composition.id desc');
+        BouquetComposition::applyFilters($query, $request);
 
         $dataProvider = new \yii\data\ActiveDataProvider([
             'query' => $query,
@@ -61,373 +35,117 @@ class BouquetController extends Controller
 
         return $this->render('index', [
             'dataProvider' => $dataProvider,
-            'matrix_type' => $matrixTypeId ? MatrixType::findOne($matrixTypeId)?->name : null,
+            'matrix_type' => $request->get('matrix_type_id')
+                ? MatrixType::findOne($request->get('matrix_type_id'))?->name
+                : null,
         ]);
     }
 
     public function actionCreate()
     {
-        $model = new BouquetComposition();
-        $errors = [];
-        $flash = null;
-
-        if (Yii::$app->request->isPost) {
-            $data = Yii::$app->request->post();
-            $model->guid = DataHelper::createGuidMy('07');
-            $model->load($data);
-            if ($model->save()) {
-                $month = $data['month'];
-                $year = $data['year'];
-
-                if ($data['matrix_type_id']) {
-                    if (!empty(BouquetCompositionMatrixTypeHistory::setData($data['matrix_type_id'], $model->id))) {
-                        $errors = array_merge($errors, BouquetCompositionMatrixTypeHistory::setData($data['matrix_type_id'], $model->id));
-                    }
-                }
-
-                $model->photo_bouquet = UploadedFile::getInstances($model, 'photo_bouquet');
-                if (!empty($model->photo_bouquet)) {
-                    Files::deleteAll(['file_type' => 'image', 'entity_id' => $model->id, 'entity' => BouquetComposition::PHOTO_BOUQUET]);
-                    foreach ($model->photo_bouquet as $photo) {
-                        FileService::saveUploadedFile($photo, BouquetComposition::PHOTO_BOUQUET, $model->id);
-                    }
-                }
-
-                $model->video_presentation = UploadedFile::getInstances($model, 'video_presentation');
-                if (!empty($model->video_presentation)) {
-                    Files::deleteAll(['file_type' => 'video', 'entity_id' => $model->id, 'entity' => BouquetComposition::VIDEO_PRESENTATION]);
-                    foreach ($model->video_presentation as $video) {
-                        FileService::saveUploadedFile($video, BouquetComposition::VIDEO_PRESENTATION, $model->id);
-                    }
-                }
-
-                $model->video_build_process = UploadedFile::getInstances($model, 'video_build_process');
-                if (!empty($model->video_build_process)) {
-                    Files::deleteAll(['file_type' => 'video', 'entity_id' => $model->id, 'entity' => BouquetComposition::VIDEO_BUILD_PROCESS]);
-                    foreach ($model->video_build_process as $video) {
-                        FileService::saveUploadedFile($video, BouquetComposition::VIDEO_BUILD_PROCESS, $model->id);
-                    }
-                }
-                if (!empty($data['BouquetForecast']['type_sales_value'])) {
-                    $salesData = $data['BouquetForecast']['type_sales_value'];
-                    $errors = [];
-
-                    if (!empty($salesData['offline'])) {
-                        $offlineErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['offline'], BouquetForecast::OFFLINE_STORES);
-                        if (!empty($offlineErrors)) {
-                            $errors = array_merge($errors, $offlineErrors);
-                        }
-                    }
-
-                    if (!empty($salesData['online'])) {
-                        $onlineErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['online'], BouquetForecast::ONLINE_STORES);
-                        if (!empty($onlineErrors)) {
-                            $errors = array_merge($errors, $onlineErrors);
-                        }
-                    }
-
-                    if (!empty($salesData['marketplace'])) {
-                        $marketplaceErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['marketplace'], BouquetForecast::MARKETPLACE);
-                        if (!empty($marketplaceErrors)) {
-                            $errors = array_merge($errors, $marketplaceErrors);
-                        }
-                    }
-
-                    if (!empty($errors)) {
-                        Yii::$app->session->setFlash('danger', implode(' ', array_map('json_encode', $errors)));
-                    }
-                }
-
-                if (array_key_exists('products_quantity', $data)) {
-                    BouquetCompositionProducts::deleteAll(['bouquet_id' => $model->id]);
-                    $bouquetProducts = Yii::$app->request->post('products_quantity');
-                    foreach ($bouquetProducts as $key => $value) {
-                        $product = new BouquetCompositionProducts([
-                            'bouquet_id' => $model->id,
-                            'product_guid' => $key,
-                            'count' => $value
-                        ]);
-                        if (!$product->save()) {
-                            $errors = array_merge($errors, $product->getErrors());
-                        }
-                    }
-                }
-                Yii::$app->session->setFlash('success', 'Данные успешно сохранены');
-                return $this->redirect(['view', 'id' => $model->id]);
-            } else {
-                $errors = $model->getErrors();
-            }
-        }
-
-        $availableItems = ArrayHelper::map(
-            Products1c::find()
-                ->where([
-                    'view' => Products1c::IS_VISIBLE,
-                    'tip' => Products1c::TYPE_PRODUCTS
-                ])
-                ->all(),
-            'id',
-            'name'
-        );
-        $storesTypeList = BouquetForecast::getStoresList(null, BouquetForecast::OFFLINE_STORES, StoreType::class, []);
-        $marketplaceList = BouquetForecast::getStoresList(null, BouquetForecast::MARKETPLACE, CityStore::class, ['visible' => CityStore::IS_VISIBLE]);
-        $onlineStoresList = BouquetForecast::getStoresList(null, BouquetForecast::ONLINE_STORES, CityStore::class, ['visible' => CityStore::IS_VISIBLE]);
-
-        if (!empty($errors)) {
-            $flatErrors = array_merge([], ...array_values($errors));
-            $flashMessage = !empty($flatErrors) ? implode(' ', $flatErrors) : 'Произошла ошибка.';
-            Yii::$app->session->setFlash('danger', $flashMessage);
-        }
-
-        return $this->render('create', [
-            'onlineStoresList' => $onlineStoresList,
-            'marketplaceList' => $marketplaceList,
-            'storesTypeList' => $storesTypeList,
-            'availableItems' => $availableItems,
-            'flash' => $flash
-        ]);
-
+        return $this->handleCreateOrUpdate(new BouquetComposition());
     }
 
     public function actionView($id)
     {
-        $model = BouquetComposition::findOne($id);
-        $errors = [];
-        $flash = null;
-
+        $model = BouquetComposition::findModel($id);
         if (Yii::$app->request->isPost) {
-            $data = Yii::$app->request->post();
-            $model->load($data);
-            if ($model->save()) {
-
-                $month = $data['month'];
-                $year = $data['year'];
-
-                if ($data['matrix_type_id']) {
-                    if (!empty(BouquetCompositionMatrixTypeHistory::setData($data['matrix_type_id'], $model->id))) {
-                        $errors = array_merge($errors, BouquetCompositionMatrixTypeHistory::setData($data['matrix_type_id'], $model->id));
-                    }
-                }
-
-                $model->photo_bouquet = UploadedFile::getInstances($model, 'photo_bouquet');
-                if (!empty($model->photo_bouquet)) {
-                    Files::deleteAll(['file_type' => 'image', 'entity_id' => $model->id, 'entity' => BouquetComposition::PHOTO_BOUQUET]);
-                    foreach ($model->photo_bouquet as $photo) {
-                        FileService::saveUploadedFile($photo, BouquetComposition::PHOTO_BOUQUET, $model->id);
-                    }
-                }
-
-                $model->video_presentation = UploadedFile::getInstances($model, 'video_presentation');
-                if (!empty($model->video_presentation)) {
-                    Files::deleteAll(['file_type' => 'video', 'entity_id' => $model->id, 'entity' => BouquetComposition::VIDEO_PRESENTATION]);
-                    foreach ($model->video_presentation as $video) {
-                        FileService::saveUploadedFile($video, BouquetComposition::VIDEO_PRESENTATION, $model->id);
-                    }
-                }
-
-                $model->video_build_process = UploadedFile::getInstances($model, 'video_build_process');
-                if (!empty($model->video_build_process)) {
-                    Files::deleteAll(['file_type' => 'video', 'entity_id' => $model->id, 'entity' => BouquetComposition::VIDEO_BUILD_PROCESS]);
-                    foreach ($model->video_build_process as $video) {
-                        FileService::saveUploadedFile($video, BouquetComposition::VIDEO_BUILD_PROCESS, $model->id);
-                    }
-                }
-
-
-                if (!empty($data['BouquetForecast']['type_sales_value'])) {
-                    $salesData = $data['BouquetForecast']['type_sales_value'];
-                    $errors = [];
-
-                    if (!empty($salesData['offline'])) {
-                        $offlineErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['offline'], BouquetForecast::OFFLINE_STORES);
-                        if (!empty($offlineErrors)) {
-                            $errors = array_merge($errors, $offlineErrors);
-                        }
-                    }
-
-                    if (!empty($salesData['online'])) {
-                        $onlineErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['online'], BouquetForecast::ONLINE_STORES);
-                        if (!empty($onlineErrors)) {
-                            $errors = array_merge($errors, $onlineErrors);
-                        }
-                    }
-
-                    if (!empty($salesData['marketplace'])) {
-                        $marketplaceErrors = BouquetForecast::processSalesData($model->id, $year, $month, $salesData['marketplace'], BouquetForecast::MARKETPLACE);
-                        if (!empty($marketplaceErrors)) {
-                            $errors = array_merge($errors, $marketplaceErrors);
-                        }
-                    }
-
-                    if (!empty($errors)) {
-                        Yii::$app->session->setFlash('danger', implode(' ', array_map('json_encode', $errors)));
-                    }
-                }
-
-                Yii::$app->session->setFlash('success', 'Данные успешно сохранены');
-            } else {
-                $errors = $model->getErrors();
-            }
+            return $this->handleCreateOrUpdate($model);
         }
 
+        return $this->renderView($model);
+    }
 
-        $photoFiles = Files::find()->where(['entity_id' => $id, 'file_type' => 'image', 'entity' => BouquetComposition::PHOTO_BOUQUET])->all();
-        $videoFiles = Files::find()->where(['entity_id' => $id, 'file_type' => 'image', 'entity' => BouquetComposition::VIDEO_PRESENTATION])->all();
-        $processFiles = Files::find()->where(['entity_id' => $id, 'file_type' => 'image', 'entity' => BouquetComposition::VIDEO_BUILD_PROCESS])->all();
-
-        $photoUrls = array_map(fn($file) => Url::to([$file->url], true), $photoFiles);
-        $videoUrls = array_map(fn($file) => Url::to([$file->url]), $videoFiles);
-        $processUrls = array_map(fn($file) => Url::to([$file->url]), $processFiles);
-
-        $bouquetCompositionProducts = BouquetCompositionProducts::find()->andWhere(['bouquet_id' => $id])->all();
-        $storesTypeList = BouquetForecast::getStoresList($id, BouquetForecast::OFFLINE_STORES, StoreType::class, []);
-        $marketplaceList = BouquetForecast::getStoresList($id, BouquetForecast::MARKETPLACE, CityStore::class, ['visible' => CityStore::IS_VISIBLE]);
-        $onlineStoresList = BouquetForecast::getStoresList($id, BouquetForecast::ONLINE_STORES, CityStore::class, ['visible' => CityStore::IS_VISIBLE]);
-
+    public function actionUpdate($id)
+    {
+        $model = BouquetComposition::findModel($id);
 
-        if (!empty($errors)) {
-            $flatErrors = array_merge([], ...array_values($errors));
-            $flashMessage = !empty($flatErrors) ? implode(' ', $flatErrors) : 'Произошла ошибка.';
-            Yii::$app->session->setFlash('danger', $flashMessage);
+        if (Yii::$app->request->isPost && $products = Yii::$app->request->post('products_quantity')) {
+            BouquetCompositionProducts::updateProducts($model->id, $products);
+            return $this->redirect(['view', 'id' => $id]);
         }
 
-
-        return $this->render('view', [
+        return $this->render('update', [
             'model' => $model,
-            'onlineStoresList' => $onlineStoresList,
-            'bouquetCompositionProducts' => $bouquetCompositionProducts,
-            'marketplaceList' => $marketplaceList,
-            'storesTypeList' => $storesTypeList,
-            'photoUrls' => $photoUrls,
-            'photoFiles' => $photoFiles,
-            'videoUrls' => $videoUrls,
-            'processUrls' => $processUrls,
-            'flash' => $flash
-        ]);
-    }
-
-
-    /**
-     * Сохранение записи в таблицу files
-     */
-    private function saveFileRecord($bouquetId, $filePath, $type)
-    {
-        $file = new Files([
-            'url' => $filePath,
-            'file_type' => $type,
-            'created_at' => time(),
-            'entity_id' => $bouquetId,
-            'entity' => 'bouquet/photo',
+            'selectedItems' => BouquetCompositionProducts::getSelectedItems($model->id),
+            'availableItems' => BouquetCompositionProducts::getAvailableItems($model->isNewRecord, $model->id),
         ]);
-        $file->save();
     }
 
-    public function actionUpdate($id)
+    private function handleCreateOrUpdate(BouquetComposition $model)
     {
-        $model = BouquetComposition::findOne($id);
-
-        if (!$model) {
-            throw new NotFoundHttpException('Букет не найден.');
+        $request = Yii::$app->request;
+        if (!$request->isPost) {
+            return $this->renderCreate($model);
         }
 
-        if (Yii::$app->request->isPost) {
-            try {
-                if (Yii::$app->request->post('products_quantity')) {
-                    BouquetCompositionProducts::deleteAll(['bouquet_id' => $model->id]);
-                    $bouquetProducts = Yii::$app->request->post('products_quantity');
-                    foreach ($bouquetProducts as $key => $value) {
-                        $product = new BouquetCompositionProducts([
-                            'bouquet_id' => $id,
-                            'product_guid' => $key,
-                            'count' => $value
-                        ]);
-                        $product->save();
-                    }
+        $data = $request->post();
+        $isNew = $model->isNewRecord;
 
-                    return $this->redirect(['view', 'id' => $id]);
-                }
-            } catch (Exception $exception) {
-                throw new NotFoundHttpException($exception->getMessage());
-            }
+        if ($isNew) {
+            $model->guid = DataHelper::createGuidMy('07');
         }
 
-        $products = BouquetCompositionProducts::find()
-            ->where(['bouquet_id' => $model->id])
-            ->with('product')
-            ->all();
+        if (isset($data['matrix_type_id'])) {
+            $model->matrix_type_id = (int) $data['matrix_type_id'];
+        }
 
-        $selectedItems = array_map(fn($product) => [
-            'id' => $product->product_guid,
-            'count' => $product->count,
-            'text' => $product->product->name ?? ''
-        ], $products);
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            if (!$model->load($data) || !$model->validate() || !$model->save()) {
+                throw new Exception('Ошибка при сохранении букета');
+            }
 
-        $selectedProductIds = array_column($selectedItems, 'id');
+            $model->processRelations($data);
+            $transaction->commit();
+            Yii::$app->session->setFlash('success', 'Данные успешно сохранены');
+            return $this->redirect(['view', 'id' => $model->id]);
+        } catch (Exception $e) {
+            $transaction->rollBack();
+            $model->setErrorFlash($model->getErrors() ?: [$e->getMessage()]);
+            return $isNew ? $this->renderCreate($model) : $this->renderView($model);
+        }
+    }
 
-        $availableItems = ArrayHelper::map(
-            Products1c::find()
-                ->where([
-                    'view' => Products1c::IS_VISIBLE,
-                    'tip' => Products1c::TYPE_PRODUCTS
-                ])
-                ->andWhere(['not in', 'id', $selectedProductIds])
-                ->all(),
-            'id',
-            'name'
-        );
 
-        return $this->render('update', [
-            'model' => $model,
-            'selectedItems' => $selectedItems,
-            'availableItems' => $availableItems,
+    private function renderCreate(BouquetComposition $model)
+    {
+        return $this->render('create', [
+            'onlineStoresList' => BouquetForecast::getStoresList(null, BouquetForecast::ONLINE_STORES, CityStore::class, ['visible' => CityStore::IS_VISIBLE]),
+            'marketplaceList' => BouquetForecast::getStoresList(null, BouquetForecast::MARKETPLACE, CityStore::class, ['visible' => CityStore::IS_VISIBLE]),
+            'storesTypeList' => BouquetForecast::getStoresList(null, BouquetForecast::OFFLINE_STORES, StoreType::class, []),
+            'availableItems' => BouquetCompositionProducts::getAvailableItems($model->isNewRecord, $model->id),
         ]);
     }
 
-    public function actionUpload()
+    private function renderView(BouquetComposition $model)
     {
-        $model = new BouquetComposition();
+        $files = $model->getFiles();
 
-        if (Yii::$app->request->isPost) {
-            $model->photo_id = UploadedFile::getInstance($model, 'photo_id');
-            if ($model->validate() && $model->upload()) {
-                // Логика для сохранения
-            }
-        }
-
-        return $this->render('upload', ['model' => $model]);
+        return $this->render('view', [
+            'model' => $model,
+            'onlineStoresList' => BouquetForecast::getStoresList($model->id, BouquetForecast::ONLINE_STORES, CityStore::class, ['visible' => CityStore::IS_VISIBLE]),
+            'marketplaceList' => BouquetForecast::getStoresList($model->id, BouquetForecast::MARKETPLACE, CityStore::class, ['visible' => CityStore::IS_VISIBLE]),
+            'storesTypeList' => BouquetForecast::getStoresList($model->id, BouquetForecast::OFFLINE_STORES, StoreType::class, []),
+            'bouquetCompositionProducts' => BouquetCompositionProducts::getCompositionProducts($model->id),
+            'photoUrls' => array_map(fn($file) => Url::to([$file->url], true), $files['photo']),
+            'photoFiles' => $files['photo'],
+            'videoUrls' => array_map(fn($file) => Url::to([$file->url]), $files['video']),
+            'processUrls' => array_map(fn($file) => Url::to([$file->url]), $files['process']),
+        ]);
     }
 
-    public function actionGetList()
+    public function actionGetCalculates()
     {
-        \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-
-        $request = Yii::$app->request->post();
-
-        $conditions = array_filter([
-            'type-num' => $request['type-num'] ?? null,
-            'color' => $request['color'] ?? null,
-            'species' => $request['species'] ?? null,
-            'category' => $request['category'] ?? null,
-            'size' => $request['size'] ?? null,
-        ]);
-
-        $subqueries = [];
-        foreach ($conditions as $field => $value) {
-            $subqueries[] = Products1cNomenclature::find()->select('id')->where([$field => $value]);
-        }
-
-        $query = Products1c::find()->where([
-            'tip' => Products1c::TYPE_PRODUCTS,
-            'view' => Products1c::IS_VISIBLE,
-        ]);
-
-        if (!empty($subqueries)) {
-            foreach ($subqueries as $subquery) {
-                $query->andWhere(['in', 'id', $subquery]);
-            }
-        }
+        Yii::$app->response->format = Response::FORMAT_JSON;
+        $data = json_decode(Yii::$app->request->getRawBody(), true);
+        $model = new BouquetComposition();
 
-        return ArrayHelper::map($query->asArray()->all(), 'id', 'name');
+        return [
+            'selfcost' => round($model->getSelfCost($data), 2),
+            'cost' => round($model->getCost($data), 2),
+            'markup' => $model->getMarkUp($data),
+        ];
     }
 
     public function actionGetSalesData()
@@ -454,18 +172,5 @@ class BouquetController extends Controller
         ];
     }
 
-    public function actionGetCalculates()
-    {
-        Yii::$app->response->format = Response::FORMAT_JSON;
-        $data = json_decode(Yii::$app->request->getRawBody(), true);
-        $model = new BouquetComposition();
-
-        return [
-            'selfcost' => round($model->getSelfCost($data), 2),
-            'cost' => round($model->getCost($data), 2),
-            'markup' => $model->getMarkUp($data),
-        ];
-    }
-
 
-}
+}
\ No newline at end of file
index 12320e4d5910318f28747037791f958ffbf465c4..30cc9b94ca353542043797811ebc05b0d99d9923 100644 (file)
@@ -2,12 +2,14 @@
 
 namespace yii_app\records;
 
+use Exception;
 use Yii;
 use yii\behaviors\BlameableBehavior;
 use yii\behaviors\TimestampBehavior;
 use yii\db\ActiveRecord;
 use yii\db\Expression;
-use yii_app\records\BouquetCompositionProducts;
+use yii\web\UploadedFile;
+use yii_app\services\FileService;
 
 /**
  * This is the model class for table "erp24.bouquet_composition".
@@ -21,23 +23,40 @@ use yii_app\records\BouquetCompositionProducts;
  * @property int|null $updated_by
  *
  * @property BouquetCompositionProducts[] $bouquetCompositionProducts
+ * @property BouquetCompositionMatrixTypeHistory $matrixType
+ * @property Files $presentation
+ * @property Files $buildProcess
+ * @property BouquetForecast[] $bouquetForecast
+ *
+ * @property UploadedFile[] $photo_bouquet
+ * @property UploadedFile[] $video_presentation
+ * @property UploadedFile[] $video_build_process
  */
 class BouquetComposition extends ActiveRecord
 {
-    public $photo_bouquet;
-    public $video_presentation;
-    public $video_build_process;
-
+    public const PHOTO_TYPE = 'image';
+    public const VIDEO_TYPE = 'video';
     public const PHOTO_BOUQUET = 'bouquet/photo_bouquet';
     public const VIDEO_PRESENTATION = 'bouquet/video_presentation';
     public const VIDEO_BUILD_PROCESS = 'bouquet/video_build_process';
 
-    public static function tableName()
+    public $photo_bouquet;
+    public $matrix_type_id;
+    public $video_presentation;
+    public $video_build_process;
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName(): string
     {
         return 'erp24.bouquet_composition';
     }
 
-    public function behaviors()
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors(): array
     {
         return [
             [
@@ -54,29 +73,33 @@ class BouquetComposition extends ActiveRecord
         ];
     }
 
-
-    public function rules()
+    /**
+     * {@inheritdoc}
+     */
+    public function rules(): array
     {
         return [
-            [['name'], 'required', 'message' => 'Поле "Название" не может быть пустым.'],
-            [['created_by', 'updated_by'], 'integer'],
+            [['matrix_type_id'], 'required', 'message' => 'Поле "Тип Матрицы" не может быть пустым'],
+            [['name'], 'required', 'message' => 'Поле "Название" не может быть пустым'],
+            [['created_by', 'updated_by'], 'integer', 'message' => '{attribute} должен быть целым числом'],
             [['created_at', 'updated_at'], 'safe'],
-            [['guid', 'name'], 'string', 'max' => 255],
-
+            [['guid', 'name'], 'string', 'max' => 255, 'tooLong' => '{attribute} не должен превышать 255 символов'],
             [['photo_bouquet'], 'file',
                 'extensions' => 'jpg, jpeg, png, gif',
                 'maxFiles' => 10,
-                'message' => 'Допустимые форматы: jpg, jpeg, png, gif. Максимум 10 файлов.'
+                'message' => 'Допустимые форматы: jpg, jpeg, png, gif. Максимум 10 файлов',
             ],
-
             [['video_presentation', 'video_build_process'], 'file',
                 'extensions' => 'mkv, mov, avi, mp4',
-                'message' => 'Допустимые форматы видео: mkv, mov, avi, mp4.'
+                'message' => 'Допустимые форматы видео: mkv, mov, avi, mp4',
             ],
         ];
     }
 
-    public function attributeLabels()
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels(): array
     {
         return [
             'id' => 'ID',
@@ -89,33 +112,210 @@ class BouquetComposition extends ActiveRecord
         ];
     }
 
+    /**
+     * Поиск модели по ID.
+     *
+     * @param int $id ID букета
+     * @return self
+     * @throws \yii\web\NotFoundHttpException Если букет не найден
+     */
+    public static function findModel(int $id): self
+    {
+        if ($model = static::findOne($id)) {
+            return $model;
+        }
+        throw new \yii\web\NotFoundHttpException('Букет не найден.');
+    }
+
+    /**
+     * Применяет фильтры к запросу букетов.
+     *
+     * @param \yii\db\ActiveQuery $query Запрос для фильтрации
+     * @param \yii\web\Request $request HTTP-запрос с параметрами фильтрации
+     */
+    public static function applyFilters($query, $request): void
+    {
+        $year = $request->get('year');
+        $month = $request->get('month');
+
+        if ($year || $month) {
+            $query->joinWith('bouquetForecast as bf');
+            if ($year) {
+                $query->andWhere(['bf.year' => $year]);
+            }
+            if ($month) {
+                $query->andWhere(['bf.month' => $month]);
+            }
+        }
+
+        if ($matrixTypeId = $request->get('matrix_type_id')) {
+            $query->leftJoin(
+                'erp24.bouquet_composition_matrix_type_history mth', // Добавили схему
+                'mth.bouquet_id = bouquet_composition.id'
+            )->andWhere([
+                'mth.matrix_type_id' => $matrixTypeId,
+                'mth.is_active' => true // Используем true без константы
+            ]);
+        }
+    }
+
+    /**
+     * Обрабатывает связанные данные букета (матрицы, файлы, продажи, продукты).
+     *
+     * @param array $data Данные из формы
+     */
+    public function processRelations(array $data): void
+    {
+        if (!$this->id) {
+            throw new Exception("ID букета не установлен при обработке связей.");
+        }
+
+        if (!isset($data['matrix_type_id'])) {
+            throw new Exception('Тип Матрицы обязателен.');
+        }
+
+        BouquetCompositionMatrixTypeHistory::updateMatrixType(
+            $this->id,
+            isset($data['matrix_type_id']) ? (int) $data['matrix_type_id'] : null
+        );
+
+        $this->processFiles();
+
+        // Проверяем, передаётся ли id букета
+        if (!empty($data)) {
+            BouquetForecast::processSalesData($this->id, $data);
+        }
+
+        if (!empty($data['products_quantity'])) {
+            BouquetCompositionProducts::updateProducts($this->id, $data['products_quantity']);
+        }
+    }
+
+    /**
+     * Обрабатывает загрузку файлов (фото и видео).
+     */
+    public function processFiles(): void
+    {
+        $fileTypes = [
+            'photo_bouquet' => [self::PHOTO_TYPE, self::PHOTO_BOUQUET],
+            'video_presentation' => [self::VIDEO_TYPE, self::VIDEO_PRESENTATION],
+            'video_build_process' => [self::VIDEO_TYPE, self::VIDEO_BUILD_PROCESS],
+        ];
+
+        foreach ($fileTypes as $attribute => [$type, $entity]) {
+            $files = UploadedFile::getInstances($this, $attribute);
+            if ($files) {
+                Files::deleteAll(['file_type' => $type, 'entity_id' => $this->id, 'entity' => $entity]);
+                foreach ($files as $file) {
+                    FileService::saveUploadedFile($file, $entity, $this->id);
+                }
+            }
+        }
+    }
+
+    /**
+     * Возвращает связанные файлы букета.
+     *
+     * @return array Ассоциативный массив файлов (photo, video, process)
+     */
+    public function getFiles(): array
+    {
+        return [
+            'photo' => Files::find()->where(['entity_id' => $this->id, 'file_type' => self::PHOTO_TYPE, 'entity' => self::PHOTO_BOUQUET])->all(),
+            'video' => Files::find()->where(['entity_id' => $this->id, 'file_type' => self::VIDEO_TYPE, 'entity' => self::VIDEO_PRESENTATION])->all(),
+            'process' => Files::find()->where(['entity_id' => $this->id, 'file_type' => self::VIDEO_TYPE, 'entity' => self::VIDEO_BUILD_PROCESS])->all(),
+        ];
+    }
+
+    /**
+     * Устанавливает сообщение об ошибке во флэш.
+     *
+     * @param array $errors Массив ошибок
+     */
+    public function setErrorFlash(array $errors): void
+    {
+        $message = 'Произошла ошибка.';
+
+        if (!empty($errors)) {
+            $flattenedErrors = [];
+
+            foreach ($errors as $error) {
+                if (is_array($error)) {
+                    $flattenedErrors = array_merge($flattenedErrors, $error);
+                } elseif (is_string($error)) {
+                    $flattenedErrors[] = $error;
+                }
+            }
+
+            $message = implode(' ', $flattenedErrors);
+        }
+
+        Yii::$app->session->setFlash('danger', $message);
+    }
+
+
+    /**
+     * Возвращает связанные продукты букета.
+     *
+     * @return \yii\db\ActiveQuery
+     */
     public function getBouquetCompositionProducts()
     {
         return $this->hasMany(BouquetCompositionProducts::class, ['bouquet_id' => 'id']);
     }
 
+    /**
+     * Возвращает активную запись истории типа матрицы.
+     *
+     * @return \yii\db\ActiveQuery
+     */
     public function getMatrixType()
     {
         return $this->hasOne(BouquetCompositionMatrixTypeHistory::class, ['bouquet_id' => 'id'])
             ->andWhere(['is_active' => BouquetCompositionMatrixTypeHistory::IS_ACTIVE]);
     }
 
+    /**
+     * Возвращает видео-презентацию букета.
+     *
+     * @return \yii\db\ActiveQuery
+     */
     public function getPresentation()
     {
         return $this->hasOne(Files::class, ['entity_id' => 'id'])
             ->andWhere(['entity' => self::VIDEO_PRESENTATION]);
     }
 
+    /**
+     * Возвращает видео процесса сборки букета.
+     *
+     * @return \yii\db\ActiveQuery
+     */
     public function getBuildProcess()
     {
         return $this->hasOne(Files::class, ['entity_id' => 'id'])
-        ->andWhere(['entity' => self::VIDEO_BUILD_PROCESS]);
+            ->andWhere(['entity' => self::VIDEO_BUILD_PROCESS]);
     }
 
-    public function getCost($data = null)
+    /**
+     * Возвращает прогнозы продаж букета.
+     *
+     * @return \yii\db\ActiveQuery
+     */
+    public function getBouquetForecast()
     {
-        $cost = 0;
+        return $this->hasMany(BouquetForecast::class, ['bouquet_id' => 'id']);
+    }
 
+    /**
+     * Рассчитывает стоимость букета.
+     *
+     * @param array|null $data Данные продуктов (опционально)
+     * @return float Стоимость
+     */
+    public function getCost(?array $data = null): float
+    {
+        $cost = 0;
         $compositionProducts = $this->bouquetCompositionProducts;
 
         if (!$compositionProducts) {
@@ -143,19 +343,34 @@ class BouquetComposition extends ActiveRecord
         return $cost;
     }
 
-    public function getMarkUp($data = null)
+    /**
+     * Рассчитывает наценку на букет.
+     *
+     * @param array|null $data Данные продуктов (опционально)
+     * @return string Наценка в формате "+X.XX% / +X.XX"
+     */
+    public function getMarkUp(?array $data = null): string
     {
-        if ($this->getSelfCost($data) == 0 || $this->getCost($data) == 0)
-            return  0;
+        $selfCost = $this->getSelfCost($data);
+        $cost = $this->getCost($data);
+
+        if ($selfCost == 0 || $cost == 0) {
+            return '0';
+        }
 
         return sprintf("+%.2f%% / +%.2f",
-            (self::getCost($data) / self::getSelfCost($data) - 1) * 100,
-            self::getCost($data) - self::getSelfCost($data)
+            ($cost / $selfCost - 1) * 100,
+            $cost - $selfCost
         );
     }
 
-
-    public function getSelfCost($data = null)
+    /**
+     * Рассчитывает себестоимость букета.
+     *
+     * @param array|null $data Данные продуктов (опционально)
+     * @return float Себестоимость
+     */
+    public function getSelfCost(?array $data = null): float
     {
         $selfCost = 0;
         $compositionProducts = $this->bouquetCompositionProducts;
@@ -166,9 +381,8 @@ class BouquetComposition extends ActiveRecord
                 return $selfCost;
             }
         }
-        
-        $productGuids  = array_filter(array_column($compositionProducts, 'product_guid'));
-        
+
+        $productGuids = array_filter(array_column($compositionProducts, 'product_guid'));
         if (empty($productGuids)) {
             return $selfCost;
         }
@@ -192,7 +406,6 @@ class BouquetComposition extends ActiveRecord
             }
 
             $prices = $pricesByProduct[$item['product_guid']] ?? [];
-
             $count = count($prices);
 
             if ($count == 0) {
@@ -211,25 +424,30 @@ class BouquetComposition extends ActiveRecord
         return $selfCost;
     }
 
-    public function getBouquetForecast()
+    /**
+     * Возвращает список доступных годов для выбора.
+     *
+     * @return array Массив годов в формате [год => год]
+     */
+    public static function getYears(): array
     {
-        return $this->hasMany(BouquetForecast::class, ['bouquet_id' => 'id']);
-    }
-
-    public static function getYears()
-    {
-        $currentYear = date('Y');
+        $currentYear = (int)date('Y');
         $years = range($currentYear - 5, $currentYear + 5);
         return array_combine($years, $years);
     }
 
-    public static function disabledButtons($isCreate = false)
+    /**
+     * Проверяет, должны ли кнопки быть отключены.
+     *
+     * @param bool $isCreate Флаг создания нового букета
+     * @return bool True, если кнопки должны быть отключены
+     */
+    public static function disabledButtons(bool $isCreate = false): bool
     {
+        return false;
         if ($isCreate) {
             return false;
         }
-
-        return date('d') > 10;
+        return (int)date('d') > 10;
     }
-
-}
+}
\ No newline at end of file
index 316aca9fa45cfa72973fcbf17cf1a2d3b6cf65c1..50d3c48157e8b6000eae11ee93f635e49ae01b61 100644 (file)
@@ -1,6 +1,8 @@
 <?php
+
 namespace yii_app\records;
 
+use Exception;
 use Yii;
 use yii\behaviors\BlameableBehavior;
 use yii\behaviors\TimestampBehavior;
@@ -15,7 +17,7 @@ use yii\db\Expression;
  * @property int|null $matrix_type_id Тип матрицы ИД
  * @property int|null $date_from Дата установки
  * @property int|null $date_to Дата изменения
- * @property boolean|true $is_active Активна ли запись
+ * @property bool $is_active Активна ли запись
  * @property string|null $created_at Дата создания
  * @property string|null $updated_at Дата обновления
  * @property int|null $created_by ID создателя записи
@@ -24,8 +26,20 @@ use yii\db\Expression;
 class BouquetCompositionMatrixTypeHistory extends ActiveRecord
 {
     public const IS_ACTIVE = true;
+    public const IS_INACTIVE = false;
 
-    public function behaviors()
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName(): string
+    {
+        return '{{%erp24.bouquet_composition_matrix_type_history}}';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors(): array
     {
         return [
             [
@@ -42,31 +56,23 @@ class BouquetCompositionMatrixTypeHistory extends ActiveRecord
         ];
     }
 
-
     /**
      * {@inheritdoc}
      */
-    public static function tableName()
-    {
-        return '{{%erp24.bouquet_composition_matrix_type_history}}';
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function rules()
+    public function rules(): array
     {
         return [
-            [['bouquet_id', 'matrix_type_id', 'created_by', 'updated_by'], 'integer'],
-            [['created_at', 'updated_at','date_to', 'date_from'], 'safe'],
-            [['is_active'], 'boolean']
+            [['bouquet_id', 'matrix_type_id', 'created_by', 'updated_by'], 'integer', 'message' => '{attribute} должен быть целым числом'],
+            [['date_from', 'date_to', 'created_at', 'updated_at'], 'safe'],
+            [['is_active'], 'boolean', 'message' => '{attribute} должен быть логическим значением'],
+            [['bouquet_id', 'matrix_type_id'], 'required', 'message' => '{attribute} обязателен для заполнения'],
         ];
     }
 
     /**
      * {@inheritdoc}
      */
-    public function attributeLabels()
+    public function attributeLabels(): array
     {
         return [
             'id' => 'ID',
@@ -82,23 +88,56 @@ class BouquetCompositionMatrixTypeHistory extends ActiveRecord
         ];
     }
 
-    public static function setData($value, $bouquetId) {
-        if (self::findOne(['bouquet_id' => $bouquetId])) {
-            BouquetCompositionMatrixTypeHistory::updateAll(['date_to' => date('Y-m-d H:i:s'), 'bouquet_id' => $bouquetId, 'date_from' => null, 'is_active' => self::IS_ACTIVE]);
+    /**
+     * Обновляет или создает запись истории типа матрицы для букета.
+     *
+     * @param int $bouquetId ID букета
+     * @param int|null $matrixTypeId ID типа матрицы
+     * @throws Exception Если сохранение не удалось
+     */
+    public static function updateMatrixType(int $bouquetId, ?int $matrixTypeId): void
+    {
+        if (!$matrixTypeId) {
+            return; // Если тип матрицы не указан, ничего не делаем
         }
 
-        $matrixHistoryType = new BouquetCompositionMatrixTypeHistory([
-            'bouquet_id' => $bouquetId,
-            'matrix_type_id' => $value,
-            'date_from' => new Expression('now()'),
-        ]);
-        if (!$matrixHistoryType->save()) {
-            return $matrixHistoryType->errors();
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            // Деактивируем текущую активную запись
+            static::updateAll(
+                [
+                    'date_to' => new Expression('NOW()'),
+                    'is_active' => self::IS_INACTIVE,
+                ],
+                [
+                    'bouquet_id' => $bouquetId,
+                    'is_active' => self::IS_ACTIVE,
+                ]
+            );
+
+            // Создаем новую запись
+            $matrixHistoryType = new self([
+                'bouquet_id' => $bouquetId,
+                'matrix_type_id' => $matrixTypeId,
+                'date_from' => new Expression('NOW()'),
+                'is_active' => self::IS_ACTIVE,
+            ]);
+
+            if (!$matrixHistoryType->save()) {
+                throw new Exception(implode(' ', $matrixHistoryType->getFirstErrors()));
+            }
+
+            $transaction->commit();
+        } catch (Exception $e) {
+            $transaction->rollBack();
+            throw $e;
         }
     }
 
     /**
-     * Отношение к букету
+     * Отношение к букету.
+     *
+     * @return \yii\db\ActiveQuery
      */
     public function getBouquet()
     {
@@ -106,10 +145,12 @@ class BouquetCompositionMatrixTypeHistory extends ActiveRecord
     }
 
     /**
-     * Отношение к типу матрицы
+     * Отношение к типу матрицы.
+     *
+     * @return \yii\db\ActiveQuery
      */
     public function getMatrixType()
     {
         return $this->hasOne(MatrixType::class, ['id' => 'matrix_type_id']);
     }
-}
+}
\ No newline at end of file
index 88c98581babb4ddb607515b6933cfb8935f7ca82..14179d593f7569b2d92710396e19078c7111e19b 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 namespace yii_app\records;
 
 use Yii;
@@ -6,29 +7,37 @@ use yii\behaviors\BlameableBehavior;
 use yii\behaviors\TimestampBehavior;
 use yii\db\ActiveRecord;
 use yii\db\Expression;
+use yii\helpers\ArrayHelper;
 
 /**
  * This is the model class for table "erp24.bouquet_composition_products".
  *
  * @property int $id
- * @property int $bouquet_id
- * @property string $product_guid
- * @property float|null $count
- * @property string|null $created_at
- * @property string|null $updated_at
- * @property int|null $created_by
- * @property int|null $updated_by
+ * @property int $bouquet_id ID букета
+ * @property string $product_guid GUID продукта
+ * @property float|null $count Количество продукта
+ * @property string|null $created_at Дата создания
+ * @property string|null $updated_at Дата обновления
+ * @property int|null $created_by ID создателя записи
+ * @property int|null $updated_by ID обновителя записи
  *
  * @property BouquetComposition $bouquet
+ * @property Products1c $product
  */
 class BouquetCompositionProducts extends ActiveRecord
 {
-    public static function tableName()
+    /**
+     * {@inheritdoc}
+     */
+    public static function tableName(): string
     {
         return 'erp24.bouquet_composition_products';
     }
 
-    public function behaviors()
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors(): array
     {
         return [
             [
@@ -45,20 +54,25 @@ class BouquetCompositionProducts extends ActiveRecord
         ];
     }
 
-
-    public function rules()
+    /**
+     * {@inheritdoc}
+     */
+    public function rules(): array
     {
         return [
-            [['bouquet_id', 'product_guid'], 'required'],
-            [['bouquet_id', 'created_by', 'updated_by'], 'integer'],
-            [['count'], 'number'],
+            [['bouquet_id', 'product_guid'], 'required', 'message' => '{attribute} обязателен для заполнения'],
+            [['bouquet_id', 'created_by', 'updated_by'], 'integer', 'message' => '{attribute} должен быть целым числом'],
+            [['count'], 'number', 'message' => '{attribute} должен быть числом'],
             [['created_at', 'updated_at'], 'safe'],
-            [['product_guid'], 'string', 'max' => 255],
-            [['bouquet_id'], 'exist', 'skipOnError' => true, 'targetClass' => BouquetComposition::class, 'targetAttribute' => ['bouquet_id' => 'id']],
+            [['product_guid'], 'string', 'max' => 255, 'tooLong' => '{attribute} не должен превышать 255 символов'],
+            [['bouquet_id'], 'exist', 'skipOnError' => true, 'targetClass' => BouquetComposition::class, 'targetAttribute' => ['bouquet_id' => 'id'], 'message' => 'Указанный {attribute} не существует'],
         ];
     }
 
-    public function attributeLabels()
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels(): array
     {
         return [
             'id' => 'ID',
@@ -72,13 +86,94 @@ class BouquetCompositionProducts extends ActiveRecord
         ];
     }
 
+    /**
+     * Обновляет продукты для указанного букета.
+     *
+     * @param int $bouquetId ID букета
+     * @param array $products Массив продуктов в формате [product_guid => count]
+     */
+    public static function updateProducts(int $bouquetId, array $products): void
+    {
+        self::deleteAll(['bouquet_id' => $bouquetId]);
+        foreach ($products as $guid => $count) {
+            $product = new self([
+                'bouquet_id' => $bouquetId,
+                'product_guid' => $guid,
+                'count' => $count,
+            ]);
+            $product->save();
+        }
+    }
+
+    /**
+     * Возвращает список доступных продуктов для выбора.
+     *
+     * @param bool $isNewRecord Является ли букет новым
+     * @param int|null $bouquetId ID букета (если не новый)
+     * @return array Массив доступных продуктов в формате [id => name]
+     */
+    public static function getAvailableItems(bool $isNewRecord, ?int $bouquetId = null): array
+    {
+        $query = Products1c::find()
+            ->where(['view' => Products1c::IS_VISIBLE, 'tip' => Products1c::TYPE_PRODUCTS]);
+
+        if (!$isNewRecord && $bouquetId) {
+            $selectedIds = self::find()
+                ->select('product_guid')
+                ->where(['bouquet_id' => $bouquetId])
+                ->column();
+            $query->andWhere(['not in', 'id', $selectedIds]);
+        }
+
+        return ArrayHelper::map($query->all(), 'id', 'name');
+    }
+
+    /**
+     * Возвращает список выбранных продуктов для букета.
+     *
+     * @param int $bouquetId ID букета
+     * @return array Массив выбранных продуктов в формате [['id' => ..., 'count' => ..., 'text' => ...], ...]
+     */
+    public static function getSelectedItems(int $bouquetId): array
+    {
+        return array_map(
+            fn($product) => [
+                'id' => $product->product_guid,
+                'count' => $product->count,
+                'text' => $product->product->name ?? ''
+            ],
+            self::find()->where(['bouquet_id' => $bouquetId])->with('product')->all()
+        );
+    }
+
+    /**
+     * Возвращает все продукты, связанные с букетом.
+     *
+     * @param int $bouquetId ID букета
+     * @return static[] Массив объектов продуктов
+     */
+    public static function getCompositionProducts(int $bouquetId): array
+    {
+        return self::find()->where(['bouquet_id' => $bouquetId])->all();
+    }
+
+    /**
+     * Отношение к букету.
+     *
+     * @return \yii\db\ActiveQuery
+     */
     public function getBouquet()
     {
         return $this->hasOne(BouquetComposition::class, ['id' => 'bouquet_id']);
     }
 
-    public function getProduct() {
+    /**
+     * Отношение к продукту.
+     *
+     * @return \yii\db\ActiveQuery
+     */
+    public function getProduct()
+    {
         return $this->hasOne(Products1c::class, ['id' => 'product_guid']);
     }
-
-}
+}
\ No newline at end of file
index 8c553ae34d36ef05947888defaad42b5cdc38c67..bb4ade6d8795bdea65b3936c5095db176fe4f813 100644 (file)
@@ -9,7 +9,7 @@ use yii\db\ActiveRecord;
 use yii\db\Expression;
 
 /**
- * This is the model class for table "bouquet_forecast".
+ * This is the model class for table "erp24.bouquet_forecast".
  *
  * @property int $id
  * @property int|null $bouquet_id ИД букета
@@ -25,7 +25,6 @@ use yii\db\Expression;
  */
 class BouquetForecast extends ActiveRecord
 {
-
     public const OFFLINE_STORES = 1;
     public const ONLINE_STORES = 2;
     public const MARKETPLACE = 3;
@@ -33,12 +32,15 @@ class BouquetForecast extends ActiveRecord
     /**
      * {@inheritdoc}
      */
-    public static function tableName()
+    public static function tableName(): string
     {
         return '{{%erp24.bouquet_forecast}}';
     }
 
-    public function behaviors()
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors(): array
     {
         return [
             [
@@ -58,19 +60,20 @@ class BouquetForecast extends ActiveRecord
     /**
      * {@inheritdoc}
      */
-    public function rules()
+    public function rules(): array
     {
         return [
-            [['bouquet_id', 'year', 'month', 'type_sales', 'type_sales_id', 'created_by', 'updated_by'], 'integer'],
-            [['type_sales_value'], 'number'],
+            [['bouquet_id', 'year', 'month', 'type_sales', 'type_sales_id', 'created_by', 'updated_by'], 'integer', 'message' => '{attribute} должен быть целым числом'],
+            [['type_sales_value'], 'number', 'message' => '{attribute} должен быть числом'],
             [['created_at', 'updated_at'], 'safe'],
+            [['bouquet_id', 'year', 'month', 'type_sales', 'type_sales_id'], 'required', 'message' => '{attribute} обязателен для заполнения'],
         ];
     }
 
     /**
      * {@inheritdoc}
      */
-    public function attributeLabels()
+    public function attributeLabels(): array
     {
         return [
             'id' => 'ID',
@@ -87,16 +90,28 @@ class BouquetForecast extends ActiveRecord
         ];
     }
 
-    public static function getStoresList($id, $typeSales, $defaultModel, $defaultCondition, $month = null, $year = null)
+    /**
+     * Возвращает список магазинов с данными о продажах или без них.
+     *
+     * @param int|null $bouquetId ID букета
+     * @param int $typeSales Тип продаж (OFFLINE_STORES, ONLINE_STORES, MARKETPLACE)
+     * @param string $defaultModel Класс модели (CityStore или StoreType)
+     * @param array $defaultCondition Условия для выборки магазинов
+     * @param int|null $month Месяц (по умолчанию текущий)
+     * @param int|null $year Год (по умолчанию текущий)
+     * @return array Список магазинов с данными о продажах
+     */
+    public static function getStoresList(?int $bouquetId, int $typeSales, string $defaultModel, array $defaultCondition, ?int $month = null, ?int $year = null): array
     {
         $joinTable = $defaultModel === CityStore::class ? 'city_store' : 'store_type';
+        $month = $month ?? (int)date('m');
+        $year = $year ?? (int)date('Y');
 
-
-        $list = BouquetForecast::find()
-            ->andWhere(['bouquet_id' => $id])
+        $list = self::find()
+            ->andWhere(['bouquet_id' => $bouquetId])
             ->andWhere(['type_sales' => $typeSales])
-            ->andWhere(['month' => $month  ?? date('m')])
-            ->andWhere([ 'year' => $year ?? date('Y')])
+            ->andWhere(['month' => $month])
+            ->andWhere(['year' => $year])
             ->leftJoin("$joinTable AS df", "df.id = bouquet_forecast.type_sales_id")
             ->select(["df.name AS name", 'type_sales_value AS value', 'type_sales_id AS id'])
             ->orderBy('type_sales_id')
@@ -119,36 +134,83 @@ class BouquetForecast extends ActiveRecord
         return $list;
     }
 
-    public static function processSalesData($id, $year, $month, $salesData, $salesType)
+    /**
+     * Обрабатывает данные о продажах для букета.
+     *
+     * @param int $bouquetId ID букета
+     * @param array $data Данные формы, содержащие BouquetForecast[type_sales_value]
+     */
+    public static function processSalesData(int $bouquetId, array $data): void
+    {
+        if (empty($data['BouquetForecast']['type_sales_value'])) {
+            return;
+        }
+
+        $salesData = $data['BouquetForecast']['type_sales_value'];
+        $year = $data['year'];
+        $month = $data['month'];
+        $errors = [];
+
+        $types = [
+            'offline' => self::OFFLINE_STORES,
+            'online' => self::ONLINE_STORES,
+            'marketplace' => self::MARKETPLACE,
+        ];
+
+        foreach ($types as $key => $type) {
+            if (empty($salesData[$key])) {
+                continue;
+            }
+
+            $typeErrors = self::updateSalesData($bouquetId, $year, $month, $salesData[$key], $type);
+            $errors = array_merge($errors, $typeErrors);
+        }
+
+        if ($errors) {
+            Yii::$app->session->setFlash('danger', implode(' ', array_map('json_encode', $errors)));
+        }
+    }
+
+    /**
+     * Обновляет или создает записи о продажах для конкретного типа.
+     *
+     * @param int $bouquetId ID букета
+     * @param int $year Год
+     * @param int $month Месяц
+     * @param array $salesData Данные о продажах в формате [type_sales_id => value]
+     * @param int $salesType Тип продаж
+     * @return array Массив ошибок (пустой, если ошибок нет)
+     */
+    private static function updateSalesData(int $bouquetId, int $year, int $month, array $salesData, int $salesType): array
     {
         $errors = [];
 
-        foreach ($salesData as $key => $value) {
-            $model = BouquetForecast::findOne([
-                'bouquet_id' => $id,
+        foreach ($salesData as $typeSalesId => $value) {
+            $model = self::findOne([
+                'bouquet_id' => $bouquetId,
                 'year' => $year,
                 'month' => $month,
                 'type_sales' => $salesType,
-                'type_sales_id' => $key
+                'type_sales_id' => $typeSalesId,
             ]);
 
-            if (empty($model)) {
-                $model = new BouquetForecast([
-                    'bouquet_id' => $id,
+            if (!$model) {
+                $model = new self([
+                    'bouquet_id' => $bouquetId,
                     'year' => $year,
                     'month' => $month,
                     'type_sales' => $salesType,
-                    'type_sales_id' => $key,
-                    'type_sales_value' => $value
+                    'type_sales_id' => $typeSalesId,
+                    'type_sales_value' => $value,
                 ]);
-
-                if (!$model->save()) {
-                    $errors[] = $model->errors;
-                }
             } elseif ($model->type_sales_value != $value) {
-                if (!$model->updateAttributes(['type_sales_value' => $value])) {
-                    $errors[] = $model->errors;
-                }
+                $model->type_sales_value = $value;
+            } else {
+                continue; // Нет изменений, пропускаем
+            }
+
+            if (!$model->save()) {
+                $errors[] = $model->getFirstErrors();
             }
         }
 
index eb08e6e195a6ee18a6fbabf7b951483cc51f8ad5..b9f3e0a0a3d9c41417d7e76d8849baf7d00a7d69 100644 (file)
@@ -32,7 +32,6 @@ $this->registerJsFile('/js/bouquet/bouquet.js', ['position' => \yii\web\View::PO
         'processUrls' => [],
         'availableItems' => $availableItems,
         'model' => null,
-        'flash' => $flash,
         'id' => null
     ]); ?>
 </div>
index 0c4817a059273e8af5bf54ea7aa9423b8a001d33..4d74d0ac6d16584e4dad978505a0052105cec48d 100644 (file)
@@ -55,7 +55,7 @@ $(document).ready(function () {
     }
 });
 
-$(document).on('click', '.calculate-btn, #w2-add, #w2-remove', function () {
+$(document).on('click', '.calculate-btn, .btn-add-item, .btn-remove-item', function () {
     $('.cost-value, .selfcost-value, .markup-value').text('');
 });
 
index f9fa93508e8bb72b587b40717e4c321454104b3e..b9acfef8de91dc26de377679591f7c41163aa885 100644 (file)
@@ -179,8 +179,8 @@ CSS;
         </select>
     </div>
     <div class='controls'>
-        <button id='{$id}-add' class='btn btn-primary' type='button'>&gt;</button>
-        <button id='{$id}-remove' class='btn btn-primary' type='button'>&lt;</button>
+        <button id='{$id}-add' class='btn btn-add-item btn-primary' type='button'>&gt;</button>
+        <button id='{$id}-remove' class='btn btn-remova-item btn-primary' type='button'>&lt;</button>
     </div>
     <div class='list-container'>
         <div class='section-label'>{$this->selectedLabel}</div>