"use strict"; /* ---------- Данные ---------- */ var REGIONS = [ { name: "г. Алматы", amount: 1284.6 }, { name: "г. Астана", amount: 947.2 }, { name: "Алматинская область", amount: 318.4 }, { name: "Жамбылская область", amount: 264.8 }, { name: "Акмолинская область", amount: 212.5 }, { name: "Костанайская область", amount: 189.3 }, { name: "Восточно-Казахстанская область", amount: 176.9 }, { name: "Кызылординская область", amount: 141.7 }, { name: "Карагандинская область", amount: 128.4 }, { name: "Атырауская область", amount: 117.2 }, { name: "Южно-Казахстанская область", amount: 98.6 }, { name: "Туркестанская область", amount: 84.3 }, { name: "Актюбинская область", amount: 76.9 }, { name: "Мангистауская область", amount: 62.4 }, { name: "Северо-Казахстанская область", amount: 58.1 }, { name: "Усть-Каменогорский край", amount: 47.6 }, { name: "Павлодарская область", amount: 33.8 } ]; var CHART_COLORS = "var(--kt-ai-chart-blue)"; var LS_KEY = "top-sales-by-region-v1"; var PERIOD = "2026 год, 9 месяцев"; /* ---------- Состояние ---------- */ var state = { sortDesc: true, search: "", top3Only: false, pinned: [] }; function loadState() { try { var raw = localStorage.getItem(LS_KEY); if (raw) { var saved = JSON.parse(raw); if (saved && typeof saved === "object") { if (typeof saved.sortDesc === "boolean") state.sortDesc = saved.sortDesc; if (typeof saved.search === "string") state.search = saved.search; if (typeof saved.top3Only === "boolean") state.top3Only = saved.top3Only; if (saved.pinned && Array.isArray(saved.pinned)) state.pinned = saved.pinned; } } } catch (e) { /* повреждённые данные — начинаем с чистого состояния */ } } function saveState() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) { /* приватный режим — молча пропускаем */ } } /* ---------- Утилиты ---------- */ function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatNumber(value, digits) { var n = Number(value); if (!isFinite(n)) return "—"; return n.toLocaleString("ru-RU", { minimumFractionDigits: digits, maximumFractionDigits: digits }); } function formatFull(value) { var n = Number(value); if (!isFinite(n)) return "—"; return n.toLocaleString("ru-RU", { maximumFractionDigits: 1 }); } function getEl(id) { return document.getElementById(id); } function shortName(name) { if (name.indexOf("область") !== -1) return name.replace("область", "обл.").replace("Восточно-Казахстанская", "Вост.-Казахст."); return name; } /* ---------- Выборка ---------- */ function totalAmount() { var t = 0; for (var i = 0; i < REGIONS.length; i++) t += REGIONS[i].amount; return t; } function currentData() { var q = state.search.trim().toLowerCase(); var byAmount = REGIONS.slice().sort(function (a, b) { return b.amount - a.amount; }); var top3Names = []; for (var t = 0; t < 3; t++) top3Names.push(byAmount[t].name); var max = byAmount[0] ? byAmount[0].amount : 0; var grandTotal = totalAmount(); var allTotal = 0; for (var a = 0; a < REGIONS.length; a++) allTotal += REGIONS[a].amount; var rows = []; for (var k = 0; k < REGIONS.length; k++) { var r = REGIONS[k]; var matches = !q || r.name.toLowerCase().indexOf(q) !== -1; var isPinned = state.pinned.indexOf(r.name) !== -1; if (state.top3Only && top3Names.indexOf(r.name) === -1 && !isPinned) matches = false; if (!matches) continue; var rank = null; for (var x = 0; x < byAmount.length; x++) { if (byAmount[x].name === r.name) { rank = x + 1; break; } } var share = grandTotal > 0 ? (r.amount / grandTotal) * 100 : 0; rows.push({ name: r.name, amount: r.amount, rank: rank, share: share, top: top3Names.indexOf(r.name) !== -1, pinned: isPinned, max: max }); } rows.sort(function (a, b) { if (a.pinned !== b.pinned) return a.pinned ? -1 : 1; if (a.name !== b.name) { var an = a.name.toLowerCase(); var bn = b.name.toLowerCase(); if (an < bn) return -1; if (an > bn) return 1; return 0; } if (state.sortDesc) return b.amount - a.amount; return a.amount - b.amount; }); return rows; } /* ---------- Рендер ---------- */ function renderMeta(rows) { var meta = getEl("dash-meta"); if (meta) meta.textContent = PERIOD + " · млн тенге"; } function renderKpis(rows) { var strip = getEl("kpi-strip"); if (!strip) return; var total = 0; var best = null; var visibleCount = rows.length; for (var i = 0; i < rows.length; i++) { total += rows[i].amount; if (!best || rows[i].amount > best.amount) best = rows[i]; } var allTotal = 0; for (var k = 0; k < REGIONS.length; k++) allTotal += REGIONS[k].amount; var top3Names = []; var byAmount = REGIONS.slice().sort(function (a, b) { return b.amount - a.amount; }).slice(0, 3); var top3Sum = 0; for (var j = 0; j < byAmount.length; j++) top3Sum += byAmount[j].amount; var kpis = []; kpis.push({ label: "Всего областей", value: String(REGIONS.length), hint: "в рейтинге" }); kpis.push({ label: "Общий объём продаж", value: formatNumber(allTotal, 1), hint: "млн тг, все области" }); kpis.push({ label: "Лидер продаж", value: best ? best.name : "—", hint: best ? formatNumber(best.amount, 1) + " млн тг" : "", long: true }); kpis.push({ label: "Объём топ-3", value: formatNumber(top3Sum, 1), hint: "млн тг, 3 области" }); var html = ""; for (var m = 0; m < kpis.length; m++) { html += '
'; html += '' + escapeHtml(kpis[m].label) + ""; html += '" + escapeHtml(kpis[m].value) + ""; if (kpis[m].hint) html += '' + escapeHtml(kpis[m].hint) + ""; html += "
"; } strip.innerHTML = html; } function renderChart(rows) { var barsBox = getEl("chart-bars"); var axisBox = getEl("chart-axis"); if (!barsBox || !axisBox) return; var show = rows.slice().sort(function (a, b) { return b.amount - a.amount; }).slice(0, 10); var html = ""; var axis = ""; var maxVal = 0; for (var i = 0; i < show.length; i++) if (show[i].amount > maxVal) maxVal = show[i].amount; for (var j = 0; j < show.length; j++) { var r = show[j]; var h = maxVal > 0 ? Math.round((r.amount / maxVal) * 100) : 0; if (h < 2) h = 2; var top3 = (j < 3); var tip = "{"title":"" + r.name + "","rows":[{"label":"Объём продаж","value":"" + formatFull(r.amount) + " млн тг"},{"label":"Доля","value":"" + formatNumber(r.share, 1) + "%"}]}"; html += '
'; axis += '' + escapeHtml(shortName(r.name)) + ""; } barsBox.innerHTML = html; axisBox.innerHTML = axis; } function renderFilters() { var chips = getEl("filter-chips"); if (!chips) return; var html = ""; var count = getEl("chip-count"); if (state.pinned.length > 0) { for (var i = 0; i < state.pinned.length; i++) { html += '' + escapeHtml("Закреплено") + " " + escapeHtml(state.pinned[i]) + ''; } } if (state.top3Only) { html += 'Фильтр Топ-3'; } chips.innerHTML = html; if (count) { var rows = currentData(); count.textContent = "Показано: " + rows.length + " из " + REGIONS.length; } var searchInput = getEl("search"); if (searchInput && searchInput.value !== state.search) searchInput.value = state.search; var t3 = getEl("btn-top3"); if (t3) t3.textContent = state.top3Only ? "Снять: Топ-3" : "Топ-3"; var sortBtn = getEl("btn-sort"); if (sortBtn) sortBtn.setAttribute("aria-sort", state.sortDesc ? "descending" : "ascending"); } function renderTable(rows) { var tbody = getEl("table-body"); var empty = getEl("empty"); if (!tbody) return; if (rows.length === 0) { tbody.innerHTML = ""; if (empty) empty.hidden = false; return; } if (empty) empty.hidden = true; var html = ""; var maxAmount = 0; for (var i = 0; i < rows.length; i++) if (rows[i].amount > maxAmount) maxAmount = rows[i].amount; for (var j = 0; j < rows.length; j++) { var r = rows[j]; var pct = maxAmount > 0 ? Math.round((r.amount / maxAmount) * 100) : 0; var rankCell = '' + (r.rank ? r.rank : "—") + ""; if (r.pinned) rankCell += ' '; html += ''; html += "" + rankCell + ""; html += '' + escapeHtml(r.name) + ""; html += '' + formatNumber(r.amount, 1) + ""; html += '' + formatNumber(r.share, 1) + "%"; html += ""; } tbody.innerHTML = html; } function renderSummary(rows) { var box = getEl("summary"); if (!box) return; if (rows.length === 0) { box.hidden = true; return; } var sorted = rows.slice().sort(function (a, b) { return b.amount - a.amount; }); var best = sorted[0]; var second = sorted[1]; var total = 0; for (var i = 0; i < rows.length; i++) total += rows[i].amount; var allTotal = 0; for (var k = 0; k < REGIONS.length; k++) allTotal += REGIONS[k].amount; var bestShare = best && allTotal > 0 ? (best.amount / allTotal) * 100 : 0; var text = "Лидер — " + best.name + ": " + formatNumber(best.amount, 1) + " млн тг, доля " + formatNumber(bestShare, 1) + "%"; if (second && second.name !== best.name) { text += ", " + second.name + " — " + formatNumber(second.amount, 1) + " млн тг"; } text += ". Итого по отфильтрованному списку: " + formatNumber(total, 1) + " млн тг."; box.textContent = text; box.hidden = false; } function renderSortControl() { var ic = getEl("sort-ic"); if (ic) ic.setAttribute("data-dir", state.sortDesc ? "desc" : "asc"); } function render() { var rows = currentData(); renderMeta(rows); renderKpis(rows); renderChart(rows); renderFilters(); renderTable(rows); renderSummary(rows); renderSortControl(); saveState(); } /* ---------- События ---------- */ function toast(message) { var stack = document.querySelector(".kt-ai-toast-stack"); if (!stack) return; var el = document.createElement("div"); el.className = "kt-ai-toast"; el.textContent = message; stack.appendChild(el); setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 2500); } function resetAll() { state.sortDesc = true; state.search = ""; state.top3Only = false; state.pinned = []; var s = getEl("search"); if (s) s.value = ""; render(); } function wireEvents() { var sortBtn = getEl("btn-sort"); if (sortBtn) { sortBtn.addEventListener("click", function () { state.sortDesc = !state.sortDesc; render(); }); } var search = getEl("search"); if (search) { search.addEventListener("input", function () { state.search = search.value; render(); }); } var top3Btn = getEl("btn-top3"); if (top3Btn) { top3Btn.addEventListener("click", function () { state.top3Only = !state.top3Only; render(); }); } var resetBtn = getEl("btn-refresh"); if (resetBtn) { resetBtn.addEventListener("click", function () { try { localStorage.removeItem(LS_KEY); } catch (e) { /* приватный режим */ } resetAll(); toast("Данные обновлены"); }); } var emptyReset = getEl("btn-empty-reset"); if (emptyReset) emptyReset.addEventListener("click", resetAll); var chips = getEl("filter-chips"); if (chips) { chips.addEventListener("click", function (ev) { var target = ev.target; while (target && target !== chips) { if (target.classList && target.classList.contains("remove")) break; target = target.parentNode; } if (!target || target === chips) return; var region = target.getAttribute ? target.getAttribute("data-region") : null; var clearTop3 = target.getAttribute ? target.getAttribute("data-clear-top3") : null; if (region) { state.pinned = state.pinned.filter(function (p) { return p !== region; }); render(); } else if (clearTop3) { state.top3Only = false; render(); } }); } var tbody = getEl("table-body"); if (tbody) { tbody.addEventListener("click", function (ev) { var target = ev.target; while (target && target !== tbody) { if (target.tagName === "TR") break; target = target.parentNode; } if (!target || target === tbody) return; var cells = target.querySelectorAll("td"); if (cells.length < 2) return; var name = cells[1].textContent.trim(); if (!name) return; var idx = state.pinned.indexOf(name); if (idx === -1) { state.pinned.push(name); toast("Закреплено: " + name); } else { state.pinned.splice(idx, 1); toast("Откреплено: " + name); } render(); }); } } function init() { loadState(); var s = getEl("search"); if (s) s.value = state.search; wireEvents(); render(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); }