<?php
+declare(strict_types=1);
namespace app\controllers;
-use app\controllers\BaseController;
use Exception;
use Yii;
use yii\helpers\Json;
-use yii_app\records\ApiCronTest;
+use yii_app\records\Admin;
use yii_app\records\AdminGradeHistory;
+use yii_app\records\ApiCronTest;
+use yii_app\records\CityStore;
+use yii_app\records\CityStoreParams;
+use yii_app\records\EmployeePosition;
use yii_app\records\ExportImportTable;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1c;
+use yii_app\records\Sales;
+use yii_app\records\SalesProducts;
+use yii_app\records\Timetable;
use yii_app\records\TimetableFact;
-class DataTestController extends BaseController {
+class DataTestController extends BaseController
+{
- public function actionRequest() {
+ public function actionRequest()
+ {
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$request = Yii::$app->request->getRawBody();
try {
$result = Json::decode($request);
- } catch (Exception $ex) {
+ } catch (Exception) {
return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
}
$apiCronTest->status = 1;
$apiCronTest->date_up = date("Y-m-d H:i:s");
- $validate = $apiCronTest->validate();
- if ($validate) {
+ if ($apiCronTest->validate()) {
$apiCronTest->save();
}
}
} catch (Exception $e) {
-
+ Yii::error($e->getMessage(), 'api2.data-test');
+ return $this->asJson(["error_id" => 2, "error" => "Internal server error"]);
}
return $this->asJson(json_decode($json, true));
}
- public function actionUpload() {
- set_time_limit(600);
+ public function actionUpload()
+ {
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$request = Yii::$app->request->getRawBody();
try {
$result = Json::decode($request);
- } catch (Exception $ex) {
+ } catch (Exception) {
return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
}
- $requestId = '';
- if (!empty($result["request_id"])) {
- $requestId = $result["request_id"];
- }
+ $requestId = !empty($result["request_id"]) ? $result["request_id"] : '';
try {
-
-
$apiCronTest = new ApiCronTest();
$apiCronTest->date = date("Y-m-d H:i:s");
$apiCronTest->date_up = date("Y-m-d H:i:s");
$apiCronTest->json_post = $request;
$apiCronTest->request_id = $requestId;
$apiCronTest->direct_id = 2;
- $validate = $apiCronTest->validate();
- if ($validate) {
- $apiCronTest->save();
- }
+ if ($apiCronTest->validate()) {
+ if (!$apiCronTest->save()) {
+ Yii::error('ApiCronTest save failed: ' . Json::encode($apiCronTest->errors), 'api2.data-test');
+ return $this->asJson(['success' => false, 'error' => 'Internal server error']);
+ }
+ }
} catch (Exception $e) {
-
+ Yii::error($e->getMessage(), 'api2.data-test');
+ return $this->asJson(['success' => false, 'error' => 'Internal server error']);
}
return $this->asJson(['response' => true]);
* Возвращает JSON со списком магазинов (id, name)
* GET /api2/data-test/get-stores
*/
- public function actionGetStores() {
+ public function actionGetStores()
+ {
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
try {
- $stores = \yii_app\records\CityStore::find()
+ $stores = CityStore::find()
->select(['id', 'name'])
->orderBy(['name' => SORT_ASC])
->asArray()
'data' => $stores
]);
} catch (Exception $e) {
+ Yii::error($e->getMessage(), 'api2.data-test');
return $this->asJson([
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => 'Internal server error'
]);
}
}
* 1 = с 8:00 до 20:00 (день)
* 2 = с 20:00 до 8:00 (ночь)
*/
- public function actionGetSales() {
- set_time_limit(300);
+ public function actionGetSales()
+ {
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$request = Yii::$app->request->getRawBody();
try {
$input = Json::decode($request);
- } catch (Exception $ex) {
+ } catch (Exception) {
return $this->asJson([
'success' => false,
'error' => 'Invalid JSON body'
]);
}
- // Валидация обязательных параметров
if (empty($input['date'])) {
return $this->asJson([
'success' => false,
]);
}
- $date = $input['date'];
- $shiftType = $input['shift_type'] ?? 0;
+ $date = $input['date'];
+ $shiftType = $input['shift_type'] ?? 0;
$storeIdFilter = $input['store_id'] ?? null;
try {
- // Определяем диапазон времени для смены
- $dateObj = new \DateTime($date);
+ $dateObj = new \DateTime($date);
$currentDateStr = $dateObj->format('Y-m-d');
+ $nextDateStr = (clone $dateObj)->modify('+1 day')->format('Y-m-d');
- // Получаем дату следующего дня
- $nextDateObj = clone $dateObj;
- $nextDateStr = $nextDateObj->modify('+1 day')->format('Y-m-d');
-
- $dayStart = $currentDateStr . ' 08:00:00';
- $dayEnd = $currentDateStr . ' 20:00:00';
- $nightStart = $currentDateStr . ' 20:00:00';
- $nightEnd = $currentDateStr . ' 23:59:59';
- $nextDayStart = $nextDateStr . ' 00:00:00';
- $nextDayEnd = $nextDateStr . ' 08:00:00';
-
- // Построение базового запроса продаж
- $query = \yii_app\records\Sales::find()
- ->where(['or', ['order_id' => ''], ['order_id' => 0]]) // Только офлайн продажи
+ $query = Sales::find()
+ ->where(['or', ['order_id' => ''], ['order_id' => 0]])
->orderBy(['store_id' => SORT_ASC, 'date' => SORT_ASC]);
- // Фильтр по смене
if ($shiftType === 1) {
- // День: 8:00 - 20:00
- $query->andWhere(['between', 'date', $dayStart, $dayEnd]);
+ $query->andWhere(['between', 'date', $currentDateStr . ' 08:00:00', $currentDateStr . ' 20:00:00']);
} elseif ($shiftType === 2) {
- // Ночь: 20:00 - 8:00 следующего дня
$query->andWhere(['OR',
- ['between', 'date', $nightStart, $nightEnd],
- ['between', 'date', $nextDayStart, $nextDayEnd]
+ ['between', 'date', $currentDateStr . ' 20:00:00', $currentDateStr . ' 23:59:59'],
+ ['between', 'date', $nextDateStr . ' 00:00:00', $nextDateStr . ' 08:00:00'],
]);
} else {
- // Все смены (вся дата)
$query->andWhere(['between', 'date', $currentDateStr . ' 00:00:00', $currentDateStr . ' 23:59:59']);
}
- // Фильтр по магазину если задан
if (!empty($storeIdFilter)) {
$query->andWhere(['store_id' => $storeIdFilter]);
}
- $sales = $query->with('products')->asArray()->all();
+ $sales = $query->asArray()->all();
if (empty($sales)) {
- return $this->asJson([
- 'success' => true,
- 'data' => []
- ]);
+ return $this->asJson(['success' => true, 'data' => []]);
}
- // Агрегируем данные и присоединяем информацию о товарах
- $aggregated = [];
- $storeRegions = [];
+ // Bulk-load store regions (avoid per-sale query)
+ $storeIds = array_unique(array_column($sales, 'store_id'));
+ $storeParamRows = CityStoreParams::find()
+ ->select(['store_id', 'address_region'])
+ ->where(['store_id' => $storeIds])
+ ->asArray()
+ ->all();
+ $storeRegions = array_column($storeParamRows, 'address_region', 'store_id');
- foreach ($sales as $sale) {
- $saleStoreId = $sale['store_id'];
+ // Bulk-load all products for all sales at once
+ $saleIds = array_column($sales, 'id');
+ $allSaleProducts = SalesProducts::find()
+ ->where(['check_id' => $saleIds])
+ ->asArray()
+ ->all();
+
+ $productsBySale = [];
+ foreach ($allSaleProducts as $sp) {
+ $productsBySale[$sp['check_id']][] = $sp;
+ }
+
+ // Bulk-load product names
+ $allProductIds = array_unique(array_column($allSaleProducts, 'product_id'));
+ $productInfoMap = [];
+ if (!empty($allProductIds)) {
+ $productInfoMap = Products1c::find()
+ ->select(['id', 'name'])
+ ->where(['id' => $allProductIds])
+ ->indexBy('id')
+ ->asArray()
+ ->all();
+ }
- // Кэшируем регион магазина
- if (!isset($storeRegions[$saleStoreId])) {
- $storeParams = \yii_app\records\CityStoreParams::findOne(['store_id' => $saleStoreId]);
- $storeRegions[$saleStoreId] = $storeParams ? $storeParams->address_region : null;
+ // Bulk-load active prices; keep latest per (product_id, region_id)
+ $pricesByProduct = [];
+ if (!empty($allProductIds)) {
+ $priceRows = PricesDynamic::find()
+ ->select(['product_id', 'region_id', 'price'])
+ ->where(['product_id' => $allProductIds, 'active' => PricesDynamic::ACTIVE])
+ ->orderBy(['date_from' => SORT_DESC])
+ ->asArray()
+ ->all();
+
+ foreach ($priceRows as $pr) {
+ $pid = $pr['product_id'];
+ $rid = $pr['region_id']; // null → '' as PHP array key
+ if (!isset($pricesByProduct[$pid][$rid])) {
+ $pricesByProduct[$pid][$rid] = (float)$pr['price'];
+ }
+ // Any-region fallback: first = latest
+ if (!isset($pricesByProduct[$pid]['_latest'])) {
+ $pricesByProduct[$pid]['_latest'] = (float)$pr['price'];
+ }
}
+ }
- $saleKey = $saleStoreId . '_' . $sale['date']; // Ключ для агрегации
+ $aggregated = [];
+
+ foreach ($sales as $sale) {
+ $saleStoreId = $sale['store_id'];
+ $regionId = $storeRegions[$saleStoreId] ?? null;
+ $saleKey = $saleStoreId . '_' . $sale['date'];
if (!isset($aggregated[$saleKey])) {
$aggregated[$saleKey] = [
- 'id' => $sale['id'],
- 'date' => $sale['date'],
- 'store_id' => (int)$saleStoreId,
- 'summ' => (float)$sale['summ'],
+ 'id' => $sale['id'],
+ 'date' => $sale['date'],
+ 'store_id' => (int)$saleStoreId,
+ 'summ' => (float)$sale['summ'],
'operation' => $sale['operation'],
- 'status' => $sale['status'],
- 'skidka' => (float)$sale['skidka'],
- 'products' => []
+ 'status' => $sale['status'],
+ 'skidka' => (float)$sale['skidka'],
+ 'products' => []
];
} else {
- // Суммируем значения
- $aggregated[$saleKey]['summ'] += (float)$sale['summ'];
+ $aggregated[$saleKey]['summ'] += (float)$sale['summ'];
$aggregated[$saleKey]['skidka'] += (float)$sale['skidka'];
}
- // Получаем товары из этого чека
- $products = \yii_app\records\SalesProducts::find()
- ->where(['check_id' => $sale['id']])
- ->asArray()
- ->all();
+ foreach ($productsBySale[$sale['id']] ?? [] as $product) {
+ $pid = $product['product_id'];
- foreach ($products as $product) {
- // Получаем информацию о товаре
- $productInfo = \yii_app\records\Products1c::find()
- ->where(['id' => $product['product_id']])
- ->select(['id', 'name'])
- ->asArray()
- ->one();
-
- // Получаем актуальную цену товара
- $regionId = $storeRegions[$saleStoreId];
- $priceQuery = \yii_app\records\PricesDynamic::find()
- ->where(['product_id' => $product['product_id']])
- ->andWhere(['active' => \yii_app\records\PricesDynamic::ACTIVE]);
-
- // Приоритет: сначала ищем с регионом, потом без
- if ($regionId) {
- $priceQuery->andWhere(['region_id' => $regionId]);
+ // Region price → any-region latest → sale line price
+ if ($regionId !== null && isset($pricesByProduct[$pid][$regionId])) {
+ $price = $pricesByProduct[$pid][$regionId];
+ } elseif (isset($pricesByProduct[$pid]['_latest'])) {
+ $price = $pricesByProduct[$pid]['_latest'];
+ } else {
+ $price = (float)$product['price'];
}
- $priceData = $priceQuery->select(['price'])
- ->orderBy(['date_from' => SORT_DESC])
- ->asArray()
- ->one();
-
- $productData = [
- 'product_id' => $product['product_id'],
- 'name' => $productInfo['name'] ?? 'Unknown',
- 'quantity' => (float)$product['quantity'],
- 'price' => $priceData ? (float)$priceData['price'] : (float)$product['price'],
- 'discount' => (float)$product['discount'],
- 'summ' => (float)$product['summ']
+ $aggregated[$saleKey]['products'][] = [
+ 'product_id' => $pid,
+ 'name' => $productInfoMap[$pid]['name'] ?? 'Unknown',
+ 'quantity' => (float)$product['quantity'],
+ 'price' => $price,
+ 'discount' => (float)$product['discount'],
+ 'summ' => (float)$product['summ']
];
-
- $aggregated[$saleKey]['products'][] = $productData;
}
}
$grouped = [];
foreach ($aggregated as $sale) {
$storeId = $sale['store_id'];
- // Убираем store_id из объекта продажи, так как он будет в ключе
unset($sale['store_id']);
$grouped[$storeId][] = $sale;
}
]);
} catch (Exception $e) {
+ Yii::error($e->getMessage(), 'api2.data-test');
return $this->asJson([
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => 'Internal server error'
]);
}
}
try {
$input = Json::decode($request);
- } catch (Exception $ex) {
+ } catch (Exception) {
return $this->asJson(['success' => false, 'error' => 'Invalid JSON body']);
}
return $this->asJson(['success' => false, 'error' => 'date is required']);
}
- $storeId = (int) $input['store_id'];
+ $storeId = (int)$input['store_id'];
$date = $input['date'];
- $shiftType = (int) ($input['shift_type'] ?? 0);
+ $shiftType = (int)($input['shift_type'] ?? 0);
try {
- $dateObj = new \DateTime($date);
- $nextDateStr = (clone $dateObj)->modify('+1 day')->format('Y-m-d');
+ $nextDateStr = (new \DateTime($date))->modify('+1 day')->format('Y-m-d');
+ // Только фактически работавшие (не отпуск, не больничный, не выходной)
$query = TimetableFact::find()
- ->select(['timetable.admin_id'])
- ->andWhere(['store_id' => $storeId]);
+ ->select(['admin_id'])
+ ->andWhere(['store_id' => $storeId])
+ ->andWhere(['slot_type_id' => [Timetable::TIMESLOT_WORK, Timetable::TIMESLOT_INTERNSHIP]]);
if ($shiftType === 1) {
- // Дневная: 08:00 – 20:00
$query->andWhere(['between', 'datetime_start',
$date . ' 08:00:00',
$date . ' 19:59:59',
]);
} elseif ($shiftType === 2) {
- // Ночная: 20:00 – 08:00 следующего дня
$query->andWhere(['OR',
['between', 'datetime_start', $date . ' 20:00:00', $date . ' 23:59:59'],
['between', 'datetime_start', $nextDateStr . ' 00:00:00', $nextDateStr . ' 07:59:59'],
]);
} else {
- // Все смены за дату
$query->andWhere(['between', 'datetime_start',
$date . ' 00:00:00',
$date . ' 23:59:59',
$adminIds = array_unique(array_column($rows, 'admin_id'));
- // Получаем данные сотрудников (имя, должность)
- $admins = \yii_app\records\Admin::find()
+ $admins = Admin::find()
->select(['id', 'name_full', 'employee_position_id'])
->where(['id' => $adminIds])
->indexBy('id')
->asArray()
->all();
- // Получаем названия должностей
- $positionIds = array_filter(array_column($admins, 'employee_position_id'));
- $positions = [];
+ $positionIds = array_filter(array_unique(array_column($admins, 'employee_position_id')));
+ $positions = [];
if (!empty($positionIds)) {
- $positions = \yii_app\records\EmployeePosition::find()
+ $positions = EmployeePosition::find()
->select(['id', 'name'])
->where(['id' => $positionIds])
->indexBy('id')
->all();
}
- // Получаем текущий грейд (активная запись: closed_at = '2100-01-01 00:00:00')
$gradeRows = AdminGradeHistory::find()
->alias('agh')
->select(['agh.admin_id', 'g.id AS grade_id', 'g.name AS grade_name'])
$grades = array_column($gradeRows, null, 'admin_id');
- // Получаем seller_id (GUID из 1С) для каждого сотрудника
$exportRows = ExportImportTable::find()
->select(['entity_id', 'export_val'])
->where([
return $this->asJson(['success' => true, 'data' => $result]);
} catch (Exception $e) {
- return $this->asJson(['success' => false, 'error' => $e->getMessage()]);
+ Yii::error($e->getMessage(), 'api2.data-test');
+ return $this->asJson(['success' => false, 'error' => 'Internal server error']);
}
}
}