485 lines
22 KiB
JavaScript
485 lines
22 KiB
JavaScript
"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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7M8 7h9v9"/></svg>';
|
||
var ARROW_DOWN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7 17 17M17 8v9H8"/></svg>';
|
||
|
||
function kpiCard(label, value, hint) {
|
||
return '<div class="kt-ai-kpi">' +
|
||
'<div class="label">' + label + '</div>' +
|
||
'<div class="value">' + value + '</div>' +
|
||
(hint ? '<div class="hint">' + hint + '</div>' : '') +
|
||
'</div>';
|
||
}
|
||
|
||
function deltaCell(delta) {
|
||
var dir = delta >= 0 ? "up" : "down";
|
||
var arrow = delta >= 0 ? ARROW_UP : ARROW_DOWN;
|
||
return '<span class="delta" data-dir="' + dir + '">' + arrow + (delta >= 0 ? "+" : "") + fmtNum(delta, 1) + " %</span>";
|
||
}
|
||
|
||
function statusPill(status, text) {
|
||
return '<span class="kt-ai-status-pill" data-status="' + status + '">' + text + '</span>';
|
||
}
|
||
|
||
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 += '<div class="toplist-item">' +
|
||
'<span class="toplist-rank">' + (i + 1) + '</span>' +
|
||
'<span class="toplist-city">' + list[i].name + '</span>' +
|
||
'<span class="toplist-val">' + fmtNum(cityRevenue(list[i]), 1) + ' млн ₸</span>' +
|
||
'</div>';
|
||
}
|
||
var el = $("cityTop");
|
||
if (el) el.innerHTML = html || '<div class="kt-ai-meta">Нет данных</div>';
|
||
}
|
||
|
||
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 += '<button type="button" class="hbar-row" data-open-city="' + c.id + '">' +
|
||
'<span class="hbar-name">' + c.name + '</span>' +
|
||
'<span class="hbar-track"><span class="hbar-fill" style="width:' + pct + '%;background:' + color + '"></span></span>' +
|
||
'<span class="hbar-val">' + fmtNum(rev, 1) + '</span>' +
|
||
'</button>';
|
||
}
|
||
var el = $("cityBars");
|
||
if (el) el.innerHTML = html || '<div class="kt-ai-meta">Нет данных</div>';
|
||
}
|
||
|
||
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 = '<tr data-open-city="' + c.id + '" style="cursor:pointer">' +
|
||
'<td><strong>' + c.name + '</strong></td>' +
|
||
'<td>' + (state.dir ? state.dir : "Все") + '</td>' +
|
||
'<td class="num">' + c.stores + '</td>' +
|
||
'<td class="num">' + fmtInt(Math.round(c.traffic * 1000)) + '</td>' +
|
||
'<td class="num"><strong>' + fmtNum(rev, 1) + '</strong></td>' +
|
||
'<td class="num">' + fmtInt(c.avgCheck) + ' ₸</td>' +
|
||
'<td class="num">' + deltaCell(d) + '</td>' +
|
||
'<td>' + statusPill(c.status, c.statusText) + '</td>' +
|
||
'</tr>';
|
||
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 += '<div class="cc-bar" data-kt-tip="{"title":"' + c.name + ' — ' + MONTHS[b] + '",' +
|
||
'"rows":[{"label":"Выручка","value":"' + fmtNum(series[b], 1) + ' млн ₸"}]}">' +
|
||
'<i style="height:' + h + '%;background:' + (last ? "var(--kt-ai-chart-blue)" : "var(--kt-ai-chart-bar-fill)") + ';"></i>' +
|
||
'</div>';
|
||
}
|
||
$("cityTrend").innerHTML = '<div class="cc-bars" style="height:190px">' + bars + '</div>' +
|
||
'<div class="cc-axis cc-axis-cat">' + MONTHS_SHORT.map(function (s) { return '<span>' + s + '</span>'; }).join("") + '</div>';
|
||
|
||
// Каналы продаж
|
||
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 += '<div class="toplist-item" style="gap:var(--kt-ai-space-3)">' +
|
||
'<span style="flex:0 0 84px;font-size:var(--kt-ai-text-sm)">' + CHANNEL_NAMES[ch] + '</span>' +
|
||
'<span class="hbar-track" style="flex:1"><span class="hbar-fill" style="width:' + cpct + '%"></span></span>' +
|
||
'<span class="toplist-val" style="flex:0 0 96px">' + fmtNum(chans[ch], 1) + ' · ' + cshare + ' %</span>' +
|
||
'</div>';
|
||
}
|
||
$("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);
|