dashbord-top-uslug-b2c-po-r.../brief.md

382 lines
44 KiB
Markdown
Raw Permalink 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.

# Дашборд: топ услуг B2C по регионам
Веб-дашборд «Топ услуг B2C по регионам»: топ услуг Казахтелекома по количеству покупок с разбивкой по всем регионам. В шапке слева — логотип Казахтелекома: файл kt-logo.svg в корне проекта (его содержимое — в разделе «Логотип» ниже, сохрани без изменений), подключи как <img src="./kt-logo.svg" alt="Казахтелеком" height="36">, справа от него заголовок «Топ услуг B2C по регионам». Экран собирай ТОЛЬКО из компонентов дизайн-системы KT AI (design-system/COMPONENTS.md), своих классов, стилей и цветов не выдумывай. Шапка — .kt-ai-pagehead с логотипом и заголовком. Под ней полоса показателей .kt-ai-kpi-strip из четырёх плашек .kt-ai-kpi (в каждой .value — число, .label — подпись, .hint — уточнение): всего покупок, всего регионов, услуга-лидер (название целиком, без обрезания), регион-лидер; на широком экране четыре в один ряд, на телефоне — в две колонки. Ниже два столбчатых графика в карточках .kt-ai-card — «Топ регионов по покупкам» и «Топ услуг по количеству»: чистый SVG или div-столбцы высотой 190–220px, столбцы 20–28px с радиусом 2px, подписи значений 11px mono muted, цвет столбцов — один токен var(--kt-ai-chart-blue) (второй график — var(--kt-ai-chart-green)), без радуги; подсказка значения при наведении — атрибут data-kt-tip и скрипт ./design-system/kt-ai-chart-tip.js. Ниже таблица топ-услуг по правилам ДС: frameless в .kt-ai-table-wrap, колонки «Услуга», «Регион», «Покупок», «Сумма, ₸», числа вправо с tabular-nums, лёгкие горизонтальные разделители, поиск по услуге и фильтр по региону над таблицей. Цвета, отступы, радиусы, шрифт — только токены var(--kt-ai-…) из ДС в светлой теме (<html lang="ru" data-theme="light">); ни одного своего hex-цвета, кроме файла логотипа. Перед публикацией проверь: в index.html не меньше десятка классов kt-ai-*. Только фронтенд — один статичный index.html с данными внутри, без серверной части (никаких package.json, server.js, node-зависимостей). Источник данных — таблица в разделе «Данные» ниже, используй её целиком и ничего не выдумывай. Все пути к CSS, скриптам, шрифтам и другим ресурсам — строго относительные (./...): сайт публикуется в подпапке, абсолютные пути с корня сломаются. Работай без уточняющих вопросов. Когда дашборд готов — опубликуй сайт.
## Основа — готовая страница
Прототип уже свёрстан и проверен, его код — в этом файле между строками `<<<BASE_HTML` и `BASE_HTML>>>`. Не перепечатывай и не переписывай его — вырежи в index.html командой:
```sh
awk '/^<<<BASE_HTML$/{f=1;next} /^BASE_HTML>>>$/{f=0} f' brief.md > index.html
sed -i 's#\.\./\.\./ds/#./design-system/#g; s#\.\./\.\./kt-logo\.svg#./kt-logo.svg#g' index.html
```
Сохрани kt-logo.svg из раздела «Логотип». После этого внеси ТОЛЬКО правку владельца: Дешборд по службе поддержки по регионам. Больше ничего не меняй и опубликуй сайт.
## Логотип
Сохрани этот SVG как файл kt-logo.svg в корне проекта, не меняя содержимое:
```svg
<svg width="199" height="138" viewBox="0 0 199 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.8375 1.6362C104.866 1.6362 134.898 31.6612 134.898 68.6902C134.898 105.726 104.865 135.744 67.8375 135.744C30.8085 135.744 0.776489 105.726 0.776489 68.6902C0.776489 31.6612 30.8095 1.6362 67.8375 1.6362Z" fill="#0096D7"/>
<path d="M67.8375 1.6362C104.866 1.6362 134.898 31.6612 134.898 68.6902C134.898 105.726 104.865 135.744 67.8375 135.744C30.8085 135.744 0.776489 105.726 0.776489 68.6902C0.776489 31.6612 30.8095 1.6362 67.8375 1.6362Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.576 86.4422H3.17049C3.17049 87.5492 3.73849 89.9082 4.50049 90.8642H133.956C133.769 89.7572 133.576 87.5492 133.576 86.4422ZM126.378 65.7272H0.531494C0.531494 66.8342 0.725494 69.5022 0.725494 70.1342H126.751C126.564 69.0412 126.378 66.8342 126.378 65.7272ZM126.378 44.4812H4.90349C4.52249 45.2652 3.95449 48.2562 3.57349 48.8882H126.753C126.566 47.7952 126.378 45.5882 126.378 44.4812ZM126.378 23.0622H18.4065C17.8315 23.5442 15.7395 26.2112 14.7895 27.4772H126.753C126.566 26.3632 126.378 24.1622 126.378 23.0622ZM133.576 108.22H13.2435C13.6245 109.636 15.7165 111.844 16.6655 112.635H133.955C133.768 111.521 133.574 109.32 133.574 108.22" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.145 1.0032C170.123 2.5342 199.702 34.1062 198.2 71.5152C196.697 108.925 164.68 138.022 126.71 136.505C88.7325 134.974 59.1605 103.402 60.6635 65.9932C62.1665 28.5832 94.1765 -0.5288 132.146 1.0032" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.0854 8.3662C117.62 4.8792 147.458 7.9992 167.058 19.9352L168.367 23.1782C174.349 25.0042 182.467 29.1602 187.73 33.0352C178.879 19.4602 165.606 8.5602 148.595 3.5782C133.331 -1.3398 111.753 0.192204 97.0864 8.3672" fill="#0096D7"/>
<path d="M97.0854 8.3662C117.62 4.8792 147.458 7.9992 167.058 19.9352L168.367 23.1782C174.349 25.0042 182.467 29.1602 187.73 33.0352C178.879 19.4602 165.606 8.5602 148.595 3.5782C133.331 -1.3398 111.753 0.192204 97.0864 8.3672L97.0854 8.3662Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M77.6375 24.1482C114.975 19.2732 154.441 20.7042 191.138 39.5492C196.344 48.8462 197.681 59.0992 198.091 68.6762C193.202 63.5352 184.804 58.2582 179.828 55.1952L179.008 51.8162C146.847 35.0562 109.128 25.9032 74.4655 27.8522C75.4645 26.7732 76.3135 25.3862 77.6365 24.1492" fill="#0096D7"/>
<path d="M77.6375 24.1482C114.975 19.2732 154.441 20.7042 191.138 39.5492C196.344 48.8462 197.681 59.0992 198.091 68.6762C193.202 63.5352 184.804 58.2582 179.828 55.1952L179.008 51.8162C146.847 35.0562 109.128 25.9032 74.4655 27.8522C75.4645 26.7732 76.3135 25.3862 77.6365 24.1492L77.6375 24.1482Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3285 45.0132C108.893 43.5822 152.363 48.6662 197.768 72.8022C197.754 81.0922 195.181 89.7562 191.183 97.8882C186.143 92.4242 180.297 87.2402 173.769 83.1562L174.373 79.9922C137.812 57.8112 100.711 48.9892 63.8905 49.1042C64.2495 48.0042 65.1275 46.1272 65.3285 45.0132Z" fill="#0096D7"/>
<path d="M65.3285 45.0132C108.893 43.5822 152.363 48.6662 197.768 72.8022C197.754 81.0922 195.181 89.7562 191.183 97.8882C186.143 92.4242 180.297 87.2402 173.769 83.1562L174.373 79.9922C137.812 57.8112 100.711 48.9892 63.8905 49.1042C64.2495 48.0042 65.1275 46.1272 65.3285 45.0132Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.9855 65.8862C110.524 66.4322 152.967 77.2172 189.283 101.469C185.63 109.134 179.469 115.734 172.703 121.522C167.943 117.028 162.134 110.902 157.525 106.567L159.704 103.95C131.792 84.5012 99.6535 71.2642 60.8495 69.2222C61.0655 67.7912 60.9345 67.1442 60.9855 65.8862Z" fill="#0096D7"/>
<path d="M60.9855 65.8862C110.524 66.4322 152.967 77.2172 189.283 101.469C185.63 109.134 179.469 115.734 172.703 121.522C167.943 117.028 162.134 110.902 157.525 106.567L159.704 103.95C131.792 84.5012 99.6535 71.2642 60.8495 69.2222C61.0655 67.7912 60.9345 67.1442 60.9855 65.8862Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.1494 87.3122C102.155 88.4052 141.096 102.886 168.784 123.909C163.305 129.273 152.801 132.839 144.266 135.046L135.911 124.822L138.068 122.687C113.651 103.684 90.4344 94.4732 64.4284 91.2022C63.8314 90.2172 63.5734 88.6062 63.1494 87.3122Z" fill="#0096D7"/>
<path d="M63.1494 87.3122C102.155 88.4052 141.096 102.886 168.784 123.909C163.305 129.273 152.801 132.839 144.266 135.046L135.911 124.822L138.068 122.687C113.651 103.684 90.4344 94.4732 64.4284 91.2022C63.8314 90.2172 63.5734 88.6062 63.1494 87.3122Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.0714 109.119C96.7274 112.405 119.519 120.343 140.586 135.535C133.986 137.182 124.754 136.815 116.996 135.535C108.627 133.608 100.775 130.746 97.9714 129.035C101.624 129.344 105.441 129.653 109.252 129.977C107.009 126.209 88.1494 115.583 76.9554 112.728C76.1934 111.743 74.8194 110.255 74.0714 109.119Z" fill="#0096D7"/>
<path d="M74.0714 109.119C96.7274 112.405 119.519 120.343 140.586 135.535C133.986 137.182 124.754 136.815 116.996 135.535C108.627 133.608 100.775 130.746 97.9714 129.035C101.624 129.344 105.441 129.653 109.252 129.977C107.009 126.209 88.1494 115.583 76.9554 112.728C76.1934 111.743 74.8194 110.255 74.0714 109.119Z" stroke="#0096D7" stroke-width="1.553" stroke-miterlimit="22.926"/>
</svg>
```
<<<BASE_HTML
<!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="../../ds/kt-ai-tokens.css">
<link rel="stylesheet" href="../../ds/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>
BASE_HTML>>>
## Объём работы
Собирай ровно то, что написано в задании. Не добавляй лишние разделы, журналы и настройки. Для статичной страницы достаточно index.html. Если запрошен статичный макет, не добавляй серверную часть на этом этапе.
Если задача требует общего хранения, настоящей регистрации или рабочего приложения, реализуй серверную часть и базу данных средствами Vibe AI. Создай необходимые файлы, таблицы и обработчики, даже если владелец не перечислил их отдельно. Не подменяй общее хранение localStorage или демонстрационным списком. Собственная база приложения не требует интеграции с системами КТ и отдельного утверждения дизайна в экспрессе. При доработке существующего проекта это правило заменяет прежнее ограничение одним HTML-файлом.
Проверь сохранение записи, чтение из другой сессии, обработку ошибок и ограничения доступа. Личные данные доступны только уполномоченному пользователю, секреты не попадают в браузерный код. Демонстрационные записи отделены от настоящих. Не придумывай доступы или готовые интеграции. Если публикация серверной части недоступна, укажи конкретную причину и не объявляй функцию работающей. Закончив, опубликуй приложение и сообщи фактический адрес и результаты проверки; сохраняй прежний адрес, если платформа это позволяет.
## Состояние данных
Примеры помечай «Демо-данные». Это не запрет базы данных: настоящие записи сохраняй в базе приложения отдельно от примеров. Пустую базу показывай пустой.
1. Управление и сообщение пустого состояния не прячутся никогда: скрывай только блок результата. Скрывать его предка (`parentElement`, `parentNode`, `closest(...)`) ЗАПРЕЩЕНО. Фильтры и надпись «Недостаточно данных» остаются видимыми.
2. Фильтр обязан менять ВСЕ представления, которые задевает: таблицу, показатели, график и текст объяснения. Объяви реестр `const FILTERS = [{id, label, affects}]` со всеми видимыми фильтрами. Не меняет чего-то в демо — скажи честно: `demoOnly: true` и видимая подпись рядом с контролом `<span data-kt-filter-note>в демо не меняет график</span>`.
3. Молчаливая подмена набора запрещена: `DATA_SET[x] || DATA_SET["7d"]` показывает выбранный период вместе с данными другого. Нет данных под выбранные фильтры — покажи пустое состояние с явной причиной («за выбранный период данных нет»), а не чужой набор. Демо-набор наполни так, чтобы под каждым значением каждого фильтра числа были РАЗНЫЕ, и один срез оставь пустым.
4. Для демонстрационных наборов: `data-kt-data-mode="demo"` на `<body>` и видимый чип `<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>` в шапке. Для записей, действительно сохраняемых в собственной базе приложения, допустим `live` и подпись «Данные приложения». Чип «Данные из системы» используй только для реально подключённой системы, когда в задании названы источник и точка доступа. Не представляй собственную базу как интеграцию с КТ.
5. Объявленный запрет обязан быть запретом, а не надписью. Если строка помечена как непроходная (конфликт данных, пустой обязательный срок, незаполненное поле), действие по ней НЕ ВЫПОЛНЯЕТСЯ: ни поштучно, ни массовой кнопкой «принять всё». Кнопку в такой строке ставь `disabled`, а рядом одной фразой скажи, ЧТО именно мешает и что сделать («срок не заполнен — укажите дату»). Массовое действие пропускает непроходные строки и честно пишет, сколько пропустило.