502 lines
24 KiB
JavaScript
502 lines
24 KiB
JavaScript
// Дашборд розничных продаж 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();
|