<?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 */
/* @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) {
}
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 . '">×</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">
</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> </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: <?= Html::encode(substr($p->id, 0, 8)) ?>…
+ <?php if ($p->category): ?>
+ · Категория: <?= 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">
</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">
</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>
// ─── 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}">×</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));
}
// ─── Модалка интервалов ─────────────────────────────────────────────────
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,
_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);
}
} else {
showToast(res.message || 'Ошибка');
}
- }).fail(() => {
- showToast('Ошибка запроса');
- });
+ }).fail(() => showToast('Ошибка запроса'));
});
$('#addIntervalBtn').on('click', function () {
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();
});
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();
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();
});
$('#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>');
+ });
+ });
+
});