Дашборд по службе поддержки: статический прототип на ДС KT AI
This commit is contained in:
parent
f5648f170f
commit
0854488a46
125
index.html
125
index.html
@ -3,25 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light">
|
||||||
<title>Топ услуг B2C по регионам</title>
|
<title>Дашборд по службе поддержки по регионам</title>
|
||||||
<link rel="icon" href="data:,">
|
<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>
|
<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-fonts.css">
|
||||||
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
|
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
|
||||||
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
|
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
|
||||||
|
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
|
||||||
<style>
|
<style>
|
||||||
/* Дашборд «Топ услуг B2C по регионам» — архетип dashboard дизайн-системы KT AI.
|
|
||||||
Свой стиль — только сетка и отступы; цвета, радиусы, шрифты — токены ДС. */
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
[hidden] { display: none !important; }
|
[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); }
|
html, body { margin: 0; background: var(--kt-ai-bg); color: var(--kt-ai-fg); }
|
||||||
body { -webkit-text-size-adjust: 100%; }
|
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); }
|
.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 { 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-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 img { width: 36px; height: 36px; flex-shrink: 0; }
|
||||||
.top .head { min-width: 0; flex: 1; }
|
.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 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 .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; }
|
||||||
@ -30,17 +28,10 @@
|
|||||||
.iconbtn:hover { background: var(--kt-ai-bg-hover); }
|
.iconbtn:hover { background: var(--kt-ai-bg-hover); }
|
||||||
.iconbtn svg { width: 16px; height: 16px; }
|
.iconbtn svg { width: 16px; height: 16px; }
|
||||||
[data-theme="light"] .ic-sun, [data-theme="dark"] .ic-moon { display: none; }
|
[data-theme="light"] .ic-sun, [data-theme="dark"] .ic-moon { display: none; }
|
||||||
|
|
||||||
/* Секции */
|
|
||||||
.section { margin-top: var(--kt-ai-space-6); }
|
.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 { 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 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); }
|
.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); }
|
.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 { 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 .ttl { white-space: nowrap; }
|
||||||
@ -55,70 +46,32 @@
|
|||||||
.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); }
|
.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); }
|
.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; }
|
.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 { 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 .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; }
|
.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 { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-3); }
|
||||||
.chips:empty { display: none; }
|
.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); }
|
.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) {
|
@media (max-width: 639.98px) {
|
||||||
.wrap { padding-left: var(--kt-ai-space-4); padding-right: var(--kt-ai-space-4); }
|
.wrap { padding-left: var(--kt-ai-space-4); padding-right: var(--kt-ai-space-4); }
|
||||||
.top .sub { display: none; }
|
.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%; }
|
.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%; }
|
.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; }
|
.brow { grid-template-columns: minmax(80px, 40%) minmax(0, 1fr) auto; }
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) { .brow .fill { transition: none; } }
|
@media (prefers-reduced-motion: reduce) { .brow .fill { transition: none; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="kt-ai-app">
|
<body class="kt-ai-app" data-kt-data-mode="demo">
|
||||||
<header class="top">
|
<header class="kt-ai-pagehead">
|
||||||
<div class="wrap top-in">
|
<div class="kt-ai-wrap" style="display:flex;align-items:center;gap:var(--kt-ai-space-4)">
|
||||||
<img src="./kt-logo.svg" alt="Казахтелеком">
|
<img src="./kt-logo.svg" alt="Казахтелеком">
|
||||||
<div class="head">
|
<div style="min-width:0;flex:1">
|
||||||
<h1>Топ услуг B2C по регионам</h1>
|
<h1 style="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">Дашборд по службе поддержки по регионам</h1>
|
||||||
<p class="sub" id="sub">Июль 2026 · покупки и выручка по регионам</p>
|
<p style="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" id="sub">Июль 2026 · покупки и выручка по регионам</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tools">
|
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
|
||||||
|
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
|
||||||
<div class="kt-ai-segmented" role="tablist" aria-label="Метрика">
|
<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="qty" data-active="true">Покупки</button>
|
||||||
<button class="seg" type="button" role="tab" data-metric="sum">Выручка</button>
|
<button class="seg" type="button" role="tab" data-metric="sum">Выручка</button>
|
||||||
@ -180,26 +133,32 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Реестр фильтров
|
||||||
|
const FILTERS = [
|
||||||
|
{ id: "region", label: "Регион", affects: ["table", "kpi", "charts"] },
|
||||||
|
{ id: "q", label: "Поиск по услуге", affects: ["table"] }
|
||||||
|
];
|
||||||
|
|
||||||
const DATA = [
|
const DATA = [
|
||||||
{"region": "Астана", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 37372, "sum": 238463786.09},
|
{"region": "Астана", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 37372, "sum": 238463786.09},
|
||||||
{"region": "г.Алматы", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 39671, "sum": 237005698.16},
|
{"region": "г.Алматы", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 39671, "sum": 237005698.16},
|
||||||
{"region": "г.Алматы", "service": "Абонплата ШПД по FTTx для B2C", "qty": 35588, "sum": 152893561.28},
|
{"region": "г.Алматы", "service": "Абонплата ШПД по FTTx для B2C", "qty": 35588, "sum": 152893561.28},
|
||||||
{"region": "Астана", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 35637, "sum": 136187938.99},
|
{"region": "Астана", "service": "2025 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 35637, "sum": 136187938.99},
|
||||||
{"region": "Астана", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 47661, "sum": 132060372.93},
|
{"region": "Астана", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 47661, "sum": 132060372.93},
|
||||||
{"region": "Астана", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 61700, "sum": 123403164.43},
|
{"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": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 57378, "sum": 110497209.55},
|
||||||
{"region": "г.Алматы", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 29997, "sum": 106749675.59},
|
{"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 Абонплата за мобильный номер MT-C GSM закр.", "qty": 52504, "sum": 99235780.41},
|
||||||
{"region": "Астана", "service": "2022 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 36946, "sum": 96430127.27},
|
{"region": "Астана", "service": "2022 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 36946, "sum": 96430127.27},
|
||||||
{"region": "Астана", "service": "Абонплата ШПД по FTTx для B2C", "qty": 19565, "sum": 88834659.73},
|
{"region": "Астана", "service": "Абонплата ШПД по FTTx для B2C", "qty": 19565, "sum": 88834659.73},
|
||||||
{"region": "Астана", "service": "2024 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 22167, "sum": 83549325.34},
|
{"region": "Астана", "service": "2024 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 22167, "sum": 83549325.34},
|
||||||
{"region": "г.Алматы", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 31221, "sum": 83511013.05},
|
{"region": "г.Алматы", "service": "B2C TV Plus Абонплата Full ГТС", "qty": 31221, "sum": 83511013.05},
|
||||||
{"region": "г.Алматы", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 40838, "sum": 82748215.46},
|
{"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": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 41486, "sum": 76400272.02},
|
||||||
{"region": "Астана", "service": "TV Plus Абонплата за Премиальный пакет ГТС", "qty": 29210, "sum": 71278165.09},
|
{"region": "Астана", "service": "TV Plus Абонплата за Премиальный пакет ГТС", "qty": 29210, "sum": 71278165.09},
|
||||||
{"region": "г.Алматы", "service": "2022 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 24001, "sum": 62521274.3},
|
{"region": "г.Алматы", "service": "2022 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 24001, "sum": 62521274.3},
|
||||||
{"region": "Астана", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM откр.", "qty": 27864, "sum": 59635624.39},
|
{"region": "Астана", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM откр.", "qty": 27864, "sum": 59635624.39},
|
||||||
{"region": "г.Алматы", "service": "2024 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 15678, "sum": 58970815.48},
|
{"region": "г.Алматы", "service": "2024 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 15678, "sum": 58970815.48},
|
||||||
{"region": "г.Актобе", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 30641, "sum": 55819936.98},
|
{"region": "г.Актобе", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 30641, "sum": 55819936.98},
|
||||||
{"region": "г.Алматы", "service": "B2C Абонплата за основной телефон ГТС", "qty": 52732, "sum": 49636818.5},
|
{"region": "г.Алматы", "service": "B2C Абонплата за основной телефон ГТС", "qty": 52732, "sum": 49636818.5},
|
||||||
{"region": "г.Актобе", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 25178, "sum": 48000483.43},
|
{"region": "г.Актобе", "service": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 25178, "sum": 48000483.43},
|
||||||
@ -207,11 +166,11 @@ const DATA = [
|
|||||||
{"region": "г.Павлодар", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 23334, "sum": 44857518.84},
|
{"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": "B2C TV Plus Аренда STB оператора новое", "qty": 96183, "sum": 43252519.71},
|
||||||
{"region": "г.Караганда", "service": "Абонплата ШПД по FTTx для B2C", "qty": 8916, "sum": 41624727.66},
|
{"region": "г.Караганда", "service": "Абонплата ШПД по FTTx для B2C", "qty": 8916, "sum": 41624727.66},
|
||||||
{"region": "Астана", "service": "2026 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 13685, "sum": 41618770.34},
|
{"region": "Астана", "service": "2026 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 13685, "sum": 41618770.34},
|
||||||
{"region": "г.Алматы", "service": "2018 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 11568, "sum": 40199964.35},
|
{"region": "г.Алматы", "service": "2018 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 11568, "sum": 40199964.35},
|
||||||
{"region": "г.Тараз", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 21880, "sum": 39564813.34},
|
{"region": "г.Тараз", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 21880, "sum": 39564813.34},
|
||||||
{"region": "г.Алматы", "service": "Абонплата ШПД по ADSL для B2C", "qty": 10710, "sum": 36238454.72},
|
{"region": "г.Алматы", "service": "Абонплата ШПД по ADSL для B2C", "qty": 10710, "sum": 36238454.72},
|
||||||
{"region": "г.Алматы", "service": "2020 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 12148, "sum": 36010481.68},
|
{"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": 18362, "sum": 35883527.39},
|
||||||
{"region": "г. Шымкент", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 19180, "sum": 35338634.51},
|
{"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": "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", "qty": 18945, "sum": 35138664.91},
|
||||||
@ -224,14 +183,15 @@ const DATA = [
|
|||||||
{"region": "Астана", "service": "B2C Аренда оборудования ONT оператора - новое", "qty": 87071, "sum": 31220264.09},
|
{"region": "Астана", "service": "B2C Аренда оборудования ONT оператора - новое", "qty": 87071, "sum": 31220264.09},
|
||||||
{"region": "г.Павлодар", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 6104, "sum": 30831455.08},
|
{"region": "г.Павлодар", "service": "B2C Абонплата ШПД по FTTx ГТС (New)", "qty": 6104, "sum": 30831455.08},
|
||||||
{"region": "г.Кокшетау", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16818, "sum": 30654924.73},
|
{"region": "г.Кокшетау", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16818, "sum": 30654924.73},
|
||||||
{"region": "г.Актобе", "service": "2025 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 8613, "sum": 30562705.78},
|
{"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": 16473, "sum": 30539180.35},
|
||||||
{"region": "Енбекшиказахский район", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 17347, "sum": 30429552.38},
|
{"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 Абонплата за Домашний интернет FTTx ГТС", "qty": 11709, "sum": 30303557.0},
|
||||||
{"region": "г.Кызылорда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16539, "sum": 29772241.67},
|
{"region": "г.Кызылорда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 16539, "sum": 29772241.67},
|
||||||
{"region": "г.Алматы", "service": "2026 B2C Абонплата за Домашний Интернет FTTx ГТС", "qty": 10206, "sum": 29200316.21},
|
{"region": "г.Алматы", "service": "2026 B2C Абонплата за Домашний интернет FTTx ГТС", "qty": 10206, "sum": 29200316.21},
|
||||||
{"region": "г.Караганда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 15714, "sum": 28837871.98}
|
{"region": "г.Караганда", "service": "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", "qty": 15714, "sum": 28837871.98}
|
||||||
];
|
];
|
||||||
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const nf = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
|
const nf = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
|
||||||
const fmt = n => nf.format(Math.round(n));
|
const fmt = n => nf.format(Math.round(n));
|
||||||
@ -260,11 +220,13 @@ function agg(key) {
|
|||||||
for (const r of DATA) m.set(r[key], (m.get(r[key]) || 0) + r[state.metric]);
|
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]);
|
return [...m.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bars(el, items, pick) {
|
function bars(el, items, pick) {
|
||||||
const max = items[0]?.[1] || 1;
|
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)}">
|
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("");
|
<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() {
|
function renderTop() {
|
||||||
const byRegion = agg("region"), byService = agg("service");
|
const byRegion = agg("region"), byService = agg("service");
|
||||||
const mlabel = state.metric === "sum" ? "по выручке" : "по покупкам";
|
const mlabel = state.metric === "sum" ? "по выручке" : "по покупкам";
|
||||||
@ -275,10 +237,12 @@ function renderTop() {
|
|||||||
bars($("c-regions"), byRegion.slice(0, 10), true);
|
bars($("c-regions"), byRegion.slice(0, 10), true);
|
||||||
bars($("c-services"), byService.slice(0, 8), false);
|
bars($("c-services"), byService.slice(0, 8), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("c-regions").addEventListener("click", e => {
|
$("c-regions").addEventListener("click", e => {
|
||||||
const b = e.target.closest("[data-region]"); if (!b) return;
|
const b = e.target.closest("[data-region]"); if (!b) return;
|
||||||
setRegion(state.region === b.dataset.region ? "" : b.dataset.region);
|
setRegion(state.region === b.dataset.region ? "" : b.dataset.region);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll(".kt-ai-segmented .seg").forEach(b => b.onclick = () => {
|
document.querySelectorAll(".kt-ai-segmented .seg").forEach(b => b.onclick = () => {
|
||||||
state.metric = b.dataset.metric;
|
state.metric = b.dataset.metric;
|
||||||
document.querySelectorAll(".kt-ai-segmented .seg").forEach(x => x.dataset.active = String(x === b));
|
document.querySelectorAll(".kt-ai-segmented .seg").forEach(x => x.dataset.active = String(x === b));
|
||||||
@ -290,33 +254,44 @@ document.querySelectorAll(".kt-ai-segmented .seg").forEach(b => b.onclick = () =
|
|||||||
function setRegion(r) { state.region = r; $("region").value = r; state.shown = PAGE; renderTop(); renderTable(); }
|
function setRegion(r) { state.region = r; $("region").value = r; state.shown = PAGE; renderTop(); renderTable(); }
|
||||||
$("region").onchange = () => setRegion($("region").value);
|
$("region").onchange = () => setRegion($("region").value);
|
||||||
$("q").oninput = () => { state.q = $("q").value.trim().toLowerCase(); state.shown = PAGE; renderTable(); };
|
$("q").oninput = () => { state.q = $("q").value.trim().toLowerCase(); state.shown = PAGE; renderTable(); };
|
||||||
|
|
||||||
document.querySelectorAll("th[data-sort]").forEach(th => th.onclick = () => {
|
document.querySelectorAll("th[data-sort]").forEach(th => th.onclick = () => {
|
||||||
const k = th.dataset.sort;
|
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.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();
|
state.sort = k; state.shown = PAGE; renderTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("more").onclick = () => { 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)); };
|
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() {
|
function renderTable() {
|
||||||
const rows = DATA.filter(r => (!state.region || r.region === state.region) && (!state.q || r.service.toLowerCase().includes(state.q)));
|
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;
|
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);
|
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"); });
|
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);
|
const shown = rows.slice(0, state.shown);
|
||||||
$("rows").innerHTML = shown.map(r => `<tr>
|
$("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="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="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.qty)}</span></td>
|
||||||
<td class="num" data-label="Сумма, ₸"><span class="v">${fmt(r.sum)}</span></td></tr>`).join("");
|
<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;
|
|
||||||
|
$("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);
|
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}`;
|
$("count").textContent = rows.length === DATA.length ? `${rows.length} позиций` : `${rows.length} из ${DATA.length}`;
|
||||||
$("foot").textContent = rows.length ? `Показано ${shown.length} из ${rows.length} · итого ${fmt(q)} покупок · ${short(sum)} ₸` : "";
|
$("foot").textContent = rows.length ? `Показано ${shown.length} из ${rows.length} · итого ${fmt(q)} покупок · ${short(sum)} ₸` : "";
|
||||||
$("more").hidden = shown.length >= rows.length;
|
$("more").hidden = shown.length >= rows.length;
|
||||||
$("more").textContent = `Показать ещё ${Math.min(PAGE, rows.length - shown.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>` : "";
|
$("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("");
|
const c = $("chip-r"); if (c) c.onclick = () => setRegion("");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTop(); renderTable();
|
renderTop(); renderTable();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user