dashbord-roznichnyh-prodazh-4/script.js

413 lines
19 KiB
JavaScript
Raw Permalink 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.

// Дашборд розничных продаж 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();
});