// Дашборд розничных продаж 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 '
' + grid + '' + dots + labels + "
"; } 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 += '

Обзор

' + 'Розничные продажи 2026 · ' + p.label + " · " + groupsLabel + "
"; h += filterBarHtml(); var kpis = overviewKpis(); h += '
'; for (var i = 0; i < kpis.length; i++) { var k = kpis[i]; var long = String(k.value).length > 9 ? " data-long=\"true\"" : ""; h += '
' + k.label + '' + '" + k.value + (k.unit ? '' + k.unit + "" : "") + "" + '' + k.hint + "
"; } 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 += '' + fmtInt(rowTot) + "" + '' + (rowPrev ? fmtPct((rowTot - rowPrev) / rowPrev * 100) : "—") + ""; } body += 'Итого'; for (var c5 = 0; c5 < gIds.length; c5++) body += '' + fmtInt(colTot[gIds[c5]]) + ""; body += '' + fmtInt(grand) + "" + '' + (grandPrev ? fmtPct((grand - grandPrev) / grandPrev * 100) : "—") + ""; var foot = "Значения в млн тг. «К 2025» — прирост того же месяца к прошлому году."; h += '
' + headCols + "" + body + "
" + '

' + foot + "

"; // Разбивка по группам h += '

Разбивка по товарным группам

'; if (state.group === "all") { h += '
' + "" + ""; 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 += "
Товарная группаОбъёмМестоДоляЛучший месяцК 2025
' + gd.name + '' + fmtInt(tot) + " млн тг' + (g + 1) + "" + Math.round(share) + "%" + MONTHS[tm.month] + " · " + fmtInt(tm.value) + '' + fmtPct(yoy) + "
"; } else { h += '
' + "" + ""; var gTot = 0; for (var m2 = 0; m2 < p.months; m2++) gTot += GROUP_DATA[state.group].m2026[m2]; for (var m3 = 0; m3 < p.months; m3++) { var mvv = GROUP_DATA[state.group].m2026[m3]; var pvv = GROUP_DATA[state.group].m2025[m3]; var sh = gTot ? mvv / gTot * 100 : 0; h += '' + '" + '"; } h += '
МесяцОбъёмДоля в группеК 2025
' + MONTHS[m3] + '" + fmtInt(mvv) + ' млн тг' + sh.toFixed(1).replace(".", ",") + "%' + (pvv ? fmtPct((mvv - pvv) / pvv * 100) : "—") + "
Итого' + fmtInt(gTot) + ' млн тг100%' + fmtPct(yoyFor(state.group, 0, p.months)) + "
"; } 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 = '' + segs + '' + fmtInt(total) + 'млн тг'; 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();