684 lines
26 KiB
HTML
684 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="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #f5f7fa;
|
||
--card-bg: #ffffff;
|
||
--text: #1a1a2e;
|
||
--text-secondary: #6b7280;
|
||
--primary: #0066cc;
|
||
--primary-light: #e6f0ff;
|
||
--accent: #00c4b4;
|
||
--border: #e5e7eb;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
* { margin: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.5;
|
||
padding: 24px;
|
||
}
|
||
|
||
.container { max-width: 1400px; margin: 0 auto; }
|
||
|
||
header {
|
||
background: linear-gradient(135deg, var(--primary) 0%, #004499 100%);
|
||
color: white;
|
||
padding: 32px 40px;
|
||
border-radius: 16px;
|
||
margin-bottom: 32px;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
header h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
|
||
header p { opacity: 0.9; font-size: 15px; }
|
||
|
||
.summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.card {
|
||
background: var(--card-bg);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.card-label { font-size: 13px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
||
.card-value { font-size: 32px; font-weight: 700; color: var(--primary); }
|
||
.card-value.accent { color: var(--accent); }
|
||
.card-sub { font-size: 13px; color: var(--text-secondary); margin-top: 8px; }
|
||
|
||
.charts-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.charts-row { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.chart-card {
|
||
background: var(--card-bg);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.chart-title { font-size: 16px; font-weight: 600; margin-bottom: 20px; color: var(--text); }
|
||
|
||
.chart-container { position: relative; height: 280px; }
|
||
|
||
.bar-chart {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
height: 100%;
|
||
padding: 0 10px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.bar-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
min-width: 50px;
|
||
flex: 1;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
.bar {
|
||
width: 100%;
|
||
max-width: 40px;
|
||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-light) 100%);
|
||
border-radius: 4px 4px 0 0;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
position: relative;
|
||
}
|
||
|
||
.bar:hover {
|
||
background: linear-gradient(180deg, var(--accent) 0%, var(--primary-light) 100%);
|
||
transform: scaleY(1.02);
|
||
}
|
||
|
||
.bar-label {
|
||
font-size: 10px;
|
||
color: var(--text-secondary);
|
||
margin-top: 8px;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 60px;
|
||
}
|
||
|
||
.bar-value {
|
||
font-size: 9px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
.table-card {
|
||
background: var(--card-bg);
|
||
border-radius: 12px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid var(--border);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
|
||
.table-title { font-size: 18px; font-weight: 600; }
|
||
|
||
.table-controls {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-input {
|
||
padding: 8px 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
width: 220px;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.search-input:focus { border-color: var(--primary); }
|
||
|
||
.filter-select {
|
||
padding: 8px 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
background: white;
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.data-table th {
|
||
background: var(--bg);
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
|
||
.data-table th:hover { background: #e8ecf1; }
|
||
.data-table th.sorted { color: var(--primary); }
|
||
.data-table th.sorted::after { content: ' ▼'; font-size: 10px; }
|
||
|
||
.data-table td {
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.data-table tbody tr:hover { background: var(--primary-light); }
|
||
|
||
.rank-cell { font-weight: 600; color: var(--primary); width: 50px; text-align: center; }
|
||
.region-cell { font-weight: 500; }
|
||
.service-cell { max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.number-cell { text-align: right; font-family: 'SF Mono', 'Consolas', monospace; }
|
||
.sum-cell { text-align: right; font-weight: 600; color: var(--text); font-family: 'SF Mono', 'Consolas', monospace; }
|
||
|
||
.pagination {
|
||
padding: 16px 24px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.pagination-info { font-size: 13px; color: var(--text-secondary); }
|
||
|
||
.pagination-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.page-btn {
|
||
padding: 6px 12px;
|
||
border: 1px solid var(--border);
|
||
background: white;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.page-btn:hover:not(:disabled) { background: var(--primary-light); border-color: var(--primary); }
|
||
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
.page-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||
|
||
.chart-tooltip {
|
||
position: absolute;
|
||
background: rgba(0,0,0,0.85);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
z-index: 100;
|
||
white-space: nowrap;
|
||
display: none;
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
padding: 32px;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
footer a { color: var(--primary); text-decoration: none; }
|
||
footer a:hover { text-decoration: underline; }
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
background: var(--primary-light);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.legend {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.legend-color {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.legend-color.primary { background: var(--primary); }
|
||
.legend-color.accent { background: var(--accent); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>Топ услуг B2C по регионам</h1>
|
||
<p>Дашборд по продажам услуг Казахтелеком • Июль 2026 • Сегмент B2C</p>
|
||
</header>
|
||
|
||
<div class="summary" id="summary"></div>
|
||
|
||
<div class="charts-row">
|
||
<div class="chart-card">
|
||
<div class="chart-title">Топ-10 регионов по количеству покупок</div>
|
||
<div class="chart-container">
|
||
<div class="bar-chart" id="chartRegions"></div>
|
||
<div class="chart-tooltip" id="tooltipRegions"></div>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title">Топ-10 услуг по количеству покупок</div>
|
||
<div class="chart-container">
|
||
<div class="bar-chart" id="chartServices"></div>
|
||
<div class="chart-tooltip" id="tooltipServices"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-card">
|
||
<div class="table-header">
|
||
<div class="table-title">Детальные данные</div>
|
||
<div class="table-controls">
|
||
<input type="text" class="search-input" id="searchInput" placeholder="Поиск по услуге или региону...">
|
||
<select class="filter-select" id="regionFilter">
|
||
<option value="">Все регионы</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th data-sort="rank">#</th>
|
||
<th data-sort="region">Регион</th>
|
||
<th data-sort="service">Услуга</th>
|
||
<th data-sort="qty" style="text-align:right">Покупок</th>
|
||
<th data-sort="sum" style="text-align:right">Сумма (₸)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
<div class="pagination">
|
||
<div class="pagination-info" id="paginationInfo"></div>
|
||
<div class="pagination-controls" id="paginationControls"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer>
|
||
<p>Источник данных: ClickHouse АО «Казахтелеком» | Таблица: sandbox.spec2_main_2spec_detail2_v2_ddt</p>
|
||
<p>Период: Июль 2026 | Сегмент: B2C | Валюта: Тенге (без НДС)</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
const DATA = [
|
||
{"rank":1,"region":"Астана","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":37372,"sum":238463786.09},
|
||
{"rank":2,"region":"г.Алматы","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":39671,"sum":237005698.16},
|
||
{"rank":3,"region":"г.Алматы","service":"Абонплата ШПД по FTTx для B2C","qty":35588,"sum":152893561.28},
|
||
{"rank":4,"region":"Астана","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","qty":35637,"sum":136187938.99},
|
||
{"rank":5,"region":"Астана","service":"B2C TV Plus Абонплата Full ГТС","qty":47661,"sum":132060372.93},
|
||
{"rank":6,"region":"Астана","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","qty":61700,"sum":123403164.43},
|
||
{"rank":7,"region":"г.Алматы","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":57378,"sum":110497209.55},
|
||
{"rank":8,"region":"г.Алматы","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","qty":29997,"sum":106749675.59},
|
||
{"rank":9,"region":"Астана","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":52504,"sum":99235780.41},
|
||
{"rank":10,"region":"Астана","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","qty":36946,"sum":96430127.27},
|
||
{"rank":11,"region":"Астана","service":"Абонплата ШПД по FTTx для B2C","qty":19565,"sum":88834659.73},
|
||
{"rank":12,"region":"Астана","service":"2024 B2C Абонплата за Домашний интернет FTTx ГТС","qty":22167,"sum":83549325.34},
|
||
{"rank":13,"region":"г.Алматы","service":"B2C TV Plus Абонплата Full ГТС","qty":31221,"sum":83511013.05},
|
||
{"rank":14,"region":"г.Алматы","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","qty":40838,"sum":82748215.46},
|
||
{"rank":15,"region":"г.Семей","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":41486,"sum":76400272.02},
|
||
{"rank":16,"region":"Астана","service":"TV Plus Абонплата за Премиальный пакет ГТС","qty":29210,"sum":71278165.09},
|
||
{"rank":17,"region":"г.Алматы","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","qty":24001,"sum":62521274.30},
|
||
{"rank":18,"region":"Астана","service":"2021 B2C Абонплата за мобильный номер Kcell GSM откр.","qty":27864,"sum":59635624.39},
|
||
{"rank":19,"region":"г.Алматы","service":"2024 B2C Абонплата за Домашний интернет FTTx ГТС","qty":15678,"sum":58970815.48},
|
||
{"rank":20,"region":"г.Актобе","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":30641,"sum":55819936.98},
|
||
{"rank":21,"region":"г.Алматы","service":"B2C Абонплата за основной телефон ГТС","qty":52732,"sum":49636818.50},
|
||
{"rank":22,"region":"г.Актобе","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","qty":25178,"sum":48000483.43},
|
||
{"rank":23,"region":"г.Алматы","service":"TV Plus Абонплата за Премиальный пакет ГТС","qty":19055,"sum":45298192.89},
|
||
{"rank":24,"region":"г.Павлодар","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":23334,"sum":44857518.84},
|
||
{"rank":25,"region":"г.Алматы","service":"B2C TV Plus Аренда STB оператора новое","qty":96183,"sum":43252519.71},
|
||
{"rank":26,"region":"г.Караганда","service":"Абонплата ШПД по FTTx для B2C","qty":8916,"sum":41624727.66},
|
||
{"rank":27,"region":"Астана","service":"2026 B2C Абонплата за Домашний интернет FTTx ГТС","qty":13685,"sum":41618770.34},
|
||
{"rank":28,"region":"г.Алматы","service":"2018 B2C Абонплата за Домашний интернет FTTx ГТС","qty":11568,"sum":40199964.35},
|
||
{"rank":29,"region":"г.Тараз","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":21880,"sum":39564813.34},
|
||
{"rank":30,"region":"г.Алматы","service":"Абонплата ШПД по ADSL для B2C","qty":10710,"sum":36238454.72},
|
||
{"rank":31,"region":"г.Алматы","service":"2020 B2C Абонплата за Домашний интернет FTTx ГТС","qty":12148,"sum":36010481.68},
|
||
{"rank":32,"region":"г.Усть-Каменогорск","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":18362,"sum":35883527.39},
|
||
{"rank":33,"region":"г. Шымкент","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":19180,"sum":35338634.51},
|
||
{"rank":34,"region":"г. Шымкент","service":"2021 B2C Абонплата за мобильный номер Kcell GSM закр.","qty":18945,"sum":35138664.91},
|
||
{"rank":35,"region":"Астана","service":"B2C TV Plus Аренда STB оператора новое","qty":76655,"sum":34746783.92},
|
||
{"rank":36,"region":"Астана","service":"2018 B2C Абонплата за Домашний интернет FTTx ГТС","qty":8705,"sum":32905725.03},
|
||
{"rank":37,"region":"г.Актобе","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":6742,"sum":32313021.91},
|
||
{"rank":38,"region":"г.Усть-Каменогорск","service":"Абонплата ШПД по FTTx для B2C","qty":7269,"sum":31834355.15},
|
||
{"rank":39,"region":"г.Атырау","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":5495,"sum":31688118.61},
|
||
{"rank":40,"region":"г.Усть-Каменогорск","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":5600,"sum":31520413.53},
|
||
{"rank":41,"region":"Астана","service":"B2C Аренда оборудования ONT оператора - новое","qty":87071,"sum":31220264.09},
|
||
{"rank":42,"region":"г.Павлодар","service":"B2C Абонплата ШПД по FTTx ГТС (New)","qty":6104,"sum":30831455.08},
|
||
{"rank":43,"region":"г.Кокшетау","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":16818,"sum":30654924.73},
|
||
{"rank":44,"region":"г.Актобе","service":"2025 B2C Абонплата за Домашний интернет FTTx ГТС","qty":8613,"sum":30562705.78},
|
||
{"rank":45,"region":"г.Костанай","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":16473,"sum":30539180.35},
|
||
{"rank":46,"region":"Енбекшиказахский район","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":17347,"sum":30429552.38},
|
||
{"rank":47,"region":"г.Актобе","service":"2022 B2C Абонплата за Домашний интернет FTTx ГТС","qty":11709,"sum":30303557.00},
|
||
{"rank":48,"region":"г.Кызылорда","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":16539,"sum":29772241.67},
|
||
{"rank":49,"region":"г.Алматы","service":"2026 B2C Абонплата за Домашний интернет FTTx ГТС","qty":10206,"sum":29200316.21},
|
||
{"rank":50,"region":"г.Караганда","service":"2022 B2C Абонплата за мобильный номер MT-C GSM закр.","qty":15714,"sum":28837871.98}
|
||
];
|
||
|
||
let filteredData = [...DATA];
|
||
let currentPage = 1;
|
||
const itemsPerPage = 15;
|
||
let sortColumn = 'rank';
|
||
let sortDirection = 'asc';
|
||
|
||
function formatNumber(n) {
|
||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||
}
|
||
|
||
function formatSum(s) {
|
||
return formatNumber(s.toFixed(2).replace('.', ',')) + ' ₸';
|
||
}
|
||
|
||
function calculateSummary() {
|
||
const totalPurchases = DATA.reduce((sum, r) => sum + r.qty, 0);
|
||
const totalSum = DATA.reduce((sum, r) => sum + r.sum, 0);
|
||
const regions = [...new Set(DATA.map(r => r.region))];
|
||
|
||
const regionTotals = {};
|
||
const serviceTotals = {};
|
||
DATA.forEach(r => {
|
||
regionTotals[r.region] = (regionTotals[r.region] || 0) + r.qty;
|
||
serviceTotals[r.service] = (serviceTotals[r.service] || 0) + r.qty;
|
||
});
|
||
|
||
const topRegion = Object.entries(regionTotals).sort((a,b) => b[1] - a[1])[0];
|
||
const topService = Object.entries(serviceTotals).sort((a,b) => b[1] - a[1])[0];
|
||
|
||
return { totalPurchases, totalSum, regions: regions.length, topRegion: topRegion[0], topRegionQty: topRegion[1], topService: topService[0].substring(0, 50), topServiceQty: topRegion[1] };
|
||
}
|
||
|
||
function renderSummary() {
|
||
const s = calculateSummary();
|
||
document.getElementById('summary').innerHTML = `
|
||
<div class="card">
|
||
<div class="card-label">Всего покупок</div>
|
||
<div class="card-value">${formatNumber(s.totalPurchases)}</div>
|
||
<div class="card-sub">за июль 2026</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Регионов в отчёте</div>
|
||
<div class="card-value accent">${s.regions}</div>
|
||
<div class="card-sub">городов и районов</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Лидер по региону</div>
|
||
<div class="card-value" style="font-size:20px">${s.topRegion}</div>
|
||
<div class="card-sub">${formatNumber(s.topRegionQty)} покупок</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Общая сумма</div>
|
||
<div class="card-value accent" style="font-size:24px">${formatSum(s.totalSum)}</div>
|
||
<div class="card-sub">без НДС</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderChartRegions() {
|
||
const regionTotals = {};
|
||
DATA.forEach(r => { regionTotals[r.region] = (regionTotals[r.region] || 0) + r.qty; });
|
||
const top10 = Object.entries(regionTotals).sort((a,b) => b[1] - a[1]).slice(0, 10);
|
||
const maxVal = top10[0][1];
|
||
|
||
const container = document.getElementById('chartRegions');
|
||
const tooltip = document.getElementById('tooltipRegions');
|
||
|
||
container.innerHTML = top10.map(([region, qty]) => {
|
||
const height = (qty / maxVal) * 220;
|
||
return `
|
||
<div class="bar-group">
|
||
<div class="bar-value">${formatNumber(qty)}</div>
|
||
<div class="bar" style="height:${height}px"
|
||
data-region="${region}" data-qty="${qty}"></div>
|
||
<div class="bar-label">${region.replace('г.','').replace('р-н','')}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.querySelectorAll('.bar').forEach(bar => {
|
||
bar.addEventListener('mouseenter', (e) => {
|
||
const region = e.target.dataset.region;
|
||
const qty = e.target.dataset.qty;
|
||
tooltip.style.display = 'block';
|
||
tooltip.innerHTML = `<strong>${region}</strong><br>${formatNumber(qty)} покупок`;
|
||
});
|
||
bar.addEventListener('mousemove', (e) => {
|
||
tooltip.style.left = e.pageX + 10 + 'px';
|
||
tooltip.style.top = e.pageY - 10 + 'px';
|
||
});
|
||
bar.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; });
|
||
});
|
||
}
|
||
|
||
function renderChartServices() {
|
||
const serviceTotals = {};
|
||
DATA.forEach(r => { serviceTotals[r.service] = (serviceTotals[r.service] || 0) + r.qty; });
|
||
const top10 = Object.entries(serviceTotals).sort((a,b) => b[1] - a[1]).slice(0, 10);
|
||
const maxVal = top10[0][1];
|
||
|
||
const container = document.getElementById('chartServices');
|
||
const tooltip = document.getElementById('tooltipServices');
|
||
|
||
container.innerHTML = top10.map(([service, qty]) => {
|
||
const height = (qty / maxVal) * 220;
|
||
const shortName = service.length > 25 ? service.substring(0, 25) + '…' : service;
|
||
return `
|
||
<div class="bar-group">
|
||
<div class="bar-value">${formatNumber(qty)}</div>
|
||
<div class="bar" style="height:${height}px"
|
||
data-service="${service}" data-qty="${qty}"></div>
|
||
<div class="bar-label">${shortName}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.querySelectorAll('.bar').forEach(bar => {
|
||
bar.addEventListener('mouseenter', (e) => {
|
||
const service = e.target.dataset.service;
|
||
const qty = e.target.dataset.qty;
|
||
tooltip.style.display = 'block';
|
||
tooltip.innerHTML = `<strong>${service}</strong><br>${formatNumber(qty)} покупок`;
|
||
});
|
||
bar.addEventListener('mousemove', (e) => {
|
||
tooltip.style.left = e.pageX + 10 + 'px';
|
||
tooltip.style.top = e.pageY - 10 + 'px';
|
||
});
|
||
bar.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; });
|
||
});
|
||
}
|
||
|
||
function populateRegionFilter() {
|
||
const regions = [...new Set(DATA.map(r => r.region))].sort();
|
||
const select = document.getElementById('regionFilter');
|
||
select.innerHTML = '<option value="">Все регионы</option>' +
|
||
regions.map(r => `<option value="${r}">${r}</option>`).join('');
|
||
}
|
||
|
||
function applyFilters() {
|
||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||
const regionFilter = document.getElementById('regionFilter').value;
|
||
|
||
filteredData = DATA.filter(r => {
|
||
const matchesSearch = r.service.toLowerCase().includes(searchTerm) ||
|
||
r.region.toLowerCase().includes(searchTerm);
|
||
const matchesRegion = !regionFilter || r.region === regionFilter;
|
||
return matchesSearch && matchesRegion;
|
||
});
|
||
|
||
applySort();
|
||
currentPage = 1;
|
||
renderTable();
|
||
}
|
||
|
||
function applySort() {
|
||
filteredData.sort((a, b) => {
|
||
let valA = a[sortColumn];
|
||
let valB = b[sortColumn];
|
||
|
||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||
|
||
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
|
||
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
function renderTable() {
|
||
const start = (currentPage - 1) * itemsPerPage;
|
||
const end = start + itemsPerPage;
|
||
const pageData = filteredData.slice(start, end);
|
||
|
||
const tbody = document.getElementById('tableBody');
|
||
tbody.innerHTML = pageData.map(r => `
|
||
<tr>
|
||
<td class="rank-cell">${r.rank}</td>
|
||
<td class="region-cell">${r.region}</td>
|
||
<td class="service-cell" title="${r.service}">${r.service}</td>
|
||
<td class="number-cell">${formatNumber(r.qty)}</td>
|
||
<td class="sum-cell">${formatSum(r.sum)}</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||
document.getElementById('paginationInfo').textContent =
|
||
`Показано ${start + 1}–${Math.min(end, filteredData.length)} из ${filteredData.length} записей`;
|
||
|
||
const controls = document.getElementById('paginationControls');
|
||
controls.innerHTML = '';
|
||
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
const btn = document.createElement('button');
|
||
btn.className = `page-btn ${i === currentPage ? 'active' : ''}`;
|
||
btn.textContent = i;
|
||
btn.disabled = i === currentPage;
|
||
btn.addEventListener('click', () => { currentPage = i; renderTable(); });
|
||
controls.appendChild(btn);
|
||
}
|
||
}
|
||
|
||
function initSort() {
|
||
document.querySelectorAll('.data-table th[data-sort]').forEach(th => {
|
||
th.addEventListener('click', () => {
|
||
const column = th.dataset.sort;
|
||
if (sortColumn === column) {
|
||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortColumn = column;
|
||
sortDirection = 'asc';
|
||
}
|
||
document.querySelectorAll('.data-table th').forEach(h => h.classList.remove('sorted'));
|
||
th.classList.add('sorted');
|
||
applySort();
|
||
renderTable();
|
||
});
|
||
});
|
||
}
|
||
|
||
function saveSettings() {
|
||
const settings = { sortColumn, sortDirection, currentPage };
|
||
localStorage.setItem('dashboardSettings', JSON.stringify(settings));
|
||
}
|
||
|
||
function loadSettings() {
|
||
const saved = localStorage.getItem('dashboardSettings');
|
||
if (saved) {
|
||
const settings = JSON.parse(saved);
|
||
sortColumn = settings.sortColumn || 'rank';
|
||
sortDirection = settings.sortDirection || 'asc';
|
||
currentPage = settings.currentPage || 1;
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadSettings();
|
||
renderSummary();
|
||
renderChartRegions();
|
||
renderChartServices();
|
||
populateRegionFilter();
|
||
initSort();
|
||
|
||
document.getElementById('searchInput').addEventListener('input', applyFilters);
|
||
document.getElementById('regionFilter').addEventListener('change', applyFilters);
|
||
|
||
applySort();
|
||
renderTable();
|
||
|
||
setInterval(saveSettings, 5000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|