520 lines
22 KiB
JavaScript
520 lines
22 KiB
JavaScript
"use strict";
|
||
|
||
var STORAGE_KEY = "dash-retail-7-v1";
|
||
|
||
var MONTHS_FULL = [
|
||
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
|
||
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"
|
||
];
|
||
var MONTHS_SHORT = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
||
|
||
var GROUPS = [
|
||
{ id: "connect", name: "Подключение в интернет", unit: "подк." },
|
||
{ id: "tv", name: "Телевидение", unit: "подк." },
|
||
{ id: "mobile", name: "Мобильная связь", unit: "актив." },
|
||
{ id: "smart", name: "Умный дом", unit: "шт." },
|
||
{ id: "devices", name: "Товары и устройства", unit: "шт." }
|
||
];
|
||
|
||
var FACT = {
|
||
connect: [1240, 1410, 1585, 1730, 1688, 1802, 1755, 1904, 1861, 2015, 2124, 2260],
|
||
tv: [980, 1045, 1120, 1188, 1253, 1204, 1316, 1382, 1410, 1466, 1523, 1590],
|
||
mobile: [2410, 2585, 2742, 2690, 2914, 3025, 2876, 3102, 3248, 3390, 3512, 3640],
|
||
smart: [85, 98, 121, 134, 149, 162, 158, 171, 189, 204, 218, 236],
|
||
devices: [1680, 1742, 1815, 1938, 2011, 1966, 2104, 2246, 2318, 2455, 2590, 2742]
|
||
};
|
||
|
||
var PLAN = {
|
||
connect: [1200, 1360, 1500, 1640, 1740, 1820, 1860, 1950, 2020, 2100, 2180, 2280],
|
||
tv: [950, 1030, 1110, 1180, 1260, 1300, 1360, 1420, 1480, 1540, 1600, 1660],
|
||
mobile: [2350, 2520, 2690, 2830, 2960, 3080, 3180, 3290, 3400, 3510, 3620, 3730],
|
||
smart: [80, 95, 110, 125, 140, 155, 168, 180, 192, 206, 220, 235],
|
||
devices: [1650, 1740, 1860, 1980, 2080, 2150, 2230, 2340, 2450, 2560, 2680, 2800]
|
||
};
|
||
|
||
var PALETTE = [
|
||
"var(--kt-ai-chart-blue)",
|
||
"var(--kt-ai-chart-green)",
|
||
"var(--kt-ai-chart-orange)",
|
||
"var(--kt-ai-chart-salmon)",
|
||
"var(--kt-ai-chart-pink)"
|
||
];
|
||
|
||
var state = {
|
||
period: "q3",
|
||
group: "all",
|
||
q: ""
|
||
};
|
||
|
||
function loadState() {
|
||
try {
|
||
var raw = localStorage.getItem(STORAGE_KEY);
|
||
if (!raw) return;
|
||
var p = JSON.parse(raw);
|
||
if (typeof p === "object" && p !== null) {
|
||
if (typeof p.period === "string") state.period = p.period;
|
||
if (typeof p.group === "string") state.group = p.group;
|
||
if (typeof p.q === "string") state.q = p.q;
|
||
}
|
||
} catch (e) { /* localStorage недоступен — работаем в памяти */ }
|
||
}
|
||
function saveState() {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
/* ---------- Вспомогательные ---------- */
|
||
|
||
function esc(s) {
|
||
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||
});
|
||
}
|
||
|
||
function groupIndex() {
|
||
for (var i = 0; i < GROUPS.length; i++) {
|
||
if (GROUPS[i].id === state.group) return i;
|
||
}
|
||
return 0;
|
||
}
|
||
function activeGroup() {
|
||
return GROUPS[groupIndex()];
|
||
}
|
||
function periodIndex() {
|
||
if (state.period === "q1") return 3;
|
||
if (state.period === "q2") return 6;
|
||
if (state.period === "full") return 12;
|
||
return 9;
|
||
}
|
||
function periodMonths() {
|
||
return windowMonths(periodIndex());
|
||
}
|
||
function windowMonths(i) {
|
||
if (i === 1) return [0, 1, 2];
|
||
if (i === 2) return [3, 4, 5];
|
||
if (i === 3) return [6, 7, 8];
|
||
if (i === 4) return [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
||
return [i - 3, i - 2, i - 1];
|
||
}
|
||
function periodLabel() {
|
||
if (state.period === "q1") return "I квартал";
|
||
if (state.period === "q2") return "II квартал";
|
||
if (state.period === "q3") return "III квартал";
|
||
if (state.period === "full") return "Янв — Сен";
|
||
var i = periodIndex();
|
||
return MONTHS_FULL[i - 2] + " — " + MONTHS_FULL[i - 1];
|
||
}
|
||
function monthsOf(g) {
|
||
var m = periodMonths();
|
||
var out = [];
|
||
for (var k = 0; k < m.length; k++) {
|
||
out.push({ mi: m[k], fact: FACT[g][m[k]], plan: PLAN[g][m[k]] });
|
||
}
|
||
return out;
|
||
}
|
||
function groupsOf() {
|
||
if (state.group !== "all") return [state.group];
|
||
return GROUPS.map(function (g) { return g.id; });
|
||
}
|
||
function groupShare() {
|
||
var gs = groupsOf();
|
||
var ms = periodMonths();
|
||
var total = 0;
|
||
for (var a = 0; a < gs.length; a++) {
|
||
for (var b = 0; b < ms.length; b++) total += FACT[gs[a]][ms[b]];
|
||
}
|
||
var out = [];
|
||
for (var i = 0; i < gs.length; i++) {
|
||
var s = 0;
|
||
var ms2 = periodMonths();
|
||
for (var b = 0; b < ms2.length; b++) s += FACT[gs[i]][ms2[b]];
|
||
out.push({
|
||
id: gs[i],
|
||
sum: s,
|
||
pct: total ? Math.round((s / total) * 1000) / 10 : 0
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
function prevMonths() {
|
||
var i = periodIndex();
|
||
if (i === 1 || i === 2 || i === 3) return [i - 3, i - 2, i - 1];
|
||
return [];
|
||
}
|
||
function fmt(n) {
|
||
return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||
}
|
||
function toneCls(p) {
|
||
if (p == null) return "flat";
|
||
if (p > 1.5) return "up";
|
||
if (p < -1.5) return "down";
|
||
return "flat";
|
||
}
|
||
function toneArrow(p) {
|
||
if (p == null) return "—";
|
||
if (p > 1.5) return "▲";
|
||
if (p < -1.5) return "▼";
|
||
return "•";
|
||
}
|
||
|
||
/* ---------- Отрисовка ---------- */
|
||
|
||
function tipAttr(item) {
|
||
var json;
|
||
try {
|
||
json = JSON.stringify(item);
|
||
} catch (e) {
|
||
json = "";
|
||
}
|
||
var v = item && item.hint ? item.hint : ((item && item.label ? item.label + " · " : "") + (item ? item.value : ""));
|
||
if (!json) return ' data-kt-tip="' + esc(v) + '" tabindex="0"';
|
||
return ' data-kt-tip="' + esc(json) + '" tabindex="0"';
|
||
}
|
||
|
||
function renderTrendChart() {
|
||
var ms = periodMonths();
|
||
if (!ms.length) return "";
|
||
var gs = groupsOf();
|
||
var W = 560, H = 196;
|
||
var x0 = 10, y0 = 16, w = W - 24, hh = H - 44;
|
||
var factVals = [], planVals = [];
|
||
for (var k = 0; k < ms.length; k++) {
|
||
var f = 0, p = 0;
|
||
for (var g = 0; g < gs.length; g++) {
|
||
f += FACT[gs[g]][ms[k]];
|
||
p += PLAN[gs[g]][ms[k]];
|
||
}
|
||
factVals.push(f);
|
||
planVals.push(p);
|
||
}
|
||
var all = factVals.concat(planVals);
|
||
var max = 0;
|
||
for (var m = 0; m < all.length; m++) if (all[m] > max) max = all[m];
|
||
max = max || 1;
|
||
var n = ms.length;
|
||
function px(i) { return x0 + (n > 1 ? i * (w / (n - 1)) : w / 2); }
|
||
function py(v) { return y0 + hh - (v / max) * hh; }
|
||
function path(vals) {
|
||
var s = "";
|
||
for (var i = 0; i < vals.length; i++) {
|
||
s += (i ? "L" : "M") + px(i).toFixed(1) + " " + py(vals[i]).toFixed(1);
|
||
}
|
||
return s;
|
||
}
|
||
var area = path(factVals) + " L" + px(n - 1).toFixed(1) + " " + (y0 + hh).toFixed(1) + " L" + px(0).toFixed(1) + " " + (y0 + hh).toFixed(1) + " Z";
|
||
var grid = "";
|
||
for (var fr = 0; fr <= 2; fr++) {
|
||
grid += '<line x1="' + x0 + '" y1="' + (y0 + hh * fr * 0.5).toFixed(1) + '" x2="' + (x0 + w) + '" y2="' + (y0 + hh * fr * 0.5).toFixed(1) + '" stroke="var(--kt-ai-border)" stroke-width="1" stroke-dasharray="2 3"/>';
|
||
}
|
||
var dots = "";
|
||
for (var i = 0; i < n; i++) {
|
||
dots += '<circle cx="' + px(i).toFixed(1) + '" cy="' + py(planVals[i]).toFixed(1) + '" r="3" fill="var(--kt-ai-fg-muted)" opacity="0.55"/>';
|
||
dots += '<circle cx="' + px(i).toFixed(1) + '" cy="' + py(factVals[i]).toFixed(1) + '" r="3.4" fill="var(--kt-ai-chart-blue)" tabindex="0" data-kt-tip="' + esc(JSON.stringify({ title: MONTHS_FULL[ms[i]] + " 2026", rows: [[activeGroup().name, "факт " + fmt(factVals[i])], ["План", fmt(planVals[i]) + (activeGroup().unit ? " " + activeGroup().unit : "")], ["Выполнение плана", Math.round((factVals[i] / planVals[i]) * 100) + "%"] })) + '"/>';
|
||
}
|
||
var labels = "";
|
||
for (var j = 0; j < n; j++) {
|
||
labels += '<text x="' + px(j).toFixed(1) + '" y="' + (H - 8) + '" text-anchor="middle" font-size="11" fill="var(--kt-ai-fg-faint)">' + esc(MONTHS_SHORT[ms[j]]) + "</text>";
|
||
}
|
||
var title = n > 1 ? MONTHS_SHORT[ms[0]].charAt(0).toUpperCase() + MONTHS_SHORT[ms[0]].slice(1) + " — " + MONTHS_SHORT[ms[n - 1]].charAt(0).toUpperCase() + MONTHS_SHORT[ms[n - 1]].slice(1) + " 2026" : MONTHS_FULL[ms[0]] + " 2026";
|
||
var sumF = 0;
|
||
for (var s2 = 0; s2 < factVals.length; s2++) sumF += factVals[s2];
|
||
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Динамика продаж по месяцам · ' + esc(title) + "</div>" +
|
||
'<div class="cc-total"><span class="num">' + fmt(sumF) + '</span><span class="unit">' + esc(activeGroup().unit) + " за период</span></div></div>" +
|
||
'<div class="chart-legend">' +
|
||
'<span><i style="background:var(--kt-ai-chart-blue)"></i>Факт</span>' +
|
||
'<span><i style="background:var(--kt-ai-fg-muted)"></i>План</span></div></div>' +
|
||
'<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;height:auto;display:block" role="img" aria-label="График динамики продаж">' +
|
||
grid + '<path d="' + area + '" fill="var(--kt-ai-chart-blue)" opacity="0.08"/>' +
|
||
'<path d="' + path(planVals) + '" fill="none" stroke="var(--kt-ai-fg-muted)" stroke-width="1.5" stroke-dasharray="4 4" opacity="0.6"/>' +
|
||
'<path d="' + path(factVals) + '" fill="none" stroke="var(--kt-ai-chart-blue)" stroke-width="2" stroke-linejoin="round"/>' +
|
||
dots + labels +
|
||
'<text x="' + x0 + '" y="10" font-size="10" fill="var(--kt-ai-fg-faint)">' + fmt(max) + " " + esc(activeGroup().unit) + "</text></svg></section>";
|
||
}
|
||
|
||
function renderDonut() {
|
||
var shares = groupShare();
|
||
var total = 0;
|
||
for (var t = 0; t < shares.length; t++) total += shares[t].sum;
|
||
if (total <= 0) return "";
|
||
var segs = "";
|
||
var off = 25;
|
||
for (var i = 0; i < shares.length; i++) {
|
||
var p = total ? (shares[i].sum / total) * 100 : 0;
|
||
segs += '<circle r="15.9155" cx="21" cy="21" fill="none" stroke="' + PALETTE[i % PALETTE.length] + '" stroke-width="5.5" stroke-dasharray="' + p.toFixed(2) + " " + (100 - p).toFixed(2) + '" stroke-dashoffset="' + off.toFixed(2) + '" data-kt-tip="' + esc(JSON.stringify({ title: groupLabel(shares[i].id), rows: [[groupLabel(shares[i].id), fmt(shares[i].sum])], [shares[i].pct + "% от периода", ""]]) + '" tabindex="0"></circle>';
|
||
off -= p;
|
||
}
|
||
var legend = shares.map(function (s, i) {
|
||
return '<div class="donut-legend-row"><span class="dot" style="background:' + PALETTE[i % PALETTE.length] + '"></span><span class="nm">' + esc(groupLabel(s.id)) + '</span><span class="val kt-ai-num">' + fmt(s.sum) + '</span><span class="pct">' + s.pct + "%</span></div>";
|
||
}).join("");
|
||
return '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">Структура продаж по группам</div>' +
|
||
'<div class="cc-total"><span class="num">' + fmt(total) + '</span><span class="unit">за ' + esc(periodLabel().toLowerCase()) + "</span></div></div></div>" +
|
||
'<div class="donut-flex">' +
|
||
'<svg viewBox="0 0 42 42" style="width:116px;height:116px;flex-shrink:0" role="img" aria-label="Структура продаж">' + segs +
|
||
'<text x="21" y="21.5" text-anchor="middle" dominant-baseline="middle" font-size="7" font-weight="600" fill="var(--kt-ai-fg)">' + fmt(total) + "</text></svg>" +
|
||
'<div class="donut-legend">' + legend + "</div></div></section>";
|
||
}
|
||
|
||
function groupLabel(id) {
|
||
for (var i = 0; i < GROUPS.length; i++) {
|
||
if (GROUPS[i].id === id) return GROUPS[i].name;
|
||
}
|
||
return id;
|
||
}
|
||
|
||
function buildKpis() {
|
||
var ms = periodMonths();
|
||
var gs = groupsOf();
|
||
var curF = 0, curP = 0;
|
||
for (var a = 0; a < gs.length; a++) {
|
||
for (var b = 0; b < ms.length; b++) {
|
||
curF += FACT[gs[a]][ms[b]];
|
||
curP += PLAN[gs[a]][ms[b]];
|
||
}
|
||
}
|
||
var pm = prevMonths();
|
||
var prevF = 0, prevP = 0;
|
||
if (pm.length) {
|
||
for (var c = 0; c < gs.length; c++) {
|
||
for (var d = 0; d < pm.length; d++) {
|
||
prevF += FACT[gs[c]][pm[d]];
|
||
prevP += PLAN[gs[c]][pm[d]];
|
||
}
|
||
}
|
||
}
|
||
var delta = null, deltaTxt = "—";
|
||
if (pm.length && prevF > 0) {
|
||
delta = Math.round(((curF - prevF) / prevF) * 1000) / 10;
|
||
deltaTxt = toneArrow(delta) + " " + Math.abs(delta) + "%";
|
||
}
|
||
var exec = curP > 0 ? Math.round((curF / curP) * 1000) / 10 : null;
|
||
var topG = null;
|
||
if (state.group === "all") {
|
||
var sh = groupShare();
|
||
var best = 0;
|
||
for (var s = 1; s < sh.length; s++) if (sh[s].sum > sh[best].sum) best = s;
|
||
topG = sh[best];
|
||
}
|
||
var lastMi = ms[ms.length - 1];
|
||
var lastVal = 0;
|
||
for (var g2 = 0; g2 < gs.length; g2++) { lastVal += FACT[gs[g2]][lastMi]; }
|
||
var prevMi = ms.length > 1 ? ms[ms.length - 2] : null;
|
||
var prevVal = 0;
|
||
if (prevMi !== null) {
|
||
for (var g3 = 0; g3 < gs.length; g3++) prevVal += FACT[gs[g3]][prevMi];
|
||
}
|
||
var mom = null, momTxt = "—";
|
||
if (prevMi !== null && prevVal > 0) {
|
||
mom = Math.round(((lastVal - prevVal) / prevVal) * 1000) / 10;
|
||
momTxt = toneArrow(mom) + " " + Math.abs(mom) + "%";
|
||
}
|
||
var unit = state.group === "all" ? "ед." : activeGroup().unit;
|
||
return [
|
||
{ label: "Продажи за период", value: fmt(curF), hint: unit + " · " + periodLabel(), data: "true" },
|
||
{ label: "К предыдущему периоду", value: deltaTxt, hint: pm.length ? "сравнение с предыдущим" : "—", data: toneCls(delta) },
|
||
{ label: "Выполнение плана", value: exec != null ? exec + "%" : "—", hint: "план " + fmt(curP) + " " + unit, data: exec != null ? (exec >= 97 ? "ok" : exec >= 92 ? "warn" : "risk") : "flat" },
|
||
{ label: "Лучшая группа", value: topG ? groupLabel(topG.id) : "—", hint: topG ? fmt(topG.sum) + " ед." : "выбрана одна группа", data: "flat" }
|
||
];
|
||
}
|
||
|
||
function renderKpis() {
|
||
var kpis = buildKpis();
|
||
return '<div class="kt-ai-kpi-strip" data-cards="true">' + kpis.map(function (k) {
|
||
var tone = "";
|
||
if (k.data === "up" || k.data === "down") tone = k.data;
|
||
return '<div class="kt-ai-kpi"><span class="label">' + esc(k.label) + '</span><span class="value"' + (tone ? ' data-tone="' + tone + '"' : "") + ">" + esc(k.value) + '</span><span class="hint">' + esc(k.hint) + "</span></div>";
|
||
}).join("") + "</div>";
|
||
}
|
||
|
||
function renderConclusion() {
|
||
var ms = periodMonths();
|
||
var gs = groupsOf();
|
||
var curF = 0, curP = 0;
|
||
for (var a = 0; a < gs.length; a++) {
|
||
for (var b = 0; b < ms.length; b++) { curF += FACT[gs[a]][ms[b]]; curP += PLAN[gs[a]][ms[b]]; }
|
||
}
|
||
var exec = curP > 0 ? Math.round((curF / curP) * 100) : 0;
|
||
var pm = prevMonths();
|
||
var deltaTxt = "";
|
||
if (pm.length && curF > 0) {
|
||
var prevF = 0;
|
||
for (var c = 0; c < gs.length; c++) {
|
||
for (var d = 0; d < pm.length; d++) prevF += FACT[gs[c]][pm[d]];
|
||
}
|
||
if (prevF > 0) {
|
||
var dl = Math.round(((curF - prevF) / prevF) * 10);
|
||
deltaTxt = dl >= 0 ? " ростом на " + dl + "%" : " снижением на " + Math.abs(dl) + "% к предыдущему периоду";
|
||
}
|
||
}
|
||
var lastMi = ms[ms.length - 1];
|
||
var lastVal = 0;
|
||
for (var g = 0; g < gs.length; g++) { lastVal += FACT[gs[g]][lastMi]; }
|
||
var topGroupTxt = "";
|
||
if (state.group === "all") {
|
||
var sh = groupShare();
|
||
var best = 0;
|
||
for (var s = 1; s < sh.length; s++) if (sh[s].sum > sh[best].sum) best = s;
|
||
topGroupTxt = "Больший вклад в период вносит «" + groupLabel(sh[best].id) + "» (" + sh[best].pct + "%). ";
|
||
}
|
||
return '<section class="kt-ai-conclusion"><div class="title">Вывод</div>' +
|
||
'<p class="text">За ' + esc(periodLabel().toLowerCase()) + " '26 розничные продажи составили " + fmt(curF) + " " + esc(unitWord()) + deltaTxt +
|
||
". Выполнение плана — " + exec + "%. Пик периода — " + MONTHS_FULL[lastMi].toLowerCase() + " (" + fmt(lastVal) + " " + esc(unitWord()) + "). " +
|
||
topGroupTxt + "Тренд по месяцам показан на графике: динамика относительно плана видна по расхождению линий.</p></section>";
|
||
}
|
||
|
||
function unitWord() {
|
||
return state.group === "all" ? "ед." : activeGroup().unit;
|
||
}
|
||
|
||
function renderTable() {
|
||
var ms = periodMonths().slice().sort(function (a, b) { return b - a; });
|
||
var gs = groupsOf();
|
||
var rows = [];
|
||
for (var i = 0; i < ms.length; i++) {
|
||
var mi = ms[i];
|
||
var f = 0, p = 0;
|
||
for (var g = 0; g < gs.length; g++) { f += FACT[gs[g]][mi]; p += PLAN[gs[g]][mi]; }
|
||
var exec = p > 0 ? Math.round((f / p) * 1000) / 10 : null;
|
||
rows.push({ month: MONTHS_FULL[mi], fact: f, plan: p, exec: exec, delta: null });
|
||
}
|
||
for (var r = 1; r < rows.length; r++) {
|
||
if (rows[r - 1].fact > 0) {
|
||
var d = Math.round(((rows[r].fact - rows[r - 1].fact) / rows[r - 1].fact) * 1000) / 10;
|
||
rows[r].delta = d;
|
||
}
|
||
}
|
||
var html = '<div class="kt-ai-card" style="padding:0;overflow:hidden"><div class="detail-head"><span class="kt-ai-label">Динамика по месяцам</span><span class="kt-ai-meta">' + esc(periodLabel()) + " · " + esc(state.group === "all" ? "все группы" : groupLabel(state.group)) + "</span></div>" +
|
||
'<div class="kt-ai-table-wrap"><table class="kt-ai-table" data-density="compact"><thead><tr>' +
|
||
"<th style=\"text-align:left\">Месяц</th><th style=\"text-align:left\">Группа</th><th style=\"text-align:right\">Факт</th><th style=\"text-align:right\">План</th><th style=\"text-align:right\">Исполнение, %</th><th style=\"text-align:right\">Δ к пред.</th>" +
|
||
"</tr></thead><tbody>";
|
||
for (var k2 = 0; k2 < rows.length; k2++) {
|
||
var row = rows[k2];
|
||
var pctTxt = row.exec != null ? row.exec + "%" : "—";
|
||
var pctTone = row.exec == null ? "flat" : row.exec >= 97 ? "ok" : row.exec >= 92 ? "warn" : "risk";
|
||
var dTxt = "—", dTone = "flat";
|
||
if (typeof row.delta === "number") {
|
||
dTxt = (row.delta >= 0 ? "+" : "−") + Math.abs(row.delta) + "%";
|
||
dTone = row.delta > 1.5 ? "up" : row.delta < -1.5 ? "down" : "flat";
|
||
}
|
||
html += "<tr>" +
|
||
'<td style="text-align:left" data-label="Месяц">' + esc(row.month) + "</td>" +
|
||
'<td style="text-align:left" data-label="Группа">' + esc(state.group === "all" ? "Все группы" : groupLabel(state.group)) + "</td>" +
|
||
'<td class="num" data-label="Факт">' + fmt(row.fact) + "</td>" +
|
||
'<td class="num" data-label="План">' + fmt(row.plan) + "</td>" +
|
||
'<td class="num" data-label="Исполнение, %">' + pctTxt + ' <span class="tone ' + pctTone + '"></span></td>' +
|
||
'<td class="num" data-label="Δ к пред."><span class="tone ' + dTone + '">' + dTxt + "</span></td>" +
|
||
"</tr>";
|
||
}
|
||
html += "</tbody></table></div></div>";
|
||
return html;
|
||
}
|
||
|
||
function renderView() {
|
||
var v = document.getElementById("view");
|
||
if (!v) return;
|
||
var h = "";
|
||
h += '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-group)">' + esc(periodIntro()) + "</div>";
|
||
h += renderKpis();
|
||
h += '<div class="dash-grid">';
|
||
h += '<div class="dash-col-main">';
|
||
h += renderTrendChart();
|
||
h += renderTable();
|
||
h += renderConclusion();
|
||
h += "</div>";
|
||
h += '<div class="dash-col-side">';
|
||
h += renderDonut();
|
||
h += "</div>";
|
||
h += "</div>";
|
||
v.innerHTML = h;
|
||
}
|
||
|
||
function periodIntro() {
|
||
var name = state.group === "all" ? "по всем товарным группам" : "по группе «" + groupLabel(state.group) + "»";
|
||
return "Продажи " + periodLabel().toLowerCase() + " 2026 года " + name + ". Выберите период и группу товара — показатели, график и таблица пересчитаются.";
|
||
}
|
||
|
||
/* ---------- События ---------- */
|
||
|
||
function onPeriod() {
|
||
var el = document.getElementById("fPeriod");
|
||
if (el && el.value) {
|
||
state.period = el.value;
|
||
saveState();
|
||
var se = document.getElementById("fSearch");
|
||
if (se && state.q) { se.value = ""; state.q = ""; saveState(); }
|
||
renderView();
|
||
}
|
||
}
|
||
function onGroup() {
|
||
var el = document.getElementById("fGroup");
|
||
if (el && el.value) {
|
||
state.group = el.value;
|
||
saveState();
|
||
var se = document.getElementById("fSearch");
|
||
if (se && state.q) { se.value = ""; state.q = ""; saveState(); }
|
||
renderView();
|
||
}
|
||
}
|
||
function onSearch() {
|
||
var el = document.getElementById("fSearch");
|
||
if (el) {
|
||
state.q = el.value;
|
||
saveState();
|
||
applySearch();
|
||
}
|
||
}
|
||
function applySearch() {
|
||
var q = state.q.trim().toLowerCase();
|
||
var gSel = document.getElementById("fGroup");
|
||
if (!q) {
|
||
if (gSel && gSel.value === "all") return;
|
||
state.group = "all";
|
||
saveState();
|
||
if (gSel) gSel.value = "all";
|
||
renderView();
|
||
return;
|
||
}
|
||
for (var i = 0; i < GROUPS.length; i++) {
|
||
if (GROUPS[i].name.toLowerCase().indexOf(q) !== -1) {
|
||
if (gSel && gSel.value !== GROUPS[i].id) gSel.value = GROUPS[i].id;
|
||
if (state.group !== GROUPS[i].id) { state.group = GROUPS[i].id; saveState(); }
|
||
renderView();
|
||
return;
|
||
}
|
||
}
|
||
var el = document.getElementById("fSearch");
|
||
if (el) el.dataset.invalid = "true";
|
||
}
|
||
function onReset() {
|
||
state.period = "q3";
|
||
state.group = "all";
|
||
state.q = "";
|
||
saveState();
|
||
var p = document.getElementById("fPeriod"); if (p) p.value = "q3";
|
||
var g = document.getElementById("fGroup"); if (g) g.value = "all";
|
||
var s = document.getElementById("fSearch"); if (s) { s.value = ""; delete s.dataset.invalid; }
|
||
renderView();
|
||
}
|
||
function navTo(name) {
|
||
state.group = name;
|
||
saveState();
|
||
var g = document.getElementById("fGroup");
|
||
if (g) g.value = name;
|
||
renderView();
|
||
window.scrollTo(0, 0);
|
||
}
|
||
|
||
function init() {
|
||
loadState();
|
||
var p = document.getElementById("fPeriod"); if (p) p.value = state.period;
|
||
var g = document.getElementById("fGroup"); if (g) g.value = state.group;
|
||
var s = document.getElementById("fSearch"); if (s) s.value = state.q;
|
||
var pe = document.getElementById("fPeriod"); if (pe) pe.addEventListener("change", onPeriod);
|
||
var ge = document.getElementById("fGroup"); if (ge) ge.addEventListener("change", onGroup);
|
||
var se = document.getElementById("fSearch"); if (se) { se.addEventListener("input", onSearch); if (se.value) se.dataset.invalid = "false"; }
|
||
var rst = document.getElementById("btnReset"); if (rst) rst.addEventListener("click", onReset);
|
||
renderView();
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|