736 lines
29 KiB
HTML
736 lines
29 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Топ услуг B2C по регионам</title>
|
||
<link rel="stylesheet" href="./design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
|
||
<style>
|
||
:root {
|
||
--kt-blue: #0096D7;
|
||
--kt-blue-dark: #0077A8;
|
||
--bg-page: #F5F7FA;
|
||
--bg-card: #FFFFFF;
|
||
--text-primary: #1A1A1A;
|
||
--text-secondary: #6B7280;
|
||
--border-color: #E5E7EB;
|
||
--success: #10B981;
|
||
--warning: #F59E0B;
|
||
--chart-1: #0096D7;
|
||
--chart-2: #00C4B4;
|
||
--chart-3: #3B82F6;
|
||
--chart-4: #8B5CF6;
|
||
--chart-5: #EC4899;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: var(--bg-page);
|
||
color: var(--text-primary);
|
||
line-height: 1.5;
|
||
}
|
||
.container { max-width: 1400px; margin: 0 auto; padding: 0 24px; }
|
||
|
||
/* Header */
|
||
.header {
|
||
background: var(--bg-card);
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding: 16px 0;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.logo-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
.logo-section img { height: 48px; width: auto; }
|
||
.header h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
.header-meta {
|
||
text-align: right;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Summary Cards */
|
||
.summary { padding: 24px 0; }
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
}
|
||
@media (max-width: 1024px) {
|
||
.summary-grid { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
@media (max-width: 640px) {
|
||
.summary-grid { grid-template-columns: 1fr; }
|
||
}
|
||
.card {
|
||
background: var(--bg-card);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
.card-label {
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 8px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.card-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: var(--kt-blue);
|
||
}
|
||
.card-value.small {
|
||
font-size: 20px;
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* Charts Section */
|
||
.charts { padding: 0 0 24px; }
|
||
.charts-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 20px;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.charts-grid { grid-template-columns: 1fr; }
|
||
}
|
||
.chart-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
.chart-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
.chart-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.chart-btn {
|
||
padding: 6px 12px;
|
||
border: 1px solid var(--border-color);
|
||
background: var(--bg-card);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.chart-btn.active {
|
||
background: var(--kt-blue);
|
||
color: white;
|
||
border-color: var(--kt-blue);
|
||
}
|
||
.chart-container {
|
||
height: 320px;
|
||
position: relative;
|
||
}
|
||
.bar-chart {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
height: 280px;
|
||
padding: 20px 10px 40px;
|
||
gap: 4px;
|
||
}
|
||
.bar-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
.bar {
|
||
width: 100%;
|
||
max-width: 50px;
|
||
border-radius: 4px 4px 0 0;
|
||
transition: height 0.3s ease;
|
||
position: relative;
|
||
}
|
||
.bar:hover { opacity: 0.8; }
|
||
.bar-label {
|
||
font-size: 10px;
|
||
color: var(--text-secondary);
|
||
margin-top: 8px;
|
||
text-align: center;
|
||
transform: rotate(-45deg);
|
||
transform-origin: top center;
|
||
white-space: nowrap;
|
||
}
|
||
.bar-value {
|
||
font-size: 9px;
|
||
color: var(--text-secondary);
|
||
position: absolute;
|
||
top: -18px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Table Section */
|
||
.table-section { padding: 0 0 40px; }
|
||
.table-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
.table-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
.table-controls {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
.filter-select {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
background: var(--bg-card);
|
||
cursor: pointer;
|
||
}
|
||
.search-input {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
width: 250px;
|
||
}
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: var(--bg-card);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
.data-table th,
|
||
.data-table td {
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
.data-table th {
|
||
background: #F9FAFB;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.data-table th:hover { background: #F3F4F6; }
|
||
.data-table th.sorted {
|
||
background: var(--kt-blue);
|
||
color: white;
|
||
}
|
||
.data-table tbody tr:hover {
|
||
background: #F9FAFB;
|
||
}
|
||
.data-table td {
|
||
font-size: 14px;
|
||
}
|
||
.rank-cell {
|
||
font-weight: 600;
|
||
color: var(--kt-blue);
|
||
width: 50px;
|
||
}
|
||
.amount-cell {
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
text-align: right;
|
||
}
|
||
.sort-indicator { margin-left: 4px; opacity: 0.5; }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
background: var(--bg-card);
|
||
border-top: 1px solid var(--border-color);
|
||
padding: 20px 0;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.footer-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.footer-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<div class="container header-content">
|
||
<div class="logo-section">
|
||
<img src="./kt-logo.svg" alt="Казахтелеком" height="48">
|
||
<h1>Топ услуг B2C по регионам</h1>
|
||
</div>
|
||
<div class="header-meta">
|
||
<div>Данные за июль 2026</div>
|
||
<div>Сегмент B2C</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
<section class="summary">
|
||
<div class="summary-grid">
|
||
<div class="card">
|
||
<div class="card-label">Всего покупок</div>
|
||
<div class="card-value" id="totalPurchases">0</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Всего регионов</div>
|
||
<div class="card-value" id="totalRegions">0</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Услуга-лидер</div>
|
||
<div class="card-value small" id="topService">-</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Регион-лидер</div>
|
||
<div class="card-value small" id="topRegion">-</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="charts">
|
||
<div class="charts-grid">
|
||
<div class="card">
|
||
<div class="chart-header">
|
||
<div class="chart-title">Топ-15 по регионам</div>
|
||
<div class="chart-controls">
|
||
<button class="chart-btn active" data-chart="region">По регионам</button>
|
||
<button class="chart-btn" data-chart="service">По услугам</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-container">
|
||
<div class="bar-chart" id="barChart"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="chart-header">
|
||
<div class="chart-title">Структура покупок</div>
|
||
</div>
|
||
<div class="chart-container">
|
||
<div class="bar-chart" id="pieChart"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="table-section">
|
||
<div class="table-header">
|
||
<div class="table-title">Детализация топ-50</div>
|
||
<div class="table-controls">
|
||
<select class="filter-select" id="regionFilter">
|
||
<option value="">Все регионы</option>
|
||
</select>
|
||
<input type="text" class="search-input" id="searchInput" placeholder="Поиск услуги...">
|
||
</div>
|
||
</div>
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th data-sort="rank"># <span class="sort-indicator">↕</span></th>
|
||
<th data-sort="region">Регион <span class="sort-indicator">↕</span></th>
|
||
<th data-sort="service">Услуга <span class="sort-indicator">↕</span></th>
|
||
<th data-sort="purchases">Покупки <span class="sort-indicator">↕</span></th>
|
||
<th data-sort="amount">Сумма (₸) <span class="sort-indicator">↕</span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
</section>
|
||
</main>
|
||
|
||
<footer class="footer">
|
||
<div class="container footer-grid">
|
||
<div>
|
||
<strong>Источник:</strong> ClickHouse АО «Казахтелеком»<br>
|
||
Таблица: sandbox.spec2_main_2spec_detail2_v2_ddt
|
||
</div>
|
||
<div>
|
||
<strong>Период:</strong> Июль 2026<br>
|
||
<strong>Сегмент:</strong> B2C
|
||
</div>
|
||
<div>
|
||
<strong>Валюта:</strong> Тенге (без НДС)<br>
|
||
<strong>Дата:</strong> 01.09.2026
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
// Данные из интервью
|
||
const rawData = [
|
||
{"rank":1,"region":"Астана","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":37372,"amount":238463786.09},
|
||
{"rank":2,"region":"г.Алматы","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":39671,"amount":237005698.16},
|
||
{"rank":3,"region":"г.Алматы","service":"Абонплата ШПД по FTTx для B2C","purchases":35588,"amount":152893561.28},
|
||
{"rank":4,"region":"Астана","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":35637,"amount":136187938.99},
|
||
{"rank":5,"region":"Астана","service":"B2C TV Plus Абонплата Full ГТС","purchases":47661,"amount":132060372.93},
|
||
{"rank":6,"region":"Астана","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","purchases":61700,"amount":123403164.43},
|
||
{"rank":7,"region":"г.Алматы","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":57378,"amount":110497209.55},
|
||
{"rank":8,"region":"г.Алматы","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":29997,"amount":106749675.59},
|
||
{"rank":9,"region":"Астана","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":52504,"amount":99235780.41},
|
||
{"rank":10,"region":"Астана","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":36946,"amount":96430127.27},
|
||
{"rank":11,"region":"Астана","service":"Абонплата ШПД по FTTx для B2C","purchases":19565,"amount":88834659.73},
|
||
{"rank":12,"region":"Астана","service":"2024 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":22167,"amount":83549325.34},
|
||
{"rank":13,"region":"г.Алматы","service":"B2C TV Plus Абонплата Full ГТС","purchases":31221,"amount":83511013.05},
|
||
{"rank":14,"region":"г.Алматы","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","purchases":40838,"amount":82748215.46},
|
||
{"rank":15,"region":"г.Семей","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":41486,"amount":76400272.02},
|
||
{"rank":16,"region":"Астана","service":"TV Plus Абонплата за Премиальный пакет ГТС","purchases":29210,"amount":71278165.09},
|
||
{"rank":17,"region":"г.Алматы","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":24001,"amount":62521274.30},
|
||
{"rank":18,"region":"Астана","service":"2021 B2C Абонплата за мобильный номер Kcell GSM откр.","purchases":27864,"amount":59635624.39},
|
||
{"rank":19,"region":"г.Алматы","service":"2024 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":15678,"amount":58970815.48},
|
||
{"rank":20,"region":"г.Актобе","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":30641,"amount":55819936.98},
|
||
{"rank":21,"region":"г.Алматы","service":"B2C Абонплата за основной телефон ГТС","purchases":52732,"amount":49636818.50},
|
||
{"rank":22,"region":"г.Актобе","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","purchases":25178,"amount":48000483.43},
|
||
{"rank":23,"region":"г.Алматы","service":"TV Plus Абонплата за Премиальный пакет ГТС","purchases":19055,"amount":45298192.89},
|
||
{"rank":24,"region":"г.Павлодар","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":23334,"amount":44857518.84},
|
||
{"rank":25,"region":"г.Алматы","service":"B2C TV Plus Аренда STB оператора новое","purchases":96183,"amount":43252519.71},
|
||
{"rank":26,"region":"г.Караганда","service":"Абонплата ШПД по FTTx для B2C","purchases":8916,"amount":41624727.66},
|
||
{"rank":27,"region":"Астана","service":"2026 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":13685,"amount":41618770.34},
|
||
{"rank":28,"region":"г.Алматы","service":"2018 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":11568,"amount":40199964.35},
|
||
{"rank":29,"region":"г.Тараз","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":21880,"amount":39564813.34},
|
||
{"rank":30,"region":"г.Алматы","service":"Абонплата ШПД по ADSL для B2C","purchases":10710,"amount":36238454.72},
|
||
{"rank":31,"region":"г.Алматы","service":"2020 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":12148,"amount":36010481.68},
|
||
{"rank":32,"region":"г.Усть-Каменогорск","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":18362,"amount":35883527.39},
|
||
{"rank":33,"region":"г. Шымкент","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":19180,"amount":35338634.51},
|
||
{"rank":34,"region":"г. Шымкент","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","purchases":18945,"amount":35138664.91},
|
||
{"rank":35,"region":"Астана","service":"B2C TV Plus Аренда STB оператора новое","purchases":76655,"amount":34746783.92},
|
||
{"rank":36,"region":"Астана","service":"2018 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":8705,"amount":32905725.03},
|
||
{"rank":37,"region":"г.Актобе","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":6742,"amount":32313021.91},
|
||
{"rank":38,"region":"г.Усть-Каменогорск","service":"Абонплата ШПД по FTTx для B2C","purchases":7269,"amount":31834355.15},
|
||
{"rank":39,"region":"г.Атырау","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":5495,"amount":31688118.61},
|
||
{"rank":40,"region":"г.Усть-Каменогорск","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":5600,"amount":31520413.53},
|
||
{"rank":41,"region":"Астана","service":"B2C Аренда оборудования ONT оператора - новое","purchases":87071,"amount":31220264.09},
|
||
{"rank":42,"region":"г.Павлодар","service":"B2C Абонплата ШПД по FTTx ГТС (New)","purchases":6104,"amount":30831455.08},
|
||
{"rank":43,"region":"г.Кокшетау","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":16818,"amount":30654924.73},
|
||
{"rank":44,"region":"г.Актобе","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":8613,"amount":30562705.78},
|
||
{"rank":45,"region":"г.Костанай","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":16473,"amount":30539180.35},
|
||
{"rank":46,"region":"Енбекшиказахский район","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":17347,"amount":30429552.38},
|
||
{"rank":47,"region":"г.Актобе","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":11709,"amount":30303557.00},
|
||
{"rank":48,"region":"г.Кызылорда","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":16539,"amount":29772241.67},
|
||
{"rank":49,"region":"г.Алматы","service":"2026 B2C Абонплата за Домашний интернет FTTx ГТС","purchases":10206,"amount":29200316.21},
|
||
{"rank":50,"region":"г.Караганда","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","purchases":15714,"amount":28837871.98}
|
||
];
|
||
|
||
// Состояние приложения
|
||
let state = {
|
||
data: [...rawData],
|
||
filteredData: [...rawData],
|
||
sortColumn: 'rank',
|
||
sortDirection: 'asc',
|
||
regionFilter: '',
|
||
searchQuery: '',
|
||
chartType: 'region'
|
||
};
|
||
|
||
// Загрузка из localStorage
|
||
function loadState() {
|
||
const saved = localStorage.getItem('dashboardState');
|
||
if (saved) {
|
||
try {
|
||
const parsed = JSON.parse(saved);
|
||
state = { ...state, ...parsed };
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
|
||
// Сохранение в localStorage
|
||
function saveState() {
|
||
localStorage.setItem('dashboardState', JSON.stringify({
|
||
sortColumn: state.sortColumn,
|
||
sortDirection: state.sortDirection,
|
||
regionFilter: state.regionFilter,
|
||
chartType: state.chartType
|
||
}));
|
||
}
|
||
|
||
// Форматирование чисел
|
||
function formatNumber(n) {
|
||
return n.toString().replace(/\s+/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||
}
|
||
|
||
function formatAmount(n) {
|
||
return formatNumber(n.toFixed(2).replace('.', ','));
|
||
}
|
||
|
||
// Подсчёт сводных показателей
|
||
function calculateSummary() {
|
||
const totalPurchases = rawData.reduce((sum, r) => sum + r.purchases, 0);
|
||
const regions = [...new Set(rawData.map(r => r.region))];
|
||
const totalRegions = regions.length;
|
||
|
||
// Услуга-лидер по количеству покупок
|
||
const serviceTotals = {};
|
||
rawData.forEach(r => {
|
||
serviceTotals[r.service] = (serviceTotals[r.service] || 0) + r.purchases;
|
||
});
|
||
const topService = Object.entries(serviceTotals).sort((a, b) => b[1] - a[1])[0];
|
||
|
||
// Регион-лидер по количеству покупок
|
||
const regionTotals = {};
|
||
rawData.forEach(r => {
|
||
regionTotals[r.region] = (regionTotals[r.region] || 0) + r.purchases;
|
||
});
|
||
const topRegion = Object.entries(regionTotals).sort((a, b) => b[1] - a[1])[0];
|
||
|
||
document.getElementById('totalPurchases').textContent = formatNumber(totalPurchases);
|
||
document.getElementById('totalRegions').textContent = totalRegions;
|
||
document.getElementById('topService').textContent = topService ? topService[0] : '-';
|
||
document.getElementById('topRegion').textContent = topRegion ? topRegion[0] : '-';
|
||
}
|
||
|
||
// Заполнение фильтра регионов
|
||
function populateRegionFilter() {
|
||
const regions = [...new Set(rawData.map(r => r.region))].sort();
|
||
const select = document.getElementById('regionFilter');
|
||
regions.forEach(region => {
|
||
const option = document.createElement('option');
|
||
option.value = region;
|
||
option.textContent = region;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
|
||
// Фильтрация и сортировка данных
|
||
function filterAndSort() {
|
||
let result = [...rawData];
|
||
|
||
if (state.regionFilter) {
|
||
result = result.filter(r => r.region === state.regionFilter);
|
||
}
|
||
|
||
if (state.searchQuery) {
|
||
const query = state.searchQuery.toLowerCase();
|
||
result = result.filter(r => r.service.toLowerCase().includes(query));
|
||
}
|
||
|
||
result.sort((a, b) => {
|
||
let aVal = a[state.sortColumn];
|
||
let bVal = b[state.sortColumn];
|
||
if (typeof aVal === 'string') aVal = aVal.toLowerCase();
|
||
if (typeof bVal === 'string') bVal = bVal.toLowerCase();
|
||
|
||
if (aVal < bVal) return state.sortDirection === 'asc' ? -1 : 1;
|
||
if (aVal > bVal) return state.sortDirection === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
|
||
state.filteredData = result;
|
||
}
|
||
|
||
// Отрисовка таблицы
|
||
function renderTable() {
|
||
const tbody = document.getElementById('tableBody');
|
||
tbody.innerHTML = state.filteredData.slice(0, 50).map((row, idx) => `
|
||
<tr>
|
||
<td class="rank-cell">${idx + 1}</td>
|
||
<td>${row.region}</td>
|
||
<td>${row.service}</td>
|
||
<td>${formatNumber(row.purchases)}</td>
|
||
<td class="amount-cell">₸ ${formatAmount(row.amount)}</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// Обновление индикаторов сортировки
|
||
document.querySelectorAll('.data-table th').forEach(th => {
|
||
th.classList.remove('sorted');
|
||
const col = th.dataset.sort;
|
||
if (col === state.sortColumn) {
|
||
th.classList.add('sorted');
|
||
th.querySelector('.sort-indicator').textContent = state.sortDirection === 'asc' ? '↑' : '↓';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Отрисовка графика
|
||
function renderChart() {
|
||
const container = document.getElementById('barChart');
|
||
const colors = ['#0096D7', '#00C4B4', '#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#EF4444'];
|
||
|
||
let chartData;
|
||
if (state.chartType === 'region') {
|
||
// Топ-15 регионов по сумме покупок
|
||
const regionMap = {};
|
||
rawData.forEach(r => {
|
||
regionMap[r.region] = (regionMap[r.region] || 0) + r.purchases;
|
||
});
|
||
chartData = Object.entries(regionMap)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 15)
|
||
.map(([name, value]) => ({ name, value }));
|
||
} else {
|
||
// Топ-15 услуг
|
||
const serviceMap = {};
|
||
rawData.forEach(r => {
|
||
serviceMap[r.service] = (serviceMap[r.service] || 0) + r.purchases;
|
||
});
|
||
chartData = Object.entries(serviceMap)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 15)
|
||
.map(([name, value]) => ({ name, value }));
|
||
}
|
||
|
||
const maxValue = Math.max(...chartData.map(d => d.value));
|
||
|
||
container.innerHTML = chartData.map((d, i) => {
|
||
const height = (d.value / maxValue) * 240;
|
||
const color = colors[i % colors.length];
|
||
return `
|
||
<div class="bar-item">
|
||
<div class="bar" style="height: ${height}px; background: ${color};" title="${d.name}: ${formatNumber(d.value)}">
|
||
<div class="bar-value">${formatNumber(d.value / 1000)}K</div>
|
||
</div>
|
||
<div class="bar-label">${d.name.length > 15 ? d.name.substring(0, 15) + '...' : d.name}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Отрисовка структуры (второй график)
|
||
function renderPieChart() {
|
||
const container = document.getElementById('pieChart');
|
||
const categories = {
|
||
'Интернет': ['ШПД', 'FTTx', 'ADSL', 'Домашний интернет'],
|
||
'Мобильная связь': ['Kcell', 'MT-C', 'мобильный номер'],
|
||
'TV': ['TV Plus', 'Аренда STB'],
|
||
'Оборудование': ['ONT', 'Аренда оборудования'],
|
||
'Телефония': ['телефон ГТС', 'основной телефон']
|
||
};
|
||
|
||
const categoryTotals = {};
|
||
rawData.forEach(r => {
|
||
let matched = false;
|
||
for (const [cat, keywords] of Object.entries(categories)) {
|
||
if (keywords.some(k => r.service.includes(k))) {
|
||
categoryTotals[cat] = (categoryTotals[cat] || 0) + r.purchases;
|
||
matched = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!matched) {
|
||
categoryTotals['Другое'] = (categoryTotals['Другое'] || 0) + r.purchases;
|
||
}
|
||
});
|
||
|
||
const data = Object.entries(categoryTotals).sort((a, b) => b[1] - a[1]);
|
||
const maxValue = Math.max(...data.map(d => d[1]));
|
||
const colors = ['#0096D7', '#00C4B4', '#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B'];
|
||
|
||
container.innerHTML = data.map((d, i) => {
|
||
const height = (d[1] / maxValue) * 240;
|
||
const color = colors[i % colors.length];
|
||
const percent = ((d[1] / rawData.reduce((s, r) => s + r.purchases, 0)) * 100).toFixed(1);
|
||
return `
|
||
<div class="bar-item">
|
||
<div class="bar" style="height: ${height}px; background: ${color};" title="${d[0]}: ${percent}%">
|
||
<div class="bar-value">${percent}%</div>
|
||
</div>
|
||
<div class="bar-label">${d[0]}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Обработчики событий
|
||
function setupEventListeners() {
|
||
// Сортировка таблицы
|
||
document.querySelectorAll('.data-table th').forEach(th => {
|
||
th.addEventListener('click', () => {
|
||
const col = th.dataset.sort;
|
||
if (state.sortColumn === col) {
|
||
state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
state.sortColumn = col;
|
||
state.sortDirection = 'asc';
|
||
}
|
||
saveState();
|
||
filterAndSort();
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
// Фильтр по региону
|
||
document.getElementById('regionFilter').addEventListener('change', (e) => {
|
||
state.regionFilter = e.target.value;
|
||
saveState();
|
||
filterAndSort();
|
||
renderTable();
|
||
renderChart();
|
||
});
|
||
|
||
// Поиск
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
state.searchQuery = e.target.value;
|
||
filterAndSort();
|
||
renderTable();
|
||
});
|
||
|
||
// Переключатель графика
|
||
document.querySelectorAll('.chart-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.chart-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
state.chartType = btn.dataset.chart;
|
||
saveState();
|
||
renderChart();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Инициализация
|
||
function init() {
|
||
loadState();
|
||
|
||
// Восстановление состояния UI
|
||
if (state.regionFilter) {
|
||
document.getElementById('regionFilter').value = state.regionFilter;
|
||
}
|
||
document.querySelectorAll('.chart-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.chart === state.chartType);
|
||
});
|
||
|
||
calculateSummary();
|
||
populateRegionFilter();
|
||
filterAndSort();
|
||
renderTable();
|
||
renderChart();
|
||
renderPieChart();
|
||
setupEventListeners();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|