462 lines
22 KiB
JavaScript
462 lines
22 KiB
JavaScript
// Дашборд розничных продаж 2026: данные, графики, таблицы, фильтры.
|
||
|
||
// ---------- Данные ----------
|
||
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)" }
|
||
];
|
||
|
||
// Объём продаж (млн тг) по месяцам за 2025 (сравнение) и 2026 (факт до сентября).
|
||
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] },
|
||
mobile: { m2025: [45, 46, 48, 49, 50, 52, 53, 55, 56, 58, 60, 61],
|
||
m2026: [64, 66, 69, 73, 77, 80, 84, 87, 89] },
|
||
devices: { m2025: [38, 40, 43, 44, 42, 46, 48, 49, 51, 53, 52, 50],
|
||
m2026: [63, 66, 71, 68, 74, 78, 82, 79, 85] },
|
||
content: { m2025: [31, 32, 34, 35, 36, 38, 39, 40, 41, 43, 44, 45],
|
||
m2026: [51, 54, 57, 61, 65, 68, 73, 76, 80] },
|
||
extras: { m2025: [27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38],
|
||
m2026: [44, 47, 50, 53, 57, 60, 64, 67, 71] }
|
||
};
|
||
|
||
var PERIODS = [
|
||
{ id: "9m", label: "Янв–Сен 2026", months: 9, yoy: true },
|
||
{ id: "q1", label: "I квартал", months: 3, yoy: true },
|
||
{ id: "q2", label: "II квартал", months: 6, yoy: true },
|
||
{ id: "m", label: "По месяцам", months: 9, yoy: false }
|
||
];
|
||
|
||
var STATE_KEY = "dash-retail-v1";
|
||
|
||
// ---------- Утилиты ----------
|
||
function fmtInt(n) {
|
||
var s = String(Math.round(n));
|
||
var out = "";
|
||
while (s.length > 3) { out = "." + s.slice(-3) + out; s = s.slice(0, -3); }
|
||
return s.length ? s + out : "0" + out;
|
||
}
|
||
function fmtMoney(mn) { return fmtInt(mn) + " млн тг"; }
|
||
function fmtPct(p) {
|
||
var sign = p > 0 ? "+" : "";
|
||
var num = (Math.abs(p)).toFixed(1).replace(".", ",");
|
||
return sign + num + "%";
|
||
}
|
||
function sum(arr) {
|
||
var t = 0;
|
||
for (var i = 0; i < arr.length; i++) t += arr[i];
|
||
return t;
|
||
}
|
||
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 stateFromJson(s) {
|
||
var st = {};
|
||
for (var k in s) st[k] = s[k];
|
||
return st;
|
||
}
|
||
function loadState() {
|
||
var def = { screen: "overview", period: "9m", group: "all" };
|
||
try {
|
||
var raw = localStorage.getItem(STATE_KEY);
|
||
if (!raw) return def;
|
||
var parsed = JSON.parse(raw);
|
||
return stateFromJson(parsed) == null ? def : Object.assign(def, parsed);
|
||
} catch (e) { return def; }
|
||
}
|
||
function saveState() {
|
||
try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {}
|
||
}
|
||
var state = loadState();
|
||
|
||
function periodDef(id) {
|
||
for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === id) return PERIODS[i];
|
||
return PERIODS[0];
|
||
}
|
||
function filteredGroups() {
|
||
var all = GROUP_DEFS;
|
||
if (state.group !== "all") {
|
||
all = [];
|
||
for (var i = 0; i < GROUP_DEFS.length; i++) {
|
||
if (GROUP_DEFS[i].id === state.group) all.push(GROUP_DEFS[i]);
|
||
}
|
||
}
|
||
return all;
|
||
}
|
||
|
||
// ---------- Рендер фильтров ----------
|
||
function filterBarHtml() {
|
||
var perSel = "";
|
||
for (var i = 0; i < PERIODS.length; i++) {
|
||
var sel = state.period === PERIODS[i].id ? " selected" : "";
|
||
perSel += "<option value=\"" + PERIODS[i].id + "\"" + sel + ">" + PERIODS[i].label + "</option>";
|
||
}
|
||
var gSel = "<option value=\"all\"" + (state.group === "all" ? " selected" : "") + ">Все группы</option>";
|
||
for (var j = 0; j < GROUP_DEFS.length; j++) {
|
||
var g = GROUP_DEFS[j];
|
||
gSel += "<option value=\"" + g.id + "\"" + (state.group === g.id ? " selected" : "") + ">" + g.name + "</option>";
|
||
}
|
||
return '<div class="tbl-toolbar">' +
|
||
'<label class="kt-ai-label" for="f-period">Период</label>' +
|
||
'<span class="kt-ai-select-wrap" style="min-width:150px"><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:170px"><select id="f-group" class="kt-ai-select" onchange="setGroup(this.value)">' + gSel + '</select></span>' +
|
||
'<span class="spacer"></span>' +
|
||
'<span class="kt-ai-chip">2026</span>' +
|
||
'</div>';
|
||
}
|
||
function setPeriod(id) {
|
||
state.period = id; saveState(); renderAll();
|
||
}
|
||
function setGroup(id) {
|
||
state.group = id; saveState(); renderAll();
|
||
}
|
||
|
||
// ---------- Графики ----------
|
||
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>2025 (сравнение)</span>' +
|
||
'<span><i style="background:var(--kt-ai-chart-blue)"></i>2026 (факт)</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 shareOf(id, months2026) {
|
||
var num = 0, den = 0;
|
||
for (var m = 0; m < months2026; m++) { num += GROUP_DATA[id].m2026[m]; }
|
||
for (var k = 0; k < GROUP_DEFS.length; k++) { den += sumRange(GROUP_DATA[GROUP_DEFS[k].id].m2026, 0, months2026); }
|
||
return den ? num / den * 100 : 0;
|
||
}
|
||
function topMonthOf(id, from, len) {
|
||
var best = 0, idx = from;
|
||
for (var m = from; m < from + len; m++) {
|
||
if (GROUP_DATA[id].m2026[m] > best) { best = GROUP_DATA[id].m2026[m]; idx = m; }
|
||
}
|
||
return { month: idx, value: best };
|
||
}
|
||
function groupTotal(id) {
|
||
return sum(GROUP_DATA[id].m2026) + sum(GROUP_DATA[id].m2025);
|
||
}
|
||
function yoyFor(id, from, len) {
|
||
var cur = sumRange(GROUP_DATA[id].m2026, from, len);
|
||
var prev = sumRange(GROUP_DATA[id].m2025, from, len);
|
||
return prev ? (cur - prev) / prev * 100 : 0;
|
||
}
|
||
function overviewKpis() {
|
||
var p = periodDef(state.period);
|
||
var cur = 0, prev = 0;
|
||
var groups = filteredGroups();
|
||
for (var i = 0; i < groups.length; i++) {
|
||
cur += sumRange(GROUP_DATA[groups[i].id].m2026, 0, p.months);
|
||
prev += sumRange(GROUP_DATA[groups[i].id].m2025, 0, p.months);
|
||
}
|
||
var yoy = prev ? (cur - prev) / prev * 100 : 0;
|
||
var topId = "all", topV = -1;
|
||
for (var j = 0; j < GROUP_DEFS.length; j++) {
|
||
var v = sumRange(GROUP_DATA[GROUP_DEFS[j].id].m2026, 0, p.months);
|
||
if (v > topV) { topV = v; topId = GROUP_DEFS[j].id; }
|
||
}
|
||
var topName = "";
|
||
for (var k = 0; k < GROUP_DEFS.length; k++) if (GROUP_DEFS[k].id === topId) topName = GROUP_DEFS[k].name;
|
||
var avg = cur / p.months;
|
||
return [
|
||
{ label: "Объём продаж", value: fmtInt(cur), unit: " млн тг", hint: p.label },
|
||
{ label: "К уровню 2025", value: fmtPct(yoy), unit: "", hint: "за период" },
|
||
{ label: "Средний месяц", value: fmtInt(avg), unit: " млн тг", hint: "за " + p.months + " мес." },
|
||
{ label: "Лучшая группа", value: topName, unit: "", hint: fmtInt(topV) + " млн тг" }
|
||
];
|
||
}
|
||
|
||
function renderOverview() {
|
||
var p = periodDef(state.period);
|
||
var groups = filteredGroups();
|
||
var groupsLabel = state.group === "all" ? "все группы" : groups[0].name.toLowerCase();
|
||
var h = "";
|
||
|
||
h += '<div class="screen-head"><h1>Обзор</h1>' +
|
||
'<span class="screen-sub">Розничные продажи 2026 · ' + p.label + " · " + groupsLabel + "</span></div>";
|
||
h += filterBarHtml();
|
||
|
||
var kpis = overviewKpis();
|
||
h += '<div class="kt-ai-kpi-strip" data-cards="true" id="kpis">';
|
||
for (var i = 0; i < kpis.length; i++) {
|
||
var k = kpis[i];
|
||
var long = String(k.value).length > 9 ? " data-long=\"true\"" : "";
|
||
h += '<div class="kt-ai-kpi"><span class="label">' + k.label + '</span>' +
|
||
'<span class="value"' + long + ">" + k.value + (k.unit ? '<span class="kt-ai-meta" style="font-weight:400">' + k.unit + "</span>" : "") + "</span>" +
|
||
'<span class="hint">' + k.hint + "</span></div>";
|
||
}
|
||
h += "</div>";
|
||
|
||
h += '<div class="chart-row">';
|
||
|
||
if (p.yoy) {
|
||
var pairs = [];
|
||
var tips = {};
|
||
for (var m = 0; m < p.months; m++) {
|
||
var curV = 0, prevV = 0;
|
||
for (var gg = 0; gg < groups.length; gg++) {
|
||
curV += GROUP_DATA[groups[gg].id].m2026[m];
|
||
prevV += GROUP_DATA[groups[gg].id].m2025[m];
|
||
}
|
||
pairs.push({ label: MONTHS_SHORT[m], vals: [prevV, curV], colors: ["var(--kt-ai-chart-bar-fill)", "var(--kt-ai-chart-blue)"], tipKey: "" + m });
|
||
tips["" + m] = MONTHS[m] + ": 2026 — " + fmtInt(curV) + " млн тг; 2025 — " + fmtInt(prevV) + " млн тг" + (prevV ? " (" + fmtPct((curV - prevV) / prevV * 100) + ")" : "");
|
||
}
|
||
h += chartCard("Динамика за месяц", "2026 против 2025, млн тг", groupedBarHtml(pairs, tips));
|
||
} else {
|
||
var pts = [];
|
||
for (var mm = 0; mm < p.months; mm++) {
|
||
var mv = 0;
|
||
for (var gg = 0; gg < groups.length; gg++) mv += GROUP_DATA[groups[gg].id].m2026[mm];
|
||
var yoyv = 0;
|
||
for (var gg2 = 0; gg2 < groups.length; gg2++) yoyv += GROUP_DATA[groups[gg2].id].m2025[mm];
|
||
var yy = yoyv ? (mv - yoyv) / yoyv * 100 : 0;
|
||
pts.push({ label: MONTHS_SHORT[mm], v: mv, tip: MONTHS[mm] + ": " + fmtInt(mv) + " млн тг, к 2025 " + (yoyv ? fmtPct(yy) : "") });
|
||
}
|
||
h += chartCard("Продажи по месяцам", "млн тг, 2026", lineChartHtml(pts));
|
||
}
|
||
|
||
if (state.group === "all") {
|
||
var donPts = [];
|
||
for (var d = 0; d < GROUP_DEFS.length; d++) {
|
||
var gd = GROUP_DEFS[d];
|
||
donPts.push({ label: gd.name, value: sumRange(GROUP_DATA[gd.id].m2026, 0, p.months), color: gd.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++) {
|
||
var gr = GROUP_DEFS[r];
|
||
rows.push({ id: gr.id, label: gr.name, value: sumRange(GROUP_DATA[gr.id].m2026, 0, p.months), color: gr.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 selMark = rr.id === state.group ? " · выбрано" : "";
|
||
body += barRowHtml(rr.label + selMark, rr.value, rmax, rr.id === state.group ? rr.color : "var(--kt-ai-chart-bar-fill)", rr.label + ": " + fmtInt(rr.value) + " млн тг");
|
||
}
|
||
h += chartCard("Сравнение групп", p.label + ", млн тг", body);
|
||
}
|
||
|
||
h += "</div>";
|
||
|
||
var t = Math.round(Date.now() / 60000) * 60000;
|
||
h += '<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">За ' + p.label.toLowerCase() + " продано на " + fmtInt(kpis[0].value.replace(/[^\d.]/g, "")) + " млн тг (" + groupsLabel + "). Лучшие результаты — " + kpis[3].value.toLowerCase() + "; к уровню 2025 года показатель " + (String(kpis[1].value).indexOf("+") === 0 ? "выше" : "ниже") + " на " + pctNum(kpis[1].value).toFixed(1).replace(".", ",") + " п.д.</span>" +
|
||
'<span class="kt-ai-meta" style="margin-left:auto;white-space:nowrap">Обновлено сегодня в ' + new Date(t).getHours() + ":" + String(new Date(t).getMinutes()).padStart(2, "0") + "</span>" +
|
||
"</section>";
|
||
|
||
return h;
|
||
}
|
||
function pctNum(v) {
|
||
return Math.abs(parseFloat(String(v).replace(",", ".")));
|
||
}
|
||
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 renderDetails() {
|
||
var p = periodDef(state.period);
|
||
var h = "";
|
||
h += '<div class="screen-head"><h1>Детали</h1>' +
|
||
'<span class="screen-sub">Разбивка 2026 по периодам и товарным группам</span></div>';
|
||
h += filterBarHtml();
|
||
|
||
// Таблица по месяцам
|
||
h += '<section class="detail-section"><h2 class="kt-ai-h3 block-title">Продажи по месяцам</h2>';
|
||
var gIds = [];
|
||
for (var i = 0; i < GROUP_DEFS.length; i++) gIds.push(GROUP_DEFS[i].id);
|
||
gIds.sort(function (a, b) { return groupTotal(b) - groupTotal(a); });
|
||
var headCols = "<th>Месяц</th>";
|
||
for (var c = 0; c < gIds.length; c++) {
|
||
headCols += "<th>" + groupName(gIds[c]) + "</th>";
|
||
}
|
||
headCols += "<th>Итого</th><th>К 2025</th>";
|
||
var body = "";
|
||
var colTot = {};
|
||
for (var c3 = 0; c3 < gIds.length; c3++) colTot[gIds[c3]] = 0;
|
||
var grand = 0, grandPrev = 0;
|
||
for (var m = 0; m < p.months; m++) {
|
||
body += "<tr><td><strong>" + MONTHS[m] + "</strong></td>";
|
||
var rowTot = 0, rowPrev = 0;
|
||
for (var c4 = 0; c4 < gIds.length; c4++) {
|
||
var v = GROUP_DATA[gIds[c4]].m2026[m];
|
||
var pv = GROUP_DATA[gIds[c4]].m2025[m];
|
||
colTot[gIds[c4]] += 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) / rowPrev * 100) : "—") + "</td></tr>";
|
||
}
|
||
body += '<tr><td><strong>Итого</strong></td>';
|
||
for (var c5 = 0; c5 < gIds.length; c5++) body += '<td class="num" style="padding-right:18px"><strong>' + fmtInt(colTot[gIds[c5]]) + "</strong></td>";
|
||
body += '<td class="num"><strong>' + fmtInt(grand) + "</strong></td>" +
|
||
'<td class="num"><strong>' + (grandPrev ? fmtPct((grand - grandPrev) / grandPrev * 100) : "—") + "</strong></td></tr>";
|
||
var foot = "Значения в млн тг. «К 2025» — прирост того же месяца к прошлому году.";
|
||
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">' + foot + "</p></section>";
|
||
|
||
// Разбивка по группам
|
||
h += '<section class="detail-section"><h2 class="kt-ai-h3 block-title">Разбивка по товарным группам</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>Лучший месяц</th><th>К 2025</th>" +
|
||
"</tr></thead><tbody>";
|
||
for (var g = 0; g < gIds.length; g++) {
|
||
var gd = null;
|
||
for (var g2 = 0; g2 < GROUP_DEFS.length; g2++) if (GROUP_DEFS[g2].id === gIds[g]) gd = GROUP_DEFS[g2];
|
||
var tot = sumRange(GROUP_DATA[gd.id].m2026, 0, p.months);
|
||
var share = shareOf(gd.id, p.months);
|
||
var tm = topMonthOf(gd.id, 0, p.months);
|
||
var yoy = yoyFor(gd.id, 0, 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">' + (g + 1) + "</td><td class=\"num\">" + Math.round(share) + "%</td>" +
|
||
"<td>" + MONTHS[tm.month] + " · " + fmtInt(tm.value) + '</td><td class="num">' + fmtPct(yoy) + "</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>К 2025</th>" +
|
||
"</tr></thead><tbody>";
|
||
var gTot = 0;
|
||
for (var m2 = 0; m2 < p.months; m2++) gTot += GROUP_DATA[state.group].m2026[m2];
|
||
for (var m3 = 0; m3 < p.months; m3++) {
|
||
var mvv = GROUP_DATA[state.group].m2026[m3];
|
||
var pvv = GROUP_DATA[state.group].m2025[m3];
|
||
var sh = gTot ? mvv / gTot * 100 : 0;
|
||
h += '<tr><td>' + MONTHS[m3] + '</td><td class="num" data-kt-tip="' + MONTHS[m3] + ": " + 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) / pvv * 100) : "—") + "</td></tr>";
|
||
}
|
||
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>' + fmtPct(yoyFor(state.group, 0, p.months)) + "</strong></td></tr></tbody></table></div>";
|
||
}
|
||
h += "</section>";
|
||
return h;
|
||
}
|
||
|
||
function donutHtml(total, pts) {
|
||
var acc = 0;
|
||
var segs = "";
|
||
for (var i = 0; i < pts.length; i++) {
|
||
var p = pts[i];
|
||
if (p.value <= 0) continue;
|
||
var share = p.value / total * 100;
|
||
segs += '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + p.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=\'' + p.label + ': ' + fmtInt(p.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 renderAll() {
|
||
applyScreen();
|
||
}
|
||
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");
|
||
}
|
||
}
|
||
function switchScreen(id) {
|
||
state.screen = id; saveState(); renderAll();
|
||
}
|
||
|
||
renderAll();
|