// Дашборд розничных продаж 2026: данные, графики, таблицы, фильтры. // ---------- Данные ---------- var YEAR = 2026; var PREV_YEAR = 2025; 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)" } ]; // Объём продаж (млн тг): факт 2026 (январь–сентябрь) и 2025 для сравнения. 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, 118, 121, 124] }, mobile: { m2025: [45, 46, 48, 49, 50, 52, 53, 55, 56, 58, 60, 61], m2026: [64, 66, 69, 73, 77, 80, 84, 87, 89, 91, 93, 95] }, devices: { m2025: [38, 40, 43, 44, 42, 46, 48, 49, 51, 53, 52, 50], m2026: [63, 66, 71, 68, 74, 78, 82, 79, 85, 88, 90, 93] }, content: { m2025: [31, 32, 34, 35, 36, 38, 39, 40, 41, 43, 44, 45], m2026: [51, 54, 57, 61, 65, 68, 73, 76, 80, 83, 86, 89] }, extras: { m2025: [27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38], m2026: [44, 47, 50, 53, 57, 60, 64, 67, 71, 74, 77, 80] } }; var PERIODS = [ { id: "9m", label: "Янв–Сен 2026", from: 0, months: 9, yoy: true }, { id: "q1", label: "I квартал", from: 0, months: 3, yoy: true }, { id: "q2", label: "II квартал", from: 3, months: 3, yoy: true }, { id: "q3", label: "III квартал", from: 6, months: 3, yoy: true }, { id: "m", label: "По месяцам", from: 0, months: 9, yoy: false } ]; var STATE_KEY = "dash-retail-v1"; // ---------- Утилиты ---------- function fmtInt(n) { var neg = n < 0 ? "-" : ""; var s = String(Math.round(Math.abs(n))); var out = ""; while (s.length > 3) { out = "." + s.slice(-3) + out; s = s.slice(0, -3); } return neg + s + out; } 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 groupSum(id, year, from, len) { var key = year === YEAR ? "m2026" : "m2025"; return sumRange(GROUP_DATA[id][key], from, len); } function fmtPct(p, dec) { var d = dec === undefined ? 1 : dec; var sign = p > 0 ? "+" : ""; return sign + p.toFixed(d).replace(".", ",") + "%"; } function periodDef(id) { for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === id) return PERIODS[i]; return PERIODS[0]; } 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 topGroupIn(groups, from, len, year) { var best = null, bv = -1; for (var i = 0; i < groups.length; i++) { var v = groupSum(groups[i].id, year, from, len); if (v > bv) { bv = v; best = groups[i]; } } return { group: best, value: bv }; } function bestMonthId(id, from, len, year) { var key = year === YEAR ? "m2026" : "m2025"; var bestIdx = from, bestV = -1; for (var m = from; m < from + len && m < 12; m++) { if (GROUP_DATA[id][key][m] > bestV) { bestV = GROUP_DATA[id][key][m]; bestIdx = m; } } return { month: bestIdx, value: bestV }; } function yoyOf(id, from, len) { var cur = groupSum(id, YEAR, from, len); var prev = groupSum(id, PREV_YEAR, from, len); return { cur: cur, prev: prev, pct: prev ? (cur / prev - 1) * 100 : null }; } function filteredGroups() { if (state.group === "all") return GROUP_DEFS.slice(); return GROUP_DEFS.filter(function (g) { return g.id === state.group; }); } // ---------- Состояние (localStorage) ---------- function loadState() { var def = { screen: "overview", period: "9m", group: "all" }; try { var raw = localStorage.getItem(STATE_KEY); if (!raw) return def; var p = JSON.parse(raw); if (!p || typeof p !== "object") return def; var out = {}; for (var k in def) out[k] = (p[k] === undefined ? def[k] : p[k]); return out; } catch (e) { return def; } } var state = loadState(); function saveState() { try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {} } // ---------- Фильтры ---------- function filterBarHtml() { var perSel = ""; for (var i = 0; i < PERIODS.length; i++) { perSel += ""; } var gSel = ""; for (var j = 0; j < GROUP_DEFS.length; j++) { gSel += ""; } return '
' + '' + YEAR + '' + '' + '' + '" + '' + '" + '' + "
"; } function setPeriod(id) { state.period = id; saveState(); applyScreen(); } function setGroup(id) { state.group = id; saveState(); applyScreen(); } function resetState() { state.period = "9m"; state.group = "all"; saveState(); applyScreen(); } function switchScreen(id) { state.screen = id; saveState(); applyScreen(); } // ---------- Графики ---------- 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 + "
" + '
' + PREV_YEAR + ' (сравнение)' + '' + YEAR + ' (факт)
'; } 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 donutHtml(total, pts) { var acc = 0; var segs = ""; for (var i = 0; i < pts.length; i++) { var pp = pts[i]; if (pp.value <= 0) continue; var share = pp.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 overviewKpis() { var p = periodDef(state.period); var groups = filteredGroups(); var cur = 0, prev = 0; for (var i = 0; i < groups.length; i++) { cur += groupSum(groups[i].id, YEAR, p.from, p.months); prev += groupSum(groups[i].id, PREV_YEAR, p.from, p.months); } var yoy = prev ? (cur / prev - 1) * 100 : 0; var avg = cur / p.months; var top = topGroupIn(groups, p.from, p.months, YEAR); var topLabel = top.group ? top.group.name : "—"; var deltaLabel = yoy >= 0 ? "выше" : "ниже"; var deltaNum = Math.abs(yoy).toFixed(1).replace(".", ","); return [ { label: "Объём продаж", value: fmtInt(cur), unit: " млн тг", hint: p.label }, { label: "К уровню " + PREV_YEAR, value: (yoy >= 0 ? "+" : "") + yoy.toFixed(1).replace(".", ",") + "%", unit: "", hint: deltaLabel + " на " + deltaNum + " п.п." }, { label: "Средний месяц", value: fmtInt(avg), unit: " млн тг", hint: "за " + p.months + " мес." }, { label: "Сильнейшая группа", value: topLabel, unit: "", hint: fmtInt(top.value) + " млн тг" } ]; } function kpiStripHtml(kpis) { var h = '
'; for (var i = 0; i < kpis.length; i++) { var k = kpis[i]; h += '
' + k.label + '' + '' + k.value + (k.unit ? '' + k.unit + "" : "") + "" + '' + k.hint + "
"; } return h + "
"; } function overviewChartsHtml() { var p = periodDef(state.period); var groups = filteredGroups(); var h = '
'; if (p.yoy) { var pairs = [], tips = {}; for (var m = 0; m < p.months; m++) { var mi = p.from + m; var curV = 0, prevV = 0; for (var g = 0; g < groups.length; g++) { curV += GROUP_DATA[groups[g].id].m2026[mi]; prevV += GROUP_DATA[groups[g].id].m2025[mi]; } var key = "m" + mi; pairs.push({ label: MONTHS_SHORT[mi], vals: [prevV, curV], colors: ["var(--kt-ai-chart-bar-fill)", "var(--kt-ai-chart-blue)"], tipKey: key }); tips[key] = MONTHS[mi] + ": " + YEAR + " — " + fmtInt(curV) + " млн тг; " + PREV_YEAR + " — " + fmtInt(prevV) + " млн тг" + (prevV ? " (" + fmtPct((curV / prevV - 1) * 100) + ")" : ""); } h += chartCard("Динамика по месяцам", YEAR + " против " + PREV_YEAR + ", млн тг", groupedBarHtml(pairs, tips)); } else { var pts = []; for (var mm = 0; mm < p.months; mm++) { var mi2 = p.from + mm; var mv = 0; for (var gg = 0; gg < groups.length; gg++) mv += GROUP_DATA[groups[gg].id].m2026[mi2]; var pv2 = 0; for (var gg2 = 0; gg2 < groups.length; gg2++) pv2 += GROUP_DATA[groups[gg2].id].m2025[mi2]; pts.push({ label: MONTHS_SHORT[mi2], v: mv, tip: MONTHS[mi2] + ": " + fmtInt(mv) + " млн тг, к " + PREV_YEAR + " " + (pv2 ? fmtPct((mv / pv2 - 1) * 100) : "") }); } h += chartCard("Продажи по месяцам", "млн тг, " + YEAR, lineChartHtml(pts)); } if (state.group === "all") { var donPts = []; for (var d = 0; d < GROUP_DEFS.length; d++) { donPts.push({ label: GROUP_DEFS[d].name, value: groupSum(GROUP_DEFS[d].id, YEAR, p.from, p.months), color: GROUP_DEFS[d].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++) { rows.push({ id: GROUP_DEFS[r].id, label: GROUP_DEFS[r].name, value: groupSum(GROUP_DEFS[r].id, YEAR, p.from, p.months), color: GROUP_DEFS[r].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 mark = rr.id === state.group ? " · выбрано" : ""; body += barRowHtml(rr.label + mark, rr.value, rmax, rr.id === state.group ? rr.color : "var(--kt-ai-chart-bar-fill)", rr.label + ": " + fmtInt(rr.value) + " млн тг за " + p.label); } h += chartCard("Сравнение групп", p.label + ", млн тг", body); } return h + "
"; } function conclusionHtml() { var p = periodDef(state.period); var groups = filteredGroups(); var cur = 0, prev = 0; for (var i = 0; i < groups.length; i++) { cur += groupSum(groups[i].id, YEAR, p.from, p.months); prev += groupSum(groups[i].id, PREV_YEAR, p.from, p.months); } var top = topGroupIn(groups, p.from, p.months, YEAR); var bm = bestMonthId(top.group ? top.group.id : GROUP_DEFS[0].id, p.from, p.months, YEAR); var yoy = prev ? (cur / prev - 1) * 100 : 0; var groupsLabel = state.group === "all" ? "суммарно по всем группам" : "в группе «" + groupName(state.group) + "»"; var text = "За " + p.label + " объём розничных продаж — " + fmtInt(cur) + " млн тг (" + groupsLabel + "). " + "Лучший месяц периода — " + MONTHS[bm.month].toLowerCase() + " (" + fmtInt(bm.value) + " млн тг). " + "Сильнейшая группа — «" + (top.group ? top.group.name : "—") + "». " + "К уровню " + PREV_YEAR + " года показатель " + (yoy >= 0 ? "выше" : "ниже") + " на " + Math.abs(yoy).toFixed(1).replace(".", ",") + " п.п."; return '
' + 'Итог за период' + '' + text + "
"; } function renderOverview() { var p = periodDef(state.period); var groupsLabel = state.group === "all" ? "все группы" : "группа «" + groupName(state.group) + "»"; var h = ""; h += '

Обзор

' + 'Розничные продажи ' + YEAR + " · " + p.label + " · " + groupsLabel + "
"; h += filterBarHtml(); h += kpiStripHtml(overviewKpis()); h += overviewChartsHtml(); h += conclusionHtml(); return h; } // ---------- Экран «Детали» ---------- function detailsMonthlyTable() { var p = periodDef(state.period); var groups = filteredGroups(); var h = '

Продажи по месяцам

'; var headCols = "Месяц"; for (var c = 0; c < groups.length; c++) headCols += "" + groups[c].name + ""; headCols += "ИтогоК " + PREV_YEAR + ""; var body = ""; var colTot = {}; for (var c2 = 0; c2 < groups.length; c2++) colTot[groups[c2].id] = 0; var grand = 0, grandPrev = 0; for (var m = 0; m < p.months; m++) { var mi = p.from + m; body += "" + MONTHS[mi] + ""; var rowTot = 0, rowPrev = 0; for (var c3 = 0; c3 < groups.length; c3++) { var v = GROUP_DATA[groups[c3].id].m2026[mi]; var pv = GROUP_DATA[groups[c3].id].m2025[mi]; colTot[groups[c3].id] += v; rowTot += v; rowPrev += pv; body += '' + fmtInt(v) + ""; } grand += rowTot; grandPrev += rowPrev; body += '' + fmtInt(rowTot) + "" + '' + (rowPrev ? fmtPct((rowTot / rowPrev - 1) * 100) : "—") + ""; } body += 'Итого'; for (var c4 = 0; c4 < groups.length; c4++) body += '' + fmtInt(colTot[groups[c4].id]) + ""; body += '' + fmtInt(grand) + "" + '' + (grandPrev ? fmtPct((grand / grandPrev - 1) * 100) : "—") + ""; h += '
' + headCols + "" + body + "
" + '

Значения в млн тг. «К ' + PREV_YEAR + "» — прирост того же месяца к прошлому году.

"; return h; } function detailsGroupsTable() { var p = periodDef(state.period); var h = '

' + (state.group === "all" ? "Разбивка по товарным группам" : "Группа «" + groupName(state.group) + "» по месяцам") + "

"; if (state.group === "all") { h += '
' + ""; var totals = []; var sumAll = 0; for (var i = 0; i < GROUP_DEFS.length; i++) { var t = groupSum(GROUP_DEFS[i].id, YEAR, p.from, p.months); totals.push(t); sumAll += t; } for (var g = 0; g < GROUP_DEFS.length; g++) { var gd = GROUP_DEFS[g]; var tot = totals[g]; var share = sumAll ? tot / sumAll * 100 : 0; var bm = bestMonthId(gd.id, p.from, p.months, YEAR); var y = yoyOf(gd.id, p.from, p.months); h += '' + '' + '" + "' + '"; } h += "
Товарная группаОбъёмДоляЛучший месяцК " + PREV_YEAR + "
' + gd.name + '' + fmtInt(tot) + ' млн тг' + share.toFixed(1).replace(".", ",") + "%" + MONTHS[bm.month] + " · " + fmtInt(bm.value) + ' млн тг' + (y.pct === null ? "—" : fmtPct(y.pct)) + "
"; } else { h += '
' + ""; var gTot = groupSum(state.group, YEAR, p.from, p.months); for (var m = 0; m < p.months; m++) { var mi = p.from + m; var mvv = GROUP_DATA[state.group].m2026[mi]; var pvv = GROUP_DATA[state.group].m2025[mi]; var sh = gTot ? mvv / gTot * 100 : 0; h += '' + '' + '" + '"; } var gy = yoyOf(state.group, p.from, p.months); h += '' + '
МесяцОбъёмДоля в группеК " + PREV_YEAR + "
' + MONTHS[mi] + '' + fmtInt(mvv) + ' млн тг' + sh.toFixed(1).replace(".", ",") + "%' + (pvv ? fmtPct((mvv / pvv - 1) * 100) : "—") + "
Итого' + fmtInt(gTot) + ' млн тг100%' + (gy.pct === null ? "—" : fmtPct(gy.pct)) + "
"; } return h + "
"; } function renderDetails() { var p = periodDef(state.period); var h = ""; h += '

Детали

' + 'Разбивка ' + YEAR + " по периодам и товарным группам
"; h += filterBarHtml(); h += detailsMonthlyTable(); h += detailsGroupsTable(); return h; } // ---------- Разметка экранов ---------- 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"); } } applyScreen();