sozday-proekt-avtomatichesko/script.js

247 lines
9.7 KiB
JavaScript
Raw Permalink 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.

(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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 "<tr data-bar=\"" + barVal + "\" data-barmax=\"" + maxVal + "\">" +
"<td><strong>" + esc(r.name) + "</strong></td>" +
"<td><span class=\"kt-ai-chip\">" + esc(r.cat) + "</span></td>" +
"<td class=\"num\">" + fmt(d25) + "</td>" +
"<td class=\"num\">" + fmt(d26) + "</td>" +
"<td class=\"num " + cls(delta) + "\">" + fmt(delta) + "</td>" +
"<td class=\"num " + cls(deltaP) + "\">" + fmtPct(deltaP) + "</td>" +
"<td class=\"bar-cell\"><div class=\"bar-bg\"><div class=\"bar\"></div></div></td>" +
"</tr>";
}).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 =
"<div class=\"kpi\"><div class=\"k\">Итого 2025</div><div class=\"v\">" + fmt(s25) + "</div>" +
"<div class=\"d\">₸ · " + n25 + " позиций</div></div>" +
"<div class=\"kpi\"><div class=\"k\">Итого 6 мес 2026</div><div class=\"v\">" + fmt(s26) + "</div>" +
"<div class=\"d\">₸ · " + n26 + " позиций</div></div>" +
"<div class=\"kpi\"><div class=\"k\">2025 vs 2025 (полугодие)</div><div class=\"v\">" + fmt(s25 / 2) + "</div>" +
"<div class=\"d\">оценка среднего полугодия 2025</div></div>" +
"<div class=\"kpi\"><div class=\"k\">Крупнейшая позиция 2025</div><div class=\"v\">" + fmt(s25Max(rows)) + "</div>" +
"<div class=\"d\">" + esc(topName(rows, "f2025")) + "</div></div>";
}
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 =
"<div class=\"cat-top\"><strong>" + esc(c.name) + "</strong>" +
"<span class=\"muted\">" + fmt(c.s25) + " · " + (c.s26 != null ? fmt(c.s26) + " (26)" : "—") + "</span></div>" +
"<div class=\"cat-bar\"><div style=\"width:" + pct.toFixed(1) + "%;height:8px;border-radius:999px;background:var(--kt-ai-primary)\"></div></div>";
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 "<div style=\"display:flex;flex-direction:column;gap:4px;margin-bottom:var(--kt-ai-space-4)\">" +
"<div class=\"cat-top\"><span>" + esc(r.name) + "</span><strong>" + fmt(v) + "</strong></div>" +
"<div class=\"bar-bg\" style=\"height:8px\"><div class=\"bar\" style=\"width:" + ((v / max) * 100).toFixed(1) + "%\"></div></div>" +
"</div>";
}).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 = "<button class=\"kt-ai-btn chip\" data-cat=\"\" data-size=\"sm\">Все</button>" +
cats.map(function (c) {
return "<button class=\"kt-ai-btn chip\" data-cat=\"" + esc(c) + "\" data-size=\"sm\">" + esc(c) + "</button>";
}).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;
});
})();