dashbord-roznichnyh-prodazh-6/script.js

502 lines
24 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 YEAR = 2026;
var PREV_YEAR = 2025;
var MONTHS = ["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"];
var MONTHS_SHORT = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];
var GROUP_DEFS = [
{ id: "connect", name: "Подключение", base: "var(--kt-ai-chart-blue)" },
{ id: "mobile", name: "Мобильная связь", base: "var(--kt-ai-chart-green)" },
{ id: "devices", name: "Устройства", base: "var(--kt-ai-chart-orange)" },
{ id: "content", name: "Тарифы и контент", base: "var(--kt-ai-chart-pink)" },
{ id: "extras", name: "Доп. услуги", base: "var(--kt-ai-chart-salmon)" }
];
// Объём продаж (млн тг): факт 2026 (январь–сентябрь) и 2025 для сравнения.
var GROUP_DATA = {
connect: {
m2025: [52, 55, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76],
m2026: [79, 83, 87, 92, 97, 101, 106, 110, 114, 118, 121, 124]
},
mobile: {
m2025: [45, 46, 48, 49, 50, 52, 53, 55, 56, 58, 60, 61],
m2026: [64, 66, 69, 73, 77, 80, 84, 87, 89, 91, 93, 95]
},
devices: {
m2025: [38, 40, 43, 44, 42, 46, 48, 49, 51, 53, 52, 50],
m2026: [63, 66, 71, 68, 74, 78, 82, 79, 85, 88, 90, 93]
},
content: {
m2025: [31, 32, 34, 35, 36, 38, 39, 40, 41, 43, 44, 45],
m2026: [51, 54, 57, 61, 65, 68, 73, 76, 80, 83, 86, 89]
},
extras: {
m2025: [27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38],
m2026: [44, 47, 50, 53, 57, 60, 64, 67, 71, 74, 77, 80]
}
};
var PERIODS = [
{ id: "9m", label: "Янв–Сен 2026", from: 0, months: 9, yoy: true },
{ id: "q1", label: "I квартал", from: 0, months: 3, yoy: true },
{ id: "q2", label: "II квартал", from: 3, months: 3, yoy: true },
{ id: "q3", label: "III квартал", from: 6, months: 3, yoy: true },
{ id: "m", label: "По месяцам", from: 0, months: 9, yoy: false }
];
var STATE_KEY = "dash-retail-v1";
// ---------- Утилиты ----------
function fmtInt(n) {
var neg = n < 0 ? "-" : "";
var s = String(Math.round(Math.abs(n)));
var out = "";
while (s.length > 3) { out = "." + s.slice(-3) + out; s = s.slice(0, -3); }
return neg + s + out;
}
function sumRange(arr, from, len) {
var t = 0;
for (var i = from; i < from + len && i < arr.length; i++) t += arr[i];
return t;
}
function groupSum(id, year, from, len) {
var key = year === YEAR ? "m2026" : "m2025";
return sumRange(GROUP_DATA[id][key], from, len);
}
function fmtPct(p, dec) {
var d = dec === undefined ? 1 : dec;
var sign = p > 0 ? "+" : "";
return sign + p.toFixed(d).replace(".", ",") + "%";
}
function periodDef(id) {
for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === id) return PERIODS[i];
return PERIODS[0];
}
function groupName(id) {
for (var i = 0; i < GROUP_DEFS.length; i++) if (GROUP_DEFS[i].id === id) return GROUP_DEFS[i].name;
return id;
}
function topGroupIn(groups, from, len, year) {
var best = null, bv = -1;
for (var i = 0; i < groups.length; i++) {
var v = groupSum(groups[i].id, year, from, len);
if (v > bv) { bv = v; best = groups[i]; }
}
return { group: best, value: bv };
}
function bestMonthId(id, from, len, year) {
var key = year === YEAR ? "m2026" : "m2025";
var bestIdx = from, bestV = -1;
for (var m = from; m < from + len && m < 12; m++) {
if (GROUP_DATA[id][key][m] > bestV) { bestV = GROUP_DATA[id][key][m]; bestIdx = m; }
}
return { month: bestIdx, value: bestV };
}
function yoyOf(id, from, len) {
var cur = groupSum(id, YEAR, from, len);
var prev = groupSum(id, PREV_YEAR, from, len);
return { cur: cur, prev: prev, pct: prev ? (cur / prev - 1) * 100 : null };
}
function filteredGroups() {
if (state.group === "all") return GROUP_DEFS.slice();
return GROUP_DEFS.filter(function (g) { return g.id === state.group; });
}
// ---------- Состояние (localStorage) ----------
function loadState() {
var def = { screen: "overview", period: "9m", group: "all" };
try {
var raw = localStorage.getItem(STATE_KEY);
if (!raw) return def;
var p = JSON.parse(raw);
if (!p || typeof p !== "object") return def;
var out = {};
for (var k in def) out[k] = (p[k] === undefined ? def[k] : p[k]);
return out;
} catch (e) { return def; }
}
var state = loadState();
function saveState() {
try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {}
}
// ---------- Фильтры ----------
function filterBarHtml() {
var perSel = "";
for (var i = 0; i < PERIODS.length; i++) {
perSel += "<option value=\"" + PERIODS[i].id + "\"" + (state.period === PERIODS[i].id ? " selected" : "") + ">" + PERIODS[i].label + "</option>";
}
var gSel = "<option value=\"all\"" + (state.group === "all" ? " selected" : "") + ">Все группы</option>";
for (var j = 0; j < GROUP_DEFS.length; j++) {
gSel += "<option value=\"" + GROUP_DEFS[j].id + "\"" + (state.group === GROUP_DEFS[j].id ? " selected" : "") + ">" + GROUP_DEFS[j].name + "</option>";
}
return '<div class="tbl-toolbar">' +
'<span class="kt-ai-chip">' + YEAR + '</span>' +
'<span class="spacer"></span>' +
'<label class="kt-ai-label" for="f-period">Период</label>' +
'<span class="kt-ai-select-wrap" style="min-width:140px"><select id="f-period" class="kt-ai-select" onchange="setPeriod(this.value)">' + perSel + "</select></span>" +
'<label class="kt-ai-label" for="f-group">Группа</label>' +
'<span class="kt-ai-select-wrap" style="min-width:160px"><select id="f-group" class="kt-ai-select" onchange="setGroup(this.value)">' + gSel + "</select></span>" +
'<button class="kt-ai-btn" data-variant="ghost" data-size="sm" type="button" onclick="resetState()">Сбросить</button>' +
"</div>";
}
function setPeriod(id) {
state.period = id;
saveState();
applyScreen();
}
function setGroup(id) {
state.group = id;
saveState();
applyScreen();
}
function resetState() {
state.period = "9m";
state.group = "all";
saveState();
applyScreen();
}
function switchScreen(id) {
state.screen = id;
saveState();
applyScreen();
}
// ---------- Графики ----------
function chartCard(title, meta, body, note) {
var head = '<div class="cc-head"><div><div class="cc-title">' + title + "</div>" +
(meta ? '<div class="kt-ai-meta" style="margin-top:2px">' + meta + "</div>" : "") + "</div></div>";
var foot = note ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-4)">' + note + "</div>" : "";
return '<section class="kt-ai-chart-card">' + head + body + foot + "</section>";
}
function groupedBarHtml(pairs, tipRows) {
var max = 0;
for (var i = 0; i < pairs.length; i++) {
for (var j = 1; j < pairs[i].vals.length; j++) {
if (pairs[i].vals[j] > max) max = pairs[i].vals[j];
}
}
if (max < 1) max = 1;
var bars = "";
for (var k = 0; k < pairs.length; k++) {
var p = pairs[k];
var inner = "";
for (var m = 0; m < p.vals.length; m++) {
var h = Math.max(2, Math.round(p.vals[m] / max * 190));
var tip = tipRows[p.tipKey || "" + k];
inner += '<span class="grp-bar" style="height:' + h + 'px;background:' + p.colors[m] + '"' +
(tip ? ' data-kt-tip=\'' + tip + '\'' : "") + "></span>";
}
bars += '<div class="grp-col" data-kt-tip=\'' + (tipRows[p.tipKey || "" + k] || "") + '\'><span class="grp-bars">' + inner + "</span>" +
'<span class="grp-label">' + p.label + "</span></div>";
}
return '<div class="grp-wrap">' + bars + "</div>" +
'<div class="grp-legend"><span><i style="background:var(--kt-ai-chart-bar-fill)"></i>' + PREV_YEAR + ' (сравнение)</span>' +
'<span><i style="background:var(--kt-ai-chart-blue)"></i>' + YEAR + ' (факт)</span></div>';
}
function lineChartHtml(points) {
var W = 520, H = 200, padX = 10, padT = 14, padB = 26, innerH = H - padT - padB;
var max = 0, min = Infinity;
for (var i = 0; i < points.length; i++) {
if (points[i].v > max) max = points[i].v;
if (points[i].v < min) min = points[i].v;
}
if (min <= 0) min = 0;
var top = max * 1.06, bot = Math.max(0, min - (max - min) * 0.35);
var n = points.length;
function X(i) { return n === 1 ? W / 2 : padX + i * (W - 2 * padX) / (n - 1); }
function Y(v) { return padT + (1 - (v - bot) / (top - bot || 1)) * innerH; }
var path = "", dots = "", labels = "";
for (var k = 0; k < n; k++) {
path += (k ? "L" : "M") + X(k).toFixed(1) + " " + Y(points[k].v).toFixed(1);
dots += '<circle cx="' + X(k).toFixed(1) + '" cy="' + Y(points[k].v).toFixed(1) + '" r="3.2" fill="var(--kt-ai-chart-blue)"' +
' data-kt-tip=\'' + points[k].tip + "'></circle>";
labels += '<text class="lc-lbl" x="' + X(k).toFixed(1) + '" y="' + (H - 8) + '">' + points[k].label + "</text>";
}
var grid = "";
for (var g = 0; g < 4; g++) {
var gy = padT + g * innerH / 3;
var gv = top - g * (top - bot) / 3;
grid += '<line x1="' + padX + '" y1="' + gy.toFixed(1) + '" x2="' + (W - padX) + '" y2="' + gy.toFixed(1) + '" stroke="var(--kt-ai-divider)"/>' +
'<text class="lc-axis" x="0" y="' + (gy - 3).toFixed(1) + '">' + fmtInt(gv) + "</text>";
}
return '<div class="lc-wrap"><svg class="lc-svg" viewBox="0 0 ' + W + " " + H + '" preserveAspectRatio="xMidYMid meet">' +
grid + '<path class="lc-line" d="' + path + '"/>' + dots + labels + "</svg></div>";
}
function barRowHtml(label, value, maxV, color, tip) {
var w = Math.max(2, Math.round(value / maxV * 100));
return '<div class="rowbar" data-kt-tip=\'' + tip + '\'><span class="rowbar-label">' + label + "</span>" +
'<span class="rowbar-track"><span class="rowbar-fill" style="width:' + w + "%;background:" + color + '"></span></span>' +
'<span class="rowbar-num kt-ai-num">' + fmtInt(value) + "</span></div>";
}
function donutHtml(total, pts) {
var acc = 0;
var segs = "";
for (var i = 0; i < pts.length; i++) {
var pp = pts[i];
if (pp.value <= 0) continue;
var share = pp.value / total * 100;
segs += '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + pp.color + '" stroke-width="5.5" pathLength="100" ' +
'stroke-dasharray="' + share.toFixed(2) + " " + (100 - share).toFixed(2) + '" stroke-dashoffset="' + (25 - acc).toFixed(2) + '"' +
' data-kt-tip=\'' + pp.label + ": " + fmtInt(pp.value) + " млн тг, " + Math.round(share) + "%'></circle>";
acc += share;
}
var legend = "";
for (var j = 0; j < pts.length; j++) {
var q = pts[j];
if (q.value <= 0) continue;
var pc = Math.round(q.value / total * 100);
legend += '<div class="dl-row"><span class="dl-dot" style="background:' + q.color + '"></span>' +
'<span class="dl-label">' + q.label + '</span><span class="dl-num kt-ai-num">' + pc + "%</span></div>";
}
var svg = '<svg class="dl-svg" viewBox="0 0 42 42" role="img"><circle cx="21" cy="21" r="15.9155" fill="none" stroke="var(--kt-ai-bg-soft)" stroke-width="5.5"/>' +
segs + '<text class="dl-center" x="21" y="20.5">' + fmtInt(total) + '</text><text class="dl-unit" x="21" y="26.5">млн тг</text></svg>';
return '<div class="dl-wrap">' + svg + '<div class="dl-legend">' + legend + "</div></div>";
}
// ---------- Экран «Обзор» ----------
function overviewKpis() {
var p = periodDef(state.period);
var groups = filteredGroups();
var cur = 0, prev = 0;
for (var i = 0; i < groups.length; i++) {
cur += groupSum(groups[i].id, YEAR, p.from, p.months);
prev += groupSum(groups[i].id, PREV_YEAR, p.from, p.months);
}
var yoy = prev ? (cur / prev - 1) * 100 : 0;
var avg = cur / p.months;
var top = topGroupIn(groups, p.from, p.months, YEAR);
var topLabel = top.group ? top.group.name : "—";
var deltaLabel = yoy >= 0 ? "выше" : "ниже";
var deltaNum = Math.abs(yoy).toFixed(1).replace(".", ",");
return [
{ label: "Объём продаж", value: fmtInt(cur), unit: " млн тг", hint: p.label },
{ label: "К уровню " + PREV_YEAR, value: (yoy >= 0 ? "+" : "") + yoy.toFixed(1).replace(".", ",") + "%", unit: "", hint: deltaLabel + " на " + deltaNum + " п.п." },
{ label: "Средний месяц", value: fmtInt(avg), unit: " млн тг", hint: "за " + p.months + " мес." },
{ label: "Сильнейшая группа", value: topLabel, unit: "", hint: fmtInt(top.value) + " млн тг" }
];
}
function kpiStripHtml(kpis) {
var h = '<div class="kt-ai-kpi-strip" data-cards="true">';
for (var i = 0; i < kpis.length; i++) {
var k = kpis[i];
h += '<div class="kt-ai-kpi"><span class="label">' + k.label + '</span>' +
'<span class="value">' + k.value + (k.unit ? '<span class="kt-ai-meta" style="font-weight:400">' + k.unit + "</span>" : "") + "</span>" +
'<span class="hint">' + k.hint + "</span></div>";
}
return h + "</div>";
}
function overviewChartsHtml() {
var p = periodDef(state.period);
var groups = filteredGroups();
var h = '<div class="chart-row">';
if (p.yoy) {
var pairs = [], tips = {};
for (var m = 0; m < p.months; m++) {
var mi = p.from + m;
var curV = 0, prevV = 0;
for (var g = 0; g < groups.length; g++) {
curV += GROUP_DATA[groups[g].id].m2026[mi];
prevV += GROUP_DATA[groups[g].id].m2025[mi];
}
var key = "m" + mi;
pairs.push({ label: MONTHS_SHORT[mi], vals: [prevV, curV], colors: ["var(--kt-ai-chart-bar-fill)", "var(--kt-ai-chart-blue)"], tipKey: key });
tips[key] = MONTHS[mi] + ": " + YEAR + " — " + fmtInt(curV) + " млн тг; " + PREV_YEAR + " — " + fmtInt(prevV) + " млн тг" + (prevV ? " (" + fmtPct((curV / prevV - 1) * 100) + ")" : "");
}
h += chartCard("Динамика по месяцам", YEAR + " против " + PREV_YEAR + ", млн тг", groupedBarHtml(pairs, tips));
} else {
var pts = [];
for (var mm = 0; mm < p.months; mm++) {
var mi2 = p.from + mm;
var mv = 0;
for (var gg = 0; gg < groups.length; gg++) mv += GROUP_DATA[groups[gg].id].m2026[mi2];
var pv2 = 0;
for (var gg2 = 0; gg2 < groups.length; gg2++) pv2 += GROUP_DATA[groups[gg2].id].m2025[mi2];
pts.push({ label: MONTHS_SHORT[mi2], v: mv, tip: MONTHS[mi2] + ": " + fmtInt(mv) + " млн тг, к " + PREV_YEAR + " " + (pv2 ? fmtPct((mv / pv2 - 1) * 100) : "") });
}
h += chartCard("Продажи по месяцам", "млн тг, " + YEAR, lineChartHtml(pts));
}
if (state.group === "all") {
var donPts = [];
for (var d = 0; d < GROUP_DEFS.length; d++) {
donPts.push({ label: GROUP_DEFS[d].name, value: groupSum(GROUP_DEFS[d].id, YEAR, p.from, p.months), color: GROUP_DEFS[d].base });
}
var donTotal = 0;
for (var d2 = 0; d2 < donPts.length; d2++) donTotal += donPts[d2].value;
h += chartCard("Структура продаж", p.label + ", млн тг", donutHtml(donTotal, donPts));
} else {
var rows = [];
for (var r = 0; r < GROUP_DEFS.length; r++) {
rows.push({ id: GROUP_DEFS[r].id, label: GROUP_DEFS[r].name, value: groupSum(GROUP_DEFS[r].id, YEAR, p.from, p.months), color: GROUP_DEFS[r].base });
}
var rmax = 0;
for (var r2 = 0; r2 < rows.length; r2++) if (rows[r2].value > rmax) rmax = rows[r2].value;
var body = "";
for (var r3 = 0; r3 < rows.length; r3++) {
var rr = rows[r3];
var mark = rr.id === state.group ? " · выбрано" : "";
body += barRowHtml(rr.label + mark, rr.value, rmax, rr.id === state.group ? rr.color : "var(--kt-ai-chart-bar-fill)", rr.label + ": " + fmtInt(rr.value) + " млн тг за " + p.label);
}
h += chartCard("Сравнение групп", p.label + ", млн тг", body);
}
return h + "</div>";
}
function conclusionHtml() {
var p = periodDef(state.period);
var groups = filteredGroups();
var cur = 0, prev = 0;
for (var i = 0; i < groups.length; i++) {
cur += groupSum(groups[i].id, YEAR, p.from, p.months);
prev += groupSum(groups[i].id, PREV_YEAR, p.from, p.months);
}
var top = topGroupIn(groups, p.from, p.months, YEAR);
var bm = bestMonthId(top.group ? top.group.id : GROUP_DEFS[0].id, p.from, p.months, YEAR);
var yoy = prev ? (cur / prev - 1) * 100 : 0;
var groupsLabel = state.group === "all" ? "суммарно по всем группам" : "в группе «" + groupName(state.group) + "»";
var text = "За " + p.label + " объём розничных продаж — " + fmtInt(cur) + " млн тг (" + groupsLabel + "). " +
"Лучший месяц периода — " + MONTHS[bm.month].toLowerCase() + " (" + fmtInt(bm.value) + " млн тг). " +
"Сильнейшая группа — «" + (top.group ? top.group.name : "—") + "». " +
"К уровню " + PREV_YEAR + " года показатель " + (yoy >= 0 ? "выше" : "ниже") + " на " + Math.abs(yoy).toFixed(1).replace(".", ",") + " п.п.";
return '<section class="kt-ai-conclusion" style="margin-top:var(--kt-ai-stack-block);padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' +
'<span class="title">Итог за период</span>' +
'<span class="text">' + text + "</span></section>";
}
function renderOverview() {
var p = periodDef(state.period);
var groupsLabel = state.group === "all" ? "все группы" : "группа «" + groupName(state.group) + "»";
var h = "";
h += '<div class="screen-head"><h1>Обзор</h1>' +
'<span class="screen-sub">Розничные продажи ' + YEAR + " · " + p.label + " · " + groupsLabel + "</span></div>";
h += filterBarHtml();
h += kpiStripHtml(overviewKpis());
h += overviewChartsHtml();
h += conclusionHtml();
return h;
}
// ---------- Экран «Детали» ----------
function detailsMonthlyTable() {
var p = periodDef(state.period);
var groups = filteredGroups();
var h = '<section class="detail-section"><h2 class="kt-ai-h3 block-title">Продажи по месяцам</h2>';
var headCols = "<th>Месяц</th>";
for (var c = 0; c < groups.length; c++) headCols += "<th>" + groups[c].name + "</th>";
headCols += "<th>Итого</th><th>К " + PREV_YEAR + "</th>";
var body = "";
var colTot = {};
for (var c2 = 0; c2 < groups.length; c2++) colTot[groups[c2].id] = 0;
var grand = 0, grandPrev = 0;
for (var m = 0; m < p.months; m++) {
var mi = p.from + m;
body += "<tr><td><strong>" + MONTHS[mi] + "</strong></td>";
var rowTot = 0, rowPrev = 0;
for (var c3 = 0; c3 < groups.length; c3++) {
var v = GROUP_DATA[groups[c3].id].m2026[mi];
var pv = GROUP_DATA[groups[c3].id].m2025[mi];
colTot[groups[c3].id] += v; rowTot += v; rowPrev += pv;
body += '<td class="num" style="padding-right:18px">' + fmtInt(v) + "</td>";
}
grand += rowTot; grandPrev += rowPrev;
body += '<td class="num"><strong>' + fmtInt(rowTot) + "</strong></td>" +
'<td class="num">' + (rowPrev ? fmtPct((rowTot / rowPrev - 1) * 100) : "—") + "</td></tr>";
}
body += '<tr><td><strong>Итого</strong></td>';
for (var c4 = 0; c4 < groups.length; c4++) body += '<td class="num" style="padding-right:18px"><strong>' + fmtInt(colTot[groups[c4].id]) + "</strong></td>";
body += '<td class="num"><strong>' + fmtInt(grand) + "</strong></td>" +
'<td class="num"><strong>' + (grandPrev ? fmtPct((grand / grandPrev - 1) * 100) : "—") + "</strong></td></tr>";
h += '<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr>' + headCols + "</tr></thead><tbody>" + body + "</tbody></table></div>" +
'<p class="kt-ai-meta" style="margin:var(--kt-ai-space-3) 0 0">Значения в млн тг. «К ' + PREV_YEAR + "» — прирост того же месяца к прошлому году.</p></section>";
return h;
}
function detailsGroupsTable() {
var p = periodDef(state.period);
var h = '<section class="detail-section"><h2 class="kt-ai-h3 block-title">' +
(state.group === "all" ? "Разбивка по товарным группам" : "Группа «" + groupName(state.group) + "» по месяцам") + "</h2>";
if (state.group === "all") {
h += '<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr>' +
"<th>Товарная группа</th><th>Объём</th><th>Доля</th><th>Лучший месяц</th><th>К " + PREV_YEAR + "</th></tr></thead><tbody>";
var totals = [];
var sumAll = 0;
for (var i = 0; i < GROUP_DEFS.length; i++) {
var t = groupSum(GROUP_DEFS[i].id, YEAR, p.from, p.months);
totals.push(t);
sumAll += t;
}
for (var g = 0; g < GROUP_DEFS.length; g++) {
var gd = GROUP_DEFS[g];
var tot = totals[g];
var share = sumAll ? tot / sumAll * 100 : 0;
var bm = bestMonthId(gd.id, p.from, p.months, YEAR);
var y = yoyOf(gd.id, p.from, p.months);
h += '<tr><td><strong>' + gd.name + '</strong></td>' +
'<td class="num" data-kt-tip=\'' + gd.name + ": " + fmtInt(tot) + " млн тг за " + p.label.toLowerCase() + '\">' + fmtInt(tot) + ' <span class="kt-ai-meta">млн тг</span></td>' +
'<td class="num">' + share.toFixed(1).replace(".", ",") + "%</td>" +
"<td>" + MONTHS[bm.month] + " · " + fmtInt(bm.value) + ' <span class="kt-ai-meta">млн тг</span></td>' +
'<td class="num">' + (y.pct === null ? "—" : fmtPct(y.pct)) + "</td></tr>";
}
h += "</tbody></table></div>";
} else {
h += '<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr>' +
"<th>Месяц</th><th>Объём</th><th>Доля в группе</th><th>К " + PREV_YEAR + "</th></tr></thead><tbody>";
var gTot = groupSum(state.group, YEAR, p.from, p.months);
for (var m = 0; m < p.months; m++) {
var mi = p.from + m;
var mvv = GROUP_DATA[state.group].m2026[mi];
var pvv = GROUP_DATA[state.group].m2025[mi];
var sh = gTot ? mvv / gTot * 100 : 0;
h += '<tr><td>' + MONTHS[mi] + '</td>' +
'<td class="num" data-kt-tip=\'' + MONTHS[mi] + ": " + fmtInt(mvv) + ' млн тг\">' + fmtInt(mvv) + ' <span class="kt-ai-meta">млн тг</span></td>' +
'<td class="num">' + sh.toFixed(1).replace(".", ",") + "%</td>" +
'<td class="num">' + (pvv ? fmtPct((mvv / pvv - 1) * 100) : "—") + "</td></tr>";
}
var gy = yoyOf(state.group, p.from, p.months);
h += '<tr><td><strong>Итого</strong></td><td class="num"><strong>' + fmtInt(gTot) + ' <span class="kt-ai-meta">млн тг</span></strong></td>' +
'<td class="num">100%</td><td class="num"><strong>' + (gy.pct === null ? "—" : fmtPct(gy.pct)) + "</strong></td></tr></tbody></table></div>";
}
return h + "</section>";
}
function renderDetails() {
var p = periodDef(state.period);
var h = "";
h += '<div class="screen-head"><h1>Детали</h1>' +
'<span class="screen-sub">Разбивка ' + YEAR + " по периодам и товарным группам</span></div>";
h += filterBarHtml();
h += detailsMonthlyTable();
h += detailsGroupsTable();
return h;
}
// ---------- Разметка экранов ----------
function applyScreen() {
var ov = document.getElementById("screen-overview");
var dt = document.getElementById("screen-details");
if (state.screen === "details") {
if (ov) ov.hidden = true;
if (dt) { dt.hidden = false; dt.innerHTML = renderDetails(); }
} else {
if (ov) { ov.hidden = false; ov.innerHTML = renderOverview(); }
if (dt) dt.hidden = true;
}
var items = document.querySelectorAll(".kt-ai-nav-item[data-screen]");
for (var i = 0; i < items.length; i++) {
items[i].setAttribute("data-active", items[i].getAttribute("data-screen") === state.screen ? "true" : "false");
}
}
applyScreen();