dashbord-top-uslug-b2c-po-r.../index.html

324 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>Топ услуг B2C по регионам</title>
<link rel="icon" href="data:,">
<script>(function(){var t=null;try{t=localStorage.getItem("demo.theme");}catch(e){}if(t==="dark"||t==="light"){document.documentElement.setAttribute("data-theme",t);}})();</script>
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
<style>
/* Дашборд «Топ услуг B2C по регионам» — архетип dashboard дизайн-системы KT AI.
Свой стиль — только сетка и отступы; цвета, радиусы, шрифты — токены ДС. */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
body { -webkit-text-size-adjust: 100%; }
.wrap { max-width: var(--kt-ai-content-wide, 1280px); margin: 0 auto; padding: 0 var(--kt-ai-space-5) var(--kt-ai-space-10); }
/* Шапка: компактная, без hero */
.top { position: sticky; top: 0; z-index: var(--kt-ai-z-sticky, 20); background: var(--kt-ai-bg); border-bottom: 1px solid var(--kt-ai-border); }
.top-in { display: flex; align-items: center; gap: var(--kt-ai-space-4); min-height: 56px; padding: var(--kt-ai-space-2) 0; }
.top img { width: 28px; height: 28px; flex-shrink: 0; }
.top .head { min-width: 0; flex: 1; }
.top h1 { margin: 0; font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); line-height: var(--kt-ai-leading-tight); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.top .sub { margin: 2px 0 0; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.top .tools { display: flex; align-items: center; gap: var(--kt-ai-space-3); flex-shrink: 0; }
.iconbtn { width: 32px; height: 32px; display: inline-grid; place-items: center; border-radius: var(--kt-ai-radius-full); border: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg-elevated); color: var(--kt-ai-fg); cursor: pointer; padding: 0; }
.iconbtn:hover { background: var(--kt-ai-bg-hover); }
.iconbtn svg { width: 16px; height: 16px; }
[data-theme="light"] .ic-sun, [data-theme="dark"] .ic-moon { display: none; }
/* Секции */
.section { margin-top: var(--kt-ai-space-6); }
.section-h { display: flex; align-items: baseline; justify-content: space-between; gap: var(--kt-ai-space-4); margin: 0 0 var(--kt-ai-space-3); }
.section-h h2 { margin: 0; font-size: var(--kt-ai-text-md); font-weight: var(--kt-ai-weight-semibold); }
.section-h .muted { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
/* KPI: карточки-сетка ДС; длинное значение — меньше кеглем и в одну строку */
.kt-ai-kpi .value[data-long="true"] { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Чарты: два в ряд на широком экране, столбиком на телефоне */
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--kt-ai-stack-block, 16px); }
.chart-card h3 { margin: 0 0 var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); display: flex; justify-content: space-between; align-items: baseline; gap: var(--kt-ai-space-3); flex-wrap: wrap; }
.chart-card h3 .ttl { white-space: nowrap; }
.chart-card h3 .muted { font-weight: var(--kt-ai-weight-regular); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); }
.bars { display: flex; flex-direction: column; gap: 6px; }
.brow { display: grid; grid-template-columns: minmax(96px, 34%) minmax(0, 1fr) auto; align-items: center; gap: var(--kt-ai-space-3); min-height: 24px; padding: 2px 6px; margin: 0 -6px; border-radius: var(--kt-ai-radius-sm); background: none; border: 0; color: inherit; font: inherit; text-align: left; cursor: default; }
.brow[data-pick="true"] { cursor: pointer; }
.brow[data-pick="true"]:hover { background: var(--kt-ai-bg-hover); }
.brow[data-active="true"] { background: var(--kt-ai-bg-active); }
.brow .lbl { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brow .track { display: block; height: 10px; border-radius: 2px; background: var(--kt-ai-bg-sunken); overflow: hidden; }
.brow .fill { display: block; height: 100%; border-radius: 2px; background: var(--kt-ai-chart-blue); transition: width var(--kt-ai-dur-base) var(--kt-ai-ease-standard); }
.chart-card[data-tone="green"] .fill { background: var(--kt-ai-chart-green); }
.brow .val { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Таблица: тулбар над ней, фильтры-чипы под ним, скролл внутри контейнера */
.tb { display: flex; align-items: center; gap: var(--kt-ai-space-3); flex-wrap: wrap; min-height: 40px; margin-bottom: var(--kt-ai-space-3); }
.tb .grow { flex: 1 1 220px; min-width: 0; }
.tb .kt-ai-select-wrap { flex: 0 1 220px; }
.tb .count { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); white-space: nowrap; margin-left: auto; }
.chips { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-3); }
.chips:empty { display: none; }
.kt-ai-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-bg-elevated); }
table.kt-ai-table { width: 100%; border-collapse: collapse; min-width: 560px; }
.kt-ai-table th, .kt-ai-table td { padding: 0 var(--kt-ai-space-4); height: var(--kt-ai-table-row, 40px); border-bottom: 1px solid var(--kt-ai-divider, var(--kt-ai-border)); text-align: left; vertical-align: middle; }
.kt-ai-table th { position: sticky; top: 0; z-index: 1; background: var(--kt-ai-bg-elevated); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); white-space: nowrap; user-select: none; }
.kt-ai-table th[data-sort] { cursor: pointer; }
.kt-ai-table th[data-sort]:hover { color: var(--kt-ai-fg); }
.kt-ai-table th[data-dir]::after { content: " ↓"; } .kt-ai-table th[data-dir="asc"]::after { content: " ↑"; }
.kt-ai-table tbody tr:hover { background: var(--kt-ai-bg-hover); }
.kt-ai-table tbody tr:last-child td { border-bottom: 0; }
.kt-ai-table td { font-size: var(--kt-ai-text-sm); }
.kt-ai-table .num { text-align: right; white-space: nowrap; }
.kt-ai-table .num .v { font-family: var(--kt-ai-font-mono); font-variant-numeric: tabular-nums; }
.kt-ai-table td.svc { max-width: 420px; }
.kt-ai-table .svc .t { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kt-ai-table .svc .m { display: none; font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); }
.kt-ai-table mark { background: var(--kt-ai-selection, var(--kt-ai-primary-subtle)); color: inherit; border-radius: 2px; }
.kt-ai-table tfoot td { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); background: var(--kt-ai-bg-soft); }
.tfoot { display: flex; align-items: center; justify-content: space-between; gap: var(--kt-ai-space-3); flex-wrap: wrap; padding: var(--kt-ai-space-3) 0; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.kt-ai-empty { padding: var(--kt-ai-space-10) var(--kt-ai-space-5); text-align: center; color: var(--kt-ai-fg-muted); }
.kt-ai-empty .title { color: var(--kt-ai-fg); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); }
/* Телефон: ДС разворачивает таблицу в карточки строк с подписями полей (data-label);
услуга — заголовок карточки, поиск ДС на телефоне прячет — возвращаем в тулбаре */
@media (max-width: 639.98px) {
.wrap { padding-left: var(--kt-ai-space-4); padding-right: var(--kt-ai-space-4); }
.top .sub { display: none; }
.kt-ai-kpi-strip[data-cards="true"] { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--kt-ai-space-3); }
.kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi:last-child { grid-column: 1 / -1; }
.kt-ai-kpi-strip[data-cards="true"] .value { font-size: var(--kt-ai-text-lg); }
.chart-card h3 .muted { flex: 1 1 100%; }
.tb .kt-ai-search-field, .tb .kt-ai-search { display: block; }
.tb .kt-ai-select-wrap { flex-basis: 100%; }
.tb .count { margin-left: 0; width: 100%; }
.kt-ai-table-wrap { border: 0; background: none; overflow: visible; }
table.kt-ai-table { min-width: 0; }
.kt-ai-table td, .kt-ai-table th { padding: var(--kt-ai-space-2) 0; height: auto; border-bottom: 0; }
.kt-ai-table td:first-child { display: block; padding: 0 0 var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-2); border-bottom: 1px solid var(--kt-ai-divider, var(--kt-ai-border)); font-weight: var(--kt-ai-weight-semibold); }
.kt-ai-table td.svc { max-width: none; }
.kt-ai-table .svc .t { white-space: normal; }
.kt-ai-table .svc .m { display: none; }
.kt-ai-table td:nth-child(2) { display: flex; padding: var(--kt-ai-space-2) 0; margin: 0; border-bottom: 0; font-weight: var(--kt-ai-weight-regular); text-align: right; }
.kt-ai-table td:nth-child(2)::before { display: block; }
.brow { grid-template-columns: minmax(80px, 40%) minmax(0, 1fr) auto; }
}
@media (prefers-reduced-motion: reduce) { .brow .fill { transition: none; } }
</style>
</head>
<body class="kt-ai-app">
<header class="top">
<div class="wrap top-in">
<img src="./kt-logo.svg" alt="Казахтелеком">
<div class="head">
<h1>Топ услуг B2C по регионам</h1>
<p class="sub" id="sub">Июль 2026 · покупки и выручка по регионам</p>
</div>
<div class="tools">
<div class="kt-ai-segmented" role="tablist" aria-label="Метрика">
<button class="seg" type="button" role="tab" data-metric="qty" data-active="true">Покупки</button>
<button class="seg" type="button" role="tab" data-metric="sum">Выручка</button>
</div>
<button class="iconbtn" id="theme" type="button" aria-label="Переключить тему">
<svg class="ic-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<svg class="ic-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
</div>
</div>
</header>
<main class="wrap">
<section class="section" aria-label="Ключевые показатели">
<div class="kt-ai-kpi-strip" data-cards="true">
<div class="kt-ai-kpi"><div class="value" id="k-qty">—</div><div class="label">Покупок</div><div class="hint">за июль 2026</div></div>
<div class="kt-ai-kpi"><div class="value" id="k-sum">—</div><div class="label">Выручка</div><div class="hint">по 50 позициям</div></div>
<div class="kt-ai-kpi"><div class="value" id="k-regions">—</div><div class="label">Регионов</div><div class="hint">в выборке</div></div>
<div class="kt-ai-kpi"><div class="value" id="k-lead" data-long="true">—</div><div class="label">Услуга-лидер</div><div class="hint" id="k-lead-hint">по покупкам</div></div>
</div>
</section>
<section class="section charts" aria-label="Графики">
<div class="kt-ai-card chart-card" data-tone="blue">
<h3><span class="ttl">Регионы</span> <span class="muted" id="c-regions-hint">по покупкам · нажмите, чтобы отфильтровать</span></h3>
<div class="bars" id="c-regions"></div>
</div>
<div class="kt-ai-card chart-card" data-tone="green">
<h3><span class="ttl">Услуги</span> <span class="muted" id="c-services-hint">по покупкам · топ-8</span></h3>
<div class="bars" id="c-services"></div>
</div>
</section>
<section class="section" aria-label="Реестр позиций">
<div class="section-h"><h2>Позиции</h2><span class="muted">услуга × регион</span></div>
<div class="tb">
<div class="kt-ai-search-field grow" data-compact="true"><input class="kt-ai-search kt-ai-input" id="q" type="search" placeholder="Поиск по услуге…" autocomplete="off" aria-label="Поиск по услуге"></div>
<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="region" aria-label="Регион"><option value="">Все регионы</option></select></div>
<span class="count" id="count"></span>
</div>
<div class="chips" id="chips"></div>
<div class="kt-ai-table-wrap">
<table class="kt-ai-table" id="tbl">
<thead><tr>
<th data-sort="service">Услуга</th>
<th class="col-region" data-sort="region">Регион</th>
<th class="num" data-sort="qty" data-dir="desc">Покупок</th>
<th class="num" data-sort="sum">Сумма, ₸</th>
</tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="kt-ai-empty" id="empty" hidden><div class="title">Ничего не найдено</div><div>Измените запрос или снимите фильтр по региону</div></div>
</div>
<div class="tfoot">
<span id="foot"></span>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" id="more" type="button" hidden>Показать ещё</button>
</div>
</section>
</main>
<script>
const DATA = [
{"region": "Астана", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 37372, "sum": 238463786.09},
{"region": "г.Алматы", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 39671, "sum": 237005698.16},
{"region": "г.Алматы", "service": "Абонплата ШПД по FTTx для B2C", "qty": 35588, "sum": 152893561.28},
{"region": "Астана", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 35637, "sum": 136187938.99},
{"region": "Астана", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 47661, "sum": 132060372.93},
{"region": "Астана", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 61700, "sum": 123403164.43},
{"region": "г.Алматы", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 57378, "sum": 110497209.55},
{"region": "г.Алматы", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 29997, "sum": 106749675.59},
{"region": "Астана", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 52504, "sum": 99235780.41},
{"region": "Астана", "service": "2022 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 36946, "sum": 96430127.27},
{"region": "Астана", "service": "Абонплата ШПД по FTTx для B2C", "qty": 19565, "sum": 88834659.73},
{"region": "Астана", "service": "2024 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 22167, "sum": 83549325.34},
{"region": "г.Алматы", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 31221, "sum": 83511013.05},
{"region": "г.Алматы", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 40838, "sum": 82748215.46},
{"region": "г.Семей", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 41486, "sum": 76400272.02},
{"region": "Астана", "service": "TV Plus Абонплата за Премиальный пакет ГТС", "qty": 29210, "sum": 71278165.09},
{"region": "г.Алматы", "service": "2022 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 24001, "sum": 62521274.3},
{"region": "Астана", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM откр.", "qty": 27864, "sum": 59635624.39},
{"region": "г.Алматы", "service": "2024 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 15678, "sum": 58970815.48},
{"region": "г.Актобе", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 30641, "sum": 55819936.98},
{"region": "г.Алматы", "service": "B2C Абонплата за основной телефон ГТС", "qty": 52732, "sum": 49636818.5},
{"region": "г.Актобе", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 25178, "sum": 48000483.43},
{"region": "г.Алматы", "service": "TV Plus Абонплата за Премиальный пакет ГТС", "qty": 19055, "sum": 45298192.89},
{"region": "г.Павлодар", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 23334, "sum": 44857518.84},
{"region": "г.Алматы", "service": "B2C TV Plus Аренда STB оператора новое", "qty": 96183, "sum": 43252519.71},
{"region": "г.Караганда", "service": "Абонплата ШПД по FTTx для B2C", "qty": 8916, "sum": 41624727.66},
{"region": "Астана", "service": "2026 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 13685, "sum": 41618770.34},
{"region": "г.Алматы", "service": "2018 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 11568, "sum": 40199964.35},
{"region": "г.Тараз", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 21880, "sum": 39564813.34},
{"region": "г.Алматы", "service": "Абонплата ШПД по ADSL для B2C", "qty": 10710, "sum": 36238454.72},
{"region": "г.Алматы", "service": "2020 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 12148, "sum": 36010481.68},
{"region": "г.Усть-Каменогорск", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 18362, "sum": 35883527.39},
{"region": "г. Шымкент", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 19180, "sum": 35338634.51},
{"region": "г. Шымкент", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 18945, "sum": 35138664.91},
{"region": "Астана", "service": "B2C TV Plus Аренда STB оператора новое", "qty": 76655, "sum": 34746783.92},
{"region": "Астана", "service": "2018 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 8705, "sum": 32905725.03},
{"region": "г.Актобе", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 6742, "sum": 32313021.91},
{"region": "г.Усть-Каменогорск", "service": "Абонплата ШПД по FTTx для B2C", "qty": 7269, "sum": 31834355.15},
{"region": "г.Атырау", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 5495, "sum": 31688118.61},
{"region": "г.Усть-Каменогорск", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 5600, "sum": 31520413.53},
{"region": "Астана", "service": "B2C Аренда оборудования ONT оператора - новое", "qty": 87071, "sum": 31220264.09},
{"region": "г.Павлодар", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 6104, "sum": 30831455.08},
{"region": "г.Кокшетау", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16818, "sum": 30654924.73},
{"region": "г.Актобе", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 8613, "sum": 30562705.78},
{"region": "г.Костанай", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16473, "sum": 30539180.35},
{"region": "Енбекшиказахский район", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 17347, "sum": 30429552.38},
{"region": "г.Актобе", "service": "2022 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 11709, "sum": 30303557.0},
{"region": "г.Кызылорда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16539, "sum": 29772241.67},
{"region": "г.Алматы", "service": "2026 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 10206, "sum": 29200316.21},
{"region": "г.Караганда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 15714, "sum": 28837871.98}
];
const $ = id => document.getElementById(id);
const nf = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
const fmt = n => nf.format(Math.round(n));
const short = n => n >= 1e9 ? (n / 1e9).toFixed(2).replace(".", ",") + " млрд" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(".", ",") + " млн" : n >= 1e3 ? (n / 1e3).toFixed(0) + " тыс." : fmt(n);
const esc = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const PAGE = 25;
const state = { metric: "qty", region: "", q: "", sort: "qty", dir: "desc", shown: PAGE };
// тема
$("theme").onclick = () => {
const t = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", t);
try { localStorage.setItem("demo.theme", t); } catch (e) {}
};
// KPI
const totalQty = DATA.reduce((s, r) => s + r.qty, 0), totalSum = DATA.reduce((s, r) => s + r.sum, 0);
const regions = [...new Set(DATA.map(r => r.region))].sort((a, b) => a.localeCompare(b, "ru"));
$("k-qty").textContent = fmt(totalQty);
$("k-sum").textContent = short(totalSum) + " ₸";
$("k-regions").textContent = regions.length;
for (const r of regions) { const o = document.createElement("option"); o.value = r; o.textContent = r; $("region").appendChild(o); }
function agg(key) {
const m = new Map();
for (const r of DATA) m.set(r[key], (m.get(r[key]) || 0) + r[state.metric]);
return [...m.entries()].sort((a, b) => b[1] - a[1]);
}
function bars(el, items, pick) {
const max = items[0]?.[1] || 1;
el.innerHTML = items.map(([label, v]) => `<${pick ? "button type=\"button\"" : "div"} class="brow" ${pick ? `data-pick="true" data-region="${esc(label)}" data-active="${state.region === label}"` : ""} title="${esc(label)}: ${fmt(v)}">
<span class="lbl">${esc(label)}</span><span class="track"><span class="fill" style="width:${Math.max(2, v / max * 100).toFixed(1)}%"></span></span><span class="val">${state.metric === "sum" ? short(v) : fmt(v)}</span></${pick ? "button" : "div"}>`).join("");
}
function renderTop() {
const byRegion = agg("region"), byService = agg("service");
const mlabel = state.metric === "sum" ? "по выручке" : "по покупкам";
const lead = byService[0];
$("k-lead").textContent = lead[0]; $("k-lead").title = lead[0]; $("k-lead-hint").textContent = mlabel + " · " + (state.metric === "sum" ? short(lead[1]) + " ₸" : fmt(lead[1]));
$("c-regions-hint").textContent = mlabel + " · нажмите, чтобы отфильтровать";
$("c-services-hint").textContent = mlabel + " · топ-8";
bars($("c-regions"), byRegion.slice(0, 10), true);
bars($("c-services"), byService.slice(0, 8), false);
}
$("c-regions").addEventListener("click", e => {
const b = e.target.closest("[data-region]"); if (!b) return;
setRegion(state.region === b.dataset.region ? "" : b.dataset.region);
});
document.querySelectorAll(".kt-ai-segmented .seg").forEach(b => b.onclick = () => {
state.metric = b.dataset.metric;
document.querySelectorAll(".kt-ai-segmented .seg").forEach(x => x.dataset.active = String(x === b));
state.sort = state.metric; state.dir = "desc"; state.shown = PAGE;
renderTop(); renderTable();
});
// таблица
function setRegion(r) { state.region = r; $("region").value = r; state.shown = PAGE; renderTop(); renderTable(); }
$("region").onchange = () => setRegion($("region").value);
$("q").oninput = () => { state.q = $("q").value.trim().toLowerCase(); state.shown = PAGE; renderTable(); };
document.querySelectorAll("th[data-sort]").forEach(th => th.onclick = () => {
const k = th.dataset.sort;
state.dir = state.sort === k && state.dir === "desc" ? "asc" : (state.sort === k ? "desc" : (k === "qty" || k === "sum" ? "desc" : "asc"));
state.sort = k; state.shown = PAGE; renderTable();
});
$("more").onclick = () => { state.shown += PAGE; renderTable(); };
const hl = (s, q) => { if (!q) return esc(s); const i = s.toLowerCase().indexOf(q); return i < 0 ? esc(s) : esc(s.slice(0, i)) + "<mark>" + esc(s.slice(i, i + q.length)) + "</mark>" + esc(s.slice(i + q.length)); };
function renderTable() {
const rows = DATA.filter(r => (!state.region || r.region === state.region) && (!state.q || r.service.toLowerCase().includes(state.q)));
const k = state.sort, d = state.dir === "asc" ? 1 : -1;
rows.sort((a, b) => (typeof a[k] === "number" ? (a[k] - b[k]) : String(a[k]).localeCompare(String(b[k]), "ru")) * d);
document.querySelectorAll("th[data-sort]").forEach(th => { if (th.dataset.sort === k) th.dataset.dir = state.dir; else th.removeAttribute("data-dir"); });
const shown = rows.slice(0, state.shown);
$("rows").innerHTML = shown.map(r => `<tr>
<td class="svc"><span class="t" title="${esc(r.service)}">${hl(r.service, state.q)}</span><span class="m">${esc(r.region)}</span></td>
<td class="col-region" data-label="Регион">${esc(r.region)}</td>
<td class="num" data-label="Покупок"><span class="v">${fmt(r.qty)}</span></td>
<td class="num" data-label="Сумма, ₸"><span class="v">${fmt(r.sum)}</span></td></tr>`).join("");
$("empty").hidden = rows.length > 0; $("tbl").hidden = rows.length === 0;
const q = rows.reduce((s, r) => s + r.qty, 0), sum = rows.reduce((s, r) => s + r.sum, 0);
$("count").textContent = rows.length === DATA.length ? `${rows.length} позиций` : `${rows.length} из ${DATA.length}`;
$("foot").textContent = rows.length ? `Показано ${shown.length} из ${rows.length} · итого ${fmt(q)} покупок · ${short(sum)} ₸` : "";
$("more").hidden = shown.length >= rows.length;
$("more").textContent = `Показать ещё ${Math.min(PAGE, rows.length - shown.length)}`;
$("chips").innerHTML = state.region ? `<button class="kt-ai-chip" data-tone="blue" data-removable type="button" id="chip-r">${esc(state.region)} <span class="remove" aria-label="Снять фильтр">×</span></button>` : "";
const c = $("chip-r"); if (c) c.onclick = () => setRegion("");
}
renderTop(); renderTable();
</script>
</body>
</html>