"use strict"; // ---------- Данные: розничные продажи по городам (февраль 2026) ---------- // revenue/ttraffic — февраль в миллионах тенге; jan — январь (для динамики); // trend — выручка за 6 месяцев; share — доля розницы в выручке города, %; // directions — направления продаж; channels — выручка по каналам, млн ₸. var CITIES = [ { id: "almaty", name: "Алматы", revenue: 148.5, traffic: 21.3, avgCheck: 6972, stores: 21, jan: 132.0, share: 34, status: "ok", statusText: "Выше плана", directions: [ { id: "inet", name: "Интернет", revenue: 38.4, jan: 36.1, trend: [33.1, 33.8, 35.0, 34.2, 35.6, 38.4], shareOfCity: 26, channels: [27.9, 7.7, 2.8] }, { id: "tv", name: "Телевидение", revenue: 29.1, jan: 26.4, trend: [24.0, 25.1, 26.0, 25.5, 27.2, 29.1], shareOfCity: 20, channels: [18.6, 7.5, 3.0] }, { id: "mobile", name: "Мобильная связь", revenue: 46.2, jan: 43.8, trend: [40.5, 41.2, 43.0, 42.1, 44.5, 46.2], shareOfCity: 31, channels: [33.7, 9.5, 3.0] }, { id: "smart", name: "Умный дом", revenue: 22.7, jan: 17.6, trend: [15.2, 15.9, 17.0, 16.8, 20.1, 22.7], shareOfCity: 15, channels: [16.0, 4.5, 2.2] }, { id: "b2b", name: "Корпоративные решения", revenue: 12.1, jan: 10.2, trend: [8.8, 9.1, 10.0, 9.6, 11.0, 12.1], shareOfCity: 8, channels: [9.1, 2.4, 0.6] } ], channels: [91.3, 26.4, 10.8] }, { id: "astana", name: "Астана", revenue: 131.7, traffic: 18.6, avgCheck: 7081, stores: 17, jan: 119.5, share: 31, status: "ok", statusText: "Выше плана", directions: [ { id: "inet", name: "Интернет", revenue: 35.2, jan: 31.9, trend: [28.5, 29.4, 31.0, 30.2, 33.0, 35.2], shareOfCity: 27, channels: [24.6, 7.2, 3.4] }, { id: "tv", name: "Телевидение", revenue: 28.4, jan: 25.8, trend: [23.5, 24.2, 25.5, 24.9, 27.0, 28.4], shareOfCity: 22, channels: [18.2, 7.0, 3.2] }, { id: "mobile", name: "Мобильная связь", revenue: 40.3, jan: 38.6, trend: [35.5, 36.1, 38.0, 37.2, 39.0, 40.3], shareOfCity: 31, channels: [28.2, 8.5, 3.6] }, { id: "smart", name: "Умный дом", revenue: 18.9, jan: 15.3, trend: [12.8, 13.4, 14.6, 14.2, 17.0, 18.9], shareOfCity: 14, channels: [13.2, 3.9, 1.8] }, { id: "b2b", name: "Корпоративные решения", revenue: 8.9, jan: 10.7, trend: [11.0, 10.8, 9.9, 9.6, 9.2, 8.9], shareOfCity: 7, channels: [6.7, 1.8, 0.4] } ], channels: [81.3, 23.4, 11.0] }, { id: "shymkent", name: "Шымкент", revenue: 79.6, traffic: 13.2, avgCheck: 6030, stores: 10, jan: 71.4, share: 28, status: "warn", statusText: "Растёт", directions: [ { id: "tv", name: "Телевидение", revenue: 30.5, jan: 27.9, trend: [25.0, 26.0, 27.0, 26.5, 29.0, 30.5], shareOfCity: 38, channels: [19.8, 6.9, 3.8] }, { id: "inet", name: "Интернет", revenue: 22.1, jan: 19.8, trend: [16.4, 17.1, 18.3, 17.8, 19.5, 22.1], shareOfCity: 28, channels: [14.4, 5.1, 2.6] }, { id: "mobile", name: "Мобильная связь", revenue: 15.8, jan: 15.1, trend: [13.9, 14.2, 14.8, 14.5, 15.2, 15.8], shareOfCity: 20, channels: [11.1, 3.2, 1.5] }, { id: "smart", name: "Умный дом", revenue: 11.2, jan: 8.6, trend: [6.4, 6.8, 7.5, 7.2, 9.8, 11.2], shareOfCity: 14, channels: [7.8, 2.4, 1.0] } ], channels: [53.1, 17.6, 8.9] }, { id: "karaganda", name: "Караганда", revenue: 56.4, traffic: 9.4, avgCheck: 5999, stores: 8, jan: 53.2, share: 26, status: "ok", statusText: "Выше плана", directions: [ { id: "tv", name: "Телевидение", revenue: 22.4, jan: 21.1, trend: [18.5, 19.0, 20.0, 19.6, 21.0, 22.4], shareOfCity: 40, channels: [14.6, 5.0, 2.8] }, { id: "inet", name: "Интернет", revenue: 17.6, jan: 16.5, trend: [14.0, 14.6, 15.4, 15.0, 16.4, 17.6], shareOfCity: 31, channels: [11.4, 4.2, 2.0] }, { id: "mobile", name: "Мобильная связь", revenue: 10.3, jan: 9.7, trend: [8.7, 9.0, 9.4, 9.2, 9.8, 10.3], shareOfCity: 18, channels: [7.2, 2.2, 0.9] }, { id: "smart", name: "Умный дом", revenue: 6.1, jan: 5.9, trend: [4.6, 4.9, 5.3, 5.1, 5.6, 6.1], shareOfCity: 11, channels: [4.3, 1.3, 0.5] } ], channels: [37.5, 12.7, 6.2] }, { id: "aktobe", name: "Актобе", revenue: 47.8, traffic: 7.9, avgCheck: 6051, stores: 7, jan: 42.1, share: 25, status: "ok", statusText: "Выше плана", directions: [ { id: "inet", name: "Интернет", revenue: 15.9, jan: 14.3, trend: [11.8, 12.3, 13.2, 12.8, 14.1, 15.9], shareOfCity: 33, channels: [10.1, 4.0, 1.8] }, { id: "tv", name: "Телевидение", revenue: 14.8, jan: 13.9, trend: [12.0, 12.5, 13.2, 12.9, 14.0, 14.8], shareOfCity: 31, channels: [9.5, 3.6, 1.7] }, { id: "mobile", name: "Мобильная связь", revenue: 9.7, jan: 8.8, trend: [7.6, 8.0, 8.4, 8.2, 9.0, 9.7], shareOfCity: 20, channels: [6.8, 2.1, 0.8] }, { id: "smart", name: "Умный дом", revenue: 7.4, jan: 5.1, trend: [3.8, 4.1, 4.4, 4.2, 6.1, 7.4], shareOfCity: 16, channels: [5.1, 1.6, 0.7] } ], channels: [31.5, 11.3, 5.0] }, { id: "petropavl", name: "Петропавловск", revenue: 38.9, traffic: 6.2, avgCheck: 6274, stores: 5, jan: 33.4, share: 24, status: "ok", statusText: "Выше плана", directions: [ { id: "tv", name: "Телевидение", revenue: 16.2, jan: 14.6, trend: [12.4, 12.9, 13.6, 13.2, 15.0, 16.2], shareOfCity: 42, channels: [10.0, 3.9, 2.3] }, { id: "inet", name: "Интернет", revenue: 12.4, jan: 11.2, trend: [9.2, 9.6, 10.3, 9.9, 11.0, 12.4], shareOfCity: 32, channels: [7.6, 3.1, 1.7] }, { id: "mobile", name: "Мобильная связь", revenue: 7.1, jan: 6.5, trend: [5.5, 5.7, 6.1, 5.9, 6.6, 7.1], shareOfCity: 18, channels: [4.9, 1.6, 0.6] }, { id: "smart", name: "Умный дом", revenue: 3.2, jan: 1.1, trend: [0.7, 0.8, 0.9, 0.8, 1.9, 3.2], shareOfCity: 8, channels: [2.0, 0.8, 0.4] } ], channels: [24.5, 9.4, 5.0] }, { id: "kostanay", name: "Костанай", revenue: 33.2, traffic: 5.3, avgCheck: 6264, stores: 4, jan: 31.7, share: 23, status: "warn", statusText: "Растёт", directions: [ { id: "tv", name: "Телевидение", revenue: 14.1, jan: 13.5, trend: [11.6, 12.0, 12.7, 12.4, 13.3, 14.1], shareOfCity: 42, channels: [8.9, 3.4, 1.8] }, { id: "inet", name: "Интернет", revenue: 10.8, jan: 10.2, trend: [8.5, 8.8, 9.4, 9.1, 10.0, 10.8], shareOfCity: 33, channels: [6.8, 2.7, 1.3] }, { id: "mobile", name: "Мобильная связь", revenue: 6.6, jan: 6.3, trend: [5.2, 5.4, 5.7, 5.5, 6.1, 6.6], shareOfCity: 20, channels: [4.6, 1.5, 0.5] }, { id: "smart", name: "Умный дом", revenue: 1.7, jan: 0.7, trend: [0.5, 0.5, 0.6, 0.6, 1.2, 1.7], shareOfCity: 5, channels: [1.1, 0.4, 0.2] } ], channels: [21.4, 8.0, 3.8] }, { id: "taraz", name: "Тараз", revenue: 28.6, traffic: 4.6, avgCheck: 6217, stores: 4, jan: 32.1, share: 22, status: "risk", statusText: "Ниже плана", directions: [ { id: "tv", name: "Телевидение", revenue: 12.3, jan: 13.2, trend: [13.5, 13.3, 13.0, 12.8, 12.5, 12.3], shareOfCity: 43, channels: [7.7, 3.0, 1.6] }, { id: "inet", name: "Интернет", revenue: 9.4, jan: 10.1, trend: [10.6, 10.3, 9.9, 9.7, 9.6, 9.4], shareOfCity: 33, channels: [5.9, 2.3, 1.2] }, { id: "mobile", name: "Мобильная связь", revenue: 5.4, jan: 5.8, trend: [6.0, 5.9, 5.7, 5.6, 5.5, 5.4], shareOfCity: 19, channels: [3.8, 1.2, 0.4] }, { id: "smart", name: "Умный дом", revenue: 1.5, jan: 3.0, trend: [3.2, 3.0, 2.6, 2.2, 1.8, 1.5], shareOfCity: 5, channels: [1.0, 0.4, 0.1] } ], channels: [18.4, 6.9, 3.3] } ]; var MONTHS = ["сент 2025", "окт 2025", "нояб 2025", "дек 2025", "янв 2026", "фев 2026"]; var MONTHS_SHORT = ["С", "О", "Н", "Д", "Я", "Ф"]; var CHANNEL_NAMES = ["Магазины", "Колл-центр", "Онлайн"]; var DIR_LABELS = { inet: "Интернет", tv: "Телевидение", mobile: "Мобильная связь", smart: "Умный дом", b2b: "Корпоратив" }; var state = { search: "", dir: "", city: null }; // ---------- Форматирование ---------- function fmtNum(n, digits) { var s = (n == null) ? "—" : Number(n).toFixed(digits == null ? 1 : digits); return s.replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, " "); } function fmtInt(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " "); } function fmtM(mn) { return fmtNum(mn, 1) + " млн ₸"; } function deltaPct(cur, prev) { if (!prev) return 0; return ((cur - prev) / prev) * 100; } // ---------- Расчёты агрегатов ---------- // Суммарная выручка с учётом фильтра направлений: // если направление выбрано — только его строки, иначе все направления города. function cityRevenue(c) { if (!state.dir) return c.revenue; for (var i = 0; i < c.directions.length; i++) { if (c.directions[i].id === state.dir) return c.directions[i].revenue; if (DIR_LABELS[c.directions[i].id] === state.dir) return c.directions[i].revenue; } return 0; } function cityJan(c) { if (!state.dir) return c.jan; for (var i = 0; i < c.directions.length; i++) { var d = c.directions[i]; if (d.id === state.dir || DIR_LABELS[d.id] === state.dir) return d.jan; } return 0; } function cityTraffic(c) { if (!state.dir || state.dir === "tv" || state.dir === "Телевидение") return c.traffic; return Math.round(c.traffic * 0.12 * 10) / 10; } function cityStores(c) { if (!state.dir) return c.stores; var map = { inet: 0.30, tv: 0.25, mobile: 0.20, smart: 0.15, b2b: 0.10 }; var f = 0.25; for (var k in map) if (k === state.dir) f = map[k]; return Math.max(1, Math.round(c.stores * f)); } function dirMatches(c, dir) { if (!dir) return true; for (var i = 0; i < c.directions.length; i++) { if (c.directions[i].id === dir || DIR_LABELS[c.directions[i].id] === dir) return true; } return false; } function visibleCities() { var q = state.search.toLowerCase(); var out = []; for (var i = 0; i < CITIES.length; i++) { var c = CITIES[i]; if (q && c.name.toLowerCase().indexOf(q) === -1) continue; if (state.dir && !dirMatches(c, state.dir)) continue; out.push(c); } return out; } function totals(cities) { var rev = 0, jan = 0, traffic = 0, stores = 0, n = cities.length; for (var i = 0; i < cities.length; i++) { rev += cityRevenue(cities[i]); jan += cityJan(cities[i]); traffic += cityTraffic(cities[i]); stores += cityStores(cities[i]); } var check = (rev * 100) / (traffic || 1); return { rev: rev, jan: jan, traffic: traffic, avgCheck: check, stores: stores, n: n, delta: deltaPct(rev, jan) }; } // ---------- Рендер: главный экран ---------- var ARROW_UP = ''; var ARROW_DOWN = ''; function kpiCard(label, value, hint) { return '
' + '
' + label + '
' + '
' + value + '
' + (hint ? '
' + hint + '
' : '') + '
'; } function deltaCell(delta) { var dir = delta >= 0 ? "up" : "down"; var arrow = delta >= 0 ? ARROW_UP : ARROW_DOWN; return '' + arrow + (delta >= 0 ? "+" : "") + fmtNum(delta, 1) + " %"; } function statusPill(status, text) { return '' + text + ''; } function renderKpis() { var t = totals(visibleCities()); var dirName = state.dir || "все направления"; var html = ""; html += kpiCard("Выручка за февраль", fmtNum(t.rev, 1) + " млн ₸", "к январю: " + (t.delta >= 0 ? "+" : "") + fmtNum(t.delta, 1) + " %"); html += kpiCard("Средний чек", fmtInt(Math.round(t.avgCheck)) + " ₸", "февраль 2026"); html += kpiCard("Трафик", fmtInt(Math.round(t.traffic * 1000)) + " визитов", "во всех магазинах сети"); html += kpiCard("Магазины", String(t.stores), "в " + t.n + " городах · " + dirName); var el = $("kpiMain"); if (el) el.innerHTML = html; } function renderTop() { var list = visibleCities().slice().sort(function (a, b) { return cityRevenue(b) - cityRevenue(a); }).slice(0, 5); var html = ""; for (var i = 0; i < list.length; i++) { html += '
' + '' + (i + 1) + '' + '' + list[i].name + '' + '' + fmtNum(cityRevenue(list[i]), 1) + ' млн ₸' + '
'; } var el = $("cityTop"); if (el) el.innerHTML = html || '
Нет данных
'; } function renderBars() { var list = visibleCities().slice().sort(function (a, b) { return cityRevenue(b) - cityRevenue(a); }); var max = 0; for (var i = 0; i < list.length; i++) { var v = cityRevenue(list[i]); if (v > max) max = v; } var html = ""; for (var j = 0; j < list.length; j++) { var c = list[j]; var rev = cityRevenue(c); var d = deltaPct(rev, cityJan(c)); var pct = max ? Math.max(2, Math.round((rev / max) * 100)) : 0; var color = d >= 0 ? "var(--kt-ai-chart-blue)" : "var(--kt-ai-chart-salmon)"; html += ''; } var el = $("cityBars"); if (el) el.innerHTML = html || '
Нет данных
'; } function renderTable() { var list = visibleCities().slice().sort(function (a, b) { return cityRevenue(b) - cityRevenue(a); }); var html = ""; for (var i = 0; i < list.length; i++) { var c = list[i]; var rev = cityRevenue(c); var d = deltaPct(rev, cityJan(c)); var tr = '' + '' + c.name + '' + '' + (state.dir ? state.dir : "Все") + '' + '' + c.stores + '' + '' + fmtInt(Math.round(c.traffic * 1000)) + '' + '' + fmtNum(rev, 1) + '' + '' + fmtInt(c.avgCheck) + ' ₸' + '' + deltaCell(d) + '' + '' + statusPill(c.status, c.statusText) + '' + ''; html += tr; } var tbody = $("cityTbody"); if (tbody) tbody.innerHTML = html; var empty = $("cityEmpty"); if (empty) empty.hidden = list.length > 0; var count = $("tableCount"); if (count) count.textContent = list.length + " из " + CITIES.length + " городов"; var sub = $("mainSub"); if (sub) sub.textContent = "Февраль 2026 · " + (state.dir || "все направления"); } function renderMain() { renderKpis(); renderTop(); renderBars(); renderTable(); } function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } function $(id) { return document.getElementById(id); } // ---------- Рендер: экран города ---------- function renderCity() { var c = state.city; if (!c) return; var dir = null; for (var k = 0; k < c.directions.length; k++) { if (c.directions[k].id === state.dir) { dir = c.directions[k]; break; } } $("cityTitle").textContent = c.name; var meta = c.stores + " магазинов"; if (dir) meta = "направление: " + dir.name; $("cityMeta").textContent = meta + " · февраль 2026"; // KPI города var rev = dir ? dir.revenue : c.revenue; var jan = dir ? dir.jan : c.jan; var d = deltaPct(rev, jan); var kpi = ""; kpi += kpiCard("Выручка за февраль", fmtNum(rev, 1) + " млн ₸", "к январю: " + (d >= 0 ? "+" : "") + fmtNum(d, 1) + " %"); if (dir) { kpi += kpiCard("Доля в выручке города", dir.shareOfCity + " %", dir.name); } else { kpi += kpiCard("Средний чек", fmtInt(c.avgCheck) + " ₸", "все направления"); } kpi += kpiCard("Трафик", fmtInt(Math.round(c.traffic * 1000)) + " визитов", "в рознице за месяц"); kpi += kpiCard("Дол. розницы выручки", c.share + " %", "февраль 2026"); $("kpiCity").innerHTML = kpi; // График по месяцам var series = dir ? dir.trend : c.directions[0].trend.map(function (x, i) { var s = 0; for (var j = 0; j < c.directions.length; j++) s += c.directions[j].trend[i]; return Math.round(s * 10) / 10; }); $("trendTitle").textContent = dir ? dir.name : "Динамика выручки"; var max = 0; for (var m = 0; m < series.length; m++) { if (series[m] > max) max = series[m]; } var bars = ""; for (var b = 0; b < series.length; b++) { var h = max ? Math.max(3, Math.round((series[b] / max) * 100)) : 3; var last = b === series.length - 1; bars += '
' + '' + '
'; } $("cityTrend").innerHTML = '
' + bars + '
' + '
' + MONTHS_SHORT.map(function (s) { return '' + s + ''; }).join("") + '
'; // Каналы продаж var chans = dir ? dir.channels : c.channels; var cmax = chans[0] || 1; var chhtml = ""; for (var ch = 0; ch < CHANNEL_NAMES.length; ch++) { var cpct = Math.max(3, Math.round((chans[ch] / cmax) * 100)); var cshare = chans[0] + chans[1] + chans[2] > 0 ? Math.round((chans[ch] / (chans[0] + chans[1] + chans[2])) * 100) : 0; chhtml += '
' + '' + CHANNEL_NAMES[ch] + '' + '' + '' + fmtNum(chans[ch], 1) + ' · ' + cshare + ' %' + '
'; } $("cityChannels").innerHTML = chhtml; $("cityShare").textContent = (dir ? dir.shareOfCity : c.share) + " %"; // Итог var sum; if (dir) { sum = c.name + ": направление «" + dir.name + "» " + (d >= 0 ? "выросло" : "снижилось") + " на " + fmtNum(Math.abs(d), 1) + " % к январю — " + fmtNum(rev, 1) + " млн ₸ за февраль. " + (d >= 0 ? "Динамика сохраняется третий месяц подряд." : "Снижение идёт второй месяц подряд — стоит проверить ассортимент и загрузку магазинов."); } else { sum = c.name + ": розничная выручка за февраль — " + fmtNum(c.revenue, 1) + " млн ₸, " + (d >= 0 ? "выше" : "ниже") + " января на " + fmtNum(Math.abs(d), 1) + " %. " + c.stores + " магазинов дали " + fmtInt(Math.round(c.traffic * 1000)) + " визитов при среднем чеке " + fmtInt(c.avgCheck) + " ₸. " + "Доля розницы в выручке города — " + c.share + " %."; var up = 0, dn = 0; for (var u = 0; u < c.directions.length; u++) { var dd = deltaPct(c.directions[u].revenue, c.directions[u].jan); if (dd >= 0) up++; else dn++; } sum += " Рост по " + up + " направлениям, снижение по " + dn + "."; } $("citySummary").textContent = sum; } // ---------- События и переходы ---------- function openCity(id) { for (var i = 0; i < CITIES.length; i++) { if (CITIES[i].id === id) { state.city = CITIES[i]; break; } } if (!state.city) return; renderCity(); var main = $("screen-main"); var city = $("screen-city"); if (main) main.classList.remove("is-active"); if (city) city.classList.add("is-active"); window.scrollTo(0, 0); } function showMain() { var city = $("screen-city"); var main = $("screen-main"); if (city) city.classList.remove("is-active"); if (main) main.classList.add("is-active"); window.scrollTo(0, 0); } function initDirSelect() { var sel = $("dirSelect"); if (!sel) return; var ids = ["inet", "tv", "mobile", "smart", "b2b"]; for (var i = 0; i < ids.length; i++) { var opt = document.createElement("option"); opt.value = ids[i]; opt.textContent = DIR_LABELS[ids[i]]; sel.appendChild(opt); } } function bindEvents() { var search = $("citySearch"); if (search) { search.addEventListener("input", function () { state.search = this.value; renderMain(); }); } var dir = $("dirSelect"); if (dir) { dir.addEventListener("change", function () { state.dir = this.value; renderMain(); }); } var back = $("backBtn"); if (back) back.addEventListener("click", showMain); document.addEventListener("click", function (e) { var t = e.target; while (t && t !== document && !(t.dataset && t.dataset.openCity)) { t = t.parentNode; } if (t && t.dataset && t.dataset.openCity) { openCity(t.dataset.openCity); } }); } function init() { initDirSelect(); bindEvents(); renderMain(); } document.addEventListener("DOMContentLoaded", init);