--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace app\controllers;
+
+use Yii;
+use yii\web\Controller;
+use yii\web\NotFoundHttpException;
+use yii\data\ActiveDataProvider;
+use yii_app\records\Products1cAutomarkPrediction;
+use yii_app\services\AutoMarkService;
+
+class AutoMarkController extends Controller
+{
+ public function actionIndex(): string
+ {
+ $query = Products1cAutomarkPrediction::find()
+ ->with('product')
+ ->orderBy(['confidence' => SORT_DESC, 'created_at' => SORT_DESC]);
+
+ $status = Yii::$app->request->get('status');
+ if ($status !== null) {
+ $query->andWhere(['status' => (int) $status]);
+ } else {
+ $query->andWhere(['status' => Products1cAutomarkPrediction::STATUS_PENDING]);
+ }
+
+ $dataProvider = new ActiveDataProvider([
+ 'query' => $query,
+ 'pagination' => ['pageSize' => 50],
+ ]);
+
+ return $this->render('index', ['dataProvider' => $dataProvider]);
+ }
+
+ public function actionReview(int $id): string
+ {
+ $prediction = $this->findPrediction($id);
+
+ if (Yii::$app->request->isPost) {
+ $action = Yii::$app->request->post('action');
+ $service = new AutoMarkService();
+
+ if ($action === 'approve') {
+ $prediction->status = Products1cAutomarkPrediction::STATUS_APPROVED;
+ $prediction->approved_by = Yii::$app->user->id;
+ $prediction->updated_at = date('Y-m-d H:i:s');
+ $prediction->save();
+ $service->applyApprovedPrediction($prediction->id);
+ Yii::$app->session->setFlash('success', 'Разметка применена.');
+ } elseif ($action === 'reject') {
+ $prediction->status = Products1cAutomarkPrediction::STATUS_REJECTED;
+ $prediction->updated_at = date('Y-m-d H:i:s');
+ $prediction->save();
+ Yii::$app->session->setFlash('info', 'Предсказание отклонено.');
+ }
+
+ return $this->redirect(['index']);
+ }
+
+ return $this->render('review', ['prediction' => $prediction]);
+ }
+
+ private function findPrediction(int $id): Products1cAutomarkPrediction
+ {
+ $model = Products1cAutomarkPrediction::find()->with('product')->where(['id' => $id])->one();
+ if ($model === null) {
+ throw new NotFoundHttpException("Предсказание #{$id} не найдено.");
+ }
+ return $model;
+ }
+}
--- /dev/null
+<?php
+/** @var \yii\data\ActiveDataProvider $dataProvider */
+use yii\grid\GridView;
+use yii\helpers\Html;
+use yii_app\records\Products1cAutomarkPrediction;
+
+$this->title = 'Авторазметка товаров';
+?>
+<div class="container-fluid">
+ <h2><?= Html::encode($this->title) ?></h2>
+
+ <div class="mb-3">
+ <?= Html::a('Pending', ['index', 'status' => Products1cAutomarkPrediction::STATUS_PENDING], ['class' => 'btn btn-warning']) ?>
+ <?= Html::a('Одобренные', ['index', 'status' => Products1cAutomarkPrediction::STATUS_APPROVED], ['class' => 'btn btn-success']) ?>
+ <?= Html::a('Отклонённые', ['index', 'status' => Products1cAutomarkPrediction::STATUS_REJECTED], ['class' => 'btn btn-secondary']) ?>
+ </div>
+
+ <?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'columns' => [
+ 'id',
+ [
+ 'label' => 'Товар',
+ 'value' => fn($m) => $m->product?->name ?? $m->product_id,
+ ],
+ 'category',
+ 'species',
+ 'sort',
+ 'color',
+ 'size',
+ [
+ 'label' => 'Confidence',
+ 'value' => fn($m) => number_format($m->confidence * 100, 1) . '%',
+ ],
+ 'method',
+ [
+ 'label' => 'Действия',
+ 'format' => 'raw',
+ 'value' => fn($m) => Html::a('Проверить', ['review', 'id' => $m->id], ['class' => 'btn btn-sm btn-primary']),
+ ],
+ ],
+ ]) ?>
+</div>
--- /dev/null
+<?php
+/** @var \yii_app\records\Products1cAutomarkPrediction $prediction */
+use yii\helpers\Html;
+use yii_app\records\Products1cAutomarkPrediction;
+
+$this->title = 'Проверка разметки #' . $prediction->id;
+?>
+<div class="container">
+ <h2><?= Html::encode($this->title) ?></h2>
+
+ <div class="card mb-4">
+ <div class="card-header"><strong>Товар из 1С</strong></div>
+ <div class="card-body">
+ <p><strong>GUID:</strong> <?= Html::encode($prediction->product_id) ?></p>
+ <p><strong>Название:</strong> <?= Html::encode($prediction->product?->name ?? '—') ?></p>
+ <p><strong>Код:</strong> <?= Html::encode($prediction->product?->code ?? '—') ?></p>
+ </div>
+ </div>
+
+ <div class="card mb-4">
+ <div class="card-header">
+ <strong>Предсказание</strong>
+ — метод: <code><?= Html::encode($prediction->method) ?></code>,
+ уверенность: <strong><?= number_format($prediction->confidence * 100, 1) ?>%</strong>
+ </div>
+ <div class="card-body">
+ <table class="table table-bordered">
+ <tr><th>Категория</th><td><?= Html::encode($prediction->category ?? '—') ?></td></tr>
+ <tr><th>Подкатегория</th><td><?= Html::encode($prediction->subcategory ?? '—') ?></td></tr>
+ <tr><th>Вид</th><td><?= Html::encode($prediction->species ?? '—') ?></td></tr>
+ <tr><th>Сорт</th><td><?= Html::encode($prediction->sort ?? '—') ?></td></tr>
+ <tr><th>Тип</th><td><?= Html::encode($prediction->type ?? '—') ?></td></tr>
+ <tr><th>Размер</th><td><?= $prediction->size ?? '—' ?></td></tr>
+ <tr><th>Цвет</th><td><?= Html::encode($prediction->color ?? '—') ?></td></tr>
+ </table>
+ </div>
+ </div>
+
+ <?php if ($prediction->isPending()): ?>
+ <div class="d-flex gap-2">
+ <?= Html::beginForm(['review', 'id' => $prediction->id], 'post') ?>
+ <?= Html::hiddenInput('action', 'approve') ?>
+ <?= Html::submitButton('Применить разметку', ['class' => 'btn btn-success']) ?>
+ <?= Html::endForm() ?>
+
+ <?= Html::beginForm(['review', 'id' => $prediction->id], 'post') ?>
+ <?= Html::hiddenInput('action', 'reject') ?>
+ <?= Html::submitButton('Отклонить', ['class' => 'btn btn-danger']) ?>
+ <?= Html::endForm() ?>
+ </div>
+ <?php endif; ?>
+
+ <div class="mt-3">
+ <?= Html::a('← Назад к списку', ['index'], ['class' => 'btn btn-secondary']) ?>
+ </div>
+</div>