dashbord-po-prodazham/script.js

451 lines
20 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.

// Дашборд по продажам — статика, состояние в localStorage.
var DATA = {
months: ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен"],
monthsFull: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь"],
cats: [
{ id: "internet", name: "Интернет" },
{ id: "tv", name: "Телевидение" },
{ id: "mobile", name: "Мобильные услуги" },
{ id: "corporate", name: "Корпоративные услуги" },
{ id: "digital", name: "Цифровые сервисы" }
],
unit: "млн тг",
// 5 категорий × 9 месяцев (факт 2026 и сравнение 2025), млн тг
v: {
internet: [3520, 3580, 3610, 3590, 3720, 3800, 3890, 3940, 4010],
tv: [1810, 1830, 1850, 1840, 1870, 1890, 1910, 1920, 1950],
mobile: [2950, 2990, 3020, 3060, 3040, 3110, 3180, 3260, 3310],
corporate: [1620, 1650, 1660, 1720, 1740, 1770, 1810, 1840, 1900],
digital: [940, 1010, 1080, 1160, 1280, 1410, 1560, 1710, 1880]
},
pv: {
internet: [3310, 3350, 3380, 3360, 3420, 3470, 3520, 3550, 3590],
tv: [1860, 1850, 1840, 1830, 1820, 1810, 1800, 1790, 1780],
mobile: [2720, 2760, 2800, 2810, 2790, 2840, 2890, 2950, 3010],
corporate: [1520, 1540, 1550, 1580, 1600, 1620, 1650, 1670, 1700],
digital: [690, 740, 800, 870, 960, 1050, 1170, 1280, 1400]
}
};
var PERIODS = [
{ id: "m1_9", label: "Янв – Сен", from: 0, to: 8 },
{ id: "q1", label: "I квартал", from: 0, to: 2 },
{ id: "q2", label: "II квартал", from: 3, to: 5 },
{ id: "q3", label: "III квартал", from: 6, to: 8 }
];
var PALETTE = ["--kt-ai-chart-blue", "--kt-ai-chart-green", "--kt-ai-chart-orange", "--kt-ai-chart-salmon", "--kt-ai-chart-pink"];
var LS_KEY = "dash-sales-v1";
var state = load();
function load() {
try {
var raw = localStorage.getItem(LS_KEY);
if (raw) {
var s = JSON.parse(raw);
if (s && typeof s === "object") return { screen: s.screen || "overview", period: s.period || "m1_9", cat: s.cat || "all" };
}
} catch (e) {}
return { screen: "overview", period: "m1_9", cat: "all" };
}
function save() {
try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {}
}
// ---------- данные ----------
function period() {
for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === state.period) return PERIODS[i];
return PERIODS[0];
}
function monthRange() {
var p = period();
var r = [];
for (var i = p.from; i <= p.to; i++) r.push(i);
return r;
}
function seriesOf(catId, monthIdx) {
var y = DATA.v[catId] || [], p = DATA.pv[catId] || [];
return { cur: y[monthIdx] || 0, prev: p[monthIdx] || 0 };
}
function sumSeries(catId) {
var r = monthRange(), cur = 0, prev = 0;
for (var i = 0; i < r.length; i++) {
var s = seriesOf(catId, r[i]);
cur += s.cur; prev += s.prev;
}
return { cur: cur, prev: prev };
}
function activeCats() {
if (state.cat === "all") return DATA.cats.slice();
for (var i = 0; i < DATA.cats.length; i++) if (DATA.cats[i].id === state.cat) return [DATA.cats[i]];
return DATA.cats.slice();
}
function totals() {
var cur = 0, prev = 0, cats = activeCats();
for (var i = 0; i < cats.length; i++) {
var t = sumSeries(cats[i].id);
cur += t.cur; prev += t.prev;
}
return { cur: cur, prev: prev };
}
function deltaPct(cur, prev) {
if (!prev) return null;
return (cur - prev) / prev * 100;
}
// ---------- форматирование ----------
function fmtInt(n) {
var s = String(Math.round(n));
var out = "", c = 0;
for (var i = s.length - 1; i >= 0; i--) {
out = s.charAt(i) + out;
c++;
if (c % 3 === 0 && i > 0) out = " " + out;
}
return out;
}
function fmtMoney(v) { return fmtInt(v) + " " + DATA.unit; }
function fmtPct(n) {
var sign = n >= 0 ? "+" : "−";
return sign + Math.abs(n).toFixed(1).replace(".", ",") + "%";
}
// ---------- рендер: сводка ----------
function kpiCard(label, value, hint, dataLong) {
var hl = dataLong ? ' data-long="true"' : "";
return '<div class="kt-ai-kpi"><span class="value"' + hl + '>' + value +
'</span><span class="label">' + label + '</span><span class="hint">' + hint + '</span></div>';
}
function barChart() {
var r = monthRange(), cats = activeCats();
var max = 0;
var sums = [];
for (var i = 0; i < r.length; i++) {
var m = 0;
for (var c = 0; c < cats.length; c++) m += (DATA.v[cats[c].id] || [])[r[i]] || 0;
sums.push(m);
if (m > max) max = m;
}
var bars = "";
var axis = "";
for (var i = 0; i < r.length; i++) {
var h = max ? Math.max(2, Math.round((sums[i] / max) * 100)) : 2;
var tip = JSON.stringify(DATA.monthsFull[r[i]] + " " + (state.cat === "all" ? "— все категории" : DATA.cats[state.cat] ? DATA.cats[state.cat].name : "") + ": " + fmtMoney(sums[i]));
bars += '<div class="cc-bar" tabindex="0" data-kt-tip="' + tip.replace(/"/g, "&quot;") + '"><i style="height:' + h + '%"></i></div>';
axis += '<span>' + DATA.months[r[i]] + '</span>';
}
var p = period();
var label = (state.cat === "all" ? "Все категории" : catName()) + ", " + p.label.toLowerCase() + " 2026";
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Выручка по месяцам, ' + label + '</div></div>' +
'<div class="cc-total"><span class="num">' + fmtInt(Math.max.apply(null, sums.concat([1]))) + '</span><span class="unit">' + DATA.unit + "</span></div></div>" +
'<div class="cc-bars">' + bars + '</div><div class="cc-axis">' + axis + "</div></section>";
}
function catName() {
for (var i = 0; i < DATA.cats.length; i++) if (DATA.cats[i].id === state.cat) return DATA.cats[i].name;
return "";
}
function donut() {
var cats = DATA.cats, vals = [], total = 0;
for (var i = 0; i < cats.length; i++) {
var t = sumSeries(cats[i].id);
vals.push(t.cur);
total += t.cur;
}
if (!total) return "";
var segs = "", off = 25;
for (var i = 0; i < cats.length; i++) {
var share = (vals[i] / total) * 100;
var c = "var(" + PALETTE[i % PALETTE.length] + ")";
segs += '<circle r="15.9155" cx="21" cy="21" fill="none" stroke="' + c + '" stroke-width="5.5" stroke-dasharray="' +
share.toFixed(2) + " " + (100 - share).toFixed(2) + '" stroke-dashoffset="' + off.toFixed(2) + '" tabindex="0" ' +
'data-kt-tip="' + cats[i].name + ": " + fmtInt(vals[i]) + " " + DATA.unit + " (" + Math.round(share) + "%)"></circle>";
off -= share;
}
var rows = "";
for (var i = 0; i < cats.length; i++) {
var c = "var(" + PALETTE[i % PALETTE.length] + ")";
var dim = (state.cat !== "all" && cats[i].id !== state.cat) ? ' style="opacity:.4"' : "";
rows += '<div class="ds-donut-row"' + dim + '><span class="sw" style="background:' + c + '"></span>' +
'<span class="lbl">' + cats[i].name + '</span>' +
'<span class="val">' + fmtInt(vals[i]) + '</span>' +
'<span class="pc">' + Math.round((vals[i] / total) * 100) + "%</span></div>";
}
return '<div class="ds-donut"><svg viewBox="0 0 42 42" role="img" aria-label="Структура по категориям">' + segs +
'<text x="21" y="21.5" text-anchor="middle" dominant-baseline="middle" font-size="5.6" font-weight="600" fill="var(--kt-ai-fg)">' +
"∑</text></svg><div class='ds-donut-list'>" + rows + "</div></div>";
}
function yoyChart() {
var cats = DATA.cats;
var max = 0, rows = [];
for (var i = 0; i < cats.length; i++) {
var t = sumSeries(cats[i].id);
if (t.cur > max) max = t.cur;
rows.push({ cat: cats[i], t: t });
}
if (!max) return "";
var body = "";
for (var i = 0; i < rows.length; i++) {
var rr = rows[i];
var w = Math.max(2, Math.round((rr.t.cur / max) * 100));
var c = "var(" + PALETTE[i % PALETTE.length] + ")";
var dim = (state.cat !== "all" && rr.cat.id !== state.cat) ? ' style="opacity:.4"' : "";
body += '<div' + dim + ' style="display:grid;grid-template-columns:minmax(90px,32%) 1fr 64px;gap:var(--kt-ai-space-3);align-items:center;padding:4px 0;font-size:var(--kt-ai-text-xs)">' +
'<span style="line-height:1.3">' + rr.cat.name + "</span>" +
'<span style="height:8px;border-radius:4px;background:var(--kt-ai-bg-soft);overflow:hidden;display:block">' +
'<span style="display:block;height:100%;border-radius:4px;width:' + w + "%;background:" + c + '"></span></span>' +
'<span class="kt-ai-num" style="text-align:right;font-weight:600">' + fmtInt(rr.t.cur) + '</span></div>';
}
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Структура по категориям, ' + period().label.toLowerCase() + " 2026</div></div>" +
'<div class="cc-total"><span class="num">' + fmtInt(max) + '</span><span class="unit">' + DATA.unit + "</span></div></div>" +
body + "</section>";
}
function summaryText() {
var t = totals(), cats = DATA.cats;
var best = null, worst = null;
for (var i = 0; i < cats.length; i++) {
var tt = sumSeries(cats[i].id);
var d = deltaPct(tt.cur, tt.prev);
if (d === null) continue;
if (best === null || d > best.d) best = { c: cats[i], d: d, t: tt };
if (worst === null || d < worst.d) worst = { c: cats[i], d: d, t: tt };
}
var p = period();
var h = '<section class="kt-ai-conclusion"><span class="title">Что показывают эти числа</span>';
h += '<span class="text">Выручка за ' + p.label.toLowerCase() + " 2026 — " + fmtMoney(t.cur) +
(deltaPct(t.cur, t.prev) !== null ? ", к прошлому году " + fmtPct(deltaPct(t.cur, t.prev)) : "") +
(state.cat === "all" ? " по всем категориям." : " в категории «" + catName() + "».") + "</span>";
if (best && worst && best.c.id !== worst.c.id) {
h += '<span class="text">Быстрее всего растёт «' + best.c.name + "» (" + fmtPct(best.d) +
"), а отстает «" + worst.c.name + "» (" + fmtPct(worst.d) + ")." + "</span>";
} else if (best) {
h += '<span class="text">Лучший результат по динамике — «' + best.c.name + "» (" + fmtPct(best.d) + ")." + "</span>";
}
return h + "</section>";
}
function renderOverview() {
var t = totals(), d = deltaPct(t.cur, t.prev);
var h = '<div class="kt-ai-kpi-strip" data-cards="true">';
h += kpiCard("Выручка", fmtMoney(t.cur), period().label + " 2026", true);
h += kpiCard("К прошлому году", d !== null ? fmtPct(d) : "—", period().label + " 2025", false);
var cats = DATA.cats, maxT = 0, lead = null;
for (var i = 0; i < cats.length; i++) {
var tt = sumSeries(cats[i].id);
if (tt.cur > maxT) { maxT = tt.cur; lead = cats[i]; }
}
var shareLead = maxT && t.cur ? Math.round(maxT / t.cur * 100) : 0;
h += kpiCard("Ведущая категория", lead ? lead.name : "—", shareLead + "% от выручки", true);
h += kpiCard("Средний месяц", fmtMoney(Math.round(t.cur / monthRange().length)), "за " + monthRange().length + " мес.", true);
h += "</div>";
h += '<div class="ds-grid-2">' + barChart() + yoyChart() + "</div>";
if (state.cat === "all") {
h += '<div class="ds-h3">Структура выручки по категориям</div>' +
'<section class="kt-ai-chart-card">' + donut() + "</section>";
}
h += summaryText();
return h;
}
// ---------- рендер: детали ----------
function detailsToolbar() {
var pOpts = "";
for (var i = 0; i < PERIODS.length; i++) {
pOpts += '<option value="' + PERIODS[i].id + '"' + (PERIODS[i].id === state.period ? " selected" : "") + ">" + PERIODS[i].label + "</option>";
}
var cOpts = '<option value="all"' + (state.cat === "all" ? " selected" : "") + ">Все категории</option>";
for (var i = 0; i < DATA.cats.length; i++) {
cOpts += '<option value="' + DATA.cats[i].id + '"' + (DATA.cats[i].id === state.cat ? " selected" : "") + ">" + DATA.cats[i].name + "</option>";
}
return '<div class="ds-filters">' +
'<select class="kt-ai-select" id="fPeriod" aria-label="Период">' + pOpts + "</select>" +
'<select class="kt-ai-select" id="fCat" aria-label="Категория">' + cOpts + "</select>" +
'<button class="kt-ai-btn ds-reset" data-variant="ghost" id="fReset">Сбросить</button></div>';
}
function detailsKpis() {
var t = totals();
var curCat = activeCats().length === 1 ? activeCats()[0] : null;
var h = '<div class="kt-ai-kpi-strip" data-cards="true">';
h += kpiCard(curCat ? curCat.name : "Все категории", fmtMoney(t.cur), period().label + " 2026", true);
var d = deltaPct(t.cur, t.prev);
h += kpiCard("К прошлому году", d !== null ? fmtPct(d) : "—", period().label + " 2025", false);
h += kpiCard("Средний месяц", fmtMoney(Math.round(t.cur / monthRange().length)), "за " + monthRange().length + " мес.", true);
if (state.cat === "all") {
var lead = null, maxT2 = 0;
for (var i = 0; i < DATA.cats.length; i++) {
var tt = sumSeries(DATA.cats[i].id);
if (tt.cur > maxT2) { maxT2 = tt.cur; lead = DATA.cats[i]; }
}
h += kpiCard("Ведущая категория", lead ? lead.name : "—", Math.round(maxT2 / (t.cur || 1) * 100) + "% от выручки", true);
} else {
var totalAll = 0;
for (var j = 0; j < DATA.cats.length; j++) totalAll += sumSeries(DATA.cats[j].id).cur;
h += kpiCard("Доля в выручке", Math.round(t.cur / (totalAll || 1) * 100) + "%", "за период", false);
}
return h + "</div>";
}
function detailsTable() {
var r = monthRange();
var cats = activeCats();
var totalCur = 0, totalPrev = 0, totalDelta = 0;
var rows = "";
for (var i = 0; i < r.length; i++) {
var mi = r[i];
var mCur = 0, mPrev = 0;
for (var c = 0; c < cats.length; c++) {
var s = seriesOf(cats[c].id, mi);
mCur += s.cur; mPrev += s.prev;
}
totalCur += mCur; totalPrev += mPrev; totalDelta += mCur - mPrev;
rows += '<tr><td>' + DATA.monthsFull[mi] + '</td>' +
'<td class="num">' + fmtMoney(mCur) + "</td>" +
'<td class="num">' + fmtMoney(mPrev) + "</td>" +
'<td class="num">' + (mCur - mPrev >= 0 ? "+" : "−") + fmtInt(Math.abs(mCur - mPrev)) + "</td>" +
'<td class="num">' + (mPrev ? fmtPct((mCur - mPrev) / mPrev * 100) : "—") + "</td></tr>";
}
var footDelta = totalDelta;
return '<div class="kt-ai-table-wrap"><table class="kt-ai-table">' +
"<thead><tr><th>Месяц</th>" +
'<th class="num">Выручка 2026</th><th class="num">' + period().label + " 2025</th>" +
'<th class="num">Δ, млн тг</th><th class="num">Δ, %</th></tr></thead>' +
"<tbody>" + rows +
'<tr style="border-top:1px solid var(--kt-ai-border-strong)"><td>Итого</td>' +
'<td class="num" style="font-weight:600">' + fmtMoney(totalCur) + "</td>" +
'<td class="num">' + fmtMoney(totalPrev) + "</td>" +
'<td class="num">' + (footDelta >= 0 ? "+" : "−") + fmtInt(Math.abs(footDelta)) + "</td>" +
'<td class="num">' + (totalPrev ? fmtPct((totalCur - totalPrev) / totalPrev * 100) : "—") + "</td></tr>" +
"</tbody></table></div>";
}
function renderDetails() {
var h = detailsToolbar();
h += detailsKpis();
h += '<div class="ds-h3">По месяцам, ' + period().label.toLowerCase();
h += state.cat === "all" ? " — все категории" : " — " + catName();
h += "</div>";
h += '<div class="kt-ai-chart-card"><div class="cc-bars" style="height:170px">' + detailsBars() + "</div>" +
'<div class="cc-axis">' + detailsAxis() + "</div></div>";
h += detailsTable();
if (state.cat === "all") {
h += '<div class="ds-h3">Разбивка по категориям</div>' + categoryBreakdown();
}
return h;
}
function detailsBars() {
var r = monthRange(), cats = activeCats();
var max = 0, sums = [];
for (var i = 0; i < r.length; i++) {
var m = 0;
for (var c = 0; c < cats.length; c++) m += (DATA.v[cats[c].id] || [])[r[i]] || 0;
sums.push(m);
if (m > max) max = m;
}
var h = "";
for (var i = 0; i < r.length; i++) {
var pct = max ? Math.max(2, Math.round((sums[i] / max) * 100)) : 2;
var tip = JSON.stringify(DATA.monthsFull[r[i]] + ": " + fmtMoney(sums[i]));
h += '<div class="cc-bar" tabindex="0" data-kt-tip="' + tip.replace(/"/g, "&quot;") + '"><i style="height:' + pct + '%"></i></div>';
}
return h;
}
function detailsAxis() {
var r = monthRange(), h = "";
for (var i = 0; i < r.length; i++) h += '<span>' + DATA.months[r[i]] + "</span>";
return h;
}
function categoryBreakdown() {
var totalAll = 0;
var sums = [];
for (var i = 0; i < DATA.cats.length; i++) {
var t = sumSeries(DATA.cats[i].id);
sums.push(t);
totalAll += t.cur;
}
var rows = "";
for (var i = 0; i < DATA.cats.length; i++) {
var t = sums[i];
var d = deltaPct(t.cur, t.prev);
var tone = d === null ? "gray" : (d >= 0 ? "green" : "red");
rows += '<tr><td>' + DATA.cats[i].name + "</td>" +
'<td class="num">' + fmtMoney(t.cur) + "</td><td class=\"num\">" + fmtMoney(t.prev) + "</td>" +
'<td class="num">' + Math.round((t.cur / (totalAll || 1)) * 100) + "%</td>" +
'<td class="num">' + (d !== null ? fmtPct(d) : "—") + "</td></tr>";
}
return '<div class="kt-ai-table-wrap"><table class="kt-ai-table">' +
"<thead><tr><th>Категория</th><th class='num'>2026, " + period().label + '</th><th class="num">2025, ' + period().label +
"</th><th class='num'>Доля</th><th class='num'>Δ, %</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
}
// ---------- главный рендер ----------
function render() {
var view = document.getElementById("view");
if (!view) return;
var p = period();
var title = document.getElementById("pageTitle");
var sub = document.getElementById("pageSub");
if (title) title.textContent = state.screen === "overview" ? "Сводка" : "Детали";
if (sub) sub.textContent = p.label + " · " + (state.cat === "all" ? "все категории" : catName());
view.innerHTML = '<div class="ds-screen" data-screen="overview"' + (state.screen === "overview" ? ' data-active="true"' : "") + ">" + renderOverview() + "</div>" +
'<div class="ds-screen" data-screen="details"' + (state.screen === "details" ? ' data-active="true"' : "") + ">" + renderDetails() + "</div>";
bindDetails();
bindNav();
}
function bindNav() {
var items = document.querySelectorAll(".ds-nav .kt-ai-nav-item");
for (var i = 0; i < items.length; i++) {
items[i].setAttribute("data-active", String(items[i].getAttribute("data-screen") === state.screen));
}
}
function go(screen) {
if (state.screen === screen) return;
state.screen = screen;
save();
render();
var view = document.getElementById("view");
if (view) view.scrollTop = 0;
window.scrollTo(0, 0);
}
function bindDetails() {
var fp = document.getElementById("fPeriod");
var fc = document.getElementById("fCat");
var fr = document.getElementById("fReset");
if (fp) fp.addEventListener("change", function () {
state.period = fp.value; save(); render();
});
if (fc) fc.addEventListener("change", function () {
state.cat = fc.value; save(); render();
});
if (fr) fr.addEventListener("click", function () {
state.period = "m1_9"; state.cat = "all"; save(); render();
});
}
document.addEventListener("DOMContentLoaded", function () {
render();
var nav = document.querySelectorAll(".ds-nav .kt-ai-nav-item");
for (var i = 0; i < nav.length; i++) {
(function (el) {
el.addEventListener("click", function () {
var s = el.getAttribute("data-screen");
if (s) go(s);
});
})(nav[i]);
}
var burger = document.querySelector(".ds-burger");
if (burger) burger.addEventListener("click", function () {
var sb = document.getElementById("sidebar");
if (sb) sb.toggleAttribute("data-open");
});
});