334 lines
21 KiB
HTML
334 lines
21 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: #2563eb;
|
||
--primary-light: #3b82f6;
|
||
--accent: #10b981;
|
||
--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, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
|
||
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
||
header { background: var(--card-bg); border-bottom: 1px solid var(--border); padding: 24px 0; margin-bottom: 32px; box-shadow: var(--shadow); }
|
||
header h1 { font-size: 28px; font-weight: 700; color: var(--text); margin-bottom: 8px; }
|
||
header p { color: var(--text-secondary); font-size: 14px; }
|
||
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 32px; }
|
||
.metric-card { background: var(--card-bg); border-radius: 12px; padding: 24px; box-shadow: var(--shadow); border: 1px solid var(--border); }
|
||
.metric-card h3 { font-size: 13px; font-weight: 500; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
|
||
.metric-card .value { font-size: 32px; font-weight: 700; color: var(--text); line-height: 1.2; }
|
||
.metric-card .sub { font-size: 13px; color: var(--text-secondary); margin-top: 8px; }
|
||
.metric-card.highlight { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); }
|
||
.metric-card.highlight h3, .metric-card.highlight .value, .metric-card.highlight .sub { color: #fff; }
|
||
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 24px; margin-bottom: 32px; }
|
||
.chart-card { background: var(--card-bg); border-radius: 12px; padding: 24px; box-shadow: var(--shadow); border: 1px solid var(--border); }
|
||
.chart-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 20px; color: var(--text); }
|
||
.bar-chart { display: flex; flex-direction: column; gap: 12px; }
|
||
.bar-item { display: flex; align-items: center; gap: 12px; }
|
||
.bar-label { width: 140px; font-size: 12px; color: var(--text-secondary); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.bar-wrapper { flex: 1; background: var(--bg); border-radius: 6px; height: 28px; overflow: hidden; }
|
||
.bar { height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%); border-radius: 6px; display: flex; align-items: center; padding-left: 12px; font-size: 11px; font-weight: 600; color: #fff; min-width: fit-content; }
|
||
.bar-value { width: 100px; font-size: 12px; color: var(--text-secondary); text-align: right; }
|
||
.table-card { background: var(--card-bg); border-radius: 12px; padding: 24px; box-shadow: var(--shadow); border: 1px solid var(--border); }
|
||
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 16px; }
|
||
.table-header h3 { font-size: 16px; font-weight: 600; color: var(--text); }
|
||
.filters { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||
.filters label { font-size: 13px; color: var(--text-secondary); }
|
||
.filters select { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; background: #fff; cursor: pointer; }
|
||
.filters select:focus { outline: 2px solid var(--primary); border-color: transparent; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th { text-align: left; padding: 12px 16px; font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid var(--border); cursor: pointer; user-select: none; }
|
||
th:hover { color: var(--primary); }
|
||
th.sorted { color: var(--primary); }
|
||
td { padding: 14px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
|
||
tr:hover { background: var(--bg); }
|
||
.rank { font-weight: 600; color: var(--text-secondary); width: 40px; }
|
||
.region-badge { display: inline-block; padding: 4px 10px; background: var(--bg); border-radius: 20px; font-size: 12px; font-weight: 500; color: var(--text); }
|
||
.amount { font-weight: 600; color: var(--text); }
|
||
footer { margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; text-align: center; }
|
||
@media (max-width: 768px) {
|
||
.container { padding: 16px; }
|
||
.metrics { grid-template-columns: 1fr; }
|
||
.charts { grid-template-columns: 1fr; }
|
||
.bar-label { width: 100px; }
|
||
table { font-size: 12px; }
|
||
th, td { padding: 10px 8px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="container">
|
||
<h1>Дашборд: Топ услуг B2C по регионам</h1>
|
||
<p>Данные за июль 2026 года | Сегмент B2C | Источник: ClickHouse АО «Казахтелеком»</p>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
<section class="metrics" id="metrics"></section>
|
||
|
||
<section class="charts">
|
||
<div class="chart-card">
|
||
<h3>Топ-10 регионов по количеству покупок</h3>
|
||
<div class="bar-chart" id="chartRegions"></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<h3>Топ-10 услуг по количеству покупок</h3>
|
||
<div class="bar-chart" id="chartServices"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="table-card">
|
||
<div class="table-header">
|
||
<h3>Все данные (топ-50)</h3>
|
||
<div class="filters">
|
||
<label for="regionFilter">Регион:</label>
|
||
<select id="regionFilter">
|
||
<option value="all">Все регионы</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th data-sort="rank">#</th>
|
||
<th data-sort="region">Регион</th>
|
||
<th data-sort="service">Услуга</th>
|
||
<th data-sort="purchases">Кол-во покупок</th>
|
||
<th data-sort="amount">Сумма (₸)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<footer>
|
||
<p>Таблица: sandbox.spec2_main_2spec_detail2_v2_ddt | Период: 202607 | Валюта: тенге (без НДС) | Дата формирования: 01.09.2026</p>
|
||
</footer>
|
||
</main>
|
||
|
||
<script>
|
||
const DATA = [
|
||
{"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}
|
||
];
|
||
|
||
function formatNumber(n) { return n.toLocaleString('ru-RU'); }
|
||
function formatAmount(n) { return n.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ₸'; }
|
||
function formatCompact(n) { return n >= 1e6 ? (n/1e6).toFixed(1) + ' млн' : n >= 1e3 ? (n/1e3).toFixed(0) + ' тыс' : n.toString(); }
|
||
|
||
function calculateMetrics() {
|
||
const totalPurchases = DATA.reduce((s, r) => s + r.purchases, 0);
|
||
const regions = [...new Set(DATA.map(r => r.region))];
|
||
const byService = {};
|
||
const byRegion = {};
|
||
DATA.forEach(r => {
|
||
byService[r.service] = (byService[r.service] || 0) + r.purchases;
|
||
byRegion[r.region] = (byRegion[r.region] || 0) + r.purchases;
|
||
});
|
||
const topService = Object.entries(byService).sort((a,b) => b[1] - a[1])[0];
|
||
const topRegion = Object.entries(byRegion).sort((a,b) => b[1] - a[1])[0];
|
||
return { totalPurchases, regionsCount: regions.length, topService, topRegion };
|
||
}
|
||
|
||
function renderMetrics() {
|
||
const m = calculateMetrics();
|
||
const container = document.getElementById('metrics');
|
||
container.innerHTML = `
|
||
<div class="metric-card">
|
||
<h3>Всего покупок</h3>
|
||
<div class="value">${formatNumber(m.totalPurchases)}</div>
|
||
<div class="sub">за июль 2026</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<h3>Всего регионов</h3>
|
||
<div class="value">${m.regionsCount}</div>
|
||
<div class="sub">в топ-50</div>
|
||
</div>
|
||
<div class="metric-card highlight">
|
||
<h3>Услуга-лидер</h3>
|
||
<div class="value" style="font-size:20px">${formatCompact(m.topService[1])}</div>
|
||
<div class="sub" style="font-size:11px;opacity:0.9">${m.topService[0].substring(0,50)}${m.topService[0].length>50?'...':''}</div>
|
||
</div>
|
||
<div class="metric-card highlight">
|
||
<h3>Регион-лидер</h3>
|
||
<div class="value">${formatCompact(m.topRegion[1])}</div>
|
||
<div class="sub">${m.topRegion[0]}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderBarChart(containerId, data, valueKey, labelKey, color = 'primary') {
|
||
const container = document.getElementById(containerId);
|
||
const maxVal = Math.max(...data.map(d => d[valueKey]));
|
||
container.innerHTML = data.slice(0, 10).map((d, i) => {
|
||
const pct = (d[valueKey] / maxVal) * 100;
|
||
return `
|
||
<div class="bar-item">
|
||
<div class="bar-label" title="${d[labelKey]}">${d[labelKey]}</div>
|
||
<div class="bar-wrapper">
|
||
<div class="bar" style="width:${pct}%">${formatCompact(d[valueKey])}</div>
|
||
</div>
|
||
<div class="bar-value">${formatNumber(d[valueKey])}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderCharts() {
|
||
const byRegion = {};
|
||
const byService = {};
|
||
DATA.forEach(r => {
|
||
byRegion[r.region] = (byRegion[r.region] || 0) + r.purchases;
|
||
byService[r.service] = (byService[r.service] || 0) + r.purchases;
|
||
});
|
||
const regionData = Object.entries(byRegion).map(([name, purchases]) => ({ name, purchases }));
|
||
const serviceData = Object.entries(byService).map(([name, purchases]) => ({ name, purchases }));
|
||
regionData.sort((a,b) => b.purchases - a.purchases);
|
||
serviceData.sort((a,b) => b.purchases - a.purchases);
|
||
renderBarChart('chartRegions', regionData, 'purchases', 'name');
|
||
renderBarChart('chartServices', serviceData, 'purchases', 'name');
|
||
}
|
||
|
||
let currentSort = { key: 'rank', dir: 'asc' };
|
||
let currentFilter = 'all';
|
||
|
||
function getFilteredData() {
|
||
let data = [...DATA];
|
||
if (currentFilter !== 'all') {
|
||
data = data.filter(r => r.region === currentFilter);
|
||
}
|
||
data.sort((a, b) => {
|
||
let aVal = a[currentSort.key];
|
||
let bVal = b[currentSort.key];
|
||
if (currentSort.dir === 'desc') [aVal, bVal] = [bVal, aVal];
|
||
return aVal > bVal ? 1 : -1;
|
||
});
|
||
return data;
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('tableBody');
|
||
const data = getFilteredData();
|
||
tbody.innerHTML = data.map((r, i) => `
|
||
<tr>
|
||
<td class="rank">${i + 1}</td>
|
||
<td><span class="region-badge">${r.region}</span></td>
|
||
<td>${r.service}</td>
|
||
<td>${formatNumber(r.purchases)}</td>
|
||
<td class="amount">${formatAmount(r.amount)}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function initFilters() {
|
||
const regions = [...new Set(DATA.map(r => r.region))].sort();
|
||
const select = document.getElementById('regionFilter');
|
||
select.innerHTML = '<option value="all">Все регионы</option>' + regions.map(r => `<option value="${r}">${r}</option>`).join('');
|
||
const saved = localStorage.getItem('regionFilter');
|
||
if (saved && regions.includes(saved)) {
|
||
currentFilter = saved;
|
||
select.value = saved;
|
||
}
|
||
select.addEventListener('change', (e) => {
|
||
currentFilter = e.target.value;
|
||
localStorage.setItem('regionFilter', currentFilter);
|
||
renderTable();
|
||
});
|
||
}
|
||
|
||
function initSorting() {
|
||
document.querySelectorAll('th[data-sort]').forEach(th => {
|
||
th.addEventListener('click', () => {
|
||
const key = th.dataset.sort;
|
||
if (currentSort.key === key) {
|
||
currentSort.dir = currentSort.dir === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
currentSort.key = key;
|
||
currentSort.dir = 'asc';
|
||
}
|
||
document.querySelectorAll('th').forEach(h => h.classList.remove('sorted'));
|
||
th.classList.add('sorted');
|
||
renderTable();
|
||
});
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderMetrics();
|
||
renderCharts();
|
||
initFilters();
|
||
initSorting();
|
||
const savedSort = localStorage.getItem('tableSort');
|
||
if (savedSort) {
|
||
currentSort = JSON.parse(savedSort);
|
||
const th = document.querySelector(`th[data-sort="${currentSort.key}"]`);
|
||
if (th) th.classList.add('sorted');
|
||
}
|
||
renderTable();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|