$transaction = $db->beginTransaction();
try {
- $db->createCommand('TRUNCATE TABLE ' . self::TARGET_TABLE)->execute();
+ $db->createCommand('TRUNCATE TABLE ' . $db->quoteTableName(self::TARGET_TABLE))->execute();
$now = date('Y-m-d H:i:s');
$inserted = $db->createCommand(<<<SQL
<?php
+declare(strict_types=1);
+
namespace app\controllers;
use Yii;
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
+ 'ajax-delete' => ['POST'],
+ 'add-activity' => ['POST'],
'ajax-save-interval' => ['POST'],
'ajax-save-assortment' => ['POST'],
'ajax-remove-label' => ['POST'],
{
$request = Yii::$app->request;
- $historyDays = $request->get('historyDays');
- $intervalMonths = $request->get('intervalMonths');
- $startFrom = $request->get('startFrom', date('Y-m-d'));
+ $historyDays = $request->post('historyDays');
+ $intervalMonths = $request->post('intervalMonths');
+ $startFrom = $request->post('startFrom', date('Y-m-d'));
if ($historyDays === null || $intervalMonths === null) {
return $this->render('add-activity', [
- 'historyDays' => $historyDays ?? 14,
- 'intervalMonths' => $intervalMonths ?? 4,
- 'startFrom' => $startFrom,
+ 'historyDays' => 14,
+ 'intervalMonths' => 4,
+ 'startFrom' => date('Y-m-d'),
]);
}
- $endDate = date('Y-m-d', strtotime($startFrom));
+ $historyDays = max(1, min(365, (int)$historyDays));
+ $intervalMonths = max(1, min(24, (int)$intervalMonths));
+ $startFrom = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$startFrom)
+ ? (string)$startFrom
+ : date('Y-m-d');
+
+ $endDate = $startFrom;
$startDate = date('Y-m-d', strtotime("-{$historyDays} days", strtotime($endDate)));
$productIds = (new Query())
{
Yii::$app->response->format = Response::FORMAT_JSON;
- $request = Yii::$app->request;
- $id = $request->post('id') ?? $request->get('id');
+ $id = Yii::$app->request->post('id');
if (empty($id)) {
throw new BadRequestHttpException('Missing parameter: id');
}
try {
- $model = $this->findModel($id);
- $model->delete();
+ $this->findModel($id)->delete();
return [
'success' => true,
'message' => 'Запись успешно удалена',
];
} catch (\Throwable $e) {
+ Yii::error($e->getMessage(), __CLASS__);
return [
'success' => false,
- 'message' => $e->getMessage(),
+ 'message' => 'Ошибка удаления записи',
];
}
}
class Products1cNomenclatureMarkupController extends Controller
{
+ private AutoMarkService $autoMarkService;
+
+ public function init(): void
+ {
+ parent::init();
+ $this->autoMarkService = new AutoMarkService();
+ }
+
public const STATUS_NOT_NEEDED = 'not_needed';
public const STATUS_APPROVED = 'approved';
public const STATUS_AUTOMARK_PENDING = 'automark_pending';
return ['success' => false, 'message' => 'Ошибка сохранения предсказания'];
}
- $service = new AutoMarkService();
try {
- if ($service->applyApprovedPrediction($prediction->id)) {
+ if ($this->autoMarkService->applyApprovedPrediction($prediction->id)) {
AuditLog::write(
Products1cAutomarkPrediction::tableName(),
(string)$prediction->id,
return ['success' => false, 'message' => implode(', ', $prediction->getFirstErrors())];
}
- $service = new AutoMarkService();
try {
- if ($service->applyApprovedPrediction($prediction->id)) {
+ if ($this->autoMarkService->applyApprovedPrediction($prediction->id)) {
AuditLog::write(
Products1cAutomarkPrediction::tableName(),
(string)$prediction->id,
->where(['id' => $ids, 'status' => Products1cAutomarkPrediction::STATUS_PENDING])
->all();
- $service = new AutoMarkService();
- $now = date('Y-m-d H:i:s');
- $userId = (int)Yii::$app->user->id;
+ $now = date('Y-m-d H:i:s');
+ $userId = (int)Yii::$app->user->id;
$applied = 0;
- foreach ($predictions as $prediction) {
- $prediction->status = Products1cAutomarkPrediction::STATUS_APPROVED;
- $prediction->approved_by = $userId;
- $prediction->updated_at = $now;
-
- if (!$prediction->save()) {
- continue;
- }
+ $transaction = Yii::$app->db->beginTransaction();
+ try {
+ foreach ($predictions as $prediction) {
+ $prediction->status = Products1cAutomarkPrediction::STATUS_APPROVED;
+ $prediction->approved_by = $userId;
+ $prediction->updated_at = $now;
+
+ if (!$prediction->save()) {
+ $transaction->rollBack();
+ return ['success' => false, 'message' => 'Ошибка сохранения предсказания #' . $prediction->id];
+ }
- try {
- if ($service->applyApprovedPrediction($prediction->id)) {
+ if ($this->autoMarkService->applyApprovedPrediction($prediction->id)) {
$applied++;
+ } else {
+ $transaction->rollBack();
+ return ['success' => false, 'message' => 'Ошибка применения разметки для товара #' . $prediction->product_id];
}
- } catch (\Exception $e) {
- Yii::error($e->getMessage(), __CLASS__);
}
- }
- if ($applied > 0) {
- AuditLog::write(
- Products1cAutomarkPrediction::tableName(),
- null,
- AuditLog::ACTION_BULK_UPDATE,
- ['status' => Products1cAutomarkPrediction::STATUS_PENDING],
- ['status' => Products1cAutomarkPrediction::STATUS_APPROVED, 'approved_by' => $userId, 'entity_ids' => $ids],
- );
+ if ($applied > 0) {
+ AuditLog::write(
+ Products1cAutomarkPrediction::tableName(),
+ null,
+ AuditLog::ACTION_BULK_UPDATE,
+ ['status' => Products1cAutomarkPrediction::STATUS_PENDING],
+ ['status' => Products1cAutomarkPrediction::STATUS_APPROVED, 'approved_by' => $userId, 'entity_ids' => $ids],
+ );
+ }
+
+ $transaction->commit();
+ } catch (\Exception $e) {
+ $transaction->rollBack();
+ Yii::error($e->getMessage(), __CLASS__);
+ return ['success' => false, 'message' => 'Ошибка пакетного подтверждения'];
}
return [
/** @var int $intervalMonths */
/* @var string $startFrom */
use dosamigos\datepicker\DatePicker;
-use yii\base\DynamicModel;
use yii\helpers\Html;
use kartik\form\ActiveForm;
</ul>
<?php $form = ActiveForm::begin([
- 'method'=>'get',
- 'action'=>['add-activity'],
+ 'method' => 'post',
+ 'action' => ['add-activity'],
]); ?>
<?= Html::label('Дата отсчета для расчета актуальности', 'startFrom') ?>
<?= DatePicker::widget([
'name' => 'startFrom',
'id' => 'startFrom',
- 'value' => date('d-m-Y'),
+ 'value' => date('Y-m-d'),
'template' => '{addon}{input}',
'language' => 'ru',
'clientOptions' => [
'autoclose' => true,
- 'format' => 'dd-mm-yyyy',
- 'todayBtn' => true
- ],
- 'clientEvents' => [
+ 'format' => 'yyyy-mm-dd',
+ 'todayBtn' => true,
],
+ 'clientEvents' => [],
'containerOptions' => ['class' => 'mb-4'],
]) ?>
<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> Загрузка...</div>
</div>
<div id="intervalForm" class="mt-3 p-3 border rounded bg-light" style="display:none">
+ <div id="intervalAlert" class="alert d-none py-2 mb-2 small" role="alert"></div>
<input type="hidden" id="intervalId">
<div class="row g-2 align-items-end">
<div class="col">
</div>
<!-- Toast container -->
-<div id="toastContainer" class="position-fixed top-0 end-0 p-3" style="z-index:9999"></div>
+<div id="toastContainer" class="position-fixed end-0 p-3" style="z-index:1100000;top:64px"></div>
<script>
window.productActualityConfig = {
document.addEventListener('DOMContentLoaded', () => {
+ // Выносим контейнер тостов прямо в <body>, чтобы он не был ограничен
+ // stacking context Yii2-лейаута (transform/will-change на обёртках)
+ const toastEl = document.getElementById('toastContainer');
+ if (toastEl && toastEl.parentElement !== document.body) {
+ document.body.appendChild(toastEl);
+ }
+
const esc = s => s == null ? '' : String(s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
el.addEventListener('hidden.bs.toast', () => el.remove());
}
+ function showModalAlert(message, type = 'danger') {
+ const $a = $('#intervalAlert');
+ $a.removeClass('alert-danger alert-success')
+ .addClass('alert-' + type)
+ .text(message)
+ .removeClass('d-none');
+ $a[0]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+
+ function clearModalAlert() {
+ $('#intervalAlert').addClass('d-none').text('');
+ }
+
// ─── Chips helpers ──────────────────────────────────────────────────────
function renderIntervalChips(intervals) {
const name = $(this).data('name');
$('#modalProductName').text(name);
$intervalForm.hide();
+ clearModalAlert();
$intervalId.val('');
bootstrap.Tab.getOrCreateInstance(document.getElementById('tab-intervals-btn')).show();
$('#addIntervalBtn').removeClass('d-none');
$('#addIntervalBtn').on('click', function () {
$intervalId.val('');
+ clearModalAlert();
initMonthSelects();
$intervalForm.show();
$intervalForm[0].scrollIntoView({ behavior: 'smooth' });
const from = $(this).data('from');
const to = $(this).data('to');
$intervalId.val(id);
+ clearModalAlert();
initMonthSelects(from, to);
$intervalForm.show();
$intervalForm[0].scrollIntoView({ behavior: 'smooth' });
$('#cancelIntervalBtn').on('click', function () {
$intervalForm.hide();
+ clearModalAlert();
$intervalId.val('');
});
const to = $toSel.val();
if (!from || !to) {
- showToast('Заполните начало и окончание интервала');
+ const msg = 'Заполните начало и окончание интервала';
+ showToast(msg);
+ showModalAlert(msg);
return;
}
if (id) data.id = id;
$(this).prop('disabled', true).text('Сохранение...');
+ clearModalAlert();
$.post(urls.saveInterval, data, res => {
if (res.success) {
- showToast('Сохранено', 'success');
- $intervalForm.hide();
+ showModalAlert('Интервал сохранён', 'success');
$intervalId.val('');
loadIntervals(currentGuid);
+ setTimeout(() => {
+ $intervalForm.hide();
+ clearModalAlert();
+ }, 1200);
} else {
- showToast(res.message || 'Ошибка сохранения');
+ const msg = res.message || 'Ошибка сохранения';
+ showToast(msg);
+ showModalAlert(msg);
}
- }).fail(() => {
- showToast('Ошибка запроса');
+ }).fail((xhr) => {
+ let msg = 'Ошибка запроса';
+ try {
+ const r = JSON.parse(xhr.responseText);
+ if (r.message) msg = r.message;
+ } catch (e) {}
+ showToast(msg);
+ showModalAlert(msg);
}).always(() => {
$('#saveIntervalBtn').prop('disabled', false).text('Сохранить');
});