}
+ public function actionTest()
+ {
+ $request = Yii::$app->request;
+
+ $filters = [
+ 'category' => 'Срезка',
+ 'subcategory' => 'Розы' ?? null,
+ 'species' => 'Роза' ?? null,
+ 'store_id' => 2 ?? [],
+ 'year' => 2025,
+ 'month' => 5,
+ 'type' => 'sales',
+ ];
+
+ $stores = ArrayHelper::map(
+ CityStore::findAll(['visible' => CityStore::IS_VISIBLE]),
+ 'id',
+ 'name'
+ );
+ // var_dump($stores);die();
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => [],
+ 'pagination' => ['pageSize' => 100],
+ ]);
+ $flatData =[];
+ $bouquetSpeciesForecast = [];
+ // Обработка даты на год и месяц
+ if (!empty($filters['year']) && !empty($filters['month'])) {
+ $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01';
+ // var_dump( $filters['plan_date']); die();
+ $service = new AutoPlannogrammaService();
+//$goals = $service->calculateFullGoalChain($filters);
+ $monthCategoryShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters);
+ $monthCategoryGoal = $service->getMonthCategoryGoal($monthCategoryShare, $filters['plan_date']);
+ $monthSubcategoryShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters);
+ $monthSubcategoryGoal = $service->getMonthSubcategoryGoal($monthSubcategoryShare, $monthCategoryGoal);
+ $monthSpeciesShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters);
+ $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesShare, $monthSubcategoryGoal);
+
+ if ($filters['type'] == AutoPlannogrammaService::TYPE_WRITE_OFFS) {
+ $monthCategoryWriteOffsShare = $service->getMonthCategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+ $monthCategoryWriteOffsGoal = $service->getMonthCategoryGoal($monthCategoryWriteOffsShare, $filters['plan_date'], $filters['type']);
+ $monthSubcategoryWriteOffsShare = $service->getMonthSubcategoryShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+ $monthSubcategoryWriteOffsGoals = $service->getMonthSubcategoryGoal($monthSubcategoryWriteOffsShare, $monthCategoryWriteOffsGoal, $filters['type']);
+ $monthSpeciesWriteOffShare = $service->getMonthSpeciesShareOrWriteOff($filters['plan_date'], $filters, $filters['type']);
+ $goals = $service->getMonthSpeciesGoalDirty($monthSpeciesWriteOffShare, $monthSubcategoryWriteOffsGoals, $filters['type'], $data);
+ }
+
+
+ $result = StorePlanService::calculateHistoricalShare(
+ $filters['store_id'],
+ $filters['month'],
+ $filters['year'],
+ $filters['category'],
+ $filters['subcategory'],
+ $filters['species']
+ );
+
+ $noHistoryProductData = $service->calculateSpeciesForecastForProductsWithoutHistory($filters['plan_date'], $filters);
+
+ $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' => $filters['year'], 'month' => $filters['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($filters['plan_date'], $filters);
+
+ $weeklySalesForecast = $service->calculateWeeklyProductForecastPieces($productForecastSpecies, $weeklySales);
+
+
+
+
+ $flatData = array_filter($weeklySalesForecast, 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;
+ });
+
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => $flatData,
+ 'pagination' => ['pageSize' => 100],
+ ]);
+ }
+ return $this->render('test', [
+ 'dataProvider' => $dataProvider,
+ 'data' => $flatData,
+ 'filters' => $filters,
+ 'stores' => $stores
+ ]);
+ }
+
+
}
--- /dev/null
+<?php
+use yii\helpers\Html;
+use kartik\select2\Select2;
+use yii\helpers\ArrayHelper;
+
+/** @var $data array - массив данных из контроллера */
+/** @var $stores array - массив магазинов id => name */
+
+// Группируем данные: category > subcategory > species > product_id
+$grouped = [];
+foreach ($data as $row) {
+ $cat = $row['category'];
+ $sub = $row['subcategory'];
+ $sp = $row['species'];
+ $pid = $row['product_id'];
+ $sid = $row['store_id'];
+
+ $grouped[$cat][$sub][$sp][$pid]['values'][$sid] = $row['forecast_week_pieces'];
+ $grouped[$cat][$sub][$sp][$pid]['product_id'] = $pid;
+}
+
+$storeHasData = [];
+foreach ($grouped as $cat => $subcats) {
+ foreach ($subcats as $sub => $species) {
+ foreach ($species as $sp => $products) {
+ foreach ($products as $p) {
+ foreach ($p['values'] as $storeId => $v) {
+ $storeHasData[$storeId] = true;
+ }
+ }
+ }
+ }
+}
+?>
+
+<div class="container py-3">
+ <h2>Автопланограмма</h2>
+ <button class="btn btn-primary mb-2" onclick="toggleFilters()">Показать / Скрыть фильтры</button>
+
+ <div id="filters" style="display: none;">
+ <div class="row g-2">
+ <div class="col-md">
+ <?= Select2::widget([
+ 'name' => 'year-filter',
+ 'data' => array_combine(range(date('Y') - 5, date('Y') + 5), range(date('Y') - 5, date('Y') + 5)),
+ 'options' => ['placeholder' => 'Год', 'id' => 'year'],
+ 'pluginOptions' => ['allowClear' => true],
+ ]) ?>
+ </div>
+ </div>
+ </div>
+
+ <div style="overflow-x: auto;">
+ <div class="pseudo-table" style="display: grid; grid-template-columns: 300px repeat(<?= count($storeHasData) ?>, 80px);">
+ <div class="header-cell"></div>
+ <?php foreach ($stores as $storeId => $storeName): ?>
+ <?php if (!isset($storeHasData[$storeId])) continue; ?>
+ <div class="header-cell vertical"> <?= Html::encode($storeName) ?> </div>
+ <?php endforeach; ?>
+
+ <?php foreach ($grouped as $category => $subcategories): ?>
+ <div class="row-label category" data-category="<?= $category ?>" onclick="toggleByAttr('category', '<?= $category ?>')">▶ <?= Html::encode($category) ?></div>
+ <?php foreach ($storeHasData as $_ => $__) echo '<div></div>'; ?>
+
+ <?php foreach ($subcategories as $subcategory => $speciesList): ?>
+ <div class="row-label subcategory" data-category="<?= $category ?>" data-subcategory="<?= $subcategory ?>" style="padding-left: 1rem; display: none;" onclick="toggleByAttr('subcategory', '<?= $subcategory ?>', 'category', '<?= $category ?>')">▶ <?= Html::encode($subcategory) ?></div>
+ <?php foreach ($storeHasData as $_ => $__) echo '<div style="display: none;" data-category="' . $category . '" data-subcategory="' . $subcategory . '"></div>'; ?>
+
+ <?php foreach ($speciesList as $species => $products): ?>
+ <div class="row-label species" data-category="<?= $category ?>" data-subcategory="<?= $subcategory ?>" data-species="<?= $species ?>" style="padding-left: 2rem; display: none;" onclick="toggleByAttr('species', '<?= $species ?>', 'subcategory', '<?= $subcategory ?>')">▶ <?= Html::encode($species) ?></div>
+ <?php foreach ($storeHasData as $_ => $__) echo '<div style="display: none;" data-category="' . $category . '" data-subcategory="' . $subcategory . '" data-species="' . $species . '"></div>'; ?>
+
+ <?php foreach ($products as $productId => $info): ?>
+ <div class="row-label product" data-category="<?= $category ?>" data-subcategory="<?= $subcategory ?>" data-species="<?= $species ?>" style="padding-left: 3rem; display: none;">Товар <?= Html::encode($productId) ?></div>
+ <?php foreach ($stores as $storeId => $storeName): ?>
+ <?php if (!isset($storeHasData[$storeId])) continue; ?>
+ <?php $val = $info['values'][$storeId] ?? ''; ?>
+ <div class="cell input-cell" data-category="<?= $category ?>" data-subcategory="<?= $subcategory ?>" data-species="<?= $species ?>" style="display: none;">
+ <input type="text" value="<?= $val ?>" onchange="markChanged(this)">
+ <button onclick="resetValue(this, '<?= $val ?>')">↩</button>
+ </div>
+ <?php endforeach; ?>
+ <?php endforeach; ?>
+ <?php endforeach; ?>
+ <?php endforeach; ?>
+ <?php endforeach; ?>
+ </div>
+ </div>
+</div>
+
+<script>
+function toggleFilters() {
+ const filters = document.getElementById('filters');
+ filters.style.display = filters.style.display === 'none' ? 'block' : 'none';
+}
+
+function toggleByAttr(level, value, parentLevel = null, parentValue = null) {
+ const trigger = event.currentTarget;
+ const open = trigger.textContent.includes('▶');
+ trigger.textContent = trigger.textContent.replace(open ? '▶' : '▼', open ? '▼' : '▶');
+
+ const selector = `[data-${level}="${value}"]`;
+ document.querySelectorAll(selector).forEach(el => {
+ el.style.display = open ? 'grid' : 'none';
+ });
+
+ // закрыть дочерние, если свернули родителя
+ if (!open && level === 'category') {
+ document.querySelectorAll(`[data-category="${value}"]`).forEach(el => {
+ if (el.dataset.subcategory || el.dataset.species) {
+ el.style.display = 'none';
+ if (el.textContent.includes('▼')) el.textContent = el.textContent.replace('▼', '▶');
+ }
+ });
+ }
+ if (!open && level === 'subcategory') {
+ document.querySelectorAll(`[data-subcategory="${value}"]`).forEach(el => {
+ if (el.dataset.species) {
+ el.style.display = 'none';
+ if (el.textContent.includes('▼')) el.textContent = el.textContent.replace('▼', '▶');
+ }
+ });
+ }
+}
+
+function markChanged(input) {
+ input.style.backgroundColor = '#ffffcc';
+}
+
+function resetValue(button, val) {
+ const input = button.previousElementSibling;
+ input.value = val;
+ input.style.backgroundColor = '';
+}
+</script>
+
+<style>
+.header-cell, .row-label, .cell {
+ border: 1px solid #ccc;
+ padding: 4px;
+ background: #fafafa;
+ text-align: center;
+}
+.row-label { font-weight: bold; cursor: pointer; text-align: left; }
+.input-cell { display: flex; gap: 4px; align-items: center; justify-content: center; }
+.input-cell input { width: 45px; }
+.header-cell.vertical {
+ writing-mode: vertical-rl;
+ transform: rotate(180deg);
+ white-space: nowrap;
+ font-size: 12px;
+}
+</style>