]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
ERP-360 Сборка страницы автопм
authormarina <m.zozirova@gmail.com>
Mon, 9 Jun 2025 19:33:56 +0000 (22:33 +0300)
committermarina <m.zozirova@gmail.com>
Mon, 9 Jun 2025 19:33:56 +0000 (22:33 +0300)
erp24/commands/CronController.php
erp24/controllers/AutoPlannogrammaController.php
erp24/migrations/m250605_201027_create_autoplannagramma_table.php
erp24/records/Autoplannogramma.php
erp24/views/auto-plannogramma/index.php
erp24/web/js/autoplannogramma/autoplannogramma.js

index 10f7199cf5e9efca81ef8a2a5250211f047c4bb4..510c6ea4c5de7112297e532a98a714d4ac339f37 100644 (file)
@@ -1556,5 +1556,121 @@ class CronController extends Controller
 
         return ExitCode::OK;
     }
+/**
+    public function actionMarinaAutoplannogrammaTest()
+    {
+        $month = date('m');
+        $year = date('Y');
+
+        $plan_date = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01';
+        $service = new AutoPlannogrammaService();
+        $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($plan_date);
+        $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $plan_date);
+        $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($plan_date);
+        $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+        $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($plan_date);
+        $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+
+        $result = StorePlanService::calculateHistoricalShare(
+            $filters['store_id'],
+            $filters['month'],
+            $filters['year'],
+            $filters['category'],
+            $filters['subcategory'],
+            $filters['species']
+        );
+
+        $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($plan_date);
+
+        $productSalesShare = StorePlanService::calculateProductSalesShareProductsWithHistory(
+            $filters['store_id'],
+            $filters['month'],
+            $result['with_history']
+        );
+
+        $productSalesForecast = $service->calculateProductForecastInPiecesProductsWithHistory(
+            $filters['store_id'],
+            $filters['month'],
+            $productSalesShare,
+            $goals,
+            $filters['subcategory'],
+            $filters['category'],
+            $filters['species']
+        );
+
+        $matrixForecast = MatrixBouquetForecast::find()
+            ->where(['year' => $year, 'month' => $month])
+            ->asArray()
+            ->all();
+        $matrixGroups = array_unique(ArrayHelper::getColumn($matrixForecast, 'group'));
+        $bouquetForecast = StorePlanService::getBouquetSpiecesMonthGoalFromForecast($filters['month'], $filters['year'], $filters['store_id'], $matrixGroups);
+        $speciesData = $bouquetForecast['final'];
+        foreach ($speciesData as $store_id  => $categoryData) {
+            foreach ($categoryData as $category => $subcategoryData) {
+                foreach ($subcategoryData as $subcategory => $species) {
+                    foreach ($species as $speciesInd => $row) {
+                        $bouquetSpeciesForecast[] = [
+                            'category' => $category,
+                            'subcategory' => $subcategory,
+                            'store_id' => $store_id,
+                            'species' => $speciesInd,
+                            'goal' => $row
+                        ];
+                    }
+                }
+            }
+
+        }
+        $cleanedSpeciesGoals = $service->subtractSpeciesGoals($goals, $bouquetSpeciesForecast, $noHistoryProductData);
+
+
+        $salesProductForecastShare = $service->calculateProductForecastShare($noHistoryProductData, $productSalesForecast);
+
+        $productForecastSpecies = $service->calculateProductSalesBySpecies($salesProductForecastShare, $cleanedSpeciesGoals);
+
+
+        $weeklySales = $service->getHistoricalSpeciesShareByWeek($plan_date);
+
+        $weeklySalesForecast = $service->calculateWeeklyProductForecastPieces($productForecastSpecies, $weeklySales);
+
+
+        $weeklySalesForecastFormated = $service->pivotWeeklyForecast($weeklySalesForecast);
+
+        $flatData = array_filter($weeklySalesForecastFormated, function ($row) use ($filters) {
+            foreach ($filters as $key => $value) {
+                if (empty($value)) continue;
+                if (!isset($row[$key])) continue;
+
+                if (stripos((string)$row[$key], (string)$value) === false) {
+                    return false;
+                }
+            }
+            return true;
+        });
+
+        foreach ($flatData as $item) {
+            foreach ($weeks as $weekKey => $weekNumber) {
+                $model = new Autoplannogramma();
+                $model->week = $weekNumber;
+                $model->month = $month;
+                $model->year = $year;
+                $model->product_id = $item['product_id'];
+                $model->store_id = $item['store_id'];
+                $model->quantity = round($item[$weekKey]);
+                $model->quantity_forecast = round($item[$weekKey]);
+                $model->is_archive = false;
+                $model->auto_forecast = true;
+
+                if (!$model->save()) {
+                    Yii::error("Ошибка сохранения: " . json_encode($model->errors), __METHOD__);
+                } else {
+                    Yii::error("Ошибка сохранения: " . json_encode($model->errors), __METHOD__);
+                }
+            }
+        }
+
+
+    }
+ **/
 
 }
index b684aafbca69ce51f4dd957c8131cccf83233efe..51aa1ef654b5abcab6a407a83748f154996389f4 100644 (file)
@@ -77,44 +77,116 @@ class AutoPlannogrammaController extends BaseController
         ]);
     }
 
-    public function actionGetProducts(string $category, string $subcategory, array $filters): array
+    public function actionGetProducts(): array
     {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
-        $models = Autoplannogramma::find()
-            ->joinWith('products p')
-            ->where(['category' => $category])
-            ->andWhere(['subcategory' => $subcategory])
-            ->andFilterWhere($filters)
-            ->select(['p.id', 'p.name', 'store_id', 'quantity'])
+        $request = Yii::$app->request;
+        $category = $request->get('category');
+        $subcategory = $request->get('subcategory');
+        $filters = $request->get();
+
+        $query = Autoplannogramma::find()
+            ->alias('a')
+            ->leftJoin('products_1c_nomenclature p1n', 'p1n.id = a.product_id')
+            ->leftJoin('city_store_params cp', 'cp.store_id = a.store_id')
+            ->where(['p1n.category' => $category])
+            ->andWhere(['p1n.subcategory' => $subcategory])
+            ->andFilterWhere(['=', 'a.year', $filters['year']])
+            ->andFilterWhere(['=', 'a.week', $filters['week']])
+            ->andFilterWhere(['=', 'cp.address_city', $filters['city']])
+            ->andFilterWhere(['=', 'cp.address_region', $filters['region']])
+            ->andFilterWhere(['=', 'cp.address_district', $filters['district']])
+            ->andFilterWhere(['=', 'cp.store_type', $filters['store_type']])
+            ->andFilterWhere(['=', 'a.capacity_type', $filters['capacity_type']]);
+
+        if (!empty($filters['territorial_manager'])) {
+            $territorialManagerStoreIds = StoreDynamic::find()
+                ->select('store_id')
+                ->where(['category' => 3, 'active' => 1, 'value_int' => $filters['territorial_manager']])
+                ->column();
+            $query->andWhere(['in', 'a.store_id', $territorialManagerStoreIds ?: [-1]]);
+        }
+
+        if (!empty($filters['bush_chef_florist'])) {
+            $bushChefFloristStoreIds = StoreDynamic::find()
+                ->select('store_id')
+                ->where(['category' => 2, 'active' => 1, 'value_int' => $filters['bush_chef_florist']])
+                ->column();
+            $query->andWhere(['in', 'a.store_id', $bushChefFloristStoreIds ?: [-1]]);
+        }
+
+        $models = $query
+            ->select([
+                'a.id AS plan_id',
+                'p1n.id AS product_id',
+                'p1n.name AS product_name',
+                'a.store_id',
+                'a.quantity',
+                'a.month'
+            ])
             ->asArray()
             ->all();
 
         $result = [];
 
         foreach ($models as $model) {
-            $guid = $model['id'];
-            $name = $model['name'];
-            $storeId = $model['store_id'];
-            $quantity = $model['quantity'];
-
-            if (!isset($result[$guid])) {
-                $result[$guid] = [
-                    'name' => $name,
-                    'guid' => $guid,
+            $productId = $model['product_id'];
+            $productName = $model['product_name'];
+
+            if (!isset($result[$productId])) {
+                $result[$productId] = [
+                    'product_id' => $productId,
+                    'name' => $productName,
                     'values' => [],
                 ];
             }
 
-            $result[$guid]['values'][] = [
-                'count' => (int) $quantity,
-                'store_id' => (int) $storeId,
+            $result[$productId]['values'][] = [
+                'id' => $model['plan_id'],
+                'quantity' => (int)$model['quantity'],
+                'store_id' => (int)$model['store_id'],
             ];
         }
 
         return array_values($result);
     }
 
+    public function actionUpdateValues()
+    {
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+
+        $data = json_decode(Yii::$app->request->getRawBody(), true);
+
+        if (!is_array($data)) {
+            return ['success' => false, 'message' => 'Неверный формат данных'];
+        }
+
+        foreach ($data as $item) {
+            if (isset($item['id'])) {
+                $model = Autoplannogramma::findOne($item['id']);
+                if ($model !== null) {
+                    $model->quantity = $item['value'];
+                    $model->auto_forecast = false;
+                    $model->save(false);
+                }
+            } else {
+                $model = new Autoplannogramma();
+                $model->product_id = $item['product_id'] ?? null;
+                $model->store_id = $item['store_id'] ?? null;
+                $model->year = $item['year'] ?? null;
+                $model->month = $item['month'] ?? null;
+                $model->week = $item['week'] ?? null;
+                $model->quantity = $item['value'];
+                $model->quantity_forecast = 0;
+                $model->auto_forecast = false;
+                $model->save(false);
+            }
+        }
+
+        return ['success' => true];
+    }
+
     public function actionGetVisibleStores()
     {
         Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
@@ -148,7 +220,7 @@ class AutoPlannogrammaController extends BaseController
             }
         }
 
-        return ['store_ids' => $q->column()];
+        return array_values($q->column());
     }
 
     public function action1()
@@ -1319,6 +1391,10 @@ class AutoPlannogrammaController extends BaseController
                 return true;
             });
 
+
+            echo '<pre>';
+            var_dump($flatData);
+            echo '</pre>';die();
             $dataProvider = new ArrayDataProvider([
                 'allModels' => $flatData,
                 'pagination' => ['pageSize' => 100],
index 64bc54a75e56bdcbc3dd44776a5afd5dce46aaf9..27ec025348faa338ddb6a898a876c846c08c825e 100644 (file)
@@ -12,7 +12,7 @@ class m250605_201027_create_autoplannagramma_table extends Migration
      */
     public function safeUp()
     {
-        $this->createTable('{{%autoplannogramma}}', [
+        $this->createTable('{{%erp24.autoplannogramma}}', [
             'id' => $this->primaryKey(),
             'week' => $this->integer()->comment('Неделя'),
             'month' => $this->integer()->comment('Месяц'),
@@ -23,7 +23,7 @@ class m250605_201027_create_autoplannagramma_table extends Migration
             'quantity' => $this->integer()->comment('Количество'),
             'quantity_forecast' => $this->integer()->comment('Количество рассчитанное'),
             'is_archive' => $this->boolean()->comment('Архивная ли запись?'),
-            'auto_forecast' => $this->boolean()->comment('Значение спрогнозировано?'),
+            'auto_forecast' => $this->boolean()->defaultValue(true)->comment('Значение спрогнозировано?'),
             'created_at' => $this->dateTime()->comment('Дата создания'),
             'updated_at' => $this->dateTime()->comment('Дата обновления'),
             'created_by' => $this->integer()->comment('Автор создания'),
index f682828848e691f495c6a80d11eb9767fc66d19e..bc399356d8f375691db6d1b34e17228d185af182 100644 (file)
@@ -4,6 +4,9 @@ namespace yii_app\records;
 
 use Product;
 use Yii;
+use yii\behaviors\BlameableBehavior;
+use yii\behaviors\TimestampBehavior;
+use yii\db\Expression;
 
 /**
  * This is the model class for table "autoplannogramma".
@@ -31,7 +34,7 @@ class Autoplannogramma extends \yii\db\ActiveRecord
      */
     public static function tableName()
     {
-        return '{{%autoplannogramma}}';
+        return 'autoplannogramma';
     }
 
     /**
@@ -47,6 +50,23 @@ class Autoplannogramma extends \yii\db\ActiveRecord
         ];
     }
 
+    public function behaviors()
+    {
+        return [
+            [
+                'class' => TimestampBehavior::class,
+                'createdAtAttribute' => 'created_at',
+                'updatedAtAttribute' => 'updated_at',
+                'value' => new Expression('NOW()')
+            ],
+            [
+                'class' => BlameableBehavior::class,
+                'createdByAttribute' => 'created_by',
+                'updatedByAttribute' => 'updated_by',
+            ],
+        ];
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -72,6 +92,6 @@ class Autoplannogramma extends \yii\db\ActiveRecord
     }
 
     public function getProducts() {
-        return $this->hasOne(Products1cNomenclature::className(), ['id' => 'product_id']);
+        return $this->hasMany(Products1cNomenclature::class, ['id' => 'product_id']);
     }
 }
index 00931e17224811303a79df3e3f8a7896fdc68da3..45c5a3bd4f65b147b972ccd34a16484e12a86c48 100644 (file)
@@ -65,7 +65,7 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
                             'id',
                             'name',
                         ),
-                        'options' => ['placeholder' => 'Тер. управляющий', 'id' => 'territorial-manger'],
+                        'options' => ['placeholder' => 'Тер. управляющий', 'id' => 'territorial-manager'],
                         'pluginOptions' => ['allowClear' => true],
                     ]) ?>
                 </div>
@@ -123,7 +123,7 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
             </div>
             <div class="row">
                 <div class="col-md d-flex">
-                    <?= Html::label('Месяца-месяцы') ?>
+                    <?= Html::label('Месяца-месяцы', [], ['class' => 'month-label']) ?>
                 </div>
                 <div class="col-md d-flex">
                     <?= Select2::widget([
@@ -156,9 +156,9 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
         <table class="table">
             <thead>
             <tr class="head">
-                <th scope="col" style="text-align: left !important;">
-                    <?= Html::label("год: 2025 Ð½ÐµÐ´ÐµÐ»Ñ\8f: 05", null, ['class' => 'label-year-week']) ?><br>
-                    <?= Html::label("январь - февраль", null, ['class' => 'label-month-range']) ?><br>
+                <th scope="col" style="text-align: left !important; display: inline-block; justify-content: flex-end;">
+                    <?= Html::label("Ð\93од: Ð\9dеделÑ\8f", null, ['class' => 'label-year-week']) ?><br>
+                    <?= Html::label("Месяц", null, ['class' => 'label-month-range']) ?><br>
                     <?= Html::label("Тип п-ма:", null, ['class' => 'label-capacity-type']) ?><br>
                     <?= Html::label("Город:", null, ['class' => 'label-city']) ?><br>
                     <?= Html::label("Регион:", null, ['class' => 'label-region']) ?><br>
@@ -173,10 +173,9 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
                     </div>
                 </th>
                 <?php foreach ($stores as $storeId => $storeName): ?>
-                    <th scope="col" class="fixed-column" data-store-id="<?= $storeId ?>">
+                    <th scope="col" class="store-label" data-store-id="<?= $storeId ?>">
                         <?= Html::label($storeName, null, [
-                            'style' => 'text-align: center; white-space: nowrap; 
-                        font-weight: bold; transform-origin: left bottom; padding-right: 7%;'
+                            'style' => 'display: block; white-space: nowrap; margin: auto; font-weight: bold; text-align: center; writing-mode: sideways-lr;'
                         ]) ?>
                     </th>
                 <?php endforeach; ?>
@@ -192,7 +191,7 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
 
             <?php foreach ($groupedCategories as $category => $subcategories): ?>
                 <tr>
-                    <td class="category">
+                    <td class="category" style="display: flex; justify-content: flex-end;">
                         <a class="list-group-item list-group-item-action">
                             <?= Html::encode($category) ?> ▲
                         </a>
@@ -203,12 +202,10 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
                 </tr>
 
                 <?php foreach ($subcategories as $subcategory): ?>
+
                     <tr>
-                        <td style="position: relative; display: flex; justify-content: flex-end;" class="subcategory">
-                            <a href="#" class="list-group-item list-group-item-action subcategory-link"
-                               style="width: 95%;"
-                               data-category="<?= Html::encode($category) ?>"
-                               data-subcategory="<?= Html::encode($subcategory) ?>">
+                        <td class="subcategory" style="display: flex; justify-content: flex-end;">
+                            <a href="#" class="list-group-item list-group-item-action subcategory-link" style="width: 95%" data-category="<?= Html::encode($category) ?>" data-subcategory="<?= Html::encode($subcategory) ?>">
                                 <?= Html::encode($subcategory) ?> ▶
                             </a>
                         </td>
@@ -224,31 +221,10 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
 </div>
 
 <style>
-    /* Стили для таблицы */
     .table {
-        width: 100%;
-        table-layout: fixed; /* Это позволяет фиксировать ширину колонок */
-        overflow-x: auto; /* Это позволяет прокручивать таблицу по горизонтали */
-        border-collapse: collapse; /* Чтобы не было промежутков между ячейками */
-    }
-
-    /* Первая колонка будет фиксированной */
-    .fixed-column {
-        width: 170px; /* Устанавливаем фиксированную ширину для первой колонки */
-        white-space: nowrap; /* Запрещаем перенос текста в ячейке */
-        border-right: 1px solid #ddd; /* Вертикальная линия между колонками */
-    }
-
-    /* Для остальных колонок можно установить фиксированную ширину, если нужно */
-    th, td {
-        width: 300px; /* Пример для остальных колонок */
-        text-align: center; /* Центрируем текст */
-        padding: 10px; /* Немного отступов для улучшения внешнего вида */
-        border-right: 1px solid #ddd; /* Вертикальная линия между колонками */
-    }
-
-    .items .input {
-        width: 100%;
+        width: max-content;
+        table-layout: fixed;
+        border-collapse: collapse;
     }
 
     .table-wrapper {
@@ -257,11 +233,35 @@ $this->registerJsFile('/js/autoplannogramma/autoplannogramma.js', ['position' =>
         position: relative;
     }
 
-    .fixed-column {
-        position: sticky;
+    th:first-child,
+    td.category,
+    td.subcategory {
+        position: sticky !important;
         left: 0;
-        background: white;
-        z-index: 2;
+        background: #f8f8fc;
+        z-index: 5;
+        min-width: 300px !important;
+        border-right: 2px solid #ddd;
+        white-space: normal;
+    }
+
+    th:not(:first-child) {
+        position: sticky;
+        top: 0;
+        background: #f8f8fc;
+        z-index: 4;
+        min-width: 150px;
+        white-space: nowrap;
+        text-align: center;
+        font-weight: bold;
+        border-right: 2px solid #ddd;
+        padding: 10px;
+    }
+
+    td:not(:first-child) {
+        min-width: 150px;
+        text-align: center;
+        padding: 10px;
         border-right: 2px solid #ddd;
     }
 </style>
\ No newline at end of file
index 79bfe1dc23dc529ac7640f552cd00381a25e8c67..5070517142f848311b45db7df5681d3ef2a54b15 100644 (file)
@@ -1,48 +1,42 @@
-document.addEventListener("DOMContentLoaded", function () {
-    // Изначально скрываем все строки, кроме .category и .head
-    document.querySelectorAll("tr").forEach(row => {
+document.addEventListener("DOMContentLoaded", () => {
+    const rows = document.querySelectorAll("tr");
+    rows.forEach(row => {
         if (!row.querySelector(".category") && !row.classList.contains("head")) {
             row.style.display = "none";
         }
     });
 
-    // Обработчик кликов для категорий
     document.querySelectorAll(".category").forEach(category => {
         category.addEventListener("click", function () {
-            let nextRow = this.parentElement.nextElementSibling;
-            let isVisible = nextRow?.style.display === "table-row";
+            let row = this.parentElement.nextElementSibling;
+            let isOpen = row?.style.display === "table-row";
 
-            // Закрываем все элементы внутри категории
-            while (nextRow && !nextRow.querySelector(".category")) {
-                nextRow.style.display = "none";
-                nextRow = nextRow.nextElementSibling;
+            while (row && !row.querySelector(".category")) {
+                row.style.display = "none";
+                row = row.nextElementSibling;
             }
 
-            // Если категория была закрыта — открываем подкатегории
-            if (!isVisible) {
-                nextRow = this.parentElement.nextElementSibling;
-                while (nextRow && !nextRow.querySelector(".category")) {
-                    if (nextRow.querySelector(".subcategory")) {
-                        nextRow.style.display = "table-row";
+            if (!isOpen) {
+                row = this.parentElement.nextElementSibling;
+                while (row && !row.querySelector(".category")) {
+                    if (row.querySelector(".subcategory")) {
+                        row.style.display = "table-row";
                     }
-                    nextRow = nextRow.nextElementSibling;
+                    row = row.nextElementSibling;
                 }
             }
         });
     });
 
-    // Обработчик кликов для подкатегорий
-    document.querySelectorAll(".subcategory").forEach(subcategory => {
-        subcategory.addEventListener("click", function (event) {
-            event.stopPropagation(); // Чтобы клик по подкатегории не закрывал категорию
+    document.querySelectorAll(".subcategory").forEach(sub => {
+        sub.addEventListener("click", function (e) {
+            e.stopPropagation();
+            let row = this.parentElement.nextElementSibling;
+            let isOpen = row?.style.display === "table-row";
 
-            let nextRow = this.parentElement.nextElementSibling;
-            let isVisible = nextRow?.style.display === "table-row";
-
-            // Переключаем видимость только айтемов внутри этой подкатегории
-            while (nextRow && !nextRow.querySelector(".subcategory") && !nextRow.querySelector(".category")) {
-                nextRow.style.display = isVisible ? "none" : "table-row";
-                nextRow = nextRow.nextElementSibling;
+            while (row && !row.querySelector(".subcategory") && !row.querySelector(".category")) {
+                row.style.display = isOpen ? "none" : "table-row";
+                row = row.nextElementSibling;
             }
         });
     });
@@ -50,125 +44,97 @@ document.addEventListener("DOMContentLoaded", function () {
 
 $('.subcategory-link').on('click', function (e) {
     e.preventDefault();
+    window.getSelection()?.removeAllRanges();
+    this.blur();
+
+    if (!$('#week').val() || !$('#year').val()) {
+        alert('Необходимо выбрать год и неделю для отображения планограммы');
+        return;
+    }
 
     const $link = $(this);
     const category = $link.data('category');
     const subcategory = $link.data('subcategory');
     const $row = $link.closest('tr');
-
-    $.ajax({
-        url: '/auto-plannogramma/get-products',
-        type: 'GET',
-        data: {
-            category: category,
-            subcategory: subcategory
-        },
-        success: function (response) {
-            // Удаляем предыдущие строки
-            $row.nextAll('tr.inserted-row').remove();
-
-            response.forEach(item => {
-                const tr = $('<tr class="inserted-row"></tr>');
-
-                // Подкатегория
-                const subcategoryTd = $(`
-            <td class="subcategory" style="display: flex; justify-content: flex-end;">
-                <a href="#" class="list-group-item list-group-item-action subcategory-link"
-                   style="width: 90%;"
-                   data-category="${category}" data-subcategory="${subcategory}">
-                    ${item.name}
-                </a>
-            </td>
-        `);
-                tr.append(subcategoryTd);
-
-                // Карта store_id => count
-                const valuesMap = {};
-                item.values.forEach(val => {
-                    valuesMap[val.store_id] = {
-                        count: val.count,
-                        guid: item.guid
-                    };
-                });
-
-                // Берём колонку из текущей строки, чтобы соблюсти порядок
-                $row.find('td[data-store-id]').each(function () {
-                    const td = $(this);
-                    const storeId = td.data('store-id');
-                    const val = valuesMap[storeId] ?? {count: '', guid: ''};
-
-                    const newTd = $(`
-                <td data-store-id="${storeId}">
-                    <div style="display: flex; align-items: center;">
-                        <input type="number"
-                               class="btn btn-primary input"
-                               value="${val.count}"
-                               data-guid="${val.guid}"
-                               data-store_id="${storeId}"
-                               data-bs-toggle="tooltip"
-                               data-bs-placement="top"
-                               data-original-value="${val.count}"
-                               style="width: 100%; padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; border-radius: 0.375rem; border: 1px solid #0d6efd;">
-                        <button class="reject-btn" style="border: none; background: transparent; cursor: pointer; margin-left: 5px;">
-                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-                                <path d="M20 11v5a1 1 0 0 1-2 0v-4H7.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.416l3-3a1 1 0 1 1 1.414 1.416L7.414 10H19a1 1 0 0 1 1 1z"
-                                      fill="grey" stroke="none"/>
-                            </svg>
-                        </button>
-                    </div>
+    const filters = getFilterData();
+    filters.category = category;
+    filters.subcategory = subcategory;
+
+    $.get('/auto-plannogramma/get-products', filters, response => {
+        $row.nextAll('tr.inserted-row').remove();
+
+        response.forEach(item => {
+            const tr = $('<tr class="inserted-row"></tr>');
+
+            const subcategoryTd = $(`
+                <td class="subcategory" style="display: flex; justify-content: flex-end;">
+                    <a href="#" class="list-group-item list-group-item-action subcategory-link"
+                       style="width: 90%;"
+                       data-category="${category}" data-subcategory="${subcategory}">
+                        ${item.name}
+                    </a>
                 </td>
             `);
+            tr.append(subcategoryTd);
 
-                    tr.append(newTd);
-                });
-
-                $row.after(tr);
+            const valuesMap = Object.fromEntries(item.values.map(val => [
+                val.store_id,
+                {
+                    quantity: val.quantity,
+                    id: val.id
+                }
+            ]));
+
+            $('table thead th').each(function (index) {
+                const $th = $(this);
+                const storeId = $th.data('store-id');
+
+                if (storeId === undefined) return;
+
+                const isVisible = $(`table tbody tr:first td:eq(${index})`).is(':visible');
+                if (!isVisible) return;
+
+                const val = valuesMap[storeId] || { quantity: '', id: '' };
+
+                const td = $(`
+                    <td data-store-id="${storeId}">
+                        <div style="display: flex; align-items: center;">
+                            <input type="number" class="btn btn-primary input"
+                                   value="${val.quantity}"
+                                   data-id="${val.id}"
+                                   data-guid="${item.product_id}"
+                                   data-store_id="${storeId}"
+                                   data-bs-toggle="tooltip" data-bs-placement="top"
+                                   data-original-value="${val.quantity}"
+                                   style="width: 100%; padding: .375rem .75rem; font-size: 1rem; line-height: 1.5; border-radius: .375rem; border: 1px solid #0d6efd;">
+                            <button class="reject-btn" style="border: none; background: transparent; cursor: pointer; margin-left: 5px;">
+                                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+                                    <path d="M20 11v5a1 1 0 0 1-2 0v-4H7.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.416l3-3a1 1 0 1 1 1.414 1.416L7.414 10H19a1 1 0 0 1 1 1z"
+                                          fill="grey" stroke="none"/>
+                                </svg>
+                            </button>
+                        </div>
+                    </td>
+                `);
+
+                tr.append(td);
             });
-        },
-        error: function (xhr) {
-            alert('Ошибка: ' + xhr.responseText);
-        }
-    });
+
+            $row.after(tr);
+        });
+    }).fail(xhr => alert('Ошибка: ' + xhr.responseText));
 });
 
 $('#autoplannogramma').on('input', '.input', function () {
-    const $input = $(this);
-    const newValue = $input.val();
-    const $td = $input.closest('td');
-    const $svg = $td.find('svg');
-
-    $svg.find('path').attr('fill', 'red');
+    $(this).closest('td').find('path').attr('fill', 'red');
 });
 
 $('#autoplannogramma').on('click', '.reject-btn', function () {
-    const $button = $(this);
-    const $td = $button.closest('td');
-    const $input = $td.find('input.input');
-    const $svg = $td.find('svg');
-
-    const originalValue = $input.data('original-value');
-
-    if (originalValue !== undefined) {
-        $input.val(originalValue);
-        $svg.find('path').attr('fill', 'grey');
-    }
+    const $input = $(this).closest('td').find('input.input');
+    $input.val($input.data('original-value'));
+    $(this).closest('td').find('path').attr('fill', 'grey');
 });
 
-// Получение значений фильтров
-function getFilterData() {
-    return {
-        year: $('#year').val(),
-        city: $('#city').val(),
-        storeType: $('#store-type').val(),
-        territorialManager: $('#territorial-manger').val(),
-        polnogrammaType: $('#polnogramma-type').val(),
-        week: $('#week').val(),
-        region: $('#region').val(),
-        bushChefFlorist: $('#bush_chef_florist').val(),
-        district: $('#district').val(),
-    };
-}
-
 function getFilterData() {
     return {
         year: $('#year').val(),
@@ -183,74 +149,125 @@ function getFilterData() {
     };
 }
 
+function getSelectedText(selector) {
+    const val = $(selector).val();
+    return val ? $(selector).find(`option[value="${val}"]`).text() : '';
+}
+
+function updateFilterLabels(data) {
+    $('.label-year-week').text(`год: ${data.year || ''} неделя: ${data.week || ''}`);
+    $('.label-month-range').text(`Месяц: ${data.month || ''}`);
+    $('.label-capacity-type').text('Тип п-ма: ' + getSelectedText('#polnogramma-type'));
+    $('.label-city').text('Город: ' + getSelectedText('#city'));
+    $('.label-region').text('Регион: ' + getSelectedText('#region'));
+    $('.label-district').text('Район: ' + getSelectedText('#district'));
+    $('.label-store-type').text('Тип магазина: ' + getSelectedText('#store-type'));
+    $('.label-tu').text('Тер. Уп.: ' + getSelectedText('#territorial-manager'));
+    $('.label-kshf').text('КШФ: ' + getSelectedText('#bush_chef_florist'));
+}
+
 function applyStoreFilter() {
     const data = getFilterData();
 
-    $.get('/auto-plannogramma/get-visible-stores', data, function (response) {
-        const allowedStoreIds = (response.store_ids || []).map(String);
+    $.get('/auto-plannogramma/get-visible-stores', data, response => {
+        const allowedStoreIds = (response || []).map(String);
 
-        $('td[data-store-id], th[data-store-id]').each(function () {
-            const storeId = String($(this).data('store-id'));
-            $(this).toggle(allowedStoreIds.includes(storeId));
-        });
-
-        // Обновляем значения рядом с лейблами, выводим текст, а не id
-        $('.label-year-week').text(`год: ${data.year || '—'} неделя: ${data.week || '—'}`);
-        $('.label-month-range').text(getMonthRangeByWeek(data.week));
+        $('[data-store-id]').each(function () {
+            const el = $(this);
+            const storeId = el.data('store-id').toString();
 
-        $('.label-capacity-type').text('Тип п-ма: ' + getSelectedText('#polnogramma-type'));
-        $('.label-city').text('Город: ' + getSelectedText('#city'));
-        $('.label-region').text('Регион: ' + getSelectedText('#region'));
-        $('.label-district').text('Район: ' + getSelectedText('#district'));
-        $('.label-store-type').text('Тип магазина: ' + getSelectedText('#store-type'));
-        $('.label-tu').text('Тер. Уп.: ' + getSelectedText('#territorial-manager'));
-        $('.label-kshf').text('КШФ: ' + getSelectedText('#bush_chef_florist'));
+            const shouldShow = allowedStoreIds.includes(storeId);
+            el.toggle(shouldShow);
+        });
 
-    }).fail(function (xhr) {
+        updateFilterLabels(data);
+    }).fail(xhr => {
         console.error('Ошибка при фильтрации магазинов:', xhr.responseText);
     });
 }
 
-function getSelectedText(selector) {
-    const select = $(selector);
-    const val = select.val();
-    const text = select.find(`option[value="${val}"]`).text();
-    return text;
-}
+$('.btn-save').on('click', function () {
+    const changedValues = [];
+    let hasErrors = false;
 
-function resetStoreFilter() {
-    // Сброс обычных инпутов
-    $('#year, #week').val('');
+    $('input.input:visible').each(function () {
+        const $input = $(this);
+        const id = $input.data('id');
+        const value = $input.val();
+        const $arrow = $input.closest('div').find('svg path');
 
-    // Сброс всех Select2
-    $('#city, #store-type, #territorial-manager, #polnogramma-type, #region, #bush_chef_florist, #district')
-        .val(null)
-        .trigger('change');
+        if ($arrow.attr('fill') !== 'red') {
+            return;
+        }
 
-    // Показ всех ячеек магазинов
-    $('td[data-store-id], th[data-store-id]').show();
+        if (id) {
+            changedValues.push({
+                id: id,
+                value: value
+            });
+        } else {
+            const product_id = $input.data('guid');
+            const store_id = $input.data('store_id');
+            const year = $('#year').val();
+            const month = $('.month-label').val();
+            const week = $('#week').val();
+            if (!product_id || !store_id || !year || !week) {
+                alert('Для новых записей обязательны: product_id, store_id, year, week.');
+                hasErrors = true;
+                return false;
+            }
 
-    // Очистка лейблов
-    $('.label-year-week').text('');
-    $('.label-month-range').text('');
-    $('.label-city, .label-store-type, .label-tu, .label-capacity-type, .label-region, .label-district, .label-kshf')
-        .text('—');
-}
+            changedValues.push({
+                product_id: product_id,
+                store_id: store_id,
+                year: year,
+                month: month,
+                week: week,
+                value: value
+            });
+        }
+    });
 
-// Привязка событий
-$(document).on('click', '.btn-apply', applyStoreFilter);
-$(document).on('click', '.btn-reset', resetStoreFilter);
-
-// Дополнительная функция: получить диапазон месяцев по номеру недели (примерная логика)
-function getMonthRangeByWeek(week) {
-    if (!week) return '—';
-    const w = parseInt(week, 10);
-    if (w <= 4) return 'январь';
-    if (w <= 8) return 'январь - февраль';
-    if (w <= 13) return 'февраль - март';
-    // ... можно доработать
-    return '—';
-}
+    if (hasErrors) return;
 
+    if (changedValues.length === 0) {
+        alert('Нет изменений для сохранения');
+        return;
+    }
 
+    $.ajax({
+        url: '/auto-plannogramma/update-values',
+        method: 'POST',
+        contentType: 'application/json',
+        data: JSON.stringify(changedValues),
+        success: function () {
+            alert('Изменения успешно сохранены');
+            $('input.input:visible').each(function () {
+                const $input = $(this);
+                const $arrow = $input.closest('div').find('svg path');
+                if ($arrow.attr('fill') === 'red') {
+                    $arrow.attr('fill', 'grey');
+                }
+            });
+        },
+        error: function (xhr) {
+            alert('Ошибка при сохранении: ' + xhr.responseText);
+        }
+    });
+});
 
+
+function resetStoreFilter() {
+    $('#year, #week').val('');
+    $('#city, #store-type, #territorial-manager, #polnogramma-type, #region, #bush_chef_florist, #district')
+        .val(null).trigger('change');
+
+    $('[data-store-id]').show();
+    updateFilterLabels({});
+}
+
+$('.btn-apply').on('click', function () {
+    applyStoreFilter();
+    $('tr.inserted-row').remove();
+});
+$('.btn-reset').on('click', resetStoreFilter);