// Дашборд розничных продаж 2026: данные, графики, таблицы, фильтры.
// ---------- Данные ----------
var MONTHS = ["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь"];
var MONTHS_SHORT = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен"];
var GROUP_DEFS = [
{ id:"connect", name:"Подключение", base:"var(--kt-ai-chart-blue)" },
{ id:"mobile", name:"Мобильная связь", base:"var(--kt-ai-chart-green)" },
{ id:"devices", name:"Устройства", base:"var(--kt-ai-chart-orange)" },
{ id:"content", name:"Тарифы и контент", base:"var(--kt-ai-chart-pink)" },
{ id:"extras", name:"Доп. услуги", base:"var(--kt-ai-chart-salmon)" }
];
// Объём продаж (млн тг) по месяцам за 2025 (сравнение) и 2026 (факт до сентября).
var GROUP_DATA = {
connect: { m2025: [52, 55, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76],
m2026: [79, 83, 87, 92, 97, 101, 106, 110, 114] },
mobile: { m2025: [45, 46, 48, 49, 50, 52, 53, 55, 56, 58, 60, 61],
m2026: [64, 66, 69, 73, 77, 80, 84, 87, 89] },
devices: { m2025: [38, 40, 43, 44, 42, 46, 48, 49, 51, 53, 52, 50],
m2026: [63, 66, 71, 68, 74, 78, 82, 79, 85] },
content: { m2025: [31, 32, 34, 35, 36, 38, 39, 40, 41, 43, 44, 45],
m2026: [51, 54, 57, 61, 65, 68, 73, 76, 80] },
extras: { m2025: [27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38],
m2026: [44, 47, 50, 53, 57, 60, 64, 67, 71] }
};
var PERIODS = [
{ id: "9m", label: "Янв–Сен 2026", months: 9, yoy: true },
{ id: "q1", label: "I квартал", months: 3, yoy: true },
{ id: "q2", label: "II квартал", months: 6, yoy: true },
{ id: "m", label: "По месяцам", months: 9, yoy: false }
];
var STATE_KEY = "dash-retail-v1";
// ---------- Утилиты ----------
function fmtInt(n) {
var s = String(Math.round(n));
var out = "";
while (s.length > 3) { out = "." + s.slice(-3) + out; s = s.slice(0, -3); }
return s.length ? s + out : "0" + out;
}
function fmtMoney(mn) { return fmtInt(mn) + " млн тг"; }
function fmtPct(p) {
var sign = p > 0 ? "+" : "";
var num = (Math.abs(p)).toFixed(1).replace(".", ",");
return sign + num + "%";
}
function sum(arr) {
var t = 0;
for (var i = 0; i < arr.length; i++) t += arr[i];
return t;
}
function sumRange(arr, from, len) {
var t = 0;
for (var i = from; i < from + len && i < arr.length; i++) t += arr[i];
return t;
}
function stateFromJson(s) {
var st = {};
for (var k in s) st[k] = s[k];
return st;
}
function loadState() {
var def = { screen: "overview", period: "9m", group: "all" };
try {
var raw = localStorage.getItem(STATE_KEY);
if (!raw) return def;
var parsed = JSON.parse(raw);
return stateFromJson(parsed) == null ? def : Object.assign(def, parsed);
} catch (e) { return def; }
}
function saveState() {
try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {}
}
var state = loadState();
function periodDef(id) {
for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === id) return PERIODS[i];
return PERIODS[0];
}
function filteredGroups() {
var all = GROUP_DEFS;
if (state.group !== "all") {
all = [];
for (var i = 0; i < GROUP_DEFS.length; i++) {
if (GROUP_DEFS[i].id === state.group) all.push(GROUP_DEFS[i]);
}
}
return all;
}
// ---------- Рендер фильтров ----------
function filterBarHtml() {
var perSel = "";
for (var i = 0; i < PERIODS.length; i++) {
var sel = state.period === PERIODS[i].id ? " selected" : "";
perSel += "";
}
var gSel = "";
for (var j = 0; j < GROUP_DEFS.length; j++) {
var g = GROUP_DEFS[j];
gSel += "";
}
return '
' +
'' +
'' +
'' +
'' +
'' +
'2026' +
'
';
}
function setPeriod(id) {
state.period = id; saveState(); renderAll();
}
function setGroup(id) {
state.group = id; saveState(); renderAll();
}
// ---------- Графики ----------
function chartCard(title, meta, body, note) {
var head = '
' + title + "
" +
(meta ? '
' + meta + "
" : "") + "
";
var foot = note ? '
' + note + "
" : "";
return '' + head + body + foot + "";
}
function groupedBarHtml(pairs, tipRows) {
var max = 0;
for (var i = 0; i < pairs.length; i++) {
for (var j = 1; j < pairs[i].vals.length; j++) {
if (pairs[i].vals[j] > max) max = pairs[i].vals[j];
}
}
if (max < 1) max = 1;
var bars = "";
for (var k = 0; k < pairs.length; k++) {
var p = pairs[k];
var inner = "";
for (var m = 0; m < p.vals.length; m++) {
var h = Math.max(2, Math.round(p.vals[m] / max * 190));
var tip = tipRows[p.tipKey || "" + k];
inner += '";
}
bars += '
' + inner + "" +
'' + p.label + "
";
}
return '
' + bars + "
" +
'
2025 (сравнение)' +
'2026 (факт)
';
}
function lineChartHtml(points) {
var W = 520, H = 200, padX = 10, padT = 14, padB = 26, innerH = H - padT - padB;
var max = 0, min = Infinity;
for (var i = 0; i < points.length; i++) {
if (points[i].v > max) max = points[i].v;
if (points[i].v < min) min = points[i].v;
}
if (min <= 0) min = 0;
var top = max * 1.06, bot = Math.max(0, min - (max - min) * 0.35);
var n = points.length;
function X(i) { return n === 1 ? W / 2 : padX + i * (W - 2 * padX) / (n - 1); }
function Y(v) { return padT + (1 - (v - bot) / (top - bot || 1)) * innerH; }
var path = "", dots = "", labels = "";
for (var k = 0; k < n; k++) {
path += (k ? "L" : "M") + X(k).toFixed(1) + " " + Y(points[k].v).toFixed(1);
dots += '";
labels += '' + points[k].label + "";
}
var grid = "";
for (var g = 0; g < 4; g++) {
var gy = padT + g * innerH / 3;
var gv = top - g * (top - bot) / 3;
grid += '' +
'' + fmtInt(gv) + "";
}
return '";
}
function barRowHtml(label, value, maxV, color, tip) {
var w = Math.max(2, Math.round(value / maxV * 100));
return '
' + label + "" +
'' +
'' + fmtInt(value) + "
";
}
function shareOf(id, months2026) {
var num = 0, den = 0;
for (var m = 0; m < months2026; m++) { num += GROUP_DATA[id].m2026[m]; }
for (var k = 0; k < GROUP_DEFS.length; k++) { den += sumRange(GROUP_DATA[GROUP_DEFS[k].id].m2026, 0, months2026); }
return den ? num / den * 100 : 0;
}
function topMonthOf(id, from, len) {
var best = 0, idx = from;
for (var m = from; m < from + len; m++) {
if (GROUP_DATA[id].m2026[m] > best) { best = GROUP_DATA[id].m2026[m]; idx = m; }
}
return { month: idx, value: best };
}
function groupTotal(id) {
return sum(GROUP_DATA[id].m2026) + sum(GROUP_DATA[id].m2025);
}
function yoyFor(id, from, len) {
var cur = sumRange(GROUP_DATA[id].m2026, from, len);
var prev = sumRange(GROUP_DATA[id].m2025, from, len);
return prev ? (cur - prev) / prev * 100 : 0;
}
function overviewKpis() {
var p = periodDef(state.period);
var cur = 0, prev = 0;
var groups = filteredGroups();
for (var i = 0; i < groups.length; i++) {
cur += sumRange(GROUP_DATA[groups[i].id].m2026, 0, p.months);
prev += sumRange(GROUP_DATA[groups[i].id].m2025, 0, p.months);
}
var yoy = prev ? (cur - prev) / prev * 100 : 0;
var topId = "all", topV = -1;
for (var j = 0; j < GROUP_DEFS.length; j++) {
var v = sumRange(GROUP_DATA[GROUP_DEFS[j].id].m2026, 0, p.months);
if (v > topV) { topV = v; topId = GROUP_DEFS[j].id; }
}
var topName = "";
for (var k = 0; k < GROUP_DEFS.length; k++) if (GROUP_DEFS[k].id === topId) topName = GROUP_DEFS[k].name;
var avg = cur / p.months;
return [
{ label: "Объём продаж", value: fmtInt(cur), unit: " млн тг", hint: p.label },
{ label: "К уровню 2025", value: fmtPct(yoy), unit: "", hint: "за период" },
{ label: "Средний месяц", value: fmtInt(avg), unit: " млн тг", hint: "за " + p.months + " мес." },
{ label: "Лучшая группа", value: topName, unit: "", hint: fmtInt(topV) + " млн тг" }
];
}
function renderOverview() {
var p = periodDef(state.period);
var groups = filteredGroups();
var groupsLabel = state.group === "all" ? "все группы" : groups[0].name.toLowerCase();
var h = "";
h += '
';
if (p.yoy) {
var pairs = [];
var tips = {};
for (var m = 0; m < p.months; m++) {
var curV = 0, prevV = 0;
for (var gg = 0; gg < groups.length; gg++) {
curV += GROUP_DATA[groups[gg].id].m2026[m];
prevV += GROUP_DATA[groups[gg].id].m2025[m];
}
pairs.push({ label: MONTHS_SHORT[m], vals: [prevV, curV], colors: ["var(--kt-ai-chart-bar-fill)", "var(--kt-ai-chart-blue)"], tipKey: "" + m });
tips["" + m] = MONTHS[m] + ": 2026 — " + fmtInt(curV) + " млн тг; 2025 — " + fmtInt(prevV) + " млн тг" + (prevV ? " (" + fmtPct((curV - prevV) / prevV * 100) + ")" : "");
}
h += chartCard("Динамика за месяц", "2026 против 2025, млн тг", groupedBarHtml(pairs, tips));
} else {
var pts = [];
for (var mm = 0; mm < p.months; mm++) {
var mv = 0;
for (var gg = 0; gg < groups.length; gg++) mv += GROUP_DATA[groups[gg].id].m2026[mm];
var yoyv = 0;
for (var gg2 = 0; gg2 < groups.length; gg2++) yoyv += GROUP_DATA[groups[gg2].id].m2025[mm];
var yy = yoyv ? (mv - yoyv) / yoyv * 100 : 0;
pts.push({ label: MONTHS_SHORT[mm], v: mv, tip: MONTHS[mm] + ": " + fmtInt(mv) + " млн тг, к 2025 " + (yoyv ? fmtPct(yy) : "") });
}
h += chartCard("Продажи по месяцам", "млн тг, 2026", lineChartHtml(pts));
}
if (state.group === "all") {
var donPts = [];
for (var d = 0; d < GROUP_DEFS.length; d++) {
var gd = GROUP_DEFS[d];
donPts.push({ label: gd.name, value: sumRange(GROUP_DATA[gd.id].m2026, 0, p.months), color: gd.base });
}
var donTotal = 0;
for (var d2 = 0; d2 < donPts.length; d2++) donTotal += donPts[d2].value;
h += chartCard("Структура продаж", p.label + ", млн тг", donutHtml(donTotal, donPts));
} else {
var rows = [];
for (var r = 0; r < GROUP_DEFS.length; r++) {
var gr = GROUP_DEFS[r];
rows.push({ id: gr.id, label: gr.name, value: sumRange(GROUP_DATA[gr.id].m2026, 0, p.months), color: gr.base });
}
var rmax = 0;
for (var r2 = 0; r2 < rows.length; r2++) if (rows[r2].value > rmax) rmax = rows[r2].value;
var body = "";
for (var r3 = 0; r3 < rows.length; r3++) {
var rr = rows[r3];
var selMark = rr.id === state.group ? " · выбрано" : "";
body += barRowHtml(rr.label + selMark, rr.value, rmax, rr.id === state.group ? rr.color : "var(--kt-ai-chart-bar-fill)", rr.label + ": " + fmtInt(rr.value) + " млн тг");
}
h += chartCard("Сравнение групп", p.label + ", млн тг", body);
}
h += "
";
var t = Math.round(Date.now() / 60000) * 60000;
h += '' +
'Итог за период' +
'За ' + p.label.toLowerCase() + " продано на " + fmtInt(kpis[0].value.replace(/[^\d.]/g, "")) + " млн тг (" + groupsLabel + "). Лучшие результаты — " + kpis[3].value.toLowerCase() + "; к уровню 2025 года показатель " + (String(kpis[1].value).indexOf("+") === 0 ? "выше" : "ниже") + " на " + pctNum(kpis[1].value).toFixed(1).replace(".", ",") + " п.д." +
'Обновлено сегодня в ' + new Date(t).getHours() + ":" + String(new Date(t).getMinutes()).padStart(2, "0") + "" +
"";
return h;
}
function pctNum(v) {
return Math.abs(parseFloat(String(v).replace(",", ".")));
}
function groupName(id) {
for (var i = 0; i < GROUP_DEFS.length; i++) if (GROUP_DEFS[i].id === id) return GROUP_DEFS[i].name;
return id;
}
function renderDetails() {
var p = periodDef(state.period);
var h = "";
h += '
Детали
' +
'Разбивка 2026 по периодам и товарным группам
';
h += filterBarHtml();
// Таблица по месяцам
h += '
Продажи по месяцам
';
var gIds = [];
for (var i = 0; i < GROUP_DEFS.length; i++) gIds.push(GROUP_DEFS[i].id);
gIds.sort(function (a, b) { return groupTotal(b) - groupTotal(a); });
var headCols = "
Месяц
";
for (var c = 0; c < gIds.length; c++) {
headCols += "
" + groupName(gIds[c]) + "
";
}
headCols += "
Итого
К 2025
";
var body = "";
var colTot = {};
for (var c3 = 0; c3 < gIds.length; c3++) colTot[gIds[c3]] = 0;
var grand = 0, grandPrev = 0;
for (var m = 0; m < p.months; m++) {
body += "
" + MONTHS[m] + "
";
var rowTot = 0, rowPrev = 0;
for (var c4 = 0; c4 < gIds.length; c4++) {
var v = GROUP_DATA[gIds[c4]].m2026[m];
var pv = GROUP_DATA[gIds[c4]].m2025[m];
colTot[gIds[c4]] += v; rowTot += v; rowPrev += pv;
body += '
' + fmtInt(v) + "
";
}
grand += rowTot; grandPrev += rowPrev;
body += '
";
var foot = "Значения в млн тг. «К 2025» — прирост того же месяца к прошлому году.";
h += '
' + headCols + "
" + body + "
" +
'
' + foot + "
";
// Разбивка по группам
h += '
Разбивка по товарным группам
';
if (state.group === "all") {
h += '
' +
"
Товарная группа
Объём
Место
Доля
Лучший месяц
К 2025
" +
"
";
for (var g = 0; g < gIds.length; g++) {
var gd = null;
for (var g2 = 0; g2 < GROUP_DEFS.length; g2++) if (GROUP_DEFS[g2].id === gIds[g]) gd = GROUP_DEFS[g2];
var tot = sumRange(GROUP_DATA[gd.id].m2026, 0, p.months);
var share = shareOf(gd.id, p.months);
var tm = topMonthOf(gd.id, 0, p.months);
var yoy = yoyFor(gd.id, 0, p.months);
h += '
";
}
h += "";
return h;
}
function donutHtml(total, pts) {
var acc = 0;
var segs = "";
for (var i = 0; i < pts.length; i++) {
var p = pts[i];
if (p.value <= 0) continue;
var share = p.value / total * 100;
segs += '";
acc += share;
}
var legend = "";
for (var j = 0; j < pts.length; j++) {
var q = pts[j];
if (q.value <= 0) continue;
var pc = Math.round(q.value / total * 100);
legend += '
' +
'' + q.label + '' + pc + "%
";
}
var svg = '';
return '
' + svg + '
' + legend + "
";
}
function renderAll() {
applyScreen();
}
function applyScreen() {
var ov = document.getElementById("screen-overview");
var dt = document.getElementById("screen-details");
if (state.screen === "details") {
if (ov) ov.hidden = true;
if (dt) { dt.hidden = false; dt.innerHTML = renderDetails(); }
} else {
if (ov) { ov.hidden = false; ov.innerHTML = renderOverview(); }
if (dt) dt.hidden = true;
}
var items = document.querySelectorAll('.kt-ai-nav-item[data-screen]');
for (var i = 0; i < items.length; i++) {
items[i].setAttribute("data-active", items[i].getAttribute("data-screen") === state.screen ? "true" : "false");
}
}
function switchScreen(id) {
state.screen = id; saveState(); renderAll();
}
renderAll();