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

607 lines
27 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">
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
<style>
.dashboard-header {
background: var(--kt-ai-card-bg);
border-bottom: 1px solid var(--kt-ai-border);
padding: 20px 0;
position: sticky;
top: 0;
z-index: 100;
}
.dashboard-header-in {
display: flex;
align-items: center;
gap: 16px;
}
.dashboard-header img { height: 48px; }
.dashboard-header h1 {
font-size: var(--kt-ai-text-xl);
font-weight: var(--kt-ai-weight-semibold);
color: var(--kt-ai-fg);
margin: 0;
}
.period {
font-size: var(--kt-ai-text-sm);
color: var(--kt-ai-fg-muted);
margin-top: 4px;
}
.summary-section { padding: 32px 0; }
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--kt-ai-space-5);
}
.summary-card {
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-card-border);
border-radius: var(--kt-ai-radius-2xl);
padding: var(--kt-ai-space-6);
}
.card-label {
font-size: var(--kt-ai-text-sm);
color: var(--kt-ai-fg-muted);
margin-bottom: var(--kt-ai-space-2);
}
.card-value {
font-size: var(--kt-ai-text-4xl);
font-weight: var(--kt-ai-weight-bold);
color: var(--kt-ai-primary);
}
.card-sub {
font-size: var(--kt-ai-text-xs);
color: var(--kt-ai-fg-faint);
margin-top: var(--kt-ai-space-2);
}
.charts-section { padding: 0 0 32px; }
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: var(--kt-ai-space-6);
}
.chart-card {
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-card-border);
border-radius: var(--kt-ai-radius-2xl);
padding: var(--kt-ai-space-6);
}
.chart-title {
font-size: var(--kt-ai-text-lg);
font-weight: var(--kt-ai-weight-semibold);
margin-bottom: var(--kt-ai-space-5);
color: var(--kt-ai-fg);
}
.chart-container { position: relative; height: 300px; }
canvas { width: 100% !important; height: 100% !important; }
.filters-section { padding: 0 0 var(--kt-ai-space-6); }
.filter-bar {
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-card-border);
border-radius: var(--kt-ai-radius-2xl);
padding: var(--kt-ai-space-5) var(--kt-ai-space-6);
display: flex;
gap: var(--kt-ai-space-4);
align-items: center;
flex-wrap: wrap;
}
.filter-label {
font-size: var(--kt-ai-text-sm);
font-weight: var(--kt-ai-weight-medium);
color: var(--kt-ai-fg);
}
.filter-select {
padding: 10px 16px;
border: 1px solid var(--kt-ai-border);
border-radius: var(--kt-ai-radius-lg);
font-size: var(--kt-ai-text-sm);
background: var(--kt-ai-card-bg);
color: var(--kt-ai-fg);
cursor: pointer;
min-width: 200px;
}
.filter-select:focus {
outline: 2px solid var(--kt-ai-primary);
border-color: var(--kt-ai-primary);
}
.chips { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; }
.chip {
padding: 8px 16px;
border-radius: var(--kt-ai-radius-full);
font-size: var(--kt-ai-text-xs);
background: var(--kt-ai-bg-soft);
border: 1px solid var(--kt-ai-border);
cursor: pointer;
transition: all 0.2s;
color: var(--kt-ai-fg);
}
.chip:hover { background: var(--kt-ai-border); }
.chip.active {
background: var(--kt-ai-primary);
color: white;
border-color: var(--kt-ai-primary);
}
.table-section { padding: 0 0 var(--kt-ai-space-10); }
.table-card {
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-card-border);
border-radius: var(--kt-ai-radius-2xl);
overflow: hidden;
}
.table-header {
padding: var(--kt-ai-space-5) var(--kt-ai-space-6);
border-bottom: 1px solid var(--kt-ai-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-size: var(--kt-ai-text-lg);
font-weight: var(--kt-ai-weight-semibold);
color: var(--kt-ai-fg);
}
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td {
padding: 14px 20px;
text-align: left;
border-bottom: 1px solid var(--kt-ai-border);
}
th {
background: var(--kt-ai-bg-soft);
font-weight: var(--kt-ai-weight-semibold);
font-size: var(--kt-ai-text-xs);
color: var(--kt-ai-fg-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
tr { transition: background 0.2s; cursor: pointer; }
tr:hover { background: var(--kt-ai-bg-soft); }
tr.selected { background: rgba(0, 150, 215, 0.08); }
.num { color: var(--kt-ai-fg-faint); font-size: var(--kt-ai-text-xs); }
.amount { font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); }
.region-tag {
display: inline-block;
padding: 4px 10px;
border-radius: var(--kt-ai-radius-lg);
font-size: var(--kt-ai-text-xs);
background: var(--kt-ai-bg-soft);
color: var(--kt-ai-fg);
}
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.on { display: flex; }
.modal {
background: var(--kt-ai-card-bg);
border-radius: var(--kt-ai-radius-3xl);
padding: var(--kt-ai-space-8);
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
border: 1px solid var(--kt-ai-card-border);
}
.modal-title {
font-size: var(--kt-ai-text-2xl);
font-weight: var(--kt-ai-weight-semibold);
margin-bottom: var(--kt-ai-space-4);
color: var(--kt-ai-fg);
}
.modal-row {
display: flex;
justify-content: space-between;
padding: var(--kt-ai-space-3) 0;
border-bottom: 1px solid var(--kt-ai-border);
}
.modal-row:last-child { border-bottom: none; }
.modal-label { color: var(--kt-ai-fg-muted); }
.modal-value { font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); }
.close-btn {
margin-top: var(--kt-ai-space-5);
padding: 12px 24px;
background: var(--kt-ai-primary);
color: white;
border: none;
border-radius: var(--kt-ai-radius-lg);
font-size: var(--kt-ai-text-sm);
cursor: pointer;
width: 100%;
}
.close-btn:hover { opacity: 0.9; }
@media (max-width: 768px) {
.dashboard-header h1 { font-size: var(--kt-ai-text-lg); }
.summary-grid { grid-template-columns: 1fr; }
.charts-grid { grid-template-columns: 1fr; }
.filter-bar { flex-direction: column; align-items: stretch; }
.filter-select { width: 100%; }
th, td { padding: 10px 12px; font-size: var(--kt-ai-text-xs); }
}
</style>
</head>
<body>
<header class="dashboard-header">
<div class="kt-ai-wrap">
<div class="dashboard-header-in">
<img src="./kt-logo.svg" alt="Казахтелеком" height="48">
<div>
<h1>Топ услуг B2C по регионам</h1>
<div class="period">Июль 2026 года</div>
</div>
</div>
</div>
</header>
<main class="kt-ai-wrap">
<section class="summary-section">
<div class="summary-grid">
<div class="summary-card">
<div class="card-label">Всего покупок</div>
<div class="card-value" id="totalPurchases">0</div>
<div class="card-sub">за июль 2026</div>
</div>
<div class="summary-card">
<div class="card-label">Всего регионов</div>
<div class="card-value" id="totalRegions">0</div>
<div class="card-sub">в выборке</div>
</div>
<div class="summary-card">
<div class="card-label">Услуга-лидер</div>
<div class="card-value" id="topService" style="font-size: var(--kt-ai-text-xl);">-</div>
<div class="card-sub">по количеству</div>
</div>
<div class="summary-card">
<div class="card-label">Регион-лидер</div>
<div class="card-value" id="topRegion">-</div>
<div class="card-sub">по покупкам</div>
</div>
</div>
</section>
<section class="charts-section">
<div class="charts-grid">
<div class="chart-card">
<div class="chart-title">Топ-10 регионов по покупкам</div>
<div class="chart-container">
<canvas id="regionChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Топ-10 услуг по количеству</div>
<div class="chart-container">
<canvas id="serviceChart"></canvas>
</div>
</div>
</div>
</section>
<section class="filters-section">
<div class="filter-bar">
<div class="filter-label">Фильтр по региону:</div>
<select class="filter-select" id="regionFilter">
<option value="all">Все регионы</option>
</select>
<div class="chips" id="serviceChips"></div>
</div>
</section>
<section class="table-section">
<div class="table-card">
<div class="table-header">
<div class="table-title">Все услуги (50)</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 60px;">#</th>
<th>Регион</th>
<th>Услуга</th>
<th>Покупок</th>
<th>Сумма (₸)</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
</section>
</main>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-title" id="modalTitle">Детали</div>
<div id="modalContent"></div>
<button class="close-btn" onclick="closeModal()">Закрыть</button>
</div>
</div>
<script>
// === ДАННЫЕ ===
const rawData = [
{ num: 1, region: "Астана", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 37372, amount: 238463786.09 },
{ num: 2, region: "г.Алматы", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 39671, amount: 237005698.16 },
{ num: 3, region: "г.Алматы", service: "Абонплата ШПД по FTTx для B2C", purchases: 35588, amount: 152893561.28 },
{ num: 4, region: "Астана", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 35637, amount: 136187938.99 },
{ num: 5, region: "Астана", service: "B2C TV Plus Абонплата Full ГТС", purchases: 47661, amount: 132060372.93 },
{ num: 6, region: "Астана", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 61700, amount: 123403164.43 },
{ num: 7, region: "г.Алматы", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 57378, amount: 110497209.55 },
{ num: 8, region: "г.Алматы", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 29997, amount: 106749675.59 },
{ num: 9, region: "Астана", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 52504, amount: 99235780.41 },
{ num: 10, region: "Астана", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 36946, amount: 96430127.27 },
{ num: 11, region: "Астана", service: "Абонплата ШПД по FTTx для B2C", purchases: 19565, amount: 88834659.73 },
{ num: 12, region: "Астана", service: "2024 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 22167, amount: 83549325.34 },
{ num: 13, region: "г.Алматы", service: "B2C TV Plus Абонплата Full ГТС", purchases: 31221, amount: 83511013.05 },
{ num: 14, region: "г.Алматы", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 40838, amount: 82748215.46 },
{ num: 15, region: "г.Семей", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 41486, amount: 76400272.02 },
{ num: 16, region: "Астана", service: "TV Plus Абонплата за Премиальный пакет ГТС", purchases: 29210, amount: 71278165.09 },
{ num: 17, region: "г.Алматы", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 24001, amount: 62521274.30 },
{ num: 18, region: "Астана", service: "2021 B2C Абонплата за мобильный номер Kcell GSM откр.", purchases: 27864, amount: 59635624.39 },
{ num: 19, region: "г.Алматы", service: "2024 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 15678, amount: 58970815.48 },
{ num: 20, region: "г.Актобе", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 30641, amount: 55819936.98 },
{ num: 21, region: "г.Алматы", service: "B2C Абонплата за основной телефон ГТС", purchases: 52732, amount: 49636818.50 },
{ num: 22, region: "г.Актобе", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 25178, amount: 48000483.43 },
{ num: 23, region: "г.Алматы", service: "TV Plus Абонплата за Премиальный пакет ГТС", purchases: 19055, amount: 45298192.89 },
{ num: 24, region: "г.Павлодар", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 23334, amount: 44857518.84 },
{ num: 25, region: "г.Алматы", service: "B2C TV Plus Аренда STB оператора новое", purchases: 96183, amount: 43252519.71 },
{ num: 26, region: "г.Караганда", service: "Абонплата ШПД по FTTx для B2C", purchases: 8916, amount: 41624727.66 },
{ num: 27, region: "Астана", service: "2026 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 13685, amount: 41618770.34 },
{ num: 28, region: "г.Алматы", service: "2018 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 11568, amount: 40199964.35 },
{ num: 29, region: "г.Тараз", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 21880, amount: 39564813.34 },
{ num: 30, region: "г.Алматы", service: "Абонплата ШПД по ADSL для B2C", purchases: 10710, amount: 36238454.72 },
{ num: 31, region: "г.Алматы", service: "2020 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 12148, amount: 36010481.68 },
{ num: 32, region: "г.Усть-Каменогорск", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 18362, amount: 35883527.39 },
{ num: 33, region: "г. Шымкент", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 19180, amount: 35338634.51 },
{ num: 34, region: "г. Шымкент", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 18945, amount: 35138664.91 },
{ num: 35, region: "Астана", service: "B2C TV Plus Аренда STB оператора новое", purchases: 76655, amount: 34746783.92 },
{ num: 36, region: "Астана", service: "2018 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 8705, amount: 32905725.03 },
{ num: 37, region: "г.Актобе", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 6742, amount: 32313021.91 },
{ num: 38, region: "г.Усть-Каменогорск", service: "Абонплата ШПД по FTTx для B2C", purchases: 7269, amount: 31834355.15 },
{ num: 39, region: "г.Атырау", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 5495, amount: 31688118.61 },
{ num: 40, region: "г.Усть-Каменогорск", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 5600, amount: 31520413.53 },
{ num: 41, region: "Астана", service: "B2C Аренда оборудования ONT оператора - новое", purchases: 87071, amount: 31220264.09 },
{ num: 42, region: "г.Павлодар", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 6104, amount: 30831455.08 },
{ num: 43, region: "г.Кокшетау", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16818, amount: 30654924.73 },
{ num: 44, region: "г.Актобе", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 8613, amount: 30562705.78 },
{ num: 45, region: "г.Костанай", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16473, amount: 30539180.35 },
{ num: 46, region: "Енбекшиказахский район", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 17347, amount: 30429552.38 },
{ num: 47, region: "г.Актобе", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 11709, amount: 30303557.00 },
{ num: 48, region: "г.Кызылорда", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16539, amount: 29772241.67 },
{ num: 49, region: "г.Алматы", service: "2026 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 10206, amount: 29200316.21 },
{ num: 50, region: "г.Караганда", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 15714, amount: 28837871.98 }
];
// === ИНИЦИАЛИЗАЦИЯ ===
let data = [...rawData];
let selectedRow = null;
function init() {
loadFromStorage();
renderSummary();
renderFilters();
renderTable();
renderCharts();
setupInteractions();
}
// === SUMMARY ===
function renderSummary() {
const totalPurchases = data.reduce((s, r) => s + r.purchases, 0);
const regions = [...new Set(data.map(r => r.region))];
const serviceCount = {};
data.forEach(r => { serviceCount[r.service] = (serviceCount[r.service] || 0) + r.purchases; });
const topService = Object.entries(serviceCount).sort((a, b) => b[1] - a[1])[0][0];
const regionCount = {};
data.forEach(r => { regionCount[r.region] = (regionCount[r.region] || 0) + r.purchases; });
const topRegion = Object.entries(regionCount).sort((a, b) => b[1] - a[1])[0][0];
document.getElementById('totalPurchases').textContent = totalPurchases.toLocaleString('ru-RU');
document.getElementById('totalRegions').textContent = regions.length;
document.getElementById('topService').textContent = topService.length > 30 ? topService.slice(0, 30) + '...' : topService;
document.getElementById('topRegion').textContent = topRegion;
}
// === FILTERS ===
function renderFilters() {
const regions = [...new Set(data.map(r => r.region))].sort();
const select = document.getElementById('regionFilter');
regions.forEach(r => {
const opt = document.createElement('option');
opt.value = r;
opt.textContent = r;
select.appendChild(opt);
});
const services = [...new Set(data.map(r => r.service))].slice(0, 8);
const chips = document.getElementById('serviceChips');
services.forEach(s => {
const chip = document.createElement('div');
chip.className = 'chip';
chip.textContent = s.length > 25 ? s.slice(0, 25) + '...' : s;
chip.onclick = () => {
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
filterByService(s);
};
chips.appendChild(chip);
});
}
function filterByRegion(region) {
data = region === 'all' ? [...rawData] : rawData.filter(r => r.region === region);
renderTable();
renderCharts();
renderSummary();
saveToStorage();
}
function filterByService(service) {
data = rawData.filter(r => r.service === service);
renderTable();
renderCharts();
renderSummary();
saveToStorage();
}
function resetFilters() {
data = [...rawData];
document.getElementById('regionFilter').value = 'all';
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
renderTable();
renderCharts();
renderSummary();
saveToStorage();
}
// === TABLE ===
function renderTable() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = data.map(r => `
<tr data-num="${r.num}" onclick="showDetails(${r.num})">
<td class="num">${r.num}</td>
<td><span class="region-tag">${r.region}</span></td>
<td>${r.service}</td>
<td>${r.purchases.toLocaleString('ru-RU')}</td>
<td class="amount">${r.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
</tr>
`).join('');
}
function showDetails(num) {
const r = rawData.find(x => x.num === num);
if (!r) return;
document.getElementById('modalTitle').textContent = r.service;
document.getElementById('modalContent').innerHTML = `
<div class="modal-row"><span class="modal-label">Регион</span><span class="modal-value">${r.region}</span></div>
<div class="modal-row"><span class="modal-label">Количество покупок</span><span class="modal-value">${r.purchases.toLocaleString('ru-RU')}</span></div>
<div class="modal-row"><span class="modal-label">Сумма</span><span class="modal-value">${r.amount.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₸</span></div>
<div class="modal-row"><span class="modal-label">Средний чек</span><span class="modal-value">${(r.amount / r.purchases).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₸</span></div>
`;
document.getElementById('modalOverlay').classList.add('on');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('on');
}
// === CHARTS ===
function renderCharts() {
const regionData = {};
data.forEach(r => { regionData[r.region] = (regionData[r.region] || 0) + r.purchases; });
const topRegions = Object.entries(regionData).sort((a, b) => b[1] - a[1]).slice(0, 10);
const serviceData = {};
data.forEach(r => { serviceData[r.service] = (serviceData[r.service] || 0) + r.purchases; });
const topServices = Object.entries(serviceData).sort((a, b) => b[1] - a[1]).slice(0, 10);
drawBarChart('regionChart', topRegions.map(x => x[0]), topRegions.map(x => x[1]), '#0096D7');
drawBarChart('serviceChart', topServices.map(x => x[0].length > 20 ? x[0].slice(0, 20) + '...' : x[0]), topServices.map(x => x[1]), '#00b89c');
}
function drawBarChart(canvasId, labels, values, color) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * 2;
canvas.height = rect.height * 2;
ctx.scale(2, 2);
const width = rect.width;
const height = rect.height;
const padding = 60;
const chartWidth = width - padding * 2;
const chartHeight = height - padding * 2;
ctx.clearRect(0, 0, width, height);
ctx.font = '11px Inter, sans-serif';
ctx.fillStyle = '#666';
const maxVal = Math.max(...values);
const barWidth = chartWidth / labels.length - 10;
// Оси
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.strokeStyle = '#e0e0e5';
ctx.stroke();
// Столбцы
values.forEach((v, i) => {
const x = padding + i * (chartWidth / labels.length) + 5;
const barHeight = (v / maxVal) * chartHeight;
const y = height - padding - barHeight;
ctx.fillStyle = color;
ctx.fillRect(x, y, barWidth, barHeight);
// Подписи
ctx.save();
ctx.translate(x + barWidth / 2, height - padding + 12);
ctx.rotate(-0.4);
ctx.textAlign = 'right';
ctx.fillText(labels[i], 0, 0);
ctx.restore();
// Значения
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText(v.toLocaleString('ru-RU'), x + barWidth / 2, y - 8);
});
}
// === INTERACTIONS ===
function setupInteractions() {
document.getElementById('regionFilter').addEventListener('change', (e) => {
filterByRegion(e.target.value);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
document.getElementById('modalOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
}
// === LOCALSTORAGE ===
function saveToStorage() {
localStorage.setItem('b2c_dashboard_filter', JSON.stringify({ data }));
}
function loadFromStorage() {
const saved = localStorage.getItem('b2c_dashboard_filter');
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed.data) data = parsed.data;
} catch (_) {}
}
}
// Запуск
init();
</script>
</body>
</html>