567 lines
26 KiB
HTML
567 lines
26 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/variables.css">
|
||
<link rel="stylesheet" href="./design-system/typography.css">
|
||
<link rel="stylesheet" href="./design-system/components.css">
|
||
<style>
|
||
:root {
|
||
--kt-blue: #0096D7;
|
||
--kt-dark: #1a1a2e;
|
||
--kt-gray: #f5f5f7;
|
||
--kt-border: #e0e0e5;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: var(--kt-gray);
|
||
color: var(--kt-dark);
|
||
line-height: 1.5;
|
||
}
|
||
.container { max-width: 1400px; margin: 0 auto; padding: 0 24px; }
|
||
|
||
/* Header */
|
||
.header {
|
||
background: white;
|
||
border-bottom: 1px solid var(--kt-border);
|
||
padding: 20px 0;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
.header img { height: 48px; }
|
||
.header h1 { font-size: 24px; font-weight: 600; color: var(--kt-dark); }
|
||
.period { font-size: 14px; color: #666; margin-top: 4px; }
|
||
|
||
/* Summary Cards */
|
||
.summary { padding: 32px 0; }
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
.card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||
border: 1px solid var(--kt-border);
|
||
}
|
||
.card-label { font-size: 14px; color: #666; margin-bottom: 8px; }
|
||
.card-value { font-size: 32px; font-weight: 700; color: var(--kt-blue); }
|
||
.card-sub { font-size: 13px; color: #888; margin-top: 8px; }
|
||
|
||
/* Charts */
|
||
.charts { padding: 0 0 32px; }
|
||
.charts-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||
gap: 24px;
|
||
}
|
||
.chart-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||
border: 1px solid var(--kt-border);
|
||
}
|
||
.chart-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; }
|
||
.chart-container { position: relative; height: 300px; }
|
||
canvas { width: 100% !important; height: 100% !important; }
|
||
|
||
/* Filters */
|
||
.filters { padding: 0 0 24px; }
|
||
.filter-bar {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px 24px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||
border: 1px solid var(--kt-border);
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.filter-label { font-size: 14px; font-weight: 500; }
|
||
.filter-select {
|
||
padding: 10px 16px;
|
||
border: 1px solid var(--kt-border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
background: white;
|
||
cursor: pointer;
|
||
min-width: 200px;
|
||
}
|
||
.filter-select:focus { outline: 2px solid var(--kt-blue); border-color: var(--kt-blue); }
|
||
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.chip {
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
background: var(--kt-gray);
|
||
border: 1px solid var(--kt-border);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.chip:hover { background: #e8e8ed; }
|
||
.chip.active { background: var(--kt-blue); color: white; border-color: var(--kt-blue); }
|
||
|
||
/* Table */
|
||
.table-section { padding: 0 0 40px; }
|
||
.table-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||
border: 1px solid var(--kt-border);
|
||
overflow: hidden;
|
||
}
|
||
.table-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--kt-border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.table-title { font-size: 18px; font-weight: 600; }
|
||
.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-border); }
|
||
th { background: var(--kt-gray); font-weight: 600; font-size: 13px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
tr { transition: background 0.2s; cursor: pointer; }
|
||
tr:hover { background: #f8f9fc; }
|
||
tr.selected { background: rgba(0, 150, 215, 0.08); }
|
||
.num { color: #999; font-size: 13px; }
|
||
.amount { font-weight: 600; color: var(--kt-dark); }
|
||
.region-tag {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
background: var(--kt-gray);
|
||
color: var(--kt-dark);
|
||
}
|
||
|
||
/* Modal */
|
||
.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: white;
|
||
border-radius: 16px;
|
||
padding: 32px;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
.modal-title { font-size: 22px; font-weight: 600; margin-bottom: 16px; }
|
||
.modal-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--kt-border); }
|
||
.modal-row:last-child { border-bottom: none; }
|
||
.modal-label { color: #666; }
|
||
.modal-value { font-weight: 600; }
|
||
.close-btn {
|
||
margin-top: 20px;
|
||
padding: 12px 24px;
|
||
background: var(--kt-blue);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
}
|
||
.close-btn:hover { opacity: 0.9; }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.header h1 { font-size: 18px; }
|
||
.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: 13px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<div class="container">
|
||
<div class="header-content">
|
||
<img src="./kt-logo.svg" alt="Казахтелеком" height="48">
|
||
<div>
|
||
<h1>Топ услуг B2C по регионам</h1>
|
||
<div class="period">Июль 2026 года</div>
|
||
</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 class="card-sub">за июль 2026</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Всего регионов</div>
|
||
<div class="card-value" id="totalRegions">0</div>
|
||
<div class="card-sub">в выборке</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Услуга-лидер</div>
|
||
<div class="card-value" id="topService" style="font-size: 20px;">-</div>
|
||
<div class="card-sub">по количеству</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Регион-лидер</div>
|
||
<div class="card-value" id="topRegion">-</div>
|
||
<div class="card-sub">по покупкам</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="charts">
|
||
<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">
|
||
<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>
|