]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Каталог категорийного менеджера - интерфейс
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 22 Apr 2026 13:07:20 +0000 (16:07 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 22 Apr 2026 13:07:20 +0000 (16:07 +0300)
erp24/controllers/Products1cNomenclatureActualityController.php
erp24/views/assortment-label/index.php
erp24/views/products1c-nomenclature-actuality/index.php
erp24/views/products1c-nomenclature-markup/index.php
erp24/web/js/products1cNomenclatureActuality/index.js

index 2fd7bd61e39cb6e64368ec153197fb51f98b8ceb..a1517ad354bc2f56bf125a1c8d377a86bf6333e8 100644 (file)
@@ -329,19 +329,27 @@ class Products1cNomenclatureActualityController extends Controller
                 ->column();
         }
 
+        $pendingMarkupCount = null;
+        if (Yii::$app->db->getTableSchema('products_1c_nomenclature') !== null) {
+            $pendingMarkupCount = Products1cNomenclature::find()
+                ->where(['classification_status' => 'pending'])
+                ->count();
+        }
+
         return $this->render('index', [
-            'filter'       => $filter,
-            'dataProvider' => $dataProvider,
-            'categories'   => array_combine($categories, $categories),
-            'subcategories' => array_combine($subcategories, $subcategories),
-            'species'      => array_combine($species, $species),
-            'types'        => array_combine($lists['type'], $lists['type']),
-            'colors'       => array_combine($lists['color'], $lists['color']),
-            'sorts'        => array_combine($lists['sort'], $lists['sort']),
-            'sizes'        => array_combine($lists['size'], $lists['size']),
-            'counters'     => $counters,
-            'pageSize'     => $pageSize,
-            'sortBy'       => $sortBy,
+            'filter'             => $filter,
+            'dataProvider'       => $dataProvider,
+            'categories'         => array_combine($categories, $categories),
+            'subcategories'      => array_combine($subcategories, $subcategories),
+            'species'            => array_combine($species, $species),
+            'types'              => array_combine($lists['type'], $lists['type']),
+            'colors'             => array_combine($lists['color'], $lists['color']),
+            'sorts'              => array_combine($lists['sort'], $lists['sort']),
+            'sizes'              => array_combine($lists['size'], $lists['size']),
+            'counters'           => $counters,
+            'pageSize'           => $pageSize,
+            'sortBy'             => $sortBy,
+            'pendingMarkupCount' => $pendingMarkupCount,
         ]);
     }
 
index 1fd00206316776f9e4de433c291153ad0b125eb0..0a07475e1b4520e7e011bf494c5b0110d334c3b9 100644 (file)
@@ -8,39 +8,83 @@ use yii\web\View;
 /* @var int $withLabel */
 /* @var float $coverage */
 
-$this->title = 'СпÑ\80авоÑ\87ник Ð»ÐµÐ¹Ð±Ð»Ð¾Ð² Ð°Ñ\81Ñ\81оÑ\80Ñ\82именÑ\82ной Ð¼Ð°Ñ\82Ñ\80иÑ\86Ñ\8b';
+$this->title = 'Ð\90Ñ\81Ñ\81оÑ\80Ñ\82именÑ\82наÑ\8f Ð¼Ð°Ñ\82Ñ\80иÑ\86а';
 $this->params['breadcrumbs'][] = $this->title;
 $this->registerJsFile('/js/assortmentLabel/index.js', ['position' => View::POS_END]);
+$this->registerCss(<<<'CSS'
+:root { --ao: #1e3a5f; --cat: #6f42c1; }
+.cm-tabs { border-bottom: 2px solid #dee2e6; margin-bottom: 20px; }
+.cm-tabs .nav-link { font-size: 14px; color: #495057; border: none; padding: 10px 20px; font-weight: 500;
+    border-bottom: 3px solid transparent; margin-bottom: -2px; background: none; }
+.cm-tabs .nav-link.active { color: var(--cat); font-weight: 700; border-bottom-color: var(--cat); }
+.cm-tabs .nav-link:hover:not(.active) { color: var(--ao); }
+.cm-tabs .tc { font-size: 10px; font-weight: 700; margin-left: 6px; padding: 1px 6px;
+    border-radius: 10px; background: #e9ecef; color: #495057; display: inline-block; }
+.cm-tabs .tc-danger { background: #dc3545 !important; color: #fff !important; }
+CSS);
 ?>
 
 <div class="assortment-label-index p-4">
 
-    <div class="d-flex justify-content-between align-items-center mb-4">
-        <h1 class="mb-0"><?= Html::encode($this->title) ?></h1>
-        <button class="btn btn-primary" id="addLabelBtn">
-            <i class="fa fa-plus me-1"></i> Добавить лейбл
+    <!-- Таб-навигация -->
+    <ul class="nav cm-tabs" role="tablist">
+        <li class="nav-item">
+            <a class="nav-link" href="<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/index']) ?>">
+                <i class="fa fa-calendar-check-o me-1"></i>Актуальность ассортимента
+            </a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link" href="<?= \yii\helpers\Url::to(['/products1c-nomenclature-markup/index']) ?>">
+                <i class="fa fa-tag me-1"></i>Разметка
+            </a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link active" href="<?= \yii\helpers\Url::to(['/assortment-label/index']) ?>">
+                <i class="fa fa-th-large me-1"></i>Ассортиментная матрица
+            </a>
+        </li>
+    </ul>
+
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h1 class="mb-0" style="font-size:20px;font-weight:600;color:var(--ao);">
+            <i class="fa fa-th-large me-2" style="color:var(--cat)"></i><?= Html::encode($this->title) ?>
+        </h1>
+        <button class="btn btn-sm btn-primary" id="addLabelBtn"
+                style="background:var(--cat);border-color:var(--cat)">
+            <i class="fa fa-plus me-1"></i>Добавить лейбл
         </button>
     </div>
 
+    <!-- Info box -->
+    <div style="background:#f0f4f8;border:1px solid #dee2e6;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:12px;">
+        <i class="fa fa-info-circle me-2" style="color:#2a5298"></i>
+        Лейблы ассортиментной матрицы определяют вхождение товара в ассортимент по каналам. Товар может входить в несколько лейблов.
+    </div>
+
     <!-- Покрытие -->
-    <div class="row g-3 mb-4">
-        <div class="col-auto">
-            <div class="card border-0 bg-light px-4 py-3 text-center" style="min-width:160px">
-                <div class="fs-2 fw-bold text-primary" id="statCoverage"><?= $coverage ?>%</div>
-                <div class="text-muted small">покрытие матрицей</div>
+    <h6 style="font-size:14px;color:var(--ao);margin-bottom:12px">
+        <i class="fa fa-pie-chart me-2"></i>Покрытие (всего товаров: <?= $totalProducts ?>)
+    </h6>
+    <div style="display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap">
+        <div style="background:#fff;border:1px solid #dee2e6;border-radius:8px;padding:12px 16px;flex:1;min-width:200px">
+            <div style="display:flex;justify-content:space-between;align-items:center">
+                <span style="font-size:12px">С лейблом матрицы</span>
+                <span style="font-size:18px;font-weight:700;color:#198754" id="statWithLabel"><?= $withLabel ?></span>
             </div>
-        </div>
-        <div class="col-auto">
-            <div class="card border-0 bg-light px-4 py-3 text-center" style="min-width:160px">
-                <div class="fs-2 fw-bold" id="statWithLabel"><?= $withLabel ?></div>
-                <div class="text-muted small">товаров с лейблом</div>
+            <div style="background:#e9ecef;height:6px;border-radius:3px;margin-top:6px">
+                <div style="background:#198754;height:100%;border-radius:3px;width:<?= min($coverage, 100) ?>%"></div>
             </div>
+            <div style="font-size:10px;color:#6c757d;margin-top:2px" id="statCoverage"><?= $coverage ?>%</div>
         </div>
-        <div class="col-auto">
-            <div class="card border-0 bg-light px-4 py-3 text-center" style="min-width:160px">
-                <div class="fs-2 fw-bold" id="statTotal"><?= $totalProducts ?></div>
-                <div class="text-muted small">всего товаров</div>
+        <div style="background:#fff;border:1px solid #dee2e6;border-radius:8px;padding:12px 16px;flex:1;min-width:200px">
+            <div style="display:flex;justify-content:space-between;align-items:center">
+                <span style="font-size:12px;color:#dc3545">Без матрицы</span>
+                <span style="font-size:18px;font-weight:700;color:#dc3545"><?= max(0, $totalProducts - $withLabel) ?></span>
+            </div>
+            <div style="background:#e9ecef;height:6px;border-radius:3px;margin-top:6px">
+                <div style="background:#dc3545;height:100%;border-radius:3px;width:<?= max(0, 100 - min($coverage, 100)) ?>%"></div>
             </div>
+            <div style="font-size:10px;color:#6c757d;margin-top:2px"><?= max(0, round(100 - $coverage, 1)) ?>%</div>
         </div>
     </div>
 
index 8b1c9c190f780eb44fd2b1b6b04d46cd996bd18e..c0c4929736eb7fdc60b7c921ace5bf2faa97de58 100644 (file)
@@ -1,10 +1,10 @@
 <?php
 
 use kartik\form\ActiveForm;
-use kartik\grid\GridView;
 use yii\helpers\Html;
 use yii\helpers\Url;
 use yii\web\View;
+use yii\widgets\LinkPager;
 
 /* @var $this yii\web\View */
 /* @var $filter yii\base\DynamicModel */
@@ -19,17 +19,111 @@ use yii\web\View;
 /* @var array|null $counters */
 /* @var int $pageSize */
 /* @var string $sortBy */
+/* @var int|null $pendingMarkupCount */
 
-$this->title = 'Актуализация номенклатуры';
+$this->title = 'Актуальность ассортимента';
 $this->params['breadcrumbs'][] = $this->title;
 $this->registerJsFile('/js/products1cNomenclatureActuality/index.js', ['position' => View::POS_END]);
-$this->registerCss('.concept-chip{display:inline-block;font-size:10px;padding:2px 7px;border-radius:4px;font-weight:500;background:#f3e8ff;color:#7c3aed;border:1px dashed #d8b4fe;}');
+$this->registerCss(<<<'CSS'
+:root { --ao: #1e3a5f; --cat: #6f42c1; }
+
+/* ── Tab nav ── */
+.cm-tabs { border-bottom: 2px solid #dee2e6; margin-bottom: 20px; }
+.cm-tabs .nav-link { font-size: 14px; color: #495057; border: none; padding: 10px 20px; font-weight: 500;
+    border-bottom: 3px solid transparent; margin-bottom: -2px; background: none; }
+.cm-tabs .nav-link.active { color: var(--cat); font-weight: 700; border-bottom-color: var(--cat); }
+.cm-tabs .nav-link:hover:not(.active) { color: var(--ao); }
+.cm-tabs .tc { font-size: 10px; font-weight: 700; margin-left: 6px; padding: 1px 6px; border-radius: 10px;
+    background: #e9ecef; color: #495057; display: inline-block; }
+.cm-tabs .nav-link.active .tc { background: var(--cat); color: #fff; }
+.cm-tabs .tc-danger { background: #dc3545 !important; color: #fff !important; }
+
+/* ── Stat cards ── */
+.sc { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
+.sc-card { background: #fff; border-radius: 8px; padding: 14px 18px; flex: 1; min-width: 130px;
+    border: 1px solid #dee2e6; }
+.sc-card .lb { font-size: 11px; color: #6c757d; text-transform: uppercase; letter-spacing: .5px; }
+.sc-card .vl { font-size: 24px; font-weight: 700; }
+.sc-card .su { font-size: 11px; color: #6c757d; }
+
+/* ── Filter bar ── */
+.fb { background: #fff; border: 1px solid #dee2e6; border-radius: 8px;
+    padding: 12px 16px; margin-bottom: 12px; display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
+.fb .fg { display: flex; flex-direction: column; gap: 2px; }
+.fb .fg > label { font-size: 11px; color: #6c757d; font-weight: 500; margin: 0; }
+.fb .form-select, .fb .form-control { font-size: 12px; padding: 4px 8px; height: 30px; }
+.fb-sep { width: 1px; height: 30px; background: #dee2e6; align-self: flex-end; flex-shrink: 0; }
+
+/* ── Bulk bar ── */
+.bulk-bar { background: var(--ao); color: #fff; border-radius: 8px; padding: 10px 16px;
+    margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
+.bulk-bar .btn { font-size: 11px; }
+
+/* ── Product card ── */
+.pr { background: #fff; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 10px;
+    padding: 14px 16px; transition: box-shadow .15s; }
+.pr:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
+.pr-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 10px; }
+.pr-left { display: flex; align-items: flex-start; gap: 10px; flex-shrink: 0; max-width: 300px; }
+.pr-name { font-size: 15px; font-weight: 600; color: var(--ao); margin: 2px 0; line-height: 1.3; }
+.pr-sub { font-size: 11px; color: #6c757d; word-break: break-all; }
+.pr-center { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
+.pr-intervals { display: flex; gap: 8px; flex-wrap: wrap; }
+.pr-int { font-size: 11px; padding: 3px 10px; border-radius: 4px; border: 1px solid #dee2e6;
+    color: #495057; background: #f8f9fa; white-space: nowrap; }
+.pr-int.active { border-color: #198754; color: #198754; background: #e8f5e9; font-weight: 600; }
+.pr-int.future { border-color: #0d6efd; color: #0d6efd; background: #e7f1ff; }
+.pr-int.past   { border-color: #dee2e6; color: #adb5bd; background: #f8f9fa; text-decoration: line-through; }
+.pr-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
+.score-inline { display: flex; align-items: center; gap: 5px; font-size: 12px; white-space: nowrap; }
+.sc-stars { display: inline-flex; gap: 1px; font-size: 12px; }
+.sc-stars i { color: #dee2e6; }
+.sc-stars i.on { color: #ffc107; }
+
+/* ── Label chips ── */
+.ch { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600;
+    margin: 1px 2px; border: 1px solid transparent; position: relative; }
+.ch-off  { background: #e8f5e9; color: #2e7d32; border-color: #c8e6c9; }
+.ch-site { background: #e3f2fd; color: #1565c0; border-color: #bbdefb; }
+.ch-mp   { background: #fce4ec; color: #c62828; border-color: #f8bbd0; }
+.ch-1p   { background: #fff8e1; color: #f57f17; border-color: #ffecb3; }
+.ch.ed { cursor: default; }
+.ch-rm { display: none; position: absolute; top: -4px; right: -4px; width: 13px; height: 13px;
+    border-radius: 50%; background: #dc3545; color: #fff; border: none; font-size: 8px;
+    line-height: 13px; text-align: center; cursor: pointer; padding: 0; }
+.ch.ed:hover .ch-rm { display: block; }
+
+/* ── Concept chips ── */
+.concept { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500;
+    margin: 1px 2px; background: #f3e8ff; color: #7c3aed; border: 1px dashed #d8b4fe; }
+
+/* ── Bottom of card ── */
+.lbl-title { font-size: 10px; color: #6c757d; text-transform: uppercase; letter-spacing: .3px;
+    display: block; margin-bottom: 3px; }
+.btn-pr-edit { font-size: 11px; padding: 4px 14px; color: #6c757d; border: 1px solid #dee2e6;
+    background: #fff; border-radius: 4px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
+.btn-pr-edit:hover { color: #495057; border-color: #adb5bd; background: #f8f9fa; }
+
+/* ── Score collapse block ── */
+.scb { margin-top: 8px; border-top: 1px solid #f0f0f0; }
+.scb-toggle { font-size: 11px; color: var(--cat); cursor: pointer; padding: 5px 0;
+    display: flex; align-items: center; gap: 6px; user-select: none; }
+.scb-toggle:hover { color: var(--ao); }
+.scb-toggle .scb-chevron { font-size: 9px; transition: transform .15s; }
+.scb-toggle.open .scb-chevron { transform: rotate(90deg); }
+.scb-content { display: none; padding: 4px 0; }
+.scb-row { display: flex; justify-content: space-between; align-items: center; padding: 3px 0;
+    font-size: 11px; border-bottom: 1px solid #f5f5f5; }
+.scb-row:last-child { border-bottom: none; }
+.scb-name { color: var(--ao); font-weight: 600; font-size: 11px; }
+.scb-comment { font-size: 10px; color: #6c757d; font-style: italic; padding: 1px 0 3px; }
+CSS);
 
 function monthList(): array
 {
-    $list = [];
-    $tz   = new DateTimeZone('Europe/Moscow');
-    $now  = new DateTime('now', $tz);
+    $list  = [];
+    $tz    = new DateTimeZone('Europe/Moscow');
+    $now   = new DateTime('now', $tz);
     $start = (clone $now)->modify('first day of january last year');
     $end   = (clone $now)->modify('last day of december next year');
     while ($start <= $end) {
@@ -39,93 +133,86 @@ function monthList(): array
     }
     return $list;
 }
-
 $months = monthList();
 
-function renderActualityChips(array $actualities): string
+function actGetStatus(array $actualities): string
 {
-    if (empty($actualities)) {
-        return '<span class="text-danger" title="Нет интервалов"><i class="fa fa-exclamation-circle"></i></span>';
-    }
-
-    $statusClasses = [
-        'active' => 'bg-success',
-        'future' => 'bg-primary',
-        'past'   => 'bg-secondary',
-    ];
-
-    $chips = [];
+    if (empty($actualities)) { return 'none'; }
     foreach ($actualities as $act) {
-        $status = $act->getStatus();
-        $cls    = $statusClasses[$status] ?? 'bg-secondary';
-        $style  = $status === 'past' ? ' style="text-decoration:line-through"' : '';
-        $chips[] = '<span class="badge ' . $cls . ' me-1"' . $style . '>' . Html::encode($act->getLabel()) . '</span>';
+        if ($act->getStatus() === 'active') { return 'active'; }
     }
-
-    $visible = array_slice($chips, 0, 3);
-    $hidden  = count($chips) - count($visible);
-    $html    = implode('', $visible);
-    if ($hidden > 0) {
-        $html .= '<span class="text-muted small ms-1">+' . $hidden . ' ещё</span>';
+    foreach ($actualities as $act) {
+        if ($act->getStatus() === 'future') { return 'future'; }
     }
-    return $html;
+    return 'inactive';
 }
 
-function renderConceptChips(array $conceptNames): string
+function actIntervalChips(array $actualities): string
 {
-    if (empty($conceptNames)) {
-        return '';
+    if (empty($actualities)) {
+        return '<span class="text-danger" style="font-size:11px;font-style:italic">Не запланирован ни один интервал актуальности</span>';
     }
-    $chips = '';
-    foreach ($conceptNames as $name) {
-        $chips .= '<span class="concept-chip me-1 mb-1">'
-            . '<i class="fa fa-palette" style="font-size:8px;margin-right:3px;"></i>'
-            . Html::encode($name)
-            . '</span>';
+    $out = '';
+    foreach ($actualities as $act) {
+        $s    = $act->getStatus();
+        $out .= '<span class="pr-int ' . Html::encode($s) . '">' . Html::encode($act->getLabel()) . '</span>';
     }
-    return '<span class="concept-chips-wrap d-block mt-1">' . $chips . '</span>';
+    return $out;
 }
 
-function renderLabelChips(array $labels, string $productGuid): string
+function actLabelChips(array $labels, string $guid): string
 {
     if (empty($labels)) {
         return '<span class="text-muted small">—</span>';
     }
-
-    $html = '';
+    static $map = [
+        'offline'     => ['ch-off',  'fa-store'],
+        'online'      => ['ch-site', 'fa-globe'],
+        'marketplace' => ['ch-mp',   'fa-shopping-bag'],
+    ];
+    $out = '';
     foreach ($labels as $label) {
-        $color   = Html::encode($label->color ?? '#6c757d');
-        $name    = Html::encode($label->name);
-        $labelId = (int)$label->id;
-        $html .= '<span class="label-chip d-inline-flex align-items-center me-1 mb-1 badge" '
-            . 'style="background-color:' . $color . ';cursor:default" '
-            . 'data-guid="' . Html::encode($productGuid) . '" '
-            . 'data-label-id="' . $labelId . '">'
-            . $name
-            . '<button type="button" class="remove-label-chip btn-close btn-close-white ms-1" '
-            . 'style="font-size:.6rem" aria-label="Убрать" '
-            . 'data-guid="' . Html::encode($productGuid) . '" '
-            . 'data-label-id="' . $labelId . '"></button>'
+        [$cls, $ico] = $map[$label->channel_type ?? ''] ?? ['ch-1p', 'fa-tag'];
+        $out .= '<span class="ch ' . $cls . ' ed label-chip" data-label-id="' . (int)$label->id . '">'
+            . '<i class="fa ' . $ico . '" style="font-size:9px;margin-right:3px"></i>'
+            . Html::encode($label->name)
+            . '<button type="button" class="remove-label-chip ch-rm"'
+            . ' data-guid="' . Html::encode($guid) . '"'
+            . ' data-label-id="' . (int)$label->id . '">&times;</button>'
             . '</span>';
     }
-    return '<span class="label-chips-wrap">' . $html . '</span>';
+    return $out;
+}
+
+function actConceptChips(array $names): string
+{
+    $out = '';
+    foreach ($names as $name) {
+        $out .= '<span class="concept"><i class="fa fa-palette" style="font-size:8px;margin-right:3px"></i>'
+            . Html::encode($name) . '</span>';
+    }
+    return $out;
 }
 ?>
 
 <div class="products1c-nomenclature-actuality-index p-4">
 
-    <h1><?= Html::encode($this->title) ?></h1>
-
-    <!-- Таб-навигация -->
-    <ul class="nav nav-tabs mb-4">
+    <!-- ── Tab nav ── -->
+    <ul class="nav cm-tabs" role="tablist">
         <li class="nav-item">
             <a class="nav-link active" href="<?= Url::to(['/products1c-nomenclature-actuality/index']) ?>">
                 <i class="fa fa-calendar-check-o me-1"></i>Актуальность ассортимента
+                <?php if ($counters): ?>
+                <span class="tc"><?= $counters['total'] ?></span>
+                <?php endif; ?>
             </a>
         </li>
         <li class="nav-item">
             <a class="nav-link" href="<?= Url::to(['/products1c-nomenclature-markup/index']) ?>">
                 <i class="fa fa-tag me-1"></i>Разметка
+                <?php if (!empty($pendingMarkupCount)): ?>
+                <span class="tc tc-danger"><?= (int)$pendingMarkupCount ?></span>
+                <?php endif; ?>
             </a>
         </li>
         <li class="nav-item">
@@ -135,385 +222,351 @@ function renderLabelChips(array $labels, string $productGuid): string
         </li>
     </ul>
 
+    <!-- ── Page header ── -->
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h1 class="mb-0" style="font-size:20px;font-weight:600;color:var(--ao);">
+            <i class="fa fa-calendar-check-o me-2" style="color:var(--cat)"></i><?= Html::encode($this->title) ?>
+        </h1>
+        <div class="d-flex gap-2">
+            <a href="<?= Url::to(array_merge(
+                ['/products1c-nomenclature-actuality/export-xlsx'],
+                array_filter(Yii::$app->request->get(), static fn($v) => $v !== '' && $v !== null)
+            )) ?>" class="btn btn-outline-secondary btn-sm">
+                <i class="fa fa-download me-1"></i>Экспорт
+            </a>
+        </div>
+    </div>
+
+    <!-- ── Stat cards ── -->
+    <?php if ($counters !== null): ?>
+    <div class="sc">
+        <div class="sc-card">
+            <div class="lb">Всего в выборке</div>
+            <div class="vl" style="color:var(--ao)"><?= $counters['total'] ?></div>
+        </div>
+        <div class="sc-card" style="border-left:4px solid #198754">
+            <div class="lb">Актуальных</div>
+            <div class="vl" style="color:#198754"><?= $counters['active'] ?></div>
+            <?php if ($counters['total'] > 0): ?>
+            <div class="su"><?= round($counters['active'] / $counters['total'] * 100, 1) ?>%</div>
+            <?php endif; ?>
+        </div>
+        <div class="sc-card" style="border-left:4px solid #ffc107">
+            <div class="lb">Без матрицы</div>
+            <div class="vl" style="color:#b86d00"><?= $counters['no_labels'] ?></div>
+            <?php if ($counters['total'] > 0): ?>
+            <div class="su"><?= round($counters['no_labels'] / $counters['total'] * 100, 1) ?>%</div>
+            <?php endif; ?>
+        </div>
+        <?php if ($counters['no_score'] !== null): ?>
+        <div class="sc-card" style="border-left:4px solid var(--cat)">
+            <div class="lb">Без оценки</div>
+            <div class="vl" style="color:var(--cat)"><?= $counters['no_score'] ?></div>
+            <?php if ($counters['total'] > 0): ?>
+            <div class="su"><?= round($counters['no_score'] / $counters['total'] * 100, 1) ?>%</div>
+            <?php endif; ?>
+        </div>
+        <?php endif; ?>
+    </div>
+    <?php endif; ?>
+
+    <!-- ── Compact filter bar ── -->
     <?php $formFilter = ActiveForm::begin([
         'method'  => 'get',
         'action'  => ['index'],
-        'options' => ['class' => 'mb-4'],
+        'options' => ['class' => 'mb-0', 'id' => 'filterForm'],
     ]); ?>
 
-    <div class="row">
-
-        <div class="col-6">
-            <div class="mb-2 fw-bold">Номенклатура</div>
-            <div class="row mb-3">
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'category', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $categories,
-                            ['prompt' => 'Категория', 'id' => 'filter-category']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-category">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'type', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $types,
-                            ['prompt' => 'Тип', 'id' => 'filter-type']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-type">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'color', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $colors,
-                            ['prompt' => 'Цвет', 'id' => 'filter-color']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-color">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="row mb-3">
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'subcategory', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $subcategories,
-                            ['prompt' => 'Подкатегория', 'id' => 'filter-subcategory']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-subcategory">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'sort', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $sorts,
-                            ['prompt' => 'Сорт', 'id' => 'filter-sort']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-sort">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col"></div>
-            </div>
-            <div class="row">
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'species', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $species,
-                            ['prompt' => 'Вид', 'id' => 'filter-species']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-species">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col">
-                    <div class="d-flex justify-content-between">
-                        <?= $formFilter->field($filter, 'size', ['options' => ['class' => 'w-90']])->dropDownList(
-                            $sizes,
-                            ['prompt' => 'Размер', 'id' => 'filter-size']
-                        )->label(false) ?>
-                        <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-size">
-                            <i class="fa fa-times"></i>
-                        </div>
-                    </div>
-                </div>
-                <div class="col"></div>
-            </div>
+    <div class="fb">
+        <div class="fg">
+            <label><i class="fa fa-list" style="font-size:9px;color:#6c757d"></i> Категория</label>
+            <?= $formFilter->field($filter, 'category', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($categories, ['prompt' => 'Все', 'id' => 'filter-category', 'class' => 'form-select'])
+                ->label(false) ?>
         </div>
-
-        <div class="col-3 ps-4" style="border-left: #ccc solid 1px">
-            <div class="mb-2 fw-bold">Актуальность ассортимента</div>
-            <div class="mb-3">
-                <div class="d-flex justify-content-between">
-                    <?= $formFilter->field($filter, 'date_from', ['options' => ['class' => 'w-100']])
-                        ->dropDownList($months, ['prompt' => 'Выбрать дату от', 'id' => 'filter-date-from'])
-                        ->label(false) ?>
-                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-date-from">
-                        <i class="fa fa-times"></i>
-                    </div>
-                </div>
-            </div>
-            <div>
-                <div class="d-flex justify-content-between">
-                    <?= $formFilter->field($filter, 'date_to', ['options' => ['class' => 'w-100']])
-                        ->dropDownList($months, ['prompt' => 'Выбрать дату до', 'id' => 'filter-date-to'])
-                        ->label(false) ?>
-                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-date-to">
-                        <i class="fa fa-times"></i>
-                    </div>
-                </div>
-            </div>
-            <div class="mb-3">
-                <?= $formFilter->field($filter, 'onlyActive')->checkbox([
-                    'label'   => 'Только активные',
-                    'uncheck' => 0,
-                    'checked' => (bool)$filter->onlyActive,
-                    'id'      => 'onlyActiveCheckbox',
-                ])->label(false) ?>
-                <?= $formFilter->field($filter, 'onlyInactive')->checkbox([
-                    'label'   => 'Только неактивные',
-                    'uncheck' => 0,
-                    'checked' => (bool)$filter->onlyInactive,
-                    'id'      => 'onlyInactiveCheckbox',
-                ])->label(false) ?>
-            </div>
+        <div class="fg">
+            <label>Подкатегория</label>
+            <?= $formFilter->field($filter, 'subcategory', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($subcategories, ['prompt' => 'Все', 'id' => 'filter-subcategory', 'class' => 'form-select'])
+                ->label(false) ?>
         </div>
-
-        <div class="col-2 ps-4" style="border-left: #ccc solid 1px">
-            <div class="mb-2 fw-bold">Поставщики</div>
-            <div class="mb-3">
-                <div class="input-group">
-                    <?= Html::dropDownList('supplier', null,
-                        ['Астра', 'Бифлористика'],
-                        ['class' => 'form-select', 'id' => 'filter-supplier', 'prompt' => 'Поставщик']) ?>
-                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-supplier">
-                        <i class="fa fa-times"></i>
-                    </div>
-                </div>
-            </div>
-            <div class="mb-4">
-                <div class="input-group">
-                    <?= Html::dropDownList('plantation', null,
-                        ['Плантация1', 'Плантация2'],
-                        ['class' => 'form-select', 'id' => 'filter-plantation', 'prompt' => 'Плантация']) ?>
-                    <div class="mb-4 ms-1 d-flex justify-content-center align-items-center clear-btn" data-target="filter-plantation">
-                        <i class="fa fa-times"></i>
-                    </div>
-                </div>
-            </div>
+        <div class="fg">
+            <label>Вид</label>
+            <?= $formFilter->field($filter, 'species', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($species, ['prompt' => 'Все', 'id' => 'filter-species', 'class' => 'form-select'])
+                ->label(false) ?>
+        </div>
+        <div class="fg">
+            <label>Сорт</label>
+            <?= $formFilter->field($filter, 'sort', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($sorts, ['prompt' => 'Все', 'id' => 'filter-sort', 'class' => 'form-select'])
+                ->label(false) ?>
+        </div>
+        <div class="fg">
+            <label><i class="fa fa-circle" style="font-size:9px;color:#e91e63"></i> Цвет</label>
+            <?= $formFilter->field($filter, 'color', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($colors, ['prompt' => 'Все', 'id' => 'filter-color', 'class' => 'form-select'])
+                ->label(false) ?>
+        </div>
+        <div class="fg">
+            <label>Высота</label>
+            <?= $formFilter->field($filter, 'size', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($sizes, ['prompt' => 'Все', 'id' => 'filter-size', 'class' => 'form-select'])
+                ->label(false) ?>
         </div>
 
-        <div class="col-1 ps-4 d-flex flex-column justify-content-end align-items-stretch gap-2">
-            <?= Html::submitButton('Применить', ['class' => 'btn btn-primary w-100']) ?>
-            <a href="<?= \yii\helpers\Url::to(array_merge(
-                ['/products1c-nomenclature-actuality/export-xlsx'],
-                array_filter(Yii::$app->request->get(), static fn($v) => $v !== '' && $v !== null)
-            )) ?>" class="btn btn-outline-success w-100">
-                <i class="fa fa-file-excel-o"></i> XLSX
-            </a>
+        <div class="fb-sep"></div>
+
+        <div class="fg">
+            <label><i class="fa fa-clock-o" style="font-size:9px;color:#198754"></i> Актуален от</label>
+            <?= $formFilter->field($filter, 'date_from', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($months, ['prompt' => '—', 'id' => 'filter-date-from', 'class' => 'form-select'])
+                ->label(false) ?>
+        </div>
+        <div class="fg">
+            <label>До</label>
+            <?= $formFilter->field($filter, 'date_to', ['options' => ['class' => 'mb-0']])
+                ->dropDownList($months, ['prompt' => '—', 'id' => 'filter-date-to', 'class' => 'form-select'])
+                ->label(false) ?>
         </div>
-    </div>
 
-    <!-- Поиск по наименованию -->
-    <div class="row mt-2">
-        <div class="col-8">
+        <div class="fb-sep"></div>
+
+        <div class="fg" style="min-width:140px">
+            <label>Поиск</label>
             <?= $formFilter->field($filter, 'search', ['options' => ['class' => 'mb-0']])
-                ->textInput(['placeholder' => 'Ð\9fоиÑ\81к Ð¿Ð¾ Ð½Ð°Ð¸Ð¼ÐµÐ½Ð¾Ð²Ð°Ð½Ð¸Ñ\8e...', 'id' => 'filter-search'])
+                ->textInput(['placeholder' => 'Ð\9dазвание, GUIDâ\80¦', 'id' => 'filter-search', 'class' => 'form-control'])
                 ->label(false) ?>
         </div>
-        <div class="col-4 d-flex align-items-center gap-3">
-            <div class="d-flex align-items-center gap-2">
-                <label class="mb-0 text-nowrap small fw-semibold">Показывать по:</label>
-                <?= $formFilter->field($filter, 'pageSize', ['options' => ['class' => 'mb-0']])
-                    ->dropDownList([50 => '50', 100 => '100', 500 => '500'], [
-                        'id' => 'filter-page-size',
-                        'class' => 'form-select form-select-sm w-auto',
-                    ])->label(false) ?>
-            </div>
-            <div class="d-flex align-items-center gap-2">
-                <label class="mb-0 text-nowrap small fw-semibold">Сортировка:</label>
-                <?= $formFilter->field($filter, 'sort_by', ['options' => ['class' => 'mb-0']])
-                    ->dropDownList(['name' => 'По наименованию', 'date_actuality' => 'По дате актуальности'], [
-                        'id' => 'filter-sort-by',
-                        'class' => 'form-select form-select-sm w-auto',
-                    ])->label(false) ?>
+
+        <div class="fg align-self-end">
+            <label>&nbsp;</label>
+            <div class="d-flex gap-1 align-items-center" style="height:30px">
+                <?= $formFilter->field($filter, 'onlyActive', ['options' => ['class' => 'mb-0 me-1']])
+                    ->checkbox(['label' => 'Только активные', 'uncheck' => 0, 'checked' => (bool)$filter->onlyActive, 'id' => 'onlyActiveCheckbox'])
+                    ->label(false) ?>
+                <?= $formFilter->field($filter, 'onlyInactive', ['options' => ['class' => 'mb-0']])
+                    ->checkbox(['label' => 'Только неактивные', 'uncheck' => 0, 'checked' => (bool)$filter->onlyInactive, 'id' => 'onlyInactiveCheckbox'])
+                    ->label(false) ?>
             </div>
         </div>
+
+        <div class="fb-sep"></div>
+
+        <div class="fg">
+            <label>На странице</label>
+            <?= $formFilter->field($filter, 'pageSize', ['options' => ['class' => 'mb-0']])
+                ->dropDownList([50 => '50', 100 => '100', 500 => '500'], ['id' => 'filter-page-size', 'class' => 'form-select'])
+                ->label(false) ?>
+        </div>
+
+        <?= Html::submitButton('<i class="fa fa-search me-1"></i>Найти', [
+            'class' => 'btn btn-sm align-self-end',
+            'style' => 'height:30px;background:var(--cat);border-color:var(--cat);color:#fff;font-size:12px',
+        ]) ?>
+        <a href="<?= Url::to(['/products1c-nomenclature-actuality/index']) ?>"
+           class="btn btn-link btn-sm text-secondary align-self-end" style="height:30px;font-size:11px">Сбросить</a>
     </div>
 
     <?php ActiveForm::end(); ?>
 
-    <?php if ($counters !== null): ?>
-    <div class="row g-2 mb-3 mt-2">
-        <div class="col-auto">
-            <div class="card border-0 bg-light px-3 py-2 text-center" style="min-width:110px">
-                <div class="fs-4 fw-bold"><?= $counters['total'] ?></div>
-                <div class="small text-muted">Всего</div>
-            </div>
+    <!-- ── Bulk toolbar ── -->
+    <div id="bulkToolbar" class="bulk-bar d-none">
+        <div>
+            <i class="fa fa-check-square-o me-2"></i>
+            <span id="bulkCount" class="fw-semibold">0 выбрано</span>
+        </div>
+        <div class="d-flex gap-2 align-items-center">
+            <select id="bulkLabelSelect" class="form-select form-select-sm"
+                    style="max-width:200px;font-size:11px;height:28px;padding:2px 8px">
+                <option value="">— Лейбл —</option>
+            </select>
+            <button class="btn btn-outline-light btn-sm" id="bulkAddBtn" style="font-size:11px">
+                <i class="fa fa-plus me-1"></i>Добавить
+            </button>
+            <button class="btn btn-outline-light btn-sm" id="bulkRemoveBtn" style="font-size:11px">
+                <i class="fa fa-minus me-1"></i>Убрать
+            </button>
+            <button class="btn btn-outline-warning btn-sm ms-2" id="bulkClearBtn" style="font-size:11px">
+                <i class="fa fa-times me-1"></i>Снять
+            </button>
+        </div>
+    </div>
+
+    <!-- ── Cards loop ── -->
+    <?php if (empty($dataProvider->getModels())): ?>
+        <div class="text-center py-5 text-muted">
+            <i class="fa fa-search fa-2x mb-2 d-block"></i>
+            Примените фильтры для отображения номенклатуры
         </div>
-        <div class="col-auto">
-            <div class="card border-0 bg-success bg-opacity-10 px-3 py-2 text-center" style="min-width:110px">
-                <div class="fs-4 fw-bold text-success"><?= $counters['active'] ?></div>
-                <div class="small text-muted">Активных</div>
+    <?php else: ?>
+
+    <div id="productCards">
+    <?php foreach ($dataProvider->getModels() as $row):
+        $p        = $row['product'];
+        $acts     = $row['actualities'];
+        $labels   = $row['labels'] ?? [];
+        $concepts = $row['concepts'] ?? [];
+
+        $status = actGetStatus($acts);
+
+        // Card outer style
+        $cardStyle = '';
+        if ($status === 'none') {
+            $cardStyle = 'border-left:4px solid #dc3545';
+        } elseif ($status === 'inactive') {
+            $cardStyle = 'opacity:.55';
+        }
+
+        // Status badge
+        [$bBg, $bColor, $bText] = match ($status) {
+            'active'   => ['#d4edda', '#0f5132', 'Актуален'],
+            'future'   => ['#cfe2ff', '#084298', 'Будущий'],
+            'inactive' => ['#e9ecef', '#6c757d', 'Не актуален'],
+            default    => ['#f8d7da', '#842029', '<i class="fa fa-exclamation-triangle" style="font-size:8px"></i> Нет интервалов'],
+        };
+    ?>
+    <div class="pr" style="<?= $cardStyle ?>" data-guid="<?= Html::encode($p->id) ?>">
+        <div class="pr-top">
+
+            <!-- Left: checkbox + status + name -->
+            <div class="pr-left">
+                <input type="checkbox" class="form-check-input row-checkbox"
+                       value="<?= Html::encode($p->id) ?>"
+                       style="width:16px;height:16px;flex-shrink:0;margin-top:4px"
+                       id="rc-<?= Html::encode($p->id) ?>">
+                <div style="min-width:0">
+                    <span style="background:<?= $bBg ?>;color:<?= $bColor ?>;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:600"><?= $bText ?></span>
+                    <h5 class="pr-name"><?= Html::encode($p->name) ?></h5>
+                    <div class="pr-sub">
+                        GUID:&nbsp;<?= Html::encode(substr($p->id, 0, 8)) ?>…
+                        <?php if ($p->category): ?>
+                        · Категория:&nbsp;<?= Html::encode($p->category) ?>
+                        <?php endif; ?>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Center: intervals -->
+            <div class="pr-center">
+                <span style="font-size:10px;color:#6c757d;text-transform:uppercase">
+                    <?= $status === 'none' ? '<span class="text-danger">Нет интервалов</span>' : 'Предстоящие интервалы' ?>
+                </span>
+                <div class="pr-intervals">
+                    <?= actIntervalChips($acts) ?>
+                </div>
+            </div>
+
+            <!-- Right: inline score (loaded lazily on scb expand) -->
+            <div class="pr-right">
+                <div class="score-inline">
+                    <span style="font-size:10px;color:#6c757d">ср. оценка</span>
+                    <span class="sc-stars" id="sc-stars-<?= Html::encode($p->id) ?>">
+                        <i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i>
+                    </span>
+                    <span id="sc-avg-<?= Html::encode($p->id) ?>" style="font-size:10px;color:#adb5bd">—</span>
+                </div>
             </div>
         </div>
-        <div class="col-auto">
-            <div class="card border-0 bg-warning bg-opacity-10 px-3 py-2 text-center" style="min-width:110px">
-                <div class="fs-4 fw-bold text-warning"><?= $counters['no_labels'] ?></div>
-                <div class="small text-muted">Без матрицы</div>
+
+        <!-- Labels + concepts + edit button -->
+        <div style="display:flex;justify-content:space-between;align-items:flex-end;margin-top:8px">
+            <div>
+                <div class="mb-1">
+                    <span class="lbl-title">Ассортиментная матрица</span>
+                    <span class="label-chips-cell"><?= actLabelChips($labels, $p->id) ?></span>
+                </div>
+                <?php if (!empty($concepts)): ?>
+                <div>
+                    <span class="lbl-title">Концепция (в букетах)</span>
+                    <?= actConceptChips($concepts) ?>
+                </div>
+                <?php endif; ?>
             </div>
+            <button type="button" class="btn-pr-edit open-intervals-modal"
+                    data-guid="<?= Html::encode($p->id) ?>"
+                    data-name="<?= Html::encode($p->name) ?>">редактировать</button>
         </div>
-        <?php if ($counters['no_score'] !== null): ?>
-        <div class="col-auto">
-            <div class="card border-0 bg-danger bg-opacity-10 px-3 py-2 text-center" style="min-width:110px">
-                <div class="fs-4 fw-bold text-danger"><?= $counters['no_score'] ?></div>
-                <div class="small text-muted">Без оценки</div>
+
+        <!-- Score collapsible block -->
+        <div class="scb">
+            <div class="scb-toggle" data-guid="<?= Html::encode($p->id) ?>">
+                <i class="fa fa-chevron-right scb-chevron"></i>
+                <i class="fa fa-star" style="color:#ffc107;font-size:10px"></i>
+                <span>Оценки и комментарии</span>
             </div>
+            <div class="scb-content"></div>
         </div>
-        <?php endif; ?>
     </div>
-    <?php endif; ?>
+    <?php endforeach; ?>
+    </div>
 
-    <!-- Bulk-тулбар (скрыт пока нет выбора) -->
-    <div id="bulkToolbar" class="d-none mb-2 p-2 bg-light rounded border d-flex flex-wrap align-items-center gap-2">
-        <span class="fw-semibold text-primary me-1" id="bulkCount">0 выбрано</span>
-        <select id="bulkLabelSelect" class="form-select form-select-sm" style="max-width:220px">
-            <option value="">— Лейбл —</option>
-        </select>
-        <button class="btn btn-sm btn-success" id="bulkAddBtn" title="Добавить лейбл выбранным товарам">
-            <i class="fa fa-plus"></i> Добавить
-        </button>
-        <button class="btn btn-sm btn-outline-danger" id="bulkRemoveBtn" title="Убрать лейбл с выбранных товаров">
-            <i class="fa fa-minus"></i> Убрать
-        </button>
-        <button class="btn btn-sm btn-link text-secondary ms-auto" id="bulkClearBtn">
-            <i class="fa fa-times"></i> Снять выделение
-        </button>
+    <!-- ── Pagination ── -->
+    <?php
+    $pagination = $dataProvider->getPagination();
+    $page       = $pagination->getPage();
+    $ps         = $pagination->getPageSize();
+    $total      = $pagination->totalCount;
+    $from       = $total > 0 ? $page * $ps + 1 : 0;
+    $to         = min(($page + 1) * $ps, $total);
+    ?>
+    <div class="d-flex justify-content-between align-items-center mt-2 px-1">
+        <div style="font-size:12px;color:#6c757d">
+            <?= $from ?>–<?= $to ?> из <?= $total ?>
+        </div>
+        <?= LinkPager::widget([
+            'pagination'  => $pagination,
+            'options'     => ['class' => 'pagination pagination-sm mb-0'],
+            'linkOptions' => ['class' => 'page-link'],
+        ]) ?>
     </div>
 
-    <?= GridView::widget([
-        'dataProvider'     => $dataProvider,
-        'responsive'       => false,
-        'hover'            => true,
-        'floatHeader'      => false,
-        'tableOptions'     => ['class' => 'table table-bordered'],
-        'containerOptions' => ['style' => 'overflow:auto; max-height:600px;'],
-        'rowOptions'       => function ($row) {
-            $hasActive = false;
-            foreach (($row['actualities'] ?? []) as $act) {
-                if ($act->getStatus() === 'active') {
-                    $hasActive = true;
-                    break;
-                }
-            }
-            return [
-                'data-guid' => $row['product']->id,
-                'class'     => $hasActive ? 'table-success' : '',
-            ];
-        },
-        'columns' => [
-            [
-                'format'         => 'raw',
-                'headerOptions'  => ['style' => 'width:36px;text-align:center;vertical-align:middle'],
-                'contentOptions' => ['style' => 'width:36px;text-align:center;vertical-align:middle'],
-                'header'         => '<input type="checkbox" id="selectAllCheckbox" class="form-check-input" title="Выбрать всё на странице">',
-                'value'          => function ($row) {
-                    return '<input type="checkbox" class="form-check-input row-checkbox" value="' . Html::encode($row['product']->id) . '">';
-                },
-            ],
-            [
-                'label'          => 'Наименование',
-                'format'         => 'raw',
-                'contentOptions' => ['style' => 'min-width:150px;'],
-                'value'          => function ($row) {
-                    $p = $row['product'];
-                    return Html::encode($p->name . ' (' . $p->id . ')');
-                },
-            ],
-            [
-                'label'          => 'Актуальность ассортимента',
-                'format'         => 'raw',
-                'contentOptions' => ['style' => 'min-width:200px;'],
-                'value'          => function ($row) {
-                    $p    = $row['product'];
-                    $acts = $row['actualities'];
-                    $chips = '<span class="intervals-chips">' . renderActualityChips($acts) . '</span>';
-                    $btn   = Html::button('<i class="fa fa-pencil"></i>', [
-                        'class'     => 'btn btn-xs btn-outline-secondary ms-2 open-intervals-modal',
-                        'title'     => 'Редактировать интервалы',
-                        'data-guid' => $p->id,
-                        'data-name' => $p->name,
-                    ]);
-                    return '<div class="d-flex align-items-center flex-wrap gap-1">' . $chips . $btn . '</div>';
-                },
-            ],
-            [
-                'label'          => 'Лейблы',
-                'format'         => 'raw',
-                'contentOptions' => ['style' => 'min-width:160px;'],
-                'value'          => function ($row) {
-                    $p      = $row['product'];
-                    $labels   = $row['labels'] ?? [];
-                    $concepts = $row['concepts'] ?? [];
-                    return '<span class="label-chips-cell">'
-                        . renderLabelChips($labels, $p->id)
-                        . renderConceptChips($concepts)
-                        . '</span>';
-                },
-            ],
-            [
-                'label'          => 'Склад NN',
-                'format'         => 'raw',
-                'contentOptions' => ['style' => 'width:60px; text-align:center;'],
-                'value'          => fn() => '<input type="checkbox">',
-            ],
-            [
-                'label'          => 'Склад MSK',
-                'format'         => 'raw',
-                'contentOptions' => ['style' => 'width:60px; text-align:center;'],
-                'value'          => fn() => '<input type="checkbox">',
-            ],
-            [
-                'label'  => 'Поставщик/Плантация',
-                'format' => 'text',
-                'contentOptions' => ['style' => 'min-width:150px;'],
-                'value'  => fn() => '–',
-            ],
-        ],
-    ]); ?>
+    <?php endif; ?>
+
+    <!-- select-all header checkbox -->
+    <div style="display:none">
+        <input type="checkbox" id="selectAllCheckbox">
+    </div>
 
 </div>
 
-<!-- ==================== Модалка интервалов / лейблов ==================== -->
+<!-- ═══════════════════════════ Modal ═══════════════════════════ -->
 <div class="modal fade" id="intervalsModal" tabindex="-1" aria-labelledby="intervalsModalLabel" aria-hidden="true">
     <div class="modal-dialog modal-lg">
         <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="intervalsModalLabel">
-                    <span id="modalProductName" class="text-primary"></span>
+            <div class="modal-header" style="background:var(--cat);color:#fff;padding:12px 16px">
+                <h5 class="modal-title" id="intervalsModalLabel" style="font-size:15px">
+                    <i class="fa fa-pencil me-2"></i><span id="modalProductName" class="text-white"></span>
                 </h5>
-                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
             </div>
             <div class="modal-body">
                 <ul class="nav nav-tabs mb-3" id="modalTabs" role="tablist">
                     <li class="nav-item" role="presentation">
                         <button class="nav-link active" id="tab-intervals-btn" data-bs-toggle="tab"
                                 data-bs-target="#tabIntervals" type="button" role="tab">
-                            Интервалы актуальности
+                            <i class="fa fa-calendar me-1"></i>Интервалы актуальности
                         </button>
                     </li>
                     <li class="nav-item" role="presentation">
                         <button class="nav-link" id="tab-labels-btn" data-bs-toggle="tab"
                                 data-bs-target="#tabLabels" type="button" role="tab">
-                            Лейблы
+                            <i class="fa fa-tags me-1"></i>Лейблы матрицы
                         </button>
                     </li>
                     <li class="nav-item" role="presentation">
                         <button class="nav-link" id="tab-scores-btn" data-bs-toggle="tab"
                                 data-bs-target="#tabScores" type="button" role="tab">
-                            Оценки
+                            <i class="fa fa-star me-1"></i>Оценки
                         </button>
                     </li>
                 </ul>
 
                 <div class="tab-content">
-                    <!-- Вкладка: Интервалы -->
                     <div class="tab-pane fade show active" id="tabIntervals" role="tabpanel">
                         <div id="intervalsTableContainer">
                             <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">
                             <input type="hidden" id="intervalId">
                             <div class="row g-2 align-items-end">
@@ -537,30 +590,23 @@ function renderLabelChips(array $labels, string $productGuid): string
                         </div>
                     </div>
 
-                    <!-- Вкладка: Лейблы -->
                     <div class="tab-pane fade" id="tabLabels" role="tabpanel">
                         <div id="labelsContainer">
                             <div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> Загрузка...</div>
                         </div>
                     </div>
 
-                    <!-- Вкладка: Оценки -->
                     <div class="tab-pane fade" id="tabScores" role="tabpanel">
-                        <!-- Avg store score -->
                         <div id="avgScoreBar" class="mb-3" style="display:none">
                             <span class="text-muted small">Средняя оценка магазинов:</span>
                             <span id="avgScoreValue" class="fw-bold ms-1"></span>
                             <span class="text-warning ms-1" id="avgScoreStars"></span>
                         </div>
-
-                        <!-- Accordion маппингов -->
                         <div id="scoresContainer">
                             <div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> Загрузка...</div>
                         </div>
-
-                        <!-- Заглушка: добавить тестовый маппинг (только DEBUG) -->
-                        <div id="testMappingBlock" class="mt-3 p-2 border border-dashed rounded bg-light" style="display:none">
-                            <div class="text-muted small mb-2"><i class="fa fa-flask me-1"></i>Режим разработки — добавить тестовый маппинг</div>
+                        <div id="testMappingBlock" class="mt-3 p-2 border rounded bg-light" style="display:none">
+                            <div class="text-muted small mb-2"><i class="fa fa-flask me-1"></i>Режим разработки</div>
                             <div class="d-flex gap-2">
                                 <input type="text" id="testMappingName" class="form-control form-control-sm"
                                        placeholder="Название у поставщика" style="max-width:300px">
@@ -570,41 +616,40 @@ function renderLabelChips(array $labels, string $productGuid): string
                     </div>
                 </div>
             </div>
-            <div class="modal-footer justify-content-between">
+            <div class="modal-footer justify-content-between" style="padding:10px 16px">
                 <div>
                     <button class="btn btn-sm btn-primary" id="addIntervalBtn">
-                        <i class="fa fa-plus"></i> Добавить интервал
+                        <i class="fa fa-plus me-1"></i>Добавить интервал
                     </button>
                     <button class="btn btn-sm btn-success d-none" id="saveLabelsBtn">
-                        <i class="fa fa-save"></i> Сохранить лейблы
+                        <i class="fa fa-save me-1"></i>Сохранить лейблы
                     </button>
                 </div>
-                <!-- кнопки вкладки Оценки — пустые, всё inline -->
-                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
+                <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Закрыть</button>
             </div>
         </div>
     </div>
 </div>
 
-<!-- Toast-контейнер -->
+<!-- Toast container -->
 <div id="toastContainer" class="position-fixed top-0 end-0 p-3" style="z-index:9999"></div>
 
 <script>
 window.productActualityConfig = {
     months: <?= json_encode($months, JSON_UNESCAPED_UNICODE) ?>,
     urls: {
-        intervals:      '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-intervals']) ?>',
-        saveInterval:   '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-save-interval']) ?>',
-        deleteInterval: '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-delete']) ?>',
-        labels:          '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-labels']) ?>',
-        saveAssortment:  '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-save-assortment']) ?>',
-        removeLabel:     '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-remove-label']) ?>',
-        scores:          '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-scores']) ?>',
-        saveScore:       '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-save-score']) ?>',
-        addTestMapping:  '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-add-test-mapping']) ?>',
-        exportXlsx:      '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/export-xlsx']) ?>',
-        bulkAssign:      '<?= \yii\helpers\Url::to(['/products1c-nomenclature-actuality/ajax-bulk-assign']) ?>',
-        labelsList:      '<?= \yii\helpers\Url::to(['/assortment-label/ajax-list']) ?>',
+        intervals:      '<?= Url::to(['/products1c-nomenclature-actuality/ajax-intervals']) ?>',
+        saveInterval:   '<?= Url::to(['/products1c-nomenclature-actuality/ajax-save-interval']) ?>',
+        deleteInterval: '<?= Url::to(['/products1c-nomenclature-actuality/ajax-delete']) ?>',
+        labels:         '<?= Url::to(['/products1c-nomenclature-actuality/ajax-labels']) ?>',
+        saveAssortment: '<?= Url::to(['/products1c-nomenclature-actuality/ajax-save-assortment']) ?>',
+        removeLabel:    '<?= Url::to(['/products1c-nomenclature-actuality/ajax-remove-label']) ?>',
+        scores:         '<?= Url::to(['/products1c-nomenclature-actuality/ajax-scores']) ?>',
+        saveScore:      '<?= Url::to(['/products1c-nomenclature-actuality/ajax-save-score']) ?>',
+        addTestMapping: '<?= Url::to(['/products1c-nomenclature-actuality/ajax-add-test-mapping']) ?>',
+        exportXlsx:     '<?= Url::to(['/products1c-nomenclature-actuality/export-xlsx']) ?>',
+        bulkAssign:     '<?= Url::to(['/products1c-nomenclature-actuality/ajax-bulk-assign']) ?>',
+        labelsList:     '<?= Url::to(['/assortment-label/ajax-list']) ?>',
     }
 };
 </script>
index 32f5bc5d3dbda604bdf2fbcfd1c2dd8461862c5a..d9bcf7c5027c1360cac5044a765dd574f9037988 100644 (file)
@@ -20,6 +20,23 @@ use yii\web\View;
 $this->title = 'Разметка номенклатуры';
 $this->params['breadcrumbs'][] = $this->title;
 $this->registerJsFile('/js/products1cNomenclatureMarkup/index.js', ['position' => View::POS_END]);
+$this->registerCss(<<<'CSS'
+:root { --ao: #1e3a5f; --cat: #6f42c1; }
+.cm-tabs { border-bottom: 2px solid #dee2e6; margin-bottom: 20px; }
+.cm-tabs .nav-link { font-size: 14px; color: #495057; border: none; padding: 10px 20px; font-weight: 500;
+    border-bottom: 3px solid transparent; margin-bottom: -2px; background: none; }
+.cm-tabs .nav-link.active { color: var(--cat); font-weight: 700; border-bottom-color: var(--cat); }
+.cm-tabs .nav-link:hover:not(.active) { color: var(--ao); }
+.cm-tabs .tc { font-size: 10px; font-weight: 700; margin-left: 6px; padding: 1px 6px; border-radius: 10px;
+    background: #e9ecef; color: #495057; display: inline-block; }
+.cm-tabs .nav-link.active .tc { background: var(--cat); color: #fff; }
+.cm-tabs .tc-danger { background: #dc3545 !important; color: #fff !important; }
+.fb { background: #fff; border: 1px solid #dee2e6; border-radius: 8px;
+    padding: 12px 16px; margin-bottom: 12px; display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
+.fb .fg { display: flex; flex-direction: column; gap: 2px; }
+.fb .fg > label { font-size: 11px; color: #6c757d; font-weight: 500; margin: 0; }
+.fb .form-select { font-size: 12px; padding: 4px 8px; height: 30px; }
+CSS);
 
 $statusLabels = MarkupCtrl::statusLabels();
 ?>
@@ -29,7 +46,7 @@ $statusLabels = MarkupCtrl::statusLabels();
     <h1><?= Html::encode($this->title) ?></h1>
 
     <!-- Таб-навигация -->
-    <ul class="nav nav-tabs mb-4">
+    <ul class="nav cm-tabs" role="tablist">
         <li class="nav-item">
             <a class="nav-link" href="<?= Url::to(['/products1c-nomenclature-actuality/index']) ?>">
                 <i class="fa fa-calendar-check-o me-1"></i>Актуальность ассортимента
@@ -38,7 +55,7 @@ $statusLabels = MarkupCtrl::statusLabels();
         <li class="nav-item">
             <a class="nav-link active" href="<?= Url::to(['/products1c-nomenclature-markup/index']) ?>">
                 <i class="fa fa-tag me-1"></i>Разметка
-                <span class="badge bg-danger ms-1"><?= $counters[MarkupCtrl::STATUS_PENDING] ?></span>
+                <span class="tc tc-danger"><?= $counters[MarkupCtrl::STATUS_PENDING] ?></span>
             </a>
         </li>
         <li class="nav-item">
@@ -81,22 +98,22 @@ $statusLabels = MarkupCtrl::statusLabels();
     </div>
 
     <!-- Фильтры -->
-    <?php $form = ActiveForm::begin(['method' => 'get', 'action' => ['index'], 'options' => ['class' => 'mb-3']]); ?>
-    <div class="d-flex flex-wrap gap-2 align-items-end">
-        <div>
-            <label class="form-label mb-1 small">Категория</label>
+    <?php $form = ActiveForm::begin(['method' => 'get', 'action' => ['index'], 'options' => ['class' => 'mb-0']]); ?>
+    <div class="fb">
+        <div class="fg">
+            <label>Категория</label>
             <?= $form->field($filter, 'category', ['options' => ['class' => 'mb-0']])
-                ->dropDownList($categories, ['prompt' => 'Все', 'class' => 'form-select form-select-sm'])
+                ->dropDownList($categories, ['prompt' => 'Все', 'class' => 'form-select'])
                 ->label(false) ?>
         </div>
-        <div>
-            <label class="form-label mb-1 small">Вид</label>
+        <div class="fg">
+            <label>Вид</label>
             <?= $form->field($filter, 'species', ['options' => ['class' => 'mb-0']])
-                ->dropDownList($speciesList, ['prompt' => 'Все', 'class' => 'form-select form-select-sm'])
+                ->dropDownList($speciesList, ['prompt' => 'Все', 'class' => 'form-select'])
                 ->label(false) ?>
         </div>
-        <div>
-            <label class="form-label mb-1 small">Статус разметки</label>
+        <div class="fg">
+            <label>Статус разметки</label>
             <?= $form->field($filter, 'classification_status', ['options' => ['class' => 'mb-0']])
                 ->dropDownList([
                     ''                               => 'Все',
@@ -104,38 +121,39 @@ $statusLabels = MarkupCtrl::statusLabels();
                     MarkupCtrl::STATUS_UNCLASSIFIED  => 'Не размечены',
                     MarkupCtrl::STATUS_PENDING       => 'Требуют подтверждения',
                     MarkupCtrl::STATUS_APPROVED      => 'Подтверждены',
-                ], ['class' => 'form-select form-select-sm'])
+                ], ['class' => 'form-select'])
                 ->label(false) ?>
         </div>
-        <div>
-            <label class="form-label mb-1 small">Уверенность</label>
+        <div class="fg">
+            <label>Уверенность</label>
             <?= $form->field($filter, 'confidence_range', ['options' => ['class' => 'mb-0']])
                 ->dropDownList([
                     ''       => 'Все',
                     'high'   => '≥ 90%',
                     'medium' => '70–89%',
                     'low'    => '< 70%',
-                ], ['class' => 'form-select form-select-sm'])
+                ], ['class' => 'form-select'])
                 ->label(false) ?>
         </div>
-        <div>
-            <label class="form-label mb-1 small">На странице</label>
+        <div class="fg">
+            <label>На странице</label>
             <?= $form->field($filter, 'pageSize', ['options' => ['class' => 'mb-0']])
-                ->dropDownList([50 => '50', 100 => '100', 500 => '500'], ['class' => 'form-select form-select-sm'])
+                ->dropDownList([50 => '50', 100 => '100', 500 => '500'], ['class' => 'form-select'])
                 ->label(false) ?>
         </div>
-        <?= Html::submitButton('Применить', ['class' => 'btn btn-primary btn-sm']) ?>
-        <a href="<?= Url::to(['index']) ?>" class="btn btn-link btn-sm text-secondary">Сбросить</a>
+        <?= Html::submitButton('Применить', ['class' => 'btn btn-sm align-self-end', 'style' => 'height:30px;background:#0d6efd;border-color:#0d6efd;color:#fff;font-size:12px']) ?>
+        <a href="<?= Url::to(['index']) ?>" class="btn btn-link btn-sm text-secondary align-self-end" style="height:30px;font-size:11px">Сбросить</a>
     </div>
     <?php ActiveForm::end(); ?>
 
     <!-- Bulk-тулбар -->
-    <div id="markupBulkBar" class="d-none mb-2 p-2 rounded border bg-primary text-white d-flex align-items-center gap-3">
+    <div id="markupBulkBar" class="d-none mb-2 p-2 rounded d-flex align-items-center gap-3"
+         style="background:var(--ao);color:#fff">
         <span class="fw-semibold" id="markupBulkCount">0 выбрано</span>
-        <button class="btn btn-sm btn-light" id="markupBulkApproveBtn">
+        <button class="btn btn-sm btn-outline-light" id="markupBulkApproveBtn">
             <i class="fa fa-check-double me-1"></i>Подтвердить выбранные
         </button>
-        <button class="btn btn-sm btn-outline-light ms-auto" id="markupBulkClearBtn">
+        <button class="btn btn-sm btn-outline-warning ms-auto" id="markupBulkClearBtn">
             <i class="fa fa-times me-1"></i>Снять выделение
         </button>
     </div>
index 09db374109fe10118abdb4d78e49fac98ed515d3..680cf6583286ffbca439b117bdeaf1f877b693c7 100644 (file)
@@ -56,47 +56,45 @@ document.addEventListener('DOMContentLoaded', () => {
 
     // ─── Chips helpers ──────────────────────────────────────────────────────
 
-    const STATUS_CLASS = { active: 'bg-success', future: 'bg-primary', past: 'bg-secondary' };
-
-    function renderChips(intervals) {
+    function renderIntervalChips(intervals) {
         if (!intervals.length) {
-            return '<span class="text-danger" title="Нет интервалов"><i class="fa fa-exclamation-circle"></i></span>';
+            return '<span class="text-danger" style="font-size:11px;font-style:italic">Не запланирован ни один интервал актуальности</span>';
         }
-        const visible = intervals.slice(0, 3);
-        const hidden  = intervals.length - visible.length;
-        let html = visible.map(iv => {
-            const cls   = STATUS_CLASS[iv.status] || 'bg-secondary';
-            const style = iv.status === 'past' ? ' style="text-decoration:line-through"' : '';
-            return `<span class="badge ${cls} me-1"${style}>${iv.label}</span>`;
+        return intervals.map(iv => {
+            const cls = iv.status === 'active' ? 'active' : iv.status === 'future' ? 'future' : 'past';
+            return `<span class="pr-int ${cls}">${iv.label}</span>`;
         }).join('');
-        if (hidden > 0) html += `<span class="text-muted small ms-1">+${hidden} ещё</span>`;
-        return html;
     }
 
     function updateRowChips(guid, intervals) {
-        const $row = $(`tr[data-guid="${guid}"]`);
-        $row.find('.intervals-chips').html(renderChips(intervals));
-        const hasActive = intervals.some(iv => iv.status === 'active');
-        $row.toggleClass('table-success', hasActive);
+        const $card = $(`.pr[data-guid="${guid}"]`);
+        $card.find('.pr-intervals').html(renderIntervalChips(intervals));
+        const hasActive  = intervals.some(iv => iv.status === 'active');
+        const hasAny     = intervals.length > 0;
+        const hasFuture  = intervals.some(iv => iv.status === 'future');
+        if (!hasAny) {
+            $card.css('border-left', '4px solid #dc3545').css('opacity', '');
+        } else if (!hasActive && !hasFuture) {
+            $card.css('border-left', '').css('opacity', '.55');
+        } else {
+            $card.css('border-left', '').css('opacity', '');
+        }
     }
 
+    const CHANNEL_CLASS = { offline: 'ch-off', online: 'ch-site', marketplace: 'ch-mp' };
+    const CHANNEL_ICON  = { offline: 'fa-store', online: 'fa-globe', marketplace: 'fa-shopping-bag' };
+
     function renderLabelChips(labels, guid) {
-        if (!labels.length) return '<span class="text-muted small">—</span>';
-        return '<span class="label-chips-wrap">' + labels.map(l => {
-            const color = l.color || '#6c757d';
-            return `<span class="label-chip d-inline-flex align-items-center me-1 mb-1 badge"
-                style="background-color:${color};cursor:default"
-                data-label-id="${l.id}">
-                ${l.name}
-                <button type="button" class="remove-label-chip btn-close btn-close-white ms-1"
-                    style="font-size:.6rem" aria-label="Убрать"
-                    data-guid="${guid}" data-label-id="${l.id}"></button>
-            </span>`;
-        }).join('') + '</span>';
+        if (!labels || !labels.length) return '<span class="text-muted small">—</span>';
+        return labels.map(l => {
+            const cls  = CHANNEL_CLASS[l.channel_type] || 'ch-1p';
+            const icon = CHANNEL_ICON[l.channel_type]  || 'fa-tag';
+            return `<span class="ch ${cls} ed label-chip" data-label-id="${l.id}"><i class="fa ${icon}" style="font-size:9px;margin-right:3px"></i>${l.name}<button type="button" class="remove-label-chip ch-rm" data-guid="${guid}" data-label-id="${l.id}">&times;</button></span>`;
+        }).join('');
     }
 
     function updateRowLabels(guid, labels) {
-        $(`tr[data-guid="${guid}"] .label-chips-cell`).html(renderLabelChips(labels, guid));
+        $(`.pr[data-guid="${guid}"] .label-chips-cell`).html(renderLabelChips(labels, guid));
     }
 
     // ─── Модалка интервалов ─────────────────────────────────────────────────
@@ -463,7 +461,7 @@ document.addEventListener('DOMContentLoaded', () => {
         e.stopPropagation();
         const guid    = $(this).data('guid');
         const labelId = parseInt($(this).data('label-id'), 10);
-        const $wrap   = $(`tr[data-guid="${guid}"] .label-chips-cell`);
+        const $cell   = $(`.pr[data-guid="${guid}"] .label-chips-cell`);
 
         $.post(urls.removeLabel, {
             guid,
@@ -471,12 +469,10 @@ document.addEventListener('DOMContentLoaded', () => {
             _csrf: yii.getCsrfToken(),
         }, res => {
             if (res.success) {
-                // Remove the chip from the grid immediately
-                $(`tr[data-guid="${guid}"] .label-chip[data-label-id="${labelId}"]`).remove();
-                if (!$wrap.find('.label-chip').length) {
-                    $wrap.html('<span class="text-muted small">—</span>');
+                $(`.pr[data-guid="${guid}"] .label-chip[data-label-id="${labelId}"]`).remove();
+                if (!$cell.find('.label-chip').length) {
+                    $cell.html('<span class="text-muted small">—</span>');
                 }
-                // Keep currentLabels in sync if modal is open for same product
                 if (guid === currentGuid) {
                     currentLabels = currentLabels.filter(l => l.id !== labelId);
                 }
@@ -484,9 +480,7 @@ document.addEventListener('DOMContentLoaded', () => {
             } else {
                 showToast(res.message || 'Ошибка');
             }
-        }).fail(() => {
-            showToast('Ошибка запроса');
-        });
+        }).fail(() => showToast('Ошибка запроса'));
     });
 
     $('#addIntervalBtn').on('click', function () {
@@ -604,10 +598,10 @@ document.addEventListener('DOMContentLoaded', () => {
         const guid = $(this).val();
         if ($(this).is(':checked')) {
             selectedGuids.add(guid);
-            $(this).closest('tr').addClass('table-active');
+            $(this).closest('.pr').addClass('table-active').css('outline', '2px solid #0d6efd');
         } else {
             selectedGuids.delete(guid);
-            $(this).closest('tr').removeClass('table-active');
+            $(this).closest('.pr').removeClass('table-active').css('outline', '');
         }
         updateBulkToolbar();
     });
@@ -619,10 +613,10 @@ document.addEventListener('DOMContentLoaded', () => {
             const guid = $(this).val();
             if (checked) {
                 selectedGuids.add(guid);
-                $(this).closest('tr').addClass('table-active');
+                $(this).closest('.pr').addClass('table-active').css('outline', '2px solid #0d6efd');
             } else {
                 selectedGuids.delete(guid);
-                $(this).closest('tr').removeClass('table-active');
+                $(this).closest('.pr').removeClass('table-active').css('outline', '');
             }
         });
         updateBulkToolbar();
@@ -632,7 +626,7 @@ document.addEventListener('DOMContentLoaded', () => {
         selectedGuids.clear();
         $('.row-checkbox').prop('checked', false);
         $('#selectAllCheckbox').prop('checked', false).prop('indeterminate', false);
-        $('tr.table-active').removeClass('table-active');
+        $('.pr.table-active').removeClass('table-active').css('outline', '');
         updateBulkToolbar();
     });
 
@@ -671,4 +665,78 @@ document.addEventListener('DOMContentLoaded', () => {
     $('#bulkAddBtn').on('click',    () => doBulkAssign('add'));
     $('#bulkRemoveBtn').on('click', () => doBulkAssign('remove'));
 
+    // ─── Scb: collapsible score block in product cards ──────────────────────
+
+    function scbStarsHtml(score) {
+        let html = '';
+        for (let i = 1; i <= 5; i++) {
+            html += `<i class="fa fa-star" style="font-size:11px;color:${i <= score ? '#ffc107' : '#dee2e6'}"></i>`;
+        }
+        return html;
+    }
+
+    $(document).on('click', '.scb-toggle', function () {
+        const $toggle  = $(this);
+        const guid     = $toggle.data('guid');
+        const $content = $toggle.next('.scb-content');
+        const isOpen   = $toggle.hasClass('open');
+
+        $toggle.toggleClass('open');
+        if (isOpen) {
+            $content.slideUp(150);
+            return;
+        }
+        $content.slideDown(150);
+
+        if ($content.data('loaded')) { return; }
+
+        $content.html('<div class="text-center py-2 text-muted small"><span class="spinner-border spinner-border-sm me-1"></span> Загрузка...</div>');
+
+        $.get(urls.scores, { guid }, data => {
+            if (!data.success) {
+                $content.html('<div class="text-muted small py-1">Ошибка загрузки</div>');
+                return;
+            }
+
+            // Update inline score summary in card header
+            if (data.avg_store_score !== null && data.avg_store_score !== undefined) {
+                const avg = parseFloat(data.avg_store_score);
+                $(`#sc-stars-${guid}`).html(scbStarsHtml(Math.round(avg)));
+                $(`#sc-avg-${guid}`).text(avg.toFixed(1)).css('color', '#495057').css('font-weight', '700');
+            }
+
+            if (!data.mappings || !data.mappings.length) {
+                $content.html('<div class="text-muted small py-1">Маппингов нет</div>');
+                $content.data('loaded', true);
+                return;
+            }
+
+            let html = '';
+            data.mappings.forEach(m => {
+                const parts = [m.supplier_name, m.plantation_name, m.supplier_product_name].filter(Boolean);
+                const label = parts.join(' · ') || '—';
+                const supplier = m.supplier_name || '—';
+                let scoreHtml = '<span class="text-muted" style="font-size:10px">без оценки</span>';
+                if (m.score) {
+                    scoreHtml = scbStarsHtml(m.score)
+                        + ` <span style="font-weight:600;font-size:11px">${Number(m.score).toFixed(1)}</span>`;
+                    if (m.count) {
+                        scoreHtml += ` <span style="font-size:9px;color:#adb5bd">(${m.count})</span>`;
+                    }
+                }
+                html += `<div class="scb-row">
+                    <div><span class="scb-name">${supplier}</span>${parts.length > 1 ? ' · <span class="text-muted">' + parts.slice(1).join(' · ') + '</span>' : ''}</div>
+                    <div class="d-flex align-items-center gap-2">${scoreHtml}</div>
+                </div>`;
+                if (m.comment) {
+                    html += `<div class="scb-comment"><strong>КМ:</strong> ${m.comment}</div>`;
+                }
+            });
+            $content.html(html);
+            $content.data('loaded', true);
+        }).fail(() => {
+            $content.html('<div class="text-muted small py-1">Ошибка загрузки</div>');
+        });
+    });
+
 });