From ad2749328d96732a8ea0b0dd04aaa0a0b2c5817c Mon Sep 17 00:00:00 2001 From: tore Date: Sat, 26 Sep 2026 05:27:15 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- brief.md | 71 +++++---- data-kt.js | 181 ++++++++++++++++++++++ index.html | 411 +++++++++++++++----------------------------------- script.js | 433 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 315 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1093 insertions(+), 318 deletions(-) create mode 100644 data-kt.js create mode 100644 script.js create mode 100644 style.css diff --git a/brief.md b/brief.md index bc74dce..53a5c28 100644 --- a/brief.md +++ b/brief.md @@ -1,38 +1,27 @@ # Дашборд продаж по регионам -## Тип решения -Статический сайт (index.html + style.css + script.js на ванильном JS с Tailwind CDN). +Сделай дашборд по продажам Казахтелекома по всем регионам Казахстана. -## Задача -Создать визуализацию показателей продаж по регионам Казахстана за текущий месяц для презентации в Telegram-группе. +Цель: Визуализировать показатели продаж по всем регионам Казахстана. -## Кто пользуется -Руководство департамента, менеджеры по продажам (просмотр). +Пользователи: Сотрудники, отслеживающие продажи -## Что делает прототип -- Отображает сводные карточки (KPI) сверху: общая выручка, выполнение плана (%), количество сделок. -- Показывает интерактивную карту или таблицу регионов (Алматы, Астана, Шымкент, ЗКО, ВКО и др.) с цветовым кодированием выполнения плана. -- Отрисовывает столбчатую диаграмму (Bar Chart) продаж по каждому региону за месяц. -- Реализует фильтр по месяцам (выпадающий список), меняющий данные на графиках без перезагрузки страницы. -- Кнопка «Обновить данные» имитирует подгрузку свежих чисел. -- Адаптивная верстка для просмотра с мобильных устройств в Telegram. +Экраны: +- Главный экран: карта Казахстана с регионами и сводные показатели продаж -## Демо-данные -- 16 регионов РК (области + города республиканского значения). -- Реалистичные суммы продаж в тенге (диапазон 10–500 млн ₸). -- Статусы выполнения плана: «Выполнен», «В работе», «Отставание». -- Даты транзакций в формате ДД.ММ.ГГГГ за текущий месяц. +Данные: Показатели продаж по каждому региону Казахстана -## Допущения -- Данные хранятся только в браузере (localStorage), при очистке кэша сбрасываются. -- Валюта отображается в тенге (₸). -- График строится на библиотеке Chart.js (подключается через CDN). +Правила брифа: интерфейс в стиле Казахтелекома на дизайн-системе KT AI, на русском; адаптивно для телефона; без входа по логину и паролю. Если настоящих данных нет — наполни приложение правдоподобными примерами. Не пиши на экране пометок «демо», «демонстрационная версия», «тестовые данные» и не утверждай, откуда взяты данные. -## Чего пока нет -- Авторизации и разграничения прав доступа. -- Интеграции с 1С, CRM и реальными базами данных. -- Выгрузки отчетов в Excel (только визуализация). -- Серверной части и постоянного хранилища. +## Объём работы + +Собирай ровно то, что написано в задании. Не добавляй лишние разделы, журналы и настройки. Для статичной страницы достаточно index.html. Если запрошен статичный макет, не добавляй серверную часть на этом этапе. +Если задача требует общего хранения, настоящей регистрации или рабочего приложения, реализуй серверную часть и базу данных средствами Vibe AI. Создай необходимые файлы, таблицы и обработчики, даже если владелец не перечислил их отдельно. Не подменяй общее хранение localStorage или демонстрационным списком. Собственная база приложения не требует интеграции с системами КТ и отдельного утверждения дизайна в экспрессе. При доработке существующего проекта это правило заменяет прежнее ограничение одним HTML-файлом. +Проверь сохранение записи, чтение из другой сессии, обработку ошибок и ограничения доступа. Личные данные доступны только уполномоченному пользователю, секреты не попадают в браузерный код. Не придумывай доступы или готовые интеграции. Если публикация серверной части недоступна, укажи конкретную причину и не объявляй функцию работающей. Закончив, опубликуй приложение и сообщи фактический адрес и результаты проверки; сохраняй прежний адрес, если платформа это позволяет. + +## Имена файлов + +Если серверная часть не нужна, скрипт интерфейса называй script.js, стили — style.css. Не создавай в корне проекта файлы app.js, main.js, index.js, server.js, bot.js, app.py, main.py, server.py и package.json со scripts.start: платформа примет проект за серверное приложение и не опубликует статичный сайт. Эти имена — только для настоящего сервера. ## Дизайн — дизайн-система KT AI @@ -41,12 +30,38 @@ Подключай файлы ДС относительными путями и в этом порядке: `./design-system/kt-ai-fonts.css`, `./design-system/kt-ai-tokens.css`, `./design-system/kt-ai-components.css`, `./design-system/kt-ai-page.css`. Тема по умолчанию — светлая: ``; не ставь `dark` и не ставь `auto` -(auto уходит в тёмную по системной теме). Переключатель темы можно оставить, но открываться страница должна светлой. +(auto уходит в тёмную по системной теме). `lang` — язык интерфейса из задания: казахский — `kk`, английский — `en`, иначе `ru`. Переключатель темы можно оставить, но открываться страница должна светлой. Цвета, отступы и размеры — только токенами `var(--kt-ai-…)`, иконки — из `design-system/icons/kt-ai-lucide-sprite.svg`. +Плашки показателей — `.kt-ai-kpi` (`.value`, `.label`, `.hint`) в полосе `.kt-ai-kpi-strip`; таблицы — по правилам ДС (frameless, `.kt-ai-table-wrap`, числа вправо, `tabular-nums`); +графики — чистый SVG/div высотой 190–220px, столбцы 20–28px с радиусом 2px, цвета только `var(--kt-ai-chart-blue|green|orange|pink|red|salmon)`, подсказки значений — `data-kt-tip` + `./design-system/kt-ai-chart-tip.js`. +Имена токенов не выдумывай — только существующие в `design-system/kt-ai-tokens.css`: --kt-ai-fg / -fg-muted / -fg-faint, --kt-ai-bg / -bg-soft, --kt-ai-card-bg / -card-border, --kt-ai-border / -border-strong, --kt-ai-primary, --kt-ai-radius-xl / -3xl, --kt-ai-font-sans / -mono, --kt-ai-chart-blue / -green / -orange / -pink / -red / -salmon, --kt-ai-status-ok-fg / -warn-fg / -risk-fg. +Несуществующий токен (`--kt-ai-text-primary`, `--kt-ai-bg-surface`, `--kt-ai-font-family`) молча не применится — страница выйдет в Times New Roman без рамок. +Классы ДС (`.kt-ai-kpi`, `.kt-ai-card`, `.kt-ai-table`, `.kt-ai-btn`, `.kt-ai-chip`, `.kt-ai-input`) в своём ` + - + -
-
- Дашборд продаж - - -
-
- -
- -
- - +
+
+

Регионы

+ +
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + +
РегионПланФакт%СделкиСтатус
+
+ +
+ + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..96d2f3c --- /dev/null +++ b/script.js @@ -0,0 +1,433 @@ +// Дашборд: состояние, карта, KPI, график, таблица, фильтры. +// Интерактив (период, поиск, выбор региона, сортировка) — в localStorage. +// Показатели за месяцы детерминированы сидом — стабильны между сессиями. + +// ---------- Состояние ---------- + +var state = { + offset: 0, // 0 = текущий месяц, 1..11 = назад + selected: null, // id выбранного региона (клик по карте) + search: '', + sort: 'actual', // 'actual' | 'percent' | 'name' + seed: '' +}; + +try { + var ss = localStorage.getItem('kt_dash_seed'); + state.seed = ss || String(Date.now()); + localStorage.setItem('kt_dash_seed', state.seed); +} catch (e) { state.seed = String(Date.now()); } +window.KT_SEED = state.seed; + +try { + var so = parseInt(localStorage.getItem('kt_dash_offset') || '0', 10); + if (!isNaN(so) && so >= 0 && so <= 11) state.offset = so; + state.selected = localStorage.getItem('kt_dash_selected') || null; + state.search = localStorage.getItem('kt_dash_search') || ''; + state.sort = localStorage.getItem('kt_dash_sort') || 'actual'; +} catch (e) {} + +function saveState() { + try { + localStorage.setItem('kt_dash_seed', state.seed); + localStorage.setItem('kt_dash_offset', String(state.offset)); + localStorage.setItem('kt_dash_selected', state.selected || ''); + localStorage.setItem('kt_dash_search', state.search); + localStorage.setItem('kt_dash_sort', state.sort); + } catch (e) {} +} + +// ---------- Данные ---------- + +var currentData = null; + +function loadData() { + currentData = ktMonthData(state.offset); +} + +// ---------- Связывание контролов ---------- + +function bindMonthSelect() { + var sel = document.getElementById('monthSelect'); + if (!sel) return; + for (var i = 0; i <= 11; i++) { + var op = document.createElement('option'); + op.value = String(i); + op.textContent = ktMonthLabel(i); + sel.appendChild(op); + } + sel.value = String(state.offset); + sel.addEventListener('change', function() { + state.offset = parseInt(sel.value, 10) || 0; + saveState(); + render(); + }); +} + +function bindRefresh() { + var btn = document.getElementById('refreshBtn'); + if (!btn) return; + var label = btn.innerHTML; + btn.addEventListener('click', function() { + // новый сид -> показатели пересчитываются, дальше снова стабильны + state.seed = String(Date.now()); + window.KT_SEED = state.seed; + saveState(); + btn.disabled = true; + document.body.style.cursor = 'progress'; + setTimeout(function() { + loadData(); + render(); + btn.disabled = false; + document.body.style.cursor = ''; + }, 450); + }); +} + +function bindSearch() { + var inp = document.getElementById('searchInput'); + var clear = document.getElementById('clearSearch'); + if (inp) { + inp.value = state.search; + inp.addEventListener('input', function() { + state.search = inp.value; + saveState(); + renderTable(); + }); + } + if (clear) { + clear.addEventListener('click', function() { + state.search = ''; + if (inp) inp.value = ''; + saveState(); + renderTable(); + }); + } +} + +function markSortButtons() { + var btns = document.querySelectorAll('.dash-sort .kt-ai-btn'); + for (var i = 0; i < btns.length; i++) { + if (btns[i].getAttribute('data-sort') === state.sort) btns[i].classList.add('is-on'); + else btns[i].classList.remove('is-on'); + } +} + +function bindSort() { + var btns = document.querySelectorAll('.dash-sort .kt-ai-btn'); + for (var i = 0; i < btns.length; i++) { + btns[i].addEventListener('click', function() { + state.sort = this.getAttribute('data-sort'); + saveState(); + markSortButtons(); + renderTable(); + }); + } + markSortButtons(); +} + +// ---------- Rендер: KPI ---------- + +function renderKpis() { + var k = currentData.kpi; + var rev = document.getElementById('kpiRevenue'); + var plan = document.getElementById('kpiPlan'); + var deals = document.getElementById('kpiDeals'); + var leaders = document.getElementById('kpiLeaders'); + if (rev) rev.textContent = ktFormatMoney(k.fact); + if (plan) plan.textContent = k.percent + '%'; + if (deals) deals.textContent = String(k.deals); + if (leaders) leaders.textContent = String(k.leaders); + + var rh = document.getElementById('kpiRevenueHint'); + var ph = document.getElementById('kpiPlanHint'); + var dh = document.getElementById('kpiDealsHint'); + if (rh) rh.textContent = currentData.label; + if (ph) ph.textContent = ktFormatMillion(k.fact) + ' / ' + ktFormatMillion(k.plan); + if (dh) dh.textContent = currentData.label; +} + +// ---------- Rендер: карта ---------- + +function buildMap() { + var mapEl = document.getElementById('kzMap'); + if (!mapEl) return; + var ns = 'http://www.w3.org/2000/svg'; + var frag = document.createDocumentFragment(); + + for (var i = 0; i < KT_REGIONS.length; i++) { + var r = KT_REGIONS[i]; + var pts = KT_MAP[r.id]; + if (!pts) continue; + var poly = document.createElementNS(ns, 'polygon'); + var str = ''; + for (var p = 0; p < pts.length; p++) str += pts[p][0] + ',' + pts[p][1] + ' '; + poly.setAttribute('points', str); + poly.setAttribute('class', 'dash-map-region'); + poly.setAttribute('data-id', r.id); + poly.setAttribute('tabindex', '0'); + poly.setAttribute('role', 'button'); + poly.setAttribute('aria-label', r.name); + frag.appendChild(poly); + } + + for (var k = 0; k < KT_REGIONS.length; k++) { + var rr = KT_REGIONS[k]; + if (!rr.cx) continue; + var lab = document.createElementNS(ns, 'text'); + lab.setAttribute('class', 'dash-map-label'); + lab.setAttribute('x', String(rr.cx)); + lab.setAttribute('y', String(rr.cy)); + lab.textContent = rr.short; + frag.appendChild(lab); + } + + mapEl.innerHTML = ''; + mapEl.appendChild(frag); + + var polys = mapEl.querySelectorAll('.dash-map-region'); + for (var b = 0; b < polys.length; b++) { + (function(p) { + var id = p.getAttribute('data-id'); + var doSel = function() { + state.selected = (state.selected === id) ? null : id; + saveState(); + renderMap(); + renderTable(); + }; + p.addEventListener('click', doSel); + p.addEventListener('keydown', function(e) { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); doSel(); } + }); + })(polys[b]); + } +} + +function renderMap() { + var mapEl = document.getElementById('kzMap'); + if (!mapEl) return; + var dataById = {}; + for (var i = 0; i < currentData.rows.length; i++) { + dataById[currentData.rows[i].id] = currentData.rows[i]; + } + var polys = mapEl.querySelectorAll('.dash-map-region'); + for (var j = 0; j < polys.length; j++) { + var p = polys[j]; + var id = p.getAttribute('data-id'); + var d = dataById[id]; + if (!d) continue; + p.setAttribute('data-tone', d.tone); + if (state.selected === id) p.setAttribute('data-selected', 'true'); + else p.removeAttribute('data-selected'); + var tip = JSON.stringify({ + title: d.name + ' — ' + currentData.label, + rows: [ + { label: 'Факт', value: ktFormatMoney(d.fact) }, + { label: 'План', value: ktFormatMoney(d.plan) }, + { label: '% плана', value: d.percent + '%' } + ] + }).replace(/"/g, '"'); + p.setAttribute('data-kt-tip', tip); + } + renderMapSelectedBlock(); +} + +function renderMapSelectedBlock() { + var box = document.getElementById('mapSelected'); + if (!box) return; + if (!state.selected) { + box.innerHTML = 'Выберите регион на карте — его показатели подсветятся в списке, и график покажет его детализацию.'; + return; + } + var d = null; + for (var i = 0; i < currentData.rows.length; i++) { + if (currentData.rows[i].id === state.selected) { d = currentData.rows[i]; break; } + } + if (!d) { state.selected = null; saveState(); renderMap(); return; } + var st = d.tone === 'ok' ? 'выполнен' : d.tone === 'warn' ? 'близко к плану' : 'ниже плана'; + box.innerHTML = + '' + d.name + ' — ' + d.percent + '% плана ' + + '(факт ' + ktFormatMoney(d.fact) + ', план ' + ktFormatMoney(d.plan) + '), ' + + d.deals + (d.deals === 1 ? ' сделка' : (d.deals < 5 ? ' сделки' : ' сделок')) + + ', статус: ' + st; +} + +// ---------- Rендер: график ---------- + +function renderChart() { + var box = document.getElementById('chartBars'); + if (!box) return; + + var rows; + var hintText; + if (state.selected) { + var one = []; + for (var s = 0; s < currentData.rows.length; s++) { + if (currentData.rows[s].id === state.selected) one.push(currentData.rows[s]); + } + // выбран регион: показываем его столбцы за последние 6 месяцев + var months = []; + for (var mm = 5; mm >= 0; mm--) { + months.push(ktRegionMonth(state.selected, mm)); + } + rows = months; + hintText = one.length ? one[0].name + ' · последние 6 месяцев' : ''; + } else { + rows = currentData.rows.slice(0, 8); + hintText = 'топ-8 регионов · ' + currentData.label; + } + if (rows.length === 0) { box.innerHTML = ''; return; } + + var W = 520, H = 220, padL = 52, padR = 8, padT = 12, padB = 52; + var plotW = W - padL - padR; + var plotH = H - padT - padB; + var maxVal = 0; + for (var i = 0; i < rows.length; i++) if (rows[i].fact > maxVal) maxVal = rows[i].fact; + maxVal = Math.ceil(maxVal / 100) * 100; + if (maxVal < 100) maxVal = 100; + var slot = plotW / rows.length; + var bw = Math.min(26, Math.max(14, slot * 0.5)); + + var lines = ['']; + + var steps = 4; + for (var g = 0; g <= steps; g++) { + var gy = (padT + plotH - (plotH * g / steps)).toFixed(1); + var gv = Math.round(maxVal * g / steps); + lines.push(''); + lines.push('' + + (gv >= 1000 ? (Math.round(gv / 10) / 100).toFixed(1).replace('.', ',') + ' млрд' : gv + ' млн') + + ''); + } + + for (var b = 0; b < rows.length; b++) { + var r = rows[b]; + var bx = (padL + slot * b + (slot - bw) / 2).toFixed(1); + var bh = Math.max(2, plotH * (r.fact / maxVal)); + var by = (padT + plotH - bh).toFixed(1); + var fill = r.tone === 'ok' ? 'var(--kt-ai-chart-green)' + : r.tone === 'warn' ? 'var(--kt-ai-chart-orange)' + : 'var(--kt-ai-chart-blue)'; + var label = r.short || r.name; + var tipTitle = state.selected ? label + ' · ' + ktMonthLabel(rows.length - 1 - b) : label + ' · ' + currentData.label; + var tip = JSON.stringify({ + title: tipTitle, + rows: [ + { label: 'Факт', value: ktFormatMoney(r.fact) }, + { label: 'План', value: ktFormatMoney(r.plan) }, + { label: '% плана', value: r.percent + '%' } + ] + }).replace(/"/g, '"'); + lines.push(''); + var lx = (padL + slot * b + slot / 2).toFixed(1); + var ly = H - padB + 16; + lines.push('' + + (state.selected ? (KT_MONTHS[new Date().getMonth() - (rows.length - 1 - b)] || '').slice(0, 3) : label) + ''); + } + + lines.push(''); + box.innerHTML = lines.join(''); + + var hint = document.getElementById('chartHint'); + if (hint) hint.textContent = hintText; +} + +// ---------- Rендер: таблица ---------- + +function renderTable() { + var tbody = document.getElementById('regionsTable'); + if (!tbody) return; + var countEl = document.getElementById('tableCount'); + var empty = document.getElementById('tableEmpty'); + + var rows = currentData.rows.slice(); + var q = state.search.trim().toLowerCase(); + if (q) { + rows = rows.filter(function(r) { + return (r.name + ' ' + r.short).toLowerCase().indexOf(q) !== -1; + }); + } + if (state.sort === 'name') rows.sort(function(a, b) { return a.name.localeCompare(b.name, 'ru'); }); + else if (state.sort === 'percent') rows.sort(function(a, b) { return b.percent - a.percent; }); + else rows.sort(function(a, b) { return b.fact - a.fact; }); + + if (rows.length === 0) { + tbody.innerHTML = ''; + if (empty) empty.hidden = false; + if (countEl) countEl.textContent = '0 из ' + currentData.rows.length; + return; + } + if (empty) empty.hidden = true; + if (countEl) countEl.textContent = rows.length + ' из ' + currentData.rows.length; + + var html = []; + for (var i = 0; i < rows.length; i++) { + var r = rows[i]; + var stText = r.tone === 'ok' ? 'Выполнен' : r.tone === 'warn' ? 'Близко к плану' : 'Ниже плана'; + var barW = Math.min(100, r.percent); + var selAttr = (state.selected === r.id) ? ' data-sel="1"' : ''; + html.push( + '' + + '' + r.name + '' + + '' + ktFormatMoney(r.plan) + '' + + '' + ktFormatMoney(r.fact) + '' + + '' + r.percent + '%' + + '' + + '' + r.deals + '' + + '' + stText + '' + + '' + ); + } + tbody.innerHTML = html.join(''); + + // Клик по строке — выбирает регион (синхронно с картой) + var trs = tbody.querySelectorAll('tr'); + for (var t = 0; t < trs.length; t++) { + (function(tr) { + tr.addEventListener('click', function() { + var id = tr.getAttribute('data-row'); + state.selected = (state.selected === id) ? null : id; + saveState(); + renderMap(); + renderChart(); + renderTable(); + }); + })(trs[t]); + } +} + +// Подсветка выбранной строки таблицы +function markSelectedRow() { + var trs = document.querySelectorAll('#regionsTable tr'); + for (var i = 0; i < trs.length; i++) { + if (trs[i].getAttribute('data-row') === state.selected) { + trs[i].setAttribute('data-active', 'true'); + } else { + trs[i].removeAttribute('data-active'); + } + } +} + +// ---------- Rендер всего ---------- + +function render() { + renderKpis(); + renderMap(); + renderChart(); + renderTable(); +} + +// ---------- Старт ---------- + +function init() { + bindMonthSelect(); + bindRefresh(); + bindSearch(); + bindSort(); + buildMap(); + loadData(); + render(); +} + +document.addEventListener('DOMContentLoaded', init); diff --git a/style.css b/style.css new file mode 100644 index 0000000..ba14cad --- /dev/null +++ b/style.css @@ -0,0 +1,315 @@ +/* ============================================================ + Дашборд продаж — только компоновка. + Цвета, шрифты и размеры — токены KT AI (kt-ai-tokens.css). + ============================================================ */ + +/* ---------- каркас: sidebar + рабочая область ---------- */ +.dash-shell { + display: grid; + grid-template-columns: 112px minmax(0, 1fr); + min-height: 100vh; +} + +.dash-sidebar { + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; + padding: var(--kt-ai-space-5) var(--kt-ai-space-4); + border-right: 1px solid var(--kt-ai-border); + background: var(--kt-ai-bg-soft); +} + +/* Липкая первая колонка таблицей — фон под строкой нужен, + иначе при горизонтальном скролле цифры «плывут» под ней */ +.kt-ai-table td.kt-ai-sticky-first, +.kt-ai-table th.kt-ai-sticky-first { + background: var(--kt-ai-card-bg); +} + +.dash-brand-name { + display: block; + margin-top: var(--kt-ai-space-2); + font-size: var(--kt-ai-text-xs); + line-height: 1.35; + color: var(--kt-ai-fg-muted); + letter-spacing: .02em; +} + +.dash-sidebar-note { + margin-top: auto; + padding-top: var(--kt-ai-space-4); + border-top: 1px solid var(--kt-ai-divider); + font-size: var(--kt-ai-text-2xs); + color: var(--kt-ai-fg-faint); +} + +.dash-main { + min-width: 0; + padding: var(--kt-ai-space-7) var(--kt-ai-space-8) var(--kt-ai-space-12); +} + +/* ---------- шапка ---------- */ +.dash-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--kt-ai-space-6); + flex-wrap: wrap; + margin-bottom: var(--kt-ai-space-6); +} + +.dash-crumb { + display: block; + font-size: var(--kt-ai-text-2xs); + letter-spacing: .06em; + text-transform: uppercase; + color: var(--kt-ai-fg-faint); +} + +.dash-title { + margin: 2px 0 0; + font-size: var(--kt-ai-text-2xl); + font-weight: var(--kt-ai-weight-bold); + letter-spacing: -.02em; +} + +.dash-topbar-actions { + display: flex; + align-items: center; + gap: var(--kt-ai-space-3); + flex-wrap: wrap; +} + +/* ---------- KPI-полоса (сетка из 4 карточек) ---------- */ +.kt-ai-kpi-strip.dash-kpis { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--kt-ai-space-4); + padding: 0 0 var(--kt-ai-space-7); +} + +.kt-ai-kpi-strip.dash-kpis .kt-ai-kpi { + padding: var(--kt-ai-space-5); + background: var(--kt-ai-card-bg); + border: 1px solid var(--kt-ai-card-border); + border-radius: var(--kt-ai-radius-xl); +} + +/* ---------- сетка: карта + график ---------- */ +.dash-grid { + display: grid; + grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); + gap: var(--kt-ai-space-5); + align-items: stretch; + margin-bottom: var(--kt-ai-space-5); +} + +.dash-panel { + min-width: 0; + padding: var(--kt-ai-space-6); + background: var(--kt-ai-card-bg); + border: 1px solid var(--kt-ai-card-border); + border-radius: var(--kt-ai-radius-xl); +} + +.dash-panel-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--kt-ai-space-4); + flex-wrap: wrap; + margin-bottom: var(--kt-ai-space-5); +} + +.dash-panel-head h2 { + margin: 0; + font-size: var(--kt-ai-text-md); + font-weight: var(--kt-ai-weight-semibold); +} + +.dash-panel-hint { + font-size: var(--kt-ai-text-2xs); + color: var(--kt-ai-fg-muted); +} + +/* ---------- карта ---------- */ +.dash-map-wrap svg { + display: block; + width: 100%; + height: auto; + max-height: 430px; +} + +.dash-map-region { + cursor: pointer; + stroke: var(--kt-ai-card-bg); + stroke-width: 2; + transition: opacity .15s ease; +} + +.dash-map-region:hover, +.dash-map-region:focus-visible { opacity: .82; outline: none; } + +.dash-map-region[data-selected="true"] { + stroke: var(--kt-ai-fg); + stroke-width: 2.5; +} + +.dash-map-region[data-tone="ok"] { fill: color-mix(in srgb, var(--kt-ai-chart-green) 34%, var(--kt-ai-bg)); } +.dash-map-region[data-tone="warn"] { fill: color-mix(in srgb, var(--kt-ai-chart-orange) 38%, var(--kt-ai-bg)); } +.dash-map-region[data-tone="risk"] { fill: color-mix(in srgb, var(--kt-ai-chart-red) 38%, var(--kt-ai-bg)); } + +.dash-map-label { + font-size: 11px; + font-weight: var(--kt-ai-weight-semibold); + fill: var(--kt-ai-fg); + pointer-events: none; + user-select: none; + text-anchor: middle; +} + +.dash-legend { + display: flex; + gap: var(--kt-ai-space-4); + flex-wrap: wrap; +} + +.dash-legend-item { + display: inline-flex; + align-items: center; + gap: var(--kt-ai-space-2); + font-size: var(--kt-ai-text-2xs); + color: var(--kt-ai-fg-muted); +} + +.dash-legend-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: var(--kt-ai-radius-full); +} + +.dash-legend-dot[data-tone="ok"] { background: color-mix(in srgb, var(--kt-ai-chart-green) 55%, var(--kt-ai-bg)); } +.dash-legend-dot[data-tone="warn"] { background: color-mix(in srgb, var(--kt-ai-chart-orange) 55%, var(--kt-ai-bg)); } +.dash-legend-dot[data-tone="risk"] { background: color-mix(in srgb, var(--kt-ai-chart-red) 55%, var(--kt-ai-bg)); } + +.dash-map-selected { + margin-top: var(--kt-ai-space-5); + padding: var(--kt-ai-space-4) var(--kt-ai-space-5); + border: 1px solid var(--kt-ai-divider); + border-radius: var(--kt-ai-radius-lg); + background: var(--kt-ai-bg-soft); + font-size: var(--kt-ai-text-sm); + color: var(--kt-ai-fg-muted); +} + +.dash-map-selected strong { + color: var(--kt-ai-fg); + font-weight: var(--kt-ai-weight-semibold); +} + +/* ---------- график: чистый SVG ---------- */ +.dash-chart { + position: relative; + width: 100%; + height: 220px; +} + +.dash-chart svg { + display: block; + width: 100%; + height: 100%; +} + +.dcc-bar { cursor: pointer; transition: opacity .15s ease; } +.dcc-bar:hover, .dcc-bar:focus-visible { opacity: .8; outline: none; } +.dcc-grid { stroke: var(--kt-ai-divider); stroke-dasharray: 3 4; } +.dcc-axis { font-size: 10px; fill: var(--kt-ai-fg-faint); font-family: var(--kt-ai-font-mono); } + +/* ---------- таблица ---------- */ +.dash-search { max-width: 240px; } + +.dash-sort { + display: flex; + gap: var(--kt-ai-space-2); + flex-wrap: wrap; +} + +.dash-sort .is-on { + background: var(--kt-ai-primary-subtle); + border-color: var(--kt-ai-primary); + color: var(--kt-ai-primary); + font-weight: var(--kt-ai-weight-semibold); +} + +.kt-ai-table .dash-num { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.kt-ai-table td.dash-pct { + font-weight: var(--kt-ai-weight-semibold); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +/* выбранная строка (клик по карте или по строке) */ +.kt-ai-table tr[data-sel="1"] td, +.kt-ai-table tr[data-active="true"] td { + background: var(--kt-ai-primary-subtle); +} + +.kt-ai-table tbody tr:hover td { + background: var(--kt-ai-bg-hover); +} + +.kt-ai-table tbody tr[data-sel="1"]:hover td, +.kt-ai-table tbody tr[data-active="true"]:hover td { + background: var(--kt-ai-primary-subtle); +} + +.dash-progress { + display: inline-block; + vertical-align: middle; + width: 56px; + height: 6px; + margin-left: var(--kt-ai-space-3); + border-radius: var(--kt-ai-radius-full); + background: var(--kt-ai-bg-sunken); + overflow: hidden; +} + +.dash-progress i { + display: block; + height: 100%; + border-radius: var(--kt-ai-radius-full); +} + +.dash-progress i[data-tone="ok"] { background: var(--kt-ai-chart-green); } +.dash-progress i[data-tone="warn"] { background: var(--kt-ai-chart-orange); } +.dash-progress i[data-tone="risk"] { background: var(--kt-ai-chart-red); } + +/* ---------- адаптив ---------- */ +@media (max-width: 1100px) { + .dash-grid { grid-template-columns: 1fr; } + .kt-ai-kpi-strip.dash-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 860px) { + .dash-shell { grid-template-columns: 1fr; } + .dash-sidebar { display: none; } + .dash-main { padding: var(--kt-ai-space-5) var(--kt-ai-space-4) var(--kt-ai-space-10); } +} + +@media (max-width: 640px) { + .dash-topbar { flex-direction: column; align-items: stretch; } + .dash-topbar-actions { justify-content: space-between; } + .dash-topbar-actions .kt-ai-btn { flex: 1; } + .kt-ai-select { flex: 1; } + .dash-search { width: 100%; max-width: none; } + .kt-ai-kpi-strip.dash-kpis { grid-template-columns: 1fr; } + .dash-panel { padding: var(--kt-ai-space-4); } +}