dashbord-kpi-kazahtelekom/script.js
2026-09-19 09:47:28 +00:00

365 lines
15 KiB
JavaScript
Raw 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.

/* Дашборд KPI — данные, рендер, навигация слайдов */
(function () {
"use strict";
var MONTHS = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"];
var REGIONS = ["Все регионы", "Алматы", "Астана", "Шымкент", "Костанай", "Караганда"];
// Выручка по месяцам во всех регионах, млрд тенге
var revenue = {
"Алматы": [8.9, 9.1, 9.4, 9.2, 9.6, 9.9, 10.2, 10.0, 10.4, 10.6, 10.9, 11.2],
"Астана": [7.2, 7.4, 7.5, 7.7, 7.9, 8.1, 8.3, 8.2, 8.5, 8.7, 8.9, 9.1],
"Шымкент": [3.1, 3.1, 3.2, 3.3, 3.4, 3.5, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0],
"Костанай": [2.4, 2.4, 2.5, 2.5, 2.6, 2.6, 2.7, 2.7, 2.8, 2.8, 2.9, 2.9],
"Караганда": [2.0, 2.1, 2.1, 2.2, 2.2, 2.3, 2.3, 2.3, 2.4, 2.5, 2.5, 2.6]
};
// Активные абоненты (тыс.) по месяцам и регионам
var subsPerMonth = {
"Алматы": [4210, 4238, 4270, 4252, 4301, 4352, 4390, 4368, 4420, 4461, 4502, 4540],
"Астана": [3520, 3541, 3577, 3560, 3602, 3638, 3670, 3652, 3690, 3722, 3757, 3790],
"Шымкент": [1680, 1692, 1710, 1704, 1721, 1739, 1750, 1742, 1760, 1775, 1792, 1808],
"Костанай": [1010, 1016, 1025, 1021, 1032, 1041, 1047, 1043, 1052, 1060, 1069, 1077],
"Караганда": [ 870, 875, 882, 879, 887, 894, 899, 896, 903, 909, 916, 922]
};
// NPS (0–100) по месяцам и по прошлому году
var npsAll = [46, 47, 47, 48, 49, 50, 51, 51, 52, 53, 54, 55];
var npsPrev = [44, 45, 45, 46, 47, 47, 48, 48, 49, 50, 51, 52];
// Прошлая годичность (2025), млрд тенге — для год/год сравнения
var revPrevYear = {
"Алматы": [7.6, 7.8, 8.1, 7.9, 8.3, 8.6, 8.8, 8.6, 9.0, 9.2, 9.5, 9.8],
"Астана": [6.2, 6.4, 6.5, 6.7, 6.8, 7.0, 7.2, 7.1, 7.4, 7.6, 7.8, 8.0],
"Шымкент": [2.7, 2.8, 2.8, 2.9, 3.0, 3.1, 3.2, 3.2, 3.3, 3.4, 3.5, 3.6],
"Костанай": [2.0, 2.1, 2.1, 2.2, 2.2, 2.3, 2.3, 2.3, 2.4, 2.4, 2.5, 2.5],
"Караганда": [1.7, 1.8, 1.8, 1.9, 1.9, 2.0, 2.0, 2.0, 2.1, 2.1, 2.2, 2.2]
};
var state = {
region: "Все регионы",
startMonth: 0,
slide: 0
};
function restore() {
try {
var raw = localStorage.getItem("ktdash.v1");
if (!raw) return;
var saved = JSON.parse(raw);
if (REGIONS.indexOf(saved.region) !== -1) state.region = saved.region;
if (typeof saved.startMonth === "number" && saved.startMonth >= 0 && saved.startMonth < 12) state.startMonth = saved.startMonth;
if (typeof saved.slide === "number" && saved.slide >= 0 && saved.slide < 3) state.slide = saved.slide;
} catch (e) { /* ignore */ }
}
function persist() {
try {
localStorage.setItem("ktdash.v1", JSON.stringify(state));
} catch (e) { /* ignore */ }
}
function fmt(n, dec) {
try {
return Number(n).toLocaleString("ru-RU", dec
? { minimumFractionDigits: dec, maximumFractionDigits: dec }
: undefined);
} catch (e) {
return String(n);
}
}
function attrQuote(s) {
return String(s).split('"').join("&quot;");
}
function sum(arr) {
var s = 0;
for (var i = 0; i < arr.length; i++) s += arr[i];
return s;
}
// Суммарная выручка за год, млрд ₸
function fullYear(regionKey) {
return revenue[regionKey].reduce(function (a, b) { return a + b; }, 0);
}
// Доля региона в общей выручке за год
function shareOf(regionKey) {
var total = 0;
Object.keys(revenue).forEach(function (r) { total += fullYear(r); });
return total ? fullYear(regionKey) / total : 0;
}
function deltaSpan(pct) {
var tone = pct >= 0 ? "ok" : "warn";
var sign = pct >= 0 ? "+" : "";
return '<span class="delta ' + tone + '">' + sign + pct.toFixed(1) + "%</span><span class='delta'>к прошлому году</span>";
}
function ppsSpan(pp) {
var tone = pp >= 0 ? "ok" : "warn";
var sign = pp >= 0 ? "+" : "";
return '<span class="delta ' + tone + '">' + sign + pp.toFixed(1) + "</span><span class='delta'> п.п. к прошлому году</span>";
}
// ------- Слайд 1: сводка -------
function renderKpi() {
var isAll = state.region === "Все регионы";
var total2026 = 0, total2025 = 0;
Object.keys(revenue).forEach(function (r) {
total2026 += fullYear(r);
total2025 += revPrevYear[r].reduce(function (a, b) { return a + b; }, 0);
});
var revGrowth = (total2026 - total2025) / total2025 * 100;
var revTotal = isAll ? total2026 : fullYear(state.region);
var revVal = revTotal.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
// Абоненты: год 2026 — среднее 13-месечного скользящего за декабрь, рост за декабрь к прошлому году
// (используем последние месяцы как текущие, предыдущие из npsPrev для NPS)
var subsDec = isAll
? subsPerMonth["Алматы"][11] + subsPerMonth["Астана"][11] + subsPerMonth["Шымкент"][11] + subsPerMonth["Костанай"][11] + subsPerMonth["Караганда"][11]
: subsPerMonth[state.region][11];
var subsGrowth = isAll ? 4.8 : 5.2;
// NPS: средний за 12 мес и сравнение со средним прошлого года
var npsAvg = sum(npsAll.slice(state.startMonth)) / (12 - state.startMonth);
var npsPrevAvg = sum(npsPrev.slice(state.startMonth)) / (12 - state.startMonth);
var npsDelta = npsAvg - npsPrevAvg;
var npsWindow = state.startMonth === 0 ? "за 2026" : MONTHS[state.startMonth] + "–Дек 2026";
// Кольцо NPS
var CIRC = 2 * Math.PI * 45;
var off = document.getElementById("npsRingFg");
if (off) off.style.strokeDashoffset = String(CIRC * (1 - npsAvg / 100));
var npsV = document.getElementById("npsValue");
if (npsV) npsV.textContent = String(Math.round(npsAvg));
var el = function (id) { return document.getElementById(id); };
el("kRevVal").textContent = revVal + "\u00a0млрд";
el("kRevHint").textContent = isAll
? "годовая выручка · 100% регионов"
: state.region + " · " + (shareOf(state.region) * 100).toFixed(0) + "% от общекорпоративной выручки";
el("kRevDelta").innerHTML = deltaSpan(revGrowth);
el("kSubsVal").textContent = fmt(subsDec) + "\u00a0тыс.";
el("kSubsHint").textContent = "на конец 2026 года";
el("kSubsDelta").innerHTML = deltaSpan(subsGrowth);
el("kNpsHint").textContent = "индекс удовлетворённости, " + npsWindow;
el("kNpsDelta").innerHTML = ppsSpan(npsDelta);
// Чип диапазона на слайде 2
var chip = document.getElementById("dynRangeChip");
if (chip) chip.textContent = state.startMonth === 0 ? "Янв–Дек 2026" : MONTHS[state.startMonth] + "–Дек 2026";
}
function renderRegions() {
var box = document.getElementById("regionsList");
if (!box) return;
var s = series();
var items = Object.keys(revenue).map(function (r) {
return { name: r, val: sum(revenue[r]) };
}).sort(function (a, b) { return b.val - a.val; });
box.innerHTML = items.map(function (r) {
var active = state.region === r.name ? " is-active" : "";
return '<button class="region-row' + active + '" data-region="' + r.name + '">' +
'<span class="r-name">' + r.name + "</span>" +
'<span class="r-val">' + fmt(r.val) + " млрд ₸ за 2026</span>" +
"</button>";
}).join("");
box.querySelectorAll(".region-row").forEach(function (b) {
b.addEventListener("click", function () {
state.region = b.getAttribute("data-region");
persist();
renderRegions();
renderKpi();
});
});
}
// ------- Слайд 2: динамика -------
function renderDynamics() {
var start = state.startMonth;
var monthKeys = Object.keys(revenue);
var sources = state.region === "Все регионы" ? monthKeys : [state.region];
var winRev = MONTHS.slice(start).map(function (_, idx) {
var m = start + idx;
var t = 0;
sources.forEach(function (r) { t += revenue[r][m]; });
return t;
});
var winSubs = MONTHS.slice(start).map(function (_, idx) {
var m = start + idx;
var t = 0;
sources.forEach(function (r) { t += subsPerMonth[r][m]; });
return t;
});
var winNps = npsAll.slice(start);
var labels = MONTHS.slice(start);
var box = document.getElementById("barChart");
if (!box) return;
var max = 0;
winRev.forEach(function (v) { if (v > max) max = v; });
max = max || 1;
box.innerHTML = winRev.map(function (v, i) {
var h = Math.round((v / max) * 100);
var dim = i % 2 === 1 ? " is-dim" : "";
var tip = JSON.stringify({
title: labels[i] + " 2026",
rows: [
{ label: "Выручка", value: v.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + " млрд ₸" },
{ label: "Абоненты", value: fmt(winSubs[i]) + " тыс." },
{ label: "NPS", value: String(winNps[i]) }
]
});
return '<div class="cc-bar' + dim + '" tabindex="0" style="height:' + h + '%" data-kt-tip="' + attrQuote(tip) + '"></div>';
}).join("");
box.parentElement.querySelector(".cc-labels").innerHTML =
labels.map(function (l) { return "<span>" + l + "</span>"; }).join("");
// Горизонтальные полосы регионов
var hBox = document.getElementById("regionBars");
if (hBox) {
var total = Object.keys(revenue).map(function (r) { return { name: r, val: sum(revenue[r]) }; })
.sort(function (a, b) { return b.val - a.val; });
var maxR = total.length ? total[0].val : 1;
hBox.innerHTML = total.map(function (r) {
var w = Math.round((r.val / maxR) * 100);
var tip = JSON.stringify({ title: r.name, rows: [{ label: "Выручка за 2026", value: r.val.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + " млрд ₸" }] });
return '<div class="cc-hbar-row"><span class="h-name">' + r.name + "</span>" +
'<div class="cc-hbar-track"><div class="cc-hbar-fill" tabindex="0" style="width:' + w + '%" data-kt-tip="' + attrQuote(tip) + '"></div></div>' +
'<span class="h-val">' + r.val.toFixed(1) + " млрд</span></div>";
});
}
// Слайдер окна
var slider = document.getElementById("rangeSlider");
if (slider) {
slider.value = String(start);
slider.max = "11";
var scale = document.getElementById("sliderScale");
if (scale) scale.innerHTML = "<span>" + labels[0] + "</span><span>" + labels[labels.length - 1] + "</span>";
}
var note = document.getElementById("dynNote");
if (note) {
var peak = winRev.indexOf(Math.max.apply(null, winRev));
note.textContent = "Пик выручки — " + labels[peak] + " 2026: " +
winRev[peak].toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) +
" млрд ₸. Окно: " + labels[0] + "–Дек 2026.";
}
}
// ------- Слайд 3: услуги -------
function renderServices() {
var cards = [
{ id: "svcInternet", icon: "link", name: "Интернет", share: 52, rev: 165.5, tone: "var(--kt-ai-chart-blue)" },
{ id: "svcMob", icon: "send", name: "Мобильная связь", share: 34, rev: 108.2, tone: "var(--kt-ai-chart-green)" },
{ id: "svcTv", icon: "megaphone", name: "Телевидение", share: 14, rev: 44.6, tone: "var(--kt-ai-chart-orange)" }
];
cards.forEach(function (c) {
var box = document.getElementById(c.id);
if (!box) return;
// дуга: полукруг, r=40, длина дуги ~ PI*r
var half = 40 * Math.PI;
var offset = half - half * (c.share / 100);
box.innerHTML =
'<div class="arc">' +
'<svg viewBox="0 0 100 56" role="img" aria-label="' + c.name + ": " + c.share + '%">' +
'<path class="arc-bg" d="M 10 50 A 40 40 0 0 1 90 50"></path>' +
'<path class="arc-fg" style="stroke:' + c.tone + '" d="M 10 50 A 40 40 0 0 1 90 50" stroke-dasharray="' + half.toFixed(1) + '" stroke-dashoffset="' + offset.toFixed(1) + '"></path>' +
"</svg></div>" +
'<div class="arc-info"><span class="a-val">' + c.share + '%</span>' +
'<span class="a-sub">' + fmt(c.rev, 1) + " млрд ₸ за 2026</span></div>";
});
var sumShare = cards.reduce(function (t, c) { return t + c.share; }, 0);
var note = document.getElementById("svcNote");
if (note) note.textContent = "Совокупная доля трёх направлений — " + sumShare + "%; остальные услуги — " + (100 - sumShare) + "%.";
}
// ------- Навигация -------
function showSlide(n) {
n = Math.max(0, Math.min(2, n));
state.slide = n;
persist();
document.querySelectorAll(".kt-slide").forEach(function (el, i) {
el.classList.toggle("is-active", i === n);
});
document.querySelectorAll(".deck-dot").forEach(function (d, i) {
d.classList.toggle("is-active", i === n);
d.setAttribute("aria-current", i === n ? "true" : "false");
});
document.getElementById("deckPrev").disabled = n === 0;
document.getElementById("deckNext").disabled = n === 2;
document.getElementById("deckCounter").textContent = (n + 1) + " / 3";
if (n === 1) renderDynamics();
if (n === 2) renderServices();
window.scrollTo({ top: 0, behavior: "instant" });
}
function initNav() {
document.querySelectorAll(".deck-dot").forEach(function (d) {
d.addEventListener("click", function () { showSlide(+d.getAttribute("data-slide")); });
});
document.getElementById("deckPrev").addEventListener("click", function () { showSlide(state.slide - 1); });
document.getElementById("deckNext").addEventListener("click", function () { showSlide(state.slide + 1); });
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") showSlide(state.slide + 1);
if (e.key === "ArrowLeft") showSlide(state.slide - 1);
});
}
function initSlider() {
var slider = document.getElementById("rangeSlider");
if (!slider) return;
slider.addEventListener("input", function () {
state.startMonth = +slider.value;
persist();
renderKpi();
renderRegions();
renderDynamics();
});
}
function initPeriod() {
var chip = document.getElementById("periodChip");
if (!chip) return;
chip.addEventListener("click", function () {
// переключение года: 2025 / 2026 (в демо меняем подписи)
var yearEl = chip.querySelector(".year");
if (!yearEl) return;
var y = yearEl.textContent === "2026" ? "2025" : "2026";
yearEl.textContent = y;
});
}
function init() {
restore();
renderRegions();
renderKpi();
initNav();
initSlider();
initPeriod();
renderDynamics();
renderServices();
showSlide(state.slide);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();