(function () { "use strict"; var DATA = []; var cat = null; // активный фильтр категории (null = все) var q = ""; // поисковый запрос var sortKey = "f2025"; // текущая сортировка var sortNum = true; // сортировать по числу или строке var sortDir = -1; // -1 убыв, 1 возр var cats = []; function num(v) { return v == null ? null : Number(v || 0); } function fmt(v) { if (v == null) return "—"; return v.toLocaleString("ru-RU", { maximumFractionDigits: 0 }); } function fmtPct(v) { if (v == null) return "—"; var s = v > 0 ? "+" : ""; return s + v.toFixed(1) + " %"; } function cls(v) { if (v == null) return ""; return v > 0 ? "up" : (v < 0 ? "down" : ""); } function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """); } function formatBars() { var rows = document.querySelectorAll("#tbl tbody tr[data-bar]"); rows.forEach(function (tr) { var val = parseFloat(tr.dataset.bar || "0"); var max = parseFloat(tr.dataset.barmax || "1"); tr.querySelector(".bar").style.width = (max ? (val / max) * 100 : 0) + "%"; }); } function renderTable(rows) { var visible = rows.filter(function (r) { if (cat && r.cat !== cat) return false; if (q && r.name.toLowerCase().indexOf(q) < 0) return false; return true; }); var maxVal = 1; visible.forEach(function (r) { var v = num(r[sortKey]); if (v != null && v > maxVal) maxVal = v; }); visible.sort(function (a, b) { var av = num(a[sortKey]); var bv = num(b[sortKey]); var c; if (sortNum) { var an = av == null ? -Infinity : av; var bn = bv == null ? -Infinity : bv; c = an - bn; } else { c = String(a[sortKey] || "").localeCompare(String(b[sortKey] || ""), "ru"); } return c * sortDir; }); var body = document.getElementById("tbl").querySelector("tbody"); body.innerHTML = visible.map(function (r) { var d25 = num(r.f2025), d26 = num(r.f2026); var delta = d25 != null && d26 != null ? d26 - d25 : null; var deltaP = d25 != null && d26 != null && d25 !== 0 ? ((d26 - d25) / Math.abs(d25)) * 100 : null; var barVal = sortNum ? (num(r[sortKey]) || 0) : (d25 || 0); return "" + "" + esc(r.name) + "" + "" + esc(r.cat) + "" + "" + fmt(d25) + "" + "" + fmt(d26) + "" + "" + fmt(delta) + "" + "" + fmtPct(deltaP) + "" + "
" + ""; }).join(""); document.getElementById("count").textContent = "Позиций показано: " + visible.length + " из " + rows.length; formatBars(); } function sumOf(rows, key) { return rows.reduce(function (acc, r) { var v = num(r[key]); return v == null ? acc : acc + v; }, 0); } function renderKpis(rows) { var s25 = sumOf(rows, "f2025"); var s26 = sumOf(rows, "f2026"); var n25 = rows.filter(function (r) { return num(r.f2025) != null; }).length; var n26 = rows.filter(function (r) { return num(r.f2026) != null; }).length; var tot26 = s26 * 2; // экстраполяция полугодия на год — только ориентир document.getElementById("kpis").innerHTML = "
Итого 2025
" + fmt(s25) + "
" + "
₸ · " + n25 + " позиций
" + "
Итого 6 мес 2026
" + fmt(s26) + "
" + "
₸ · " + n26 + " позиций
" + "
2025 vs 2025 (полугодие)
" + fmt(s25 / 2) + "
" + "
оценка среднего полугодия 2025
" + "
Крупнейшая позиция 2025
" + fmt(s25Max(rows)) + "
" + "
" + esc(topName(rows, "f2025")) + "
"; } function s25Max(rows) { var m = 0; rows.forEach(function (r) { var v = num(r.f2025); if (v != null && v > m) m = v; }); return m; } function topName(rows, key) { var best = null, bv = -1; rows.forEach(function (r) { var v = num(r[key]); if (v != null && v > bv) { bv = v; best = r; } }); return best ? best.name : ""; } function renderCats(rows) { var by = {}; rows.forEach(function (r) { if (!by[r.cat]) by[r.cat] = { name: r.cat, s25: 0, s26: 0 }; var v25 = num(r.f2025), v26 = num(r.f2026); if (v25 != null) by[r.cat].s25 += v25; if (v26 != null) by[r.cat].s26 += v26; }); var total = Object.keys(by).reduce(function (a, k) { return a + by[k].s25; }, 0) || 1; Object.keys(by).sort(function (a, b) { return by[b].s25 - by[a].s25; }).forEach(function (k) { var c = by[k]; var pct = (c.s25 / total) * 100; var el = document.createElement("div"); el.innerHTML = "
" + esc(c.name) + "" + "" + fmt(c.s25) + " · " + (c.s26 != null ? fmt(c.s26) + " (26)" : "—") + "
" + "
"; document.getElementById("cats").appendChild(el); }); } function renderTop(rows, key, elId) { var sorted = rows.slice().filter(function (r) { return num(r[key]) != null; }) .sort(function (a, b) { return num(b[key]) - num(a[key]); }).slice(0, 10); var max = num(sorted[0] ? sorted[0][key] : 1) || 1; document.getElementById(elId).innerHTML = sorted.map(function (r) { var v = num(r[key]); return "
" + "
" + esc(r.name) + "" + fmt(v) + "
" + "
" + "
"; }).join(""); } function renderCatFilters(rows) { cats = []; rows.forEach(function (r) { if (cats.indexOf(r.cat) < 0) cats.push(r.cat); }); cats.sort(); var wrap = document.getElementById("catFilter"); wrap.innerHTML = "" + cats.map(function (c) { return ""; }).join(""); markActiveCat(); } function markActiveCat() { document.querySelectorAll("#catFilter .chip").forEach(function (b) { var active = (b.dataset.cat || "") === (cat || ""); b.classList.toggle("active", active); if (active) b.style.borderColor = "var(--kt-ai-primary)"; else b.style.borderColor = ""; }); } function exportCsv(rows) { var head = ["Позиция", "Категория", "2025, KZT", "6 мес 2026, KZT", "Изменение, KZT", "Изменение, %"]; var body = rows.map(function (r) { var d25 = num(r.f2025), d26 = num(r.f2026); var delta = d25 != null && d26 != null ? d26 - d25 : ""; var deltaP = d25 != null && d26 != null && d25 !== 0 ? (((d26 - d25) / Math.abs(d25)) * 100).toFixed(1) : ""; return [r.name, r.cat, d25 == null ? "" : d25, d26 == null ? "" : d26, delta, deltaP] .map(function (v) { return '"' + String(v).replace(/"/g, '""') + '"'; }).join(";"); }).join("\n"); var csv = "\uFEFF" + [head.join(";"), body].join("\n"); var a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" })); a.download = "reviya_servisnyh_kontraktov.csv"; a.click(); } function init() { var rows = DATA; renderKpis(rows); renderCats(rows); renderTop(rows, "f2025", "top2025"); renderCatFilters(rows); renderTable(rows); document.getElementById("q").addEventListener("input", function (e) { q = e.target.value.trim().toLowerCase(); renderTable(rows); }); document.getElementById("catFilter").addEventListener("click", function (e) { var b = e.target.closest(".chip"); if (!b) return; cat = b.dataset.cat ? b.dataset.cat : null; markActiveCat(); renderTable(rows); }); document.getElementById("btnExport").addEventListener("click", function () { exportCsv(rows); }); document.querySelectorAll("#tbl th").forEach(function (th) { th.addEventListener("click", function () { var k = th.dataset.k; var wasNum = th.dataset.num; if (sortKey === k) sortDir = -sortDir; else { sortKey = k; sortDir = k === "name" ? 1 : -1; } sortNum = wasNum === "1"; renderTable(rows); }); }); } fetch("data.json").then(function (r) { return r.json(); }).then(function (rows) { DATA = rows; init(); }).catch(function (err) { document.getElementById("count").textContent = "Ошибка загрузки данных: " + err.message; }); })();