dashbord-top-prodazh-po-obla/script.js

433 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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 += '<div class="kt-ai-kpi">';
html += '<span class="label">' + escapeHtml(kpis[m].label) + "</span>";
html += '<span class="value"' + (kpis[m].long ? ' data-long="true"' : "") + ">" + escapeHtml(kpis[m].value) + "</span>";
if (kpis[m].hint) html += '<span class="hint">' + escapeHtml(kpis[m].hint) + "</span>";
html += "</div>";
}
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 = "{&quot;title&quot;:&quot;" + r.name + "&quot;,&quot;rows&quot;:[{&quot;label&quot;:&quot;Объём продаж&quot;,&quot;value&quot;:&quot;" + formatFull(r.amount) + " млн тг&quot;},{&quot;label&quot;:&quot;Доля&quot;,&quot;value&quot;:&quot;" + formatNumber(r.share, 1) + "%&quot;}]}";
html += '<div class="cc-bar"' + (top3 ? ' data-top="true"' : "") + ' data-kt-tip="' + tip + '" tabindex="0"><i style="height:' + h + '%;' + (top3 ? "--cc-color:" + CHART_COLORS + ";" : "") + '"></i></div>';
axis += '<span title="' + escapeHtml(r.name) + '">' + escapeHtml(shortName(r.name)) + "</span>";
}
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 += '<span class="kt-ai-filter-chip" data-active="true"><span class="k">' + escapeHtml("Закреплено") + "</span> " +
escapeHtml(state.pinned[i]) +
'<span class="remove" data-region="' + escapeHtml(state.pinned[i]) + '" title="Открепить"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></span></span>';
}
}
if (state.top3Only) {
html += '<span class="kt-ai-filter-chip" data-active="true"><span class="k">Фильтр</span> Топ-3<span class="remove" data-clear-top3="1" title="Снять"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></span></span>';
}
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 = '<span class="rank">' + (r.rank ? r.rank : "—") + "</span>";
if (r.pinned) rankCell += ' <svg class="kt-icon" style="color:var(--kt-ai-primary)" aria-hidden="true" title="Закреплено"><use href="#pin"></use></svg>';
html += '<tr data-top="' + (r.top ? "true" : "false") + '">';
html += "<td>" + rankCell + "</td>";
html += '<td class="row-bar"><i style="width:' + pct + '%"></i><span class="ct">' + escapeHtml(r.name) + "</span></td>";
html += '<td class="num">' + formatNumber(r.amount, 1) + "</td>";
html += '<td class="num"><span class="share">' + formatNumber(r.share, 1) + "%</span></td>";
html += "</tr>";
}
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();
}