// Дашборд розничных продаж 2026: данные, экраны, фильтры. var MONTHS = ["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь"]; var MONTHS_SHORT = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен"]; var GROUPS = ["Связь","Интернет","ТВ","Устройства","Услуги"]; var GROUP_COLORS = ["var(--kt-ai-chart-blue)","var(--kt-ai-chart-green)","var(--kt-ai-chart-orange)","var(--kt-ai-chart-pink)","var(--kt-ai-chart-salmon)"]; // данные[группа][месяц] = { revenue (млн ₸), orders (шт) } var DATA = { "Связь": [ {revenue:312.4,orders:14820},{revenue:305.1,orders:14410},{revenue:328.9,orders:15260},{revenue:341.2,orders:15780},{revenue:356.8,orders:16340},{revenue:362.4,orders:16520},{revenue:359.0,orders:16390},{revenue:371.5,orders:16980},{revenue:384.7,orders:17540} ], "Интернет": [ {revenue:201.3,orders:8340},{revenue:198.7,orders:8210},{revenue:210.6,orders:8720},{revenue:224.8,orders:9230},{revenue:237.1,orders:9780},{revenue:245.9,orders:10150},{revenue:243.2,orders:10020},{revenue:254.6,orders:10480},{revenue:268.3,orders:11020} ], "ТВ": [ {revenue:118.6,orders:5120},{revenue:115.2,orders:4980},{revenue:121.9,orders:5260},{revenue:128.4,orders:5510},{revenue:133.7,orders:5730},{revenue:139.2,orders:5940},{revenue:137.5,orders:5860},{revenue:142.8,orders:6070},{revenue:149.6,orders:6340} ], "Устройства": [ {revenue:87.2,orders:2740},{revenue:84.9,orders:2660},{revenue:91.3,orders:2870},{revenue:96.7,orders:3050},{revenue:102.4,orders:3240},{revenue:108.1,orders:3420},{revenue:106.8,orders:3370},{revenue:112.5,orders:3550},{revenue:118.9,orders:3760} ], "Услуги": [ {revenue:56.4,orders:4310},{revenue:54.8,orders:4210},{revenue:58.2,orders:4440},{revenue:61.9,orders:4680},{revenue:65.3,orders:4890},{revenue:67.8,orders:5060},{revenue:66.9,orders:5000},{revenue:70.2,orders:5210},{revenue:74.5,orders:5480} ] }; var state = { screen: "overview", period: "year", metric: "revenue", mixMonth: "year", search: "", sort: { key: "label", dir: 1 }, detailPeriod: "year", detailGroup: "all", mixFilter: "all" }; var ui = { sortDetail: { key: "revenue", dir: -1 } }; var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); }; var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); }; function byId(id) { return document.getElementById(id); } function el(name, attrs, children) { var node = document.createElement(name); if (attrs) for (var k in attrs) if (node.setAttribute) { if (k === "text") node.textContent = attrs[k]; else node.setAttribute(k, attrs[k]); } if (children) children.forEach(function (c) { if (c) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c); }); return node; } function fmt(n, dec) { var s = Number(n).toLocaleString("ru-RU", { minimumFractionDigits: dec == null ? 1 : dec, maximumFractionDigits: dec == null ? 1 : dec }); return s; } function fmtInt(n) { return Number(n).toLocaleString("ru-RU"); } function pct(n) { return ((Number(n) * 100)).toFixed(1).replace(".", ",") + " %"; } // ---------- сборка месяцев ---------- var cacheMonths = null; function monthRows() { if (cacheMonths) return cacheMonths; cacheMonths = []; for (var i = 0; i < MONTHS.length; i++) { var rev = 0, ord = 0; GROUPS.forEach(function (g) { rev += DATA[g][i].revenue; ord += DATA[g][i].orders; }); cacheMonths.push({ idx: i, label: MONTHS[i], rev: rev, ord: ord, avg: rev * 1000000 / ord }); } return cacheMonths; } function sumRange(idx) { var rows = monthRows().slice(idx[0], idx[1] + 1); var rev = 0, ord = 0; rows.forEach(function (r) { rev += r.rev; ord += r.ord; }); return { rev: rev, ord: ord }; } function quarterIndex(q) { var s = (q - 1) * 3; return [s, s + 2]; } function periodRange(period) { if (period === "year") return [0, 8]; if (period === "q1") return quarterIndex(1); if (period === "q2") return quarterIndex(2); if (period === "q3") return quarterIndex(3); return [8, 8]; } function periodLabel(period) { if (period === "year") return "Янв — Сен 2026"; if (period === "q1") return "Январь — Март 2026"; if (period === "q2") return "Апрель — Июнь 2026"; if (period === "q3") return "Июль — Сентябрь 2026"; if (period === "sep") return "Сентябрь 2026"; return "Сентябрь 2026"; } // ---------- KPI ---------- function renderKpis(id) { var range = periodRange(state.period); var cur = sumRange(range); var growth = prevMonthGrowth(); byId(id).innerHTML = ""; byId(id).appendChild(kpi(fmt(cur.rev), "Выручка, млн ₸", periodLabel(state.period))); byId(id).appendChild(kpi(fmtInt(cur.ord), "Заказы", periodLabel(state.period))); byId(id).appendChild(kpi(fmt(cur.rev * 1000000 / cur.ord / 1000, 1) + " т. ₸", "Средний чек", "за период")); byId(id).appendChild(kpi((growth >= 0 ? "+" : "") + fmt(growth / 100, 1) + " %", "К пред. месяцу", "Сентябрь к Августу")); } function prevMonthGrowth() { var rows = monthRows(); var a = rows[rows.length - 1], b = rows[rows.length - 2]; return (a.rev - b.rev) / b.rev * 100; } function kpi(value, label, hint) { return el("div", { "class": "kt-ai-kpi" }, [ el("div", { "class": "value", text: value }), el("div", { "class": "label", text: label }), el("div", { "class": "hint", text: hint }) ]); } // ---------- график: динамика ---------- function renderChartOverview() { var chart = byId("chart-overview"); chart.innerHTML = ""; var range = periodRange(state.period); var idxs = []; for (var i = range[0]; i <= range[1]; i++) idxs.push(i); var values = idxs.map(function (i) { if (state.metric === "revenue") { var s = 0; GROUPS.forEach(function (g) { s += DATA[g][i].revenue; }); return s; } if (state.metric === "orders") { var s2 = 0; GROUPS.forEach(function (g) { s2 += DATA[g][i].orders; }); return s2; } return 0; }); var max = Math.max.apply(null, values); values.forEach(function (v, j) { var i = idxs[j]; var h = Math.max(4, Math.round(v / max * 100)); var bar = el("div", { "class": "cc-bar", style: "height:" + h + "%" }); if (state.metric === "orders") bar.setAttribute("data-series", "green"); bar.setAttribute("data-kt-tip", JSON.stringify({ title: MONTHS[i] + " 2026", rows: [[ "Выручка", fmt(monthRows()[i].rev) + " млн ₸" ], [ "Заказы", fmtInt(monthRows()[i].ord) ]] })); var col = el("div", { "class": "cc-col" }, [bar, el("div", { "class": "cc-label", text: MONTHS_SHORT[i] })]); chart.appendChild(col); }); var legend = byId("legend-overview"); legend.innerHTML = ""; legend.appendChild(legendItem(state.metric === "revenue" ? "Выручка" : "Заказы", state.metric === "revenue" ? "Var(--kt-ai-chart-blue)" : "var(--kt-ai-chart-green)")); } function legendItem(name, color) { var item = el("span", { "class": "kt-ai-legend-item" }, [ el("span", { "class": "kt-ai-legend-swatch", style: "background:" + color }), document.createTextNode(name) ]); return item; } // ---------- структура: mix ---------- function renderMix() { var range = periodRange(state.period); var allTotals = GROUPS.map(function (g) { var s = 0; for (var i = range[0]; i <= range[1]; i++) s += DATA[g][i].revenue; return s; }); var allSum = allTotals.reduce(function (a, b) { return a + b; }, 0); var chips = byId("mix-chips"); chips.innerHTML = ""; var chipAll = el("button", { "class": "kt-ai-filter-chip", type: "button", text: "Все" }); if (state.mixFilter === "all") chipAll.setAttribute("data-active", "true"); chipAll.addEventListener("click", function () { state.mixFilter = "all"; renderMix(); }); chips.appendChild(chipAll); GROUPS.forEach(function (g, k) { var b = el("button", { "class": "kt-ai-filter-chip", type: "button", text: g }); if (state.mixFilter === g) b.setAttribute("data-active", "true"); b.addEventListener("click", function () { state.mixFilter = g; renderMix(); }); chips.appendChild(b); }); var visible = state.mixFilter === "all" ? GROUPS : [state.mixFilter]; var totals = {}; visible.forEach(function (g) { var k = GROUPS.indexOf(g); totals[g] = { v: allTotals[k], c: GROUP_COLORS[k] }; }); var bars = byId("mix-bars"); bars.innerHTML = ""; var sorted = visible.map(function (g) { return { g: g, v: totals[g].v, c: totals[g].c }; }) .sort(function (a, b) { return b.v - a.v; }); var vSum = sorted.reduce(function (a, r) { return a + r.v; }, 0); sorted.forEach(function (row) { var share = vSum ? row.v / vSum * 100 : 0; bars.appendChild(el("div", { "class": "kt-ai-mix-row" }, [ el("div", { "class": "kt-ai-mix-name", text: row.g }), el("div", { "class": "kt-ai-mix-track" }, [el("div", { "class": "kt-ai-mix-fill", style: "width:" + share + "%;background:" + row.c })]), el("div", { "class": "kt-ai-mix-pct", text: share.toFixed(1).replace(".", ",") + " %" }) ])); }); var tbody = $("#table-mix tbody"); tbody.innerHTML = ""; sorted.forEach(function (row) { var share = vSum ? row.v / vSum * 100 : 0; tbody.appendChild(el("tr", null, [ el("td", null, [el("span", { "class": "kt-ai-legend-swatch", style: "display:inline-block;width:8px;height:8px;margin-right:6px;background:" + row.c + ";border-radius:2px;vertical-align:middle" }), document.createTextNode(row.g)]), el("td", { "class": "rt", text: fmt(row.v) }), el("td", { "class": "rt", text: share.toFixed(1).replace(".", ",") + " %" }) ])); }); } // ---------- детали: KPI ---------- function renderKpisDetail() { var range = periodRange(state.detailPeriod); var cur = sumRange(range); var groupsData = GROUPS.map(function (g) { var rev = 0, ord = 0; for (var i = range[0]; i <= range[1]; i++) { rev += DATA[g][i].revenue; ord += DATA[g][i].orders; } return { g: g, rev: rev, ord: ord }; }); var top = groupsData.slice().sort(function (a, b) { return b.rev - a.rev; })[0]; byId("kpi-detail").innerHTML = ""; byId("kpi-detail").appendChild(kpi(fmt(cur.rev), "Выручка, млн ₸", periodLabel(state.detailPeriod))); byId("kpi-detail").appendChild(kpi(fmtInt(cur.ord), "Заказы", periodLabel(state.detailPeriod))); byId("kpi-detail").appendChild(kpi(top.g, "Лидер по выручке", fmt(top.rev) + " млн ₸ · " + (top.rev / cur.rev * 100).toFixed(1).replace(".", ",") + " %")); byId("kpi-detail").appendChild(kpi(fmt(cur.rev * 1000000 / cur.ord / 1000, 1) + " т. ₸", "Средний чек", "за период")); } // ---------- детали: таблица месяцев ---------- function renderMonths() { var rows = monthRows(); var q = state.search.trim().toLowerCase(); var shown = rows.filter(function (r) { return !q || r.label.toLowerCase().indexOf(q) >= 0; }); var key = state.sort.key, dir = state.sort.dir; shown.sort(function (a, b) { var va = a[key], vb = b[key]; if (typeof va === "string") return va.localeCompare(vb, "ru") * dir; return (va - vb) * dir; }); var tbody = $("#table-months tbody"); tbody.innerHTML = ""; shown.forEach(function (r, i) { var prev = i > 0 ? shown[i - 1] : null; var mom = prev ? (r.rev - prev.rev) / prev.rev * 100 : null; var momCell; if (mom == null) momCell = el("span", { "class": "kt-ai-meta", text: "—" }); else momCell = el("span", { "class": "kt-ai-status-pill", "data-status": mom >= 0 ? "ok" : "risk", text: (mom >= 0 ? "+" : "") + mom.toFixed(1).replace(".", ",") + "%" }); tbody.appendChild(el("tr", { "class": "kt-ai-row-hover" }, [ el("td", { text: r.label }), el("td", { "class": "rt", text: fmt(r.rev) }), el("td", { "class": "rt", text: fmtInt(r.ord) }), el("td", { "class": "rt", text: fmtInt(Math.round(r.avg)) }), el("td", { "class": "rt" }, [momCell]) ])); }); byId("empty-months").hidden = shown.length !== 0; byId("table-months").style.display = shown.length ? "" : "none"; // aria-sort $$("#table-months th.sortable").forEach(function (th) { if (th.getAttribute("data-sort") === key) th.setAttribute("aria-sort", dir > 0 ? "ascending" : "descending"); else th.removeAttribute("aria-sort"); }); } // ---------- детали: разбивка ---------- function renderDetail() { var range = periodRange(state.detailPeriod); var idxs = []; for (var i = range[0]; i <= range[1]; i++) idxs.push(i); var groups = state.detailGroup === "all" ? GROUPS : [state.detailGroup]; var rows = []; var total = 0; idxs.forEach(function (i) { var rev = 0, ord = 0; groups.forEach(function (g) { rev += DATA[g][i].revenue; ord += DATA[g][i].orders; }); rows.push({ label: MONTHS[i], rev: rev, ord: ord, avg: rev * 1000000 / ord }); total += rev; }); rows.forEach(function (r) { r.share = total ? r.rev / total : 0; }); var key = ui.sortDetail.key, dir = ui.sortDetail.dir; rows.sort(function (a, b) { var va = a[key], vb = b[key]; if (typeof va === "string") return va.localeCompare(vb, "ru") * dir; return (va - vb) * dir; }); // график: столбцы по периодам var chart = byId("chart-detail"); chart.innerHTML = ""; var max = Math.max.apply(null, idxs.map(function (i) { var s = 0; groups.forEach(function (g) { s += DATA[g][i].revenue; }); return s; })); idxs.forEach(function (i) { var s = 0; groups.forEach(function (g) { s += DATA[g][i].revenue; }); var h = Math.max(4, Math.round(s / max * 100)); var bar = el("div", { "class": "cc-bar", style: "height:" + h + "%" }); bar.setAttribute("data-kt-tip", JSON.stringify({ title: MONTHS[i] + " 2026", rows: [[ "Выручка", fmt(s) + " млн ₸" ]] })); var col = el("div", { "class": "cc-col" }, [bar, el("div", { "class": "cc-label", text: MONTHS_SHORT[i] })]); chart.appendChild(col); }); var tbody = $("#table-detail tbody"); tbody.innerHTML = ""; rows.forEach(function (r) { tbody.appendChild(el("tr", null, [ el("td", { text: r.label }), el("td", { "class": "rt", text: fmt(r.rev) }), el("td", { "class": "rt", text: fmtInt(r.ord) }), el("td", { "class": "rt", text: fmtInt(Math.round(r.avg)) }), el("td", { "class": "rt", text: pct(r.share) }) ])); }); byId("empty-detail").hidden = rows.length !== 0; $$("#table-detail th.sortable").forEach(function (th) { if (th.getAttribute("data-sort") === key) th.setAttribute("aria-sort", dir > 0 ? "ascending" : "descending"); else th.removeAttribute("aria-sort"); }); } // ---------- тулбар ---------- function renderPeriodTabs() { var tabs = [ { id: "year", label: "Год" }, { id: "q1", label: "Q1" }, { id: "q2", label: "Q2" }, { id: "q3", label: "Q3" } ]; var box = byId("period-tabs"); box.classList.add("kt-ai-segmented"); box.innerHTML = ""; tabs.forEach(function (t) { var b = el("button", { "class": "seg", type: "button", text: t.label }); if (state.period === t.id) b.setAttribute("data-active", "true"); b.addEventListener("click", function () { state.period = t.id; byId("period-chip").textContent = periodLabel(state.period); renderAll(); }); box.appendChild(b); }); var mt = byId("metric-tabs"); mt.innerHTML = ""; [["revenue", "Выручка"], ["orders", "Заказы"]].forEach(function (m) { var b = el("button", { "class": "seg", type: "button", text: m[1] }); if (state.metric === m[0]) b.setAttribute("data-active", "true"); b.addEventListener("click", function () { state.metric = m[0]; renderAll(); }); mt.appendChild(b); }); var ms = byId("month-select"); ms.innerHTML = ""; [["year", "Янв — Сен"], ["q1", "Q1"], ["q2", "Q2"], ["q3", "Q3"], ["sep", "Сен"]].forEach(function (m) { var b = el("button", { "class": "seg", type: "button", text: m[1] }); if (state.period === m[0]) b.setAttribute("data-active", "true"); b.addEventListener("click", function () { state.period = m[0]; byId("period-chip").textContent = periodLabel(state.period); renderAll(); }); ms.appendChild(b); }); var selP = byId("sel-period"); selP.innerHTML = ""; [["year", "Год (Янв — Сен)"], ["q1", "Q1 (Янв — Мар)"], ["q2", "Q2 (Апр — Июн)"], ["q3", "Q3 (Июл — Сен)"]].forEach(function (o) { var opt = el("option", { value: o[0], text: o[1] }); if (state.detailPeriod === o[0]) opt.setAttribute("selected", ""); selP.appendChild(opt); }); selP.onchange = function () { state.detailPeriod = selP.value; renderKpisDetail(); renderDetail(); }; var selG = byId("sel-group"); selG.innerHTML = ""; var optAll = el("option", { value: "all", text: "Все группы" }); if (state.detailGroup === "all") optAll.setAttribute("selected", ""); selG.appendChild(optAll); GROUPS.forEach(function (g) { var opt = el("option", { value: g, text: g }); if (state.detailGroup === g) opt.setAttribute("selected", ""); selG.appendChild(opt); }); selG.onchange = function () { state.detailGroup = selG.value; renderDetail(); }; byId("period-chip").textContent = periodLabel(state.period); } // ---------- переключение экранов ---------- function showScreen(name) { state.screen = name; byId("screen-overview").hidden = name !== "overview"; byId("screen-detail").hidden = name !== "detail"; byId("screen-title").textContent = name === "overview" ? "Обзор" : "Детали"; $$(".kt-ai-nav-item").forEach(function (a) { if (a.getAttribute("data-screen") === name) a.classList.add("is-active"); else a.classList.remove("is-active"); }); } function renderAll() { renderKpis("kpi-overview"); renderChartOverview(); renderMix(); renderKpisDetail(); renderMonths(); renderDetail(); renderPeriodTabs(); } // ---------- события ---------- document.addEventListener("DOMContentLoaded", function () { // навигация $$(".kt-ai-nav-item").forEach(function (a) { a.addEventListener("click", function (e) { e.preventDefault(); showScreen(a.getAttribute("data-screen")); }); }); // сортировка table-months $$("#table-months th.sortable").forEach(function (th) { th.addEventListener("click", function () { var k = th.getAttribute("data-sort"); if (state.sort.key === k) state.sort.dir = -state.sort.dir; else { state.sort.key = k; state.sort.dir = k === "label" ? 1 : -1; } renderMonths(); }); }); // сортировка table-detail $$("#table-detail th.sortable").forEach(function (th) { th.addEventListener("click", function () { var k = th.getAttribute("data-sort"); if (ui.sortDetail.key === k) ui.sortDetail.dir = -ui.sortDetail.dir; else { ui.sortDetail.key = k; ui.sortDetail.dir = 1; } renderDetail(); }); }); // поиск var search = byId("search-months"); if (search) search.addEventListener("input", function () { state.search = search.value; renderMonths(); }); var clear = byId("clear-search"); if (clear) clear.addEventListener("click", function () { state.search = ""; if (search) search.value = ""; renderMonths(); }); showScreen("overview"); renderAll(); });