dashbord-top-uslug-b2c-po-re-7/index.html

737 lines
29 KiB
HTML
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.

<!doctype html>
<html lang="ru" data-theme="light">
<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: 360px;
position: relative;
padding-top: 40px;
}
.bar-chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 280px;
padding: 0 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) * 200;
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" style="top: -24px;">${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) * 200;
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" style="top: -24px;">${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>