--- /dev/null
+<?php
+declare(strict_types = 1);
+
+namespace yii_app\actions\dashboard;
+
+use Yii;
+use yii\base\Action;
+use yii\helpers\ArrayHelper;
+use yii\widgets\ActiveForm;
+use yii_app\forms\dashboard\DaysSearchForm;
+use yii_app\helpers\DateHelper;
+use yii_app\records\CityStore;
+use yii_app\records\ExportImportTable;
+use yii_app\records\Products1c;
+use yii_app\records\Sales;
+use yii_app\services\ExportImportService;
+
+/**
+ * Детальный просмотр продаж по магазинам с записями из таблицы sales
+ * Показывает отдельные чеки для офлайн и доставки
+ * @package yii_app\actions\dashboard
+ */
+class SalesDetailAction extends Action
+{
+ /**
+ * @throws \Exception
+ */
+ public function run()
+ {
+ $request = Yii::$app->getRequest();
+
+ $date1 = date("Y-m-d", time());
+ $date2 = date("Y-m-d", time());
+
+ $daysSearchForm = new DaysSearchForm();
+
+ if (Yii::$app->request->isAjax && $daysSearchForm->load(Yii::$app->request->post())) {
+ Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+ return ActiveForm::validate($daysSearchForm);
+ }
+
+ if ($request->isPost && !empty($this->controller->request->post())) {
+ $daysSearchForm->load($this->controller->request->post());
+ $daysSearchForm->validate();
+ }
+
+ if (empty($daysSearchForm->dateFrom)) {
+ $daysSearchForm->dateFrom = $date1;
+ }
+
+ if (empty($daysSearchForm->dateTo)) {
+ $daysSearchForm->dateTo = $date2;
+ }
+
+ $getDate1 = $daysSearchForm->dateFrom;
+
+ if (!empty($getDate1)) {
+ $date_s = htmlentities($getDate1);
+ $date1 = "$date_s";
+ $date2 = "$date_s";
+ }
+
+ $getDate2 = $daysSearchForm->dateTo;
+
+ if (!empty($getDate2)) {
+ $date_s = htmlentities($getDate2);
+ $date2 = "$date_s";
+ }
+
+ $entityByCityStore = ExportImportService::getEntityByCityStore();
+ $exportData = ExportImportService::getExportData($entityByCityStore);
+
+ $export = ArrayHelper::getValue($exportData, 'export');
+ $export_revers = ArrayHelper::getValue($exportData, 'export_revers');
+
+ $storeIds = array_values($export_revers);
+
+ $cityStores = CityStore::find()
+ ->select(['id', 'name', 'export_val'])
+ ->joinWith('storeGuid')
+ ->andWhere(['id' => $storeIds])
+ ->indexBy('id')
+ ->asArray()
+ ->all();
+
+ $city_stores = Products1c::find()
+ ->select(['name', 'id'])
+ ->indexBy('id')
+ ->andWhere('tip=:tip', [':tip' => 'city_store'])
+ ->column();
+
+ // Получение продаж без доставки (офлайн)
+ $salesOffline = $this->getSalesDetailRecords($date1, $date2, false);
+
+ // Получение продаж с доставкой
+ $salesDelivery = $this->getSalesDetailRecords($date1, $date2, true);
+
+ // Группировка по магазинам
+ $salesOfflineByStore = $this->groupSalesByStore($salesOffline);
+ $salesDeliveryByStore = $this->groupSalesByStore($salesDelivery);
+
+ $params = [
+ 'date1' => $date1,
+ 'date2' => $date2,
+ 'daysSearchForm' => $daysSearchForm,
+ 'cityStores' => $cityStores,
+ 'city_stores' => $city_stores,
+ 'export_revers' => $export_revers,
+ 'salesOfflineByStore' => $salesOfflineByStore,
+ 'salesDeliveryByStore' => $salesDeliveryByStore,
+ ];
+
+ return $this->controller->render('/dashboard/sales-detail.php', $params);
+ }
+
+ /**
+ * Получить детальные записи о продажах
+ *
+ * @param string $dateFrom
+ * @param string $dateTo
+ * @param bool $withDelivery - true для доставки, false для офлайн
+ * @return array
+ * @throws \Exception
+ */
+ private function getSalesDetailRecords(string $dateFrom, string $dateTo, bool $withDelivery = false): array
+ {
+ $query = Sales::find()
+ ->alias('s')
+ ->select([
+ 's.id',
+ 's.date',
+ 's.summ',
+ 's.skidka',
+ 's.operation',
+ 's.order_id',
+ 's.store_id_1c',
+ 's.store_id',
+ 's.number',
+ ])
+ ->joinWith('saleCheck')
+ ->andWhere(['>=', 's.date', DateHelper::getDateTimeStartDay($dateFrom, true)])
+ ->andWhere(['<=', 's.date', DateHelper::getDateTimeEndDay($dateTo, true)])
+ ->orderBy(['s.date' => SORT_DESC, 's.id' => SORT_DESC]);
+
+ if (!$withDelivery) {
+ // Офлайн продажи - без заказов
+ $query->andWhere([
+ 'and',
+ ['s.order_id' => ['', '0']],
+ [
+ 'or',
+ 'sc.order_id IS NULL',
+ ['sc.order_id' => ['', '0']]
+ ]
+ ]);
+ } else {
+ // Продажи с доставкой - с заказами
+ $query->leftJoin('create_checks cc', 'CAST(cc.order_id AS TEXT) = s.order_id')
+ ->andFilterWhere(['or',
+ ['not in', 's.order_id', ['', '0']],
+ ['not in', 'sc.order_id', ['', '0']]
+ ])
+ ->andWhere(['or',
+ ['cc.date' => null],
+ ['DATE(cc.date)' => new \yii\db\Expression('DATE(s.date)')]
+ ]);
+ }
+
+ return $query->asArray()->all();
+ }
+
+ /**
+ * Группировка продаж по магазинам
+ *
+ * @param array $sales
+ * @return array
+ */
+ private function groupSalesByStore(array $sales): array
+ {
+ $grouped = [];
+
+ foreach ($sales as $sale) {
+ $storeId1c = $sale['store_id_1c'];
+
+ if (!isset($grouped[$storeId1c])) {
+ $grouped[$storeId1c] = [
+ 'store_id_1c' => $storeId1c,
+ 'sales' => [],
+ 'total_summ' => 0,
+ 'total_skidka' => 0,
+ ];
+ }
+
+ $saleAmount = $sale['summ'] - $sale['skidka'];
+
+ // Применяем операцию (продажа или возврат)
+ if ($sale['operation'] === Sales::OPERATION_SALE) {
+ $grouped[$storeId1c]['total_summ'] += $saleAmount;
+ } elseif ($sale['operation'] === Sales::OPERATION_RETURN) {
+ $grouped[$storeId1c]['total_summ'] -= $saleAmount;
+ }
+
+ $grouped[$storeId1c]['sales'][] = $sale;
+ }
+
+ // Сортировка магазинов по сумме продаж (убывание)
+ uasort($grouped, function ($a, $b) {
+ return $b['total_summ'] <=> $a['total_summ'];
+ });
+
+ return $grouped;
+ }
+}
+
--- /dev/null
+<?php
+/**
+ * @var $date1
+ * @var $date2
+ * @var $daysSearchForm
+ * @var $cityStores
+ * @var $city_stores
+ * @var $export_revers
+ * @var $salesOfflineByStore
+ * @var $salesDeliveryByStore
+ * @var \yii\web\View $this
+ */
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+?>
+
+<h2>Детальный просмотр продаж</h2>
+
+<?php $searchForm = ActiveForm::begin([
+ 'id' => 'days-search-form-detail',
+ 'enableAjaxValidation' => true,
+ 'validationUrl' => 'validate',
+ 'options' => ['enctype' => 'multipart/form-data']
+]); ?>
+
+<div class="row mb-3">
+ <div class="col-lg-4">
+ <?= $searchForm->field($daysSearchForm, 'dateFrom', [
+ 'inputOptions' => [
+ 'class' => 'form-control datetime',
+ 'type' => 'date',
+ 'placeholder' => 'начало',
+ ],
+ 'options' => ['tag' => null],
+ ])->label(false)->textInput() ?>
+ </div>
+ <div class="col-lg-4">
+ <?= $searchForm->field($daysSearchForm, 'dateTo', [
+ 'inputOptions' => [
+ 'class' => 'form-control datetime',
+ 'type' => 'date',
+ 'placeholder' => 'конец',
+ ],
+ 'options' => ['tag' => null],
+ ])->label(false)->textInput() ?>
+ </div>
+ <div class="col-lg-4">
+ <div class="form-group">
+ <label class="control-label"> </label>
+ <?= Html::submitButton('Показать', ['class' => 'btn btn-success']) ?>
+ </div>
+ </div>
+</div>
+
+<?php ActiveForm::end() ?>
+
+<div class="sales-detail-container">
+ <?php if (!empty($salesOfflineByStore) || !empty($salesDeliveryByStore)): ?>
+
+ <!-- ПРОДАЖИ БЕЗ ДОСТАВКИ (ОФЛАЙН) -->
+ <div class="mb-5">
+ <h3 class="text-primary">
+ <i class="fas fa-store"></i> Офлайн продажи (без доставки) за <?= $date1 ?>
+ </h3>
+
+ <?php if (!empty($salesOfflineByStore)): ?>
+ <div class="accordion" id="accordionOffline">
+ <?php foreach ($salesOfflineByStore as $storeId1c => $storeData): ?>
+ <?php
+ $storeNameExport = $city_stores[$storeId1c] ?? 'Неизвестный магазин';
+ $storeName = $storeNameExport;
+ $totalSumm = $storeData['total_summ'];
+ $salesCount = count($storeData['sales']);
+ ?>
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="headingOffline<?= htmlspecialchars($storeId1c) ?>">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+ data-bs-target="#collapseOffline<?= htmlspecialchars($storeId1c) ?>"
+ aria-expanded="false"
+ aria-controls="collapseOffline<?= htmlspecialchars($storeId1c) ?>">
+ <strong><?= Html::encode($storeName) ?></strong>
+ <span class="badge bg-info ms-2"><?= $salesCount ?> чеков</span>
+ <span class="badge bg-success ms-2"><?= number_format($totalSumm, 2, '.', ' ') ?> ₽</span>
+ </button>
+ </h2>
+ <div id="collapseOffline<?= htmlspecialchars($storeId1c) ?>" class="accordion-collapse collapse"
+ aria-labelledby="headingOffline<?= htmlspecialchars($storeId1c) ?>"
+ data-bs-parent="#accordionOffline">
+ <div class="accordion-body p-0">
+ <table class="table table-sm table-hover mb-0">
+ <thead class="table-light">
+ <tr>
+ <th style="width: 60px;">ID</th>
+ <th style="width: 180px;">Дата и время</th>
+ <th style="width: 100px;">Сумма</th>
+ <th style="width: 80px;">Операция</th>
+ <th style="width: 100px;">Номер</th>
+ <th style="width: 80px;">Order ID</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($storeData['sales'] as $sale): ?>
+ <?php
+ $saleAmount = $sale['summ'] - $sale['skidka'];
+ $operationClass = $sale['operation'] === 'Продажа' ? 'text-success' : 'text-danger';
+ $operationIcon = $sale['operation'] === 'Продажа' ? '✓' : '✕';
+ $dateTime = date('d.m.Y H:i:s', strtotime($sale['date']));
+ ?>
+ <tr>
+ <td><small class="text-muted"><?= $sale['id'] ?></small></td>
+ <td><small><?= $dateTime ?></small></td>
+ <td>
+ <strong class="<?= $operationClass ?>">
+ <?= number_format($saleAmount, 2, '.', ' ') ?>
+ </strong>
+ </td>
+ <td>
+ <span class="badge <?= $sale['operation'] === 'Продажа' ? 'bg-success' : 'bg-danger' ?>">
+ <?= Html::encode($sale['operation']) ?>
+ </span>
+ </td>
+ <td><small><?= Html::encode($sale['number']) ?></small></td>
+ <td>
+ <?php if (!empty($sale['order_id']) && $sale['order_id'] !== '0'): ?>
+ <small class="badge bg-warning">
+ <?= Html::encode($sale['order_id']) ?>
+ </small>
+ <?php else: ?>
+ <small class="text-muted">—</small>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ <?php else: ?>
+ <div class="alert alert-info">Нет данных об офлайн продажах за выбранный период</div>
+ <?php endif; ?>
+ </div>
+
+ <!-- ПРОДАЖИ С ДОСТАВКОЙ -->
+ <div class="mb-5">
+ <h3 class="text-info">
+ <i class="fas fa-truck"></i> Продажи с доставкой за <?= $date1 ?>
+ </h3>
+
+ <?php if (!empty($salesDeliveryByStore)): ?>
+ <div class="accordion" id="accordionDelivery">
+ <?php foreach ($salesDeliveryByStore as $storeId1c => $storeData): ?>
+ <?php
+ $storeNameExport = $city_stores[$storeId1c] ?? 'Неизвестный магазин';
+ $storeName = $storeNameExport;
+ $totalSumm = $storeData['total_summ'];
+ $salesCount = count($storeData['sales']);
+ ?>
+ <div class="accordion-item">
+ <h2 class="accordion-header" id="headingDelivery<?= htmlspecialchars($storeId1c) ?>">
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
+ data-bs-target="#collapseDelivery<?= htmlspecialchars($storeId1c) ?>"
+ aria-expanded="false"
+ aria-controls="collapseDelivery<?= htmlspecialchars($storeId1c) ?>">
+ <strong><?= Html::encode($storeName) ?></strong>
+ <span class="badge bg-info ms-2"><?= $salesCount ?> заказов</span>
+ <span class="badge bg-success ms-2"><?= number_format($totalSumm, 2, '.', ' ') ?> ₽</span>
+ </button>
+ </h2>
+ <div id="collapseDelivery<?= htmlspecialchars($storeId1c) ?>" class="accordion-collapse collapse"
+ aria-labelledby="headingDelivery<?= htmlspecialchars($storeId1c) ?>"
+ data-bs-parent="#accordionDelivery">
+ <div class="accordion-body p-0">
+ <table class="table table-sm table-hover mb-0">
+ <thead class="table-light">
+ <tr>
+ <th style="width: 60px;">ID</th>
+ <th style="width: 180px;">Дата и время</th>
+ <th style="width: 100px;">Сумма</th>
+ <th style="width: 80px;">Операция</th>
+ <th style="width: 100px;">Номер</th>
+ <th style="width: 150px;">Order ID</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($storeData['sales'] as $sale): ?>
+ <?php
+ $saleAmount = $sale['summ'] - $sale['skidka'];
+ $operationClass = $sale['operation'] === 'Продажа' ? 'text-success' : 'text-danger';
+ $dateTime = date('d.m.Y H:i:s', strtotime($sale['date']));
+ ?>
+ <tr>
+ <td><small class="text-muted"><?= $sale['id'] ?></small></td>
+ <td><small><?= $dateTime ?></small></td>
+ <td>
+ <strong class="<?= $operationClass ?>">
+ <?= number_format($saleAmount, 2, '.', ' ') ?>
+ </strong>
+ </td>
+ <td>
+ <span class="badge <?= $sale['operation'] === 'Продажа' ? 'bg-success' : 'bg-danger' ?>">
+ <?= Html::encode($sale['operation']) ?>
+ </span>
+ </td>
+ <td><small><?= Html::encode($sale['number']) ?></small></td>
+ <td>
+ <?php if (!empty($sale['order_id']) && $sale['order_id'] !== '0'): ?>
+ <small class="badge bg-warning text-dark">
+ <?= Html::encode($sale['order_id']) ?>
+ </small>
+ <?php else: ?>
+ <small class="text-muted">—</small>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ <?php else: ?>
+ <div class="alert alert-info">Нет данных о доставке за выбранный период</div>
+ <?php endif; ?>
+ </div>
+
+ <?php else: ?>
+ <div class="alert alert-warning">
+ <strong>Нет данных</strong> за выбранный период <?= $date1 ?> - <?= $date2 ?>
+ </div>
+ <?php endif; ?>
+</div>
+
+<?php
+// CSS для оформления
+$this->registerCss('
+.sales-detail-container {
+ padding: 15px;
+}
+
+.accordion-button {
+ padding: 12px 15px;
+ font-size: 14px;
+}
+
+.accordion-button:not(.collapsed) {
+ background-color: #f8f9fa;
+ color: #000;
+}
+
+.table-hover tbody tr:hover {
+ background-color: #f5f5f5;
+}
+
+.badge {
+ margin-left: 5px;
+ padding: 5px 8px;
+}
+
+.text-muted {
+ color: #999;
+}
+
+.text-success {
+ color: #28a745;
+}
+
+.text-danger {
+ color: #dc3545;
+}
+');
+?>
+