"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 tipForMonth(mi, g, f, p) { return { title: MONTHS_FULL[mi] + " 2026", rows: [ [g.name, "факт " + fmt(f)], ["План", fmt(p) + (g.unit ? " " + g.unit : "")], ["Выполнение плана", Math.round((f / p) * 100) + "%"] ] }; } 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 += ''; } function factTip(mi) { var g = activeGroup(); return ''; } var dots = ""; for (var i = 0; i < n; i++) { dots += ''; dots += factTip(i); } 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) + " за период
" + '
' + 'Факт' + 'План
' + '' + grid + '' + '' + '' + dots + labels + '' + fmt(max) + " " + 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 '
' + esc(groupLabel(s.id)) + '' + fmt(s.sum) + '' + s.pct + "%
"; }).join(""); return '
Структура продаж по группам
' + '
' + fmt(total) + 'за ' + esc(periodLabel().toLowerCase()) + "
" + '
' + '' + segs + '' + fmt(total) + "" + '
' + legend + "
"; } 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 '
' + kpis.map(function (k) { var tone = ""; if (k.data === "up" || k.data === "down") tone = k.data; return '
' + esc(k.label) + '" + esc(k.value) + '' + esc(k.hint) + "
"; }).join("") + "
"; } 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 = '
Динамика по месяцам' + esc(periodLabel()) + " · " + esc(state.group === "all" ? "все группы" : groupLabel(state.group)) + "
" + '
' + "" + ""; 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 += "" + '" + '" + '" + '" + '' + '" + ""; } html += "
МесяцГруппаФактПланИсполнение, %Δ к пред.
' + esc(row.month) + "' + esc(state.group === "all" ? "Все группы" : groupLabel(state.group)) + "' + fmt(row.fact) + "' + fmt(row.plan) + "' + pctTxt + ' ' + dTxt + "
"; 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);