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

684 lines
26 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">
<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>