"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 += '';
}
var dots = "";
for (var i = 0; i < n; i++) {
dots += '';
dots += '';
}
var labels = "";
for (var j = 0; j < n; j++) {
labels += '' + esc(MONTHS_SHORT[ms[j]]) + "";
}
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 '
Динамика продаж по месяцам · ' + esc(title) + "
" +
'
' + fmt(sumF) + '' + esc(activeGroup().unit) + " за период
" +
'
' +
'Факт' +
'План
' +
'";
}
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 += '';
off -= p;
}
var legend = shares.map(function (s, i) {
return '
";
}
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 '
Вывод
' +
'
За ' + esc(periodLabel().toLowerCase()) + " '26 розничные продажи составили " + fmt(curF) + " " + esc(unitWord()) + deltaTxt +
". Выполнение плана — " + exec + "%. Пик периода — " + MONTHS_FULL[lastMi].toLowerCase() + " (" + fmt(lastVal) + " " + esc(unitWord()) + "). " +
topGroupTxt + "Тренд по месяцам показан на графике: динамика относительно плана видна по расхождению линий.
";
}
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 = '
";
return html;
}
function renderView() {
var v = document.getElementById("view");
if (!v) return;
var h = "";
h += '
' + esc(periodIntro()) + "
";
h += renderKpis();
h += '
';
h += '
';
h += renderTrendChart();
h += renderTable();
h += renderConclusion();
h += "
";
h += '
';
h += renderDonut();
h += "
";
h += "
";
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);