sozday-rabochee-veb-prilozhe/app.js

1003 lines
49 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.

/* КФУ — финансовая устойчивость. Статическое приложение, весь анализ — в браузере. */
(function () {
"use strict";
var GROUP_NAME = "Консолидированная группа 1";
var EBI_ROWS = [[26, 1], [27, 1], [28, 1], [31, 1], [32, 1], [40, 1], [41, 1], [45, -1], [56, -1], [64, -1]];
var EBI_ROWS_TXT = "26+27+28+31+32+40+41−45−56−64";
var S = {
source: null,
sheetNames: [],
diag: [],
entries: [],
isMock: false,
sort: { k: "name", d: 1 },
q: "",
zone: "",
norm: "",
onlyRisk: false,
scen: {}
};
/* ---------- утилиты ---------- */
function $(s) { return document.querySelector(s); }
function normName(s) {
if (s == null) return "";
return String(s).replace(/[«»„“”]/g, '"').replace(/[’‘]/g, "'").replace(/[–—−]/g, "-")
.replace(/\s+/g, " ").trim().toLowerCase();
}
function colOfLetter(s) {
s = String(s).toUpperCase().replace(/\$/g, "").replace(/\d+$/, "").trim();
var n = 0;
for (var i = 0; i < s.length; i++) n = n * 26 + (s.charCodeAt(i) - 64);
return n;
}
function toNum(v) {
if (typeof v === "number") return isFinite(v) ? v : null;
if (typeof v === "string") {
var t = v.replace(/\s|,/g, "");
if (t === "" ) return null;
var n = parseFloat(t);
return isFinite(n) ? n : null;
}
return null;
}
function fmtMoney(v) {
if (v == null) return '<span class="none">нет данных</span>';
return nfmt(v, 1);
}
function fmtR(v) {
if (v == null) return '<span class="none">нет данных</span>';
if (v === Infinity) return '<span class="inf">∞</span>';
return nfmt(v, 2);
}
var _nfCache = {};
function nfmt(v, d) {
d = d == null ? 2 : d;
if (!_nfCache[d]) {
_nfCache[d] = new Intl.NumberFormat("ru-RU", { minimumFractionDigits: d, maximumFractionDigits: d, useGrouping: true });
}
return _nfCache[d].format(v).replace(/-\s?/g, "−").replace(/-/g, "−");
}
function fmtC(v) {
if (v == null) return "—";
if (typeof v === "number") return nfmt(Number.isInteger(v) ? v : Math.round(v * 1e6) / 1e6, 6);
return String(v);
}
function pct(v) { return nfmt(v, 4); }
function esc(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function toast(msg, isErr) {
var el = document.createElement("div");
el.className = "kfu-toast" + (isErr ? " err" : "");
el.textContent = msg;
$("#toasts").appendChild(el);
setTimeout(function () { el.remove(); }, 4200);
}
/* ---------- извлечение ячеек из листа (SheetJS: Sheets[name] — объект A1→cell) ---------- */
function getCell(wb, sheet, r, c) {
var ws = wb.Sheets[sheet];
if (!ws) return null;
var addr = XLSX.utils.encode_cell({ r: r - 1, c: c - 1 });
return ws[addr] || null;
}
function cellNum(wb, sheet, r, c) {
var cell = getCell(wb, sheet, r, c);
if (cell == null) return null;
if (cell.t === "e") return null; /* #DIV/0! и т.п. — не число */
var n = toNum(cell.v != null ? cell.v : cell.w);
return n;
}
function cellStr(wb, sheet, r, c) {
var cell = getCell(wb, sheet, r, c);
if (cell == null) return null;
var s = cell.w != null ? cell.w : cell.v;
s = String(s == null ? "" : s).trim();
return s || null;
}
function findSheet(names, subs) {
var low = names.map(function (n) { return n.toLowerCase(); });
for (var i = 0; i < subs.length; i++) {
for (var j = 0; j < low.length; j++) {
if (low[j].indexOf(subs[i]) !== -1) return names[j];
}
}
return null;
}
/* ---------- чтение одного объекта (строка с именами + данные) ---------- */
function readEntry(wb, name, isGroup, maps) {
var d = { name: name, group: isGroup, debt: null, other: null, ebitda: null, intExp: 0,
equity: null, addr: {}, issues: [] };
var colL = XLSX.utils.encode_col;
var normN = normName(name);
if (maps.dol) {
var c = isGroup ? GROUP_COL.dol : maps.dol[normN];
var v = c ? cellNum(wb, maps.dolSheet, 24, c) : null;
d.debt = v == null ? null : v / -1000;
d.addr.debt = colL(c - 1) + "24 / −1000";
d.addr.debtSheet = maps.dolSheet;
if (v == null && !isGroup) d.issues.push("долг: пустая ячейка " + colL(c - 1) + "24 (обнулять нельзя)");
} else if (!isGroup) {
d.issues.push("долг: компания не найдена в листе долга");
}
if (maps.grp) {
var gc = isGroup ? null : maps.grp[normN];
if (isGroup) {
var s9 = 0, has9 = false;
for (var cs = 3; cs <= 17; cs++) {
var v9 = cellNum(wb, maps.grpSheet, 9, cs);
if (v9 != null) { s9 += v9; has9 = true; }
}
/* группа: при любом отсутствующем C9:Q9 — null (долг+прочие считается null) */
d.other = has9 ? s9 * 1000 : null;
d.addr.other = "C9:Q9 · млрд ₸ × 1000";
if (!has9) d.issues.push("прочие/гарантии: весь диапазон C9:Q9 пуст — долг+прочие недоступен");
} else if (gc) {
var gv = cellNum(wb, maps.grpSheet, 9, gc);
d.other = gv == null ? null : gv * 1000;
d.addr.other = colL(gc - 1) + "9 · млрд ₸ × 1000";
if (gv == null) d.issues.push("прочие/гарантии: пустая ячейка — показатель отсутствует");
} else {
d.other = null;
d.issues.push("прочие/гарантии: компания не найдена в листе группы — показатель отсутствует");
}
d.addr.otherSheet = maps.grpSheet;
} else if (!isGroup) {
d.other = null;
d.issues.push("прочие/гарантии: лист группы не найден — показатель отсутствует");
}
if (maps.ebi) {
var ec = isGroup ? GROUP_COL.ebi : maps.ebi[normN];
/* итоговая EBITDA (row 83) — проверка наличия; само значение = сумма слагаемых */
var tot = ec ? cellNum(wb, maps.ebiSheet, 83, ec) : null;
if (tot == null) {
d.ebitda = null;
d.issues.push("EBITDA: итог (row 83) пуст — расчёт заблокирован");
} else {
var parts = [];
var badEbi = null;
for (var i = 0; i < EBI_ROWS.length; i++) {
var rr = EBI_ROWS[i][0], sg = EBI_ROWS[i][1];
var pv = ec ? cellNum(wb, maps.ebiSheet, rr, ec) : null;
if (pv == null) {
var raw = getCell(wb, maps.ebiSheet, rr, ec);
if (raw && raw.t === "e") { badEbi = rr; break; }
/* пустое слагаемое — 0 */
} else parts.push(sg * pv);
}
if (badEbi != null) { d.ebitda = null; d.issues.push("EBITDA: ошибка в слагаемой строке " + badEbi + " — расчёт заблокирован"); }
else d.ebitda = parts.length ? parts.reduce(function (a, b) { return a + b; }, 0) / -1000 : null;
}
/* проценты: отсутствуют = отсутствием остаются (не 0), делится на 1000 */
var iv = ec ? cellNum(wb, maps.ebiSheet, 43, ec) : null;
d.intExp = iv == null ? null : iv / 1000;
d.addr.ebitda = colL(ec - 1) + " стр." + EBI_ROWS_TXT + " / −1000 (итог row83)";
d.addr.ebitdaSheet = maps.ebiSheet;
d.addr.intexp = colL(ec - 1) + "43 / 1000";
d.addr.intexpSheet = maps.ebiSheet;
if (iv == null) d.issues.push("проценты: ячейка 43 пуста — показатель отсутствует (не заменяется нулём)");
} else if (!isGroup) {
d.issues.push("EBITDA: компания не найдена в листе EBITDA");
}
if (maps.bs) {
var bc = isGroup ? GROUP_COL.bs : maps.bs[normN];
var bv = bc ? cellNum(wb, maps.bsSheet, 74, bc) : null;
d.equity = bv == null ? null : bv / -1000;
d.addr.equity = colL(bc - 1) + "74 / −1000";
d.addr.equitySheet = maps.bsSheet;
if (bv == null) d.issues.push("капитал: пустая ячейка (обнулять нельзя)");
} else if (!isGroup) {
d.issues.push("капитал: компания не найдена в балансе");
}
return d;
}
/* ---------- парсер книги ---------- */
/* собственный столбец консолидированной группы (ищем по имени из строки имён) */
var GROUP_COL = { dol: null, ebi: null, bs: null };
function analyzeWorkbook(wb, opts) {
opts = opts || {};
var names = wb.SheetNames;
var out = { diag: [], diagErrors: [], entries: [], sheetNames: names };
/* диагностика: все ячейки с cell.t === "e" по всем листам */
for (var si0 = 0; si0 < names.length; si0++) {
var ws0 = wb.Sheets[names[si0]];
if (!ws0) continue;
var range = XLSX.utils.decode_range(ws0["!ref"] || "A1");
for (var r0 = range.s.r; r0 <= range.e.r; r0++) {
for (var c0 = range.s.c; c0 <= range.e.c; c0++) {
var addr0 = XLSX.utils.encode_cell({ r: r0, c: c0 });
var cell0 = ws0[addr0];
if (cell0 && cell0.t === "e") {
/* cell.v — числовой код ошибки (23=REF, 7=VALUE); показываем как текст */
var codeMap = { 5: "#NAME?", 7: "#VALUE!", 15: "#DIV/0!", 23: "#REF!", 24: "#N/A", 27: "#NULL!", 29: "#NUM!" };
var txt = cell0.w != null ? cell0.w : (codeMap[cell0.v] != null ? codeMap[cell0.v] : "#" + cell0.v);
out.diagErrors.push({ sheet: names[si0], addr: addr0, val: txt });
}
}
}
}
function diagErr(wb, sheet, r, c, field) {
var addr = XLSX.utils.encode_cell({ r: r - 1, c: c - 1 });
var cell = getCell(wb, sheet, r, c);
var val;
if (cell == null) val = "пусто";
else if (cell.t === "e") val = "ошибка " + cell.v;
else val = String(cell.w != null ? cell.w : cell.v);
out.diag.push("«" + sheet + "» " + addr + " (" + field + "): " + val);
}
var dolSheet = opts.sheetOverrides ? opts.sheetOverrides.dol : findSheet(names, ["долг 4q2025_24042026", "долг"]);
var ebiSheet = opts.sheetOverrides ? opts.sheetOverrides.ebi : findSheet(names, ["ebitda 2025_06042026", "ebitda"]);
var bsSheet = opts.sheetOverrides ? opts.sheetOverrides.bs : findSheet(names, ["bs 4q2025_06042026", "bs"]);
var grpSheet = opts.sheetOverrides ? opts.sheetOverrides.grp : findSheet(names, ["группа фонда", "группа"]);
if (!dolSheet) out.diag.push("не найден лист долга — долг недоступен");
if (!ebiSheet) out.diag.push("не найден лист EBITDA");
if (!bsSheet) out.diag.push("не найден лист баланса BS");
if (!grpSheet) out.diag.push("не найден лист «Группа Фонда» — прочие/гарантии и нормативы недоступны");
function findGroupCol(sheet, nr, c1, c2) {
if (!sheet) return null;
for (var c = c1; c <= c2; c++) {
var v = cellStr(wb, sheet, nr, c);
if (v && normName(v) === normName(GROUP_NAME)) return c;
}
return null;
}
GROUP_COL.dol = findGroupCol(dolSheet, 22, 21, 39);
GROUP_COL.ebi = findGroupCol(ebiSheet, 22, 21, 39);
GROUP_COL.bs = findGroupCol(bsSheet, 21, 15, 40);
if (!GROUP_COL.dol) out.diag.push("лист долга: столбец «" + GROUP_NAME + "» не найден — долг группы недоступен");
if (!GROUP_COL.ebi) out.diag.push("лист EBITDA: столбец «" + GROUP_NAME + "» не найден");
if (!GROUP_COL.bs) out.diag.push("лист BS: столбец «" + GROUP_NAME + "» не найден");
var maps = {};
function readNames(wb, sheet, r, c1, c2) {
var m = {}, order = [];
for (var c = c1; c <= c2; c++) {
var v = cellStr(wb, sheet, r, c);
if (v) {
var k = normName(v);
if (!m[k]) { m[k] = c; order.push(k); }
}
}
return { map: m, order: order };
}
var planSheet = opts.sheetOverrides ? opts.sheetOverrides.plan : findSheet(names, ["план 2026", "план"]);
var planNames = [];
if (planSheet) {
for (var r = 5; r <= 20; r++) {
var nm = cellStr(wb, planSheet, r, 1);
if (nm) planNames.push(nm);
}
}
for (var si = 0; si < 4; si++) {
/* строка/диапазон имён для каждого листа */
var defs = [
{ key: "dol", sheet: dolSheet, nr: 22, c1: 21, c2: 39 },
{ key: "ebi", sheet: ebiSheet, nr: 22, c1: 21, c2: 39 },
{ key: "bs", sheet: bsSheet, nr: 21, c1: 15, c2: 40 },
{ key: "grp", sheet: grpSheet, nr: 3, c1: 2, c2: 17 }
];
var def = defs[si];
if (def.sheet) {
var res = readNames(wb, def.sheet, def.nr, def.c1, def.c2);
maps[def.key] = res.map;
maps[def.key + "Sheet"] = def.sheet;
}
}
/* нормативы из листа группы: K1 строки 36(порог)/37(цель), K2 40/41, K3 32/33;
B — группа, C..Q — компании по строке 3 */
function normVal(sheet, r, c) {
var cell = getCell(wb, sheet, r, c);
if (cell == null) return null;
if (cell.t === "e") return { err: "#" + cell.v };
var n = toNum(cell.v != null ? cell.v : cell.w);
if (n != null) return n;
var s = String(cell.w != null ? cell.w : cell.v).trim().toLowerCase();
if (s === "n/a" || s === "н/а" || s === "") return null;
return { text: s };
}
var norms = {};
if (grpSheet) {
var names3 = {};
for (var c3 = 2; c3 <= 17; c3++) {
var n3 = cellStr(wb, grpSheet, 3, c3);
if (n3) names3[normName(n3)] = c3;
}
for (var nk in names3) {
var cc = names3[nk];
norms[nk] = {
k1th: normVal(grpSheet, 36, cc), k1t: normVal(grpSheet, 37, cc),
k2th: normVal(grpSheet, 40, cc), k2t: normVal(grpSheet, 41, cc),
k3th: normVal(grpSheet, 32, cc), k3t: normVal(grpSheet, 33, cc)
};
}
var keys = ["k1th", "k1t", "k2th", "k2t", "k3th", "k3t"];
for (var nk2 in norms) {
for (var ki2 = 0; ki2 < keys.length; ki2++) {
var vv = norms[nk2][keys[ki2]];
if (vv && typeof vv === "object" && vv.err) {
out.diag.push("норматив " + keys[ki2] + " в «" + grpSheet + "»: ошибка " + vv.err);
}
}
}
/* нормативы группы — из столбца B */
var gN = {
k1th: normVal(grpSheet, 36, 2), k1t: normVal(grpSheet, 37, 2),
k2th: normVal(grpSheet, 40, 2), k2t: normVal(grpSheet, 37, 2), /* исправим ниже */
k3th: normVal(grpSheet, 32, 2), k3t: normVal(grpSheet, 33, 2)
};
gN.k2t = normVal(grpSheet, 41, 2);
norms.group = gN;
}
/* список объектов: 15 из плана + группа */
var list = [];
if (planNames.length === 15 || planNames.length >= 14) {
for (var pi = 0; pi < planNames.length; pi++) list.push({ name: planNames[pi], group: planNames[pi] === GROUP_NAME });
} else {
var pool = {};
if (maps.dol) { var dd = order_of(maps.dol); for (var x = 0; x < dd.length; x++) pool[normName(dd[x])] = dd[x]; }
for (var k in pool) list.push({ name: k, group: false });
out.diag.push("список компаний из «План 2026» не распознан — использованы имена листов");
}
var entries = [];
for (var ei = 0; ei < list.length; ei++) {
var e = list[ei];
var nm = e.name;
var isG = e.group;
var en = readEntry(wb, nm, isG, {
dol: maps.dol, dolSheet: dolSheet,
ebi: maps.ebi, ebiSheet: ebiSheet,
bs: maps.bs, bsSheet: bsSheet,
grp: maps.grp, grpSheet: grpSheet
});
var nkey = isG ? "group" : normName(nm);
en.norms = norms[nkey] || null;
coefFor(en, 0);
entries.push(en);
}
/* диагностика ключевых ячеек */
for (var di = 0; di < entries.length; di++) {
var en2 = entries[di];
(function (en) {
if (en.debt == null && en.addr.debtSheet) diagErr(wb, en.addr.debtSheet, 24, colOfLetter(en.addr.debt), "долг");
if (en.ebitda == null && en.addr.ebitdaSheet) diagErr(wb, en.addr.ebitdaSheet, 83, colOfLetter(en.addr.ebitda), "EBITDA");
if (en.equity == null && en.addr.equitySheet) diagErr(wb, en.addr.equitySheet, 74, colOfLetter(en.addr.equity), "капитал");
})(en2);
}
/* сверка с План 2026!G5:J20 */
var recon = null;
if (planSheet && planNames.length) {
var rows = [];
var total = 0, ok = 0;
for (var ri = 0; ri < planNames.length; ri++) {
var rn = planNames[ri];
var en = null;
for (var ej = 0; ej < entries.length; ej++) if (normName(entries[ej].name) === normName(rn)) en = entries[ej];
if (!en) continue;
for (var ci = 0; ci < 4; ci++) {
var ref = cellNum(wb, planSheet, ri + 5, 7 + ci);
var val = [en.debtTotal, en.ebitda, en.intExp, en.equity][ci];
var labels = ["Долг (+прочие)", "EBITDA", "Проценты", "Капитал"];
var pass = ref == null && val == null ? null : (ref != null && val != null && Math.abs(ref - val) <= 0.01);
total++;
if (pass === true) ok++;
rows.push({ name: rn, field: labels[ci], ref: ref, val: val, pass: pass });
}
}
recon = { total: total, ok: ok, rows: rows, sheet: planSheet, addr: "G5:J" + (4 + planNames.length) };
}
out.entries = entries;
out.recon = recon;
out.diag = out.diag;
return out;
}
function order_of(map) {
var out = [];
for (var k in map) out.push(k);
return out;
}
/* ---------- коэфф и зоны ---------- */
function normNum(v) {
if (v == null) return null;
if (typeof v === "object") return null;
if (typeof v === "number") return v;
return null;
}
function coefFor(en, delta) {
var other = en.other; /* null сохраняется */
var debtTotal = (en.debt == null || other == null) ? null : en.debt + other + (delta || 0);
var e = en.ebitda, q = en.equity, intr = en.intExp;
var k1 = null, k2 = null, k3 = null, why = { k1: null, k2: null, k3: null };
if (en.debt == null) why.k1 = "нет долга";
if (en.ebitda == null) {
why.k1 = why.k1 || "нет EBITDA";
why.k2 = "нет EBITDA";
} else if (en.ebitda <= 0) {
why.k1 = why.k1 || "EBITDA ≤ 0";
why.k2 = "EBITDA ≤ 0";
} else {
if (debtTotal != null) k1 = debtTotal / e;
if (intr == null) why.k2 = "нет процентов";
else if (intr === 0) { k2 = Infinity; why.k2 = "проценты = 0 (∞)"; }
else if (intr < 0) why.k2 = "проценты < 0";
else k2 = e / intr;
}
if (en.equity == null) why.k3 = "нет капитала";
else if (en.equity === 0) why.k3 = "капитал = 0 (нет данных)";
else if (en.equity < 0) why.k3 = "капитал < 0";
else if (debtTotal != null) k3 = debtTotal / q;
z(k1, "k1", "green", why.k1, en.norms && en.norms.k1t, en.norms && en.norms.k1th, false, en, debtTotal);
z(k2, "k2", "green", why.k2, en.norms && en.norms.k2t, en.norms && en.norms.k2th, true, en, debtTotal);
z(k3, "k3", "green", why.k3, en.norms && en.norms.k3t, en.norms && en.norms.k3th, false, en, debtTotal);
en.debtTotal = debtTotal;
en.k1 = k1; en.k2 = k2; en.k3 = k3;
en.why = why;
/* если долг/капитал отсутствует — коэффициент не вычислен и не зелёный */
if (en.debt == null && k1 != null) { en.k1 = null; en.why.k1 = "нет долга"; }
if (en.debt == null && k3 != null) { en.k3 = null; en.why.k3 = "нет долга"; }
return en;
}
function z(v, key, dummy, reason, target, threshold, higherBetter, en, debtTotal) {
var t = normNum(target), th = normNum(threshold);
var zone, label;
if (t == null && th == null) {
/* «Без норматива» проверяется ПЕРВЫМ — даже при EBITDA ≤ 0 или отсутствующих данных */
zone = "nodefault"; label = "Без норматива";
} else if (v == null) {
if (reason && /EBITDA ≤ 0/.test(reason)) { zone = "red"; label = "EBITDA ≤ 0"; }
else if (reason && /≤ 0/.test(reason)) { zone = "red"; label = "EBITDA ≤ 0"; }
else if (reason && /< 0/.test(reason)) { zone = "red"; label = "нарушено"; }
else if (reason && /= 0 \(нет данных\)/.test(reason)) { zone = "nodata"; label = "нет данных"; }
else { zone = "nodata"; label = "нет данных"; }
} else {
var ok = higherBetter ? v >= (t != null ? t : th) : v <= (t != null ? t : th);
var bad = th != null && (higherBetter ? v < th : v > th);
zone = bad ? "red" : (ok ? "green" : "yellow");
label = zone === "red" ? "нарушен порог" : (zone === "yellow" ? "между целью и порогом" : "в норме");
}
en[key + "Zone"] = zone;
en[key + "ZoneLabel"] = label;
en[key + "Target"] = t;
en[key + "Threshold"] = th;
if (reason && v == null && zone !== "red") en[key + "ZoneReason"] = reason;
}
function worst(en) {
var order = { nodata: 0, nodefault: 1, green: 2, yellow: 3, red: 4 };
var zs = [en.k1Zone || "nodata", en.k2Zone || "nodata", en.k3Zone || "nodata"];
/* если есть красный или жёлтый — риск, возвращаем его */
for (var i = 0; i < zs.length; i++) if (zs[i] === "red" || zs[i] === "yellow") return zs[i] === "red" ? "red" : "yellow";
/* если есть nodata — итог «Нет данных» (регулируемый показатель отсутствует) */
for (var j = 0; j < zs.length; j++) if (zs[j] === "nodata") return "nodata";
/* иначе — самое худшее из nodefault/green */
var w = zs[0];
for (var k = 1; k < zs.length; k++) if (order[zs[k]] > order[w]) w = zs[k];
return w;
}
function zoneLabel(z) {
return { green: "Зелёная", yellow: "Жёлтая", red: "Красная", nodefault: "Без норматива", nodata: "Нет данных" }[z] || z;
}
/* ---------- учебный (синтетический) пример ---------- */
function syntheticDemo() {
function E(name, debt, other, ebitda, intexp, equity, norms) {
var en = { name: name, group: false, debt: debt, other: other || 0, ebitda: ebitda, intExp: intexp == null ? 0 : intexp,
equity: equity, addr: {}, issues: [], norms: norms || null };
en.addr.debt = "синтетические данные"; en.addr.ebitda = "синтетические данные";
en.addr.intexp = "синтетические данные"; en.addr.equity = "синтетические данные";
en.addr.other = "синтетические данные";
coefFor(en, 0);
return en;
}
var list = [
E("ТОО «ВостокСтрой»", 12000, 800, 3200, 620, 9500, { k1th: 3, k1t: 2, k2th: 2, k2t: 5, k3th: 1, k3t: 0.7 }),
E("АО «МегаХим»", 8500, 120, 4100, 950, 15200, { k1th: 3.5, k1t: 2.5, k2th: 2.5, k2t: 6, k3th: 1.2, k3t: 0.9 }),
E("ТОО «ТрансЛогистик»", 2200, 0, 700, 310, 5400, null),
E("АО «ЮгСталь»", 6800, 250, 620, 480, 7800, { k1th: 4, k1t: 3, k2th: 1.5, k2t: 4, k3th: 1.5, k3t: 1.1 }),
E("ТОО «АгроДержава»", 1500, 60, 40, 55, 2200, null),
E("АО «СеверЭнерго»", null, 0, 1800, 300, 6000, { k1th: 3, k1t: 2, k2th: 2, k2t: 5, k3th: 1, k3t: 0.7 }),
E("ТОО «АлтынМаркет»", 3200, 0, 0, 0, 4100, null),
E("АО «Самрук-Инвест Демо»", 5400, 100, 2900, 12, 300, null),
E("ТОО «Полярис Ритейл»", 900, 0, 1400, 0, 2600, { k1th: 2.5, k1t: 1.5, k2th: 1, k2t: 3, k3th: 0.8, k3t: 0.5 }),
E("АО «Казах-Нефть Демо»", 9800, 460, 5100, 1500, 12000, { k1th: 3, k1t: 2, k2th: 2, k2t: 6, k3th: 1, k3t: 0.6 })
];
var g = E(GROUP_NAME, 38200, 2000, 19160, 4227, 58600, { k1th: 3, k1t: 2.5, k2th: 2, k2t: 4, k3th: 1, k3t: 0.8 });
g.group = true;
list.push(g);
return { source: null, sheetNames: ["<демо>"], diag: [], entries: list, recon: null, isMock: true };
}
/* ---------- вычисление зон (обёртка) ---------- */
/* ---------- рендер ---------- */
function screenEntry(e) {
return recomputeFor(e, S.scen[e.name] || 0);
}
function visibleEntries() {
var q = S.q.toLowerCase();
var out = [];
for (var i = 0; i < S.entries.length; i++) {
var e = S.entries[i];
var sc = screenEntry(e);
if (q && e.name.toLowerCase().indexOf(q) === -1) continue;
var wzone = worst(sc);
if (S.zone) {
if (S.zone === "red" ? wzone !== "red" : wzone !== S.zone) continue;
}
function hasNumericNorm(x) {
var n = x.norms; if (!n) return false;
var keys = ["k1t", "k1th", "k2t", "k2th", "k3t", "k3th"];
for (var j = 0; j < keys.length; j++) if (typeof n[keys[j]] === "number") return true;
return false;
}
if (S.norm === "with" && !hasNumericNorm(e)) continue;
if (S.norm === "without" && hasNumericNorm(e)) continue;
if (S.onlyRisk && !(sc.k1Zone === "red" || sc.k2Zone === "red" || sc.k3Zone === "red")) continue;
sc._name = e.name;
out.push(sc);
}
var k = S.sort.k, d = S.sort.d;
out.sort(function (a, b) {
var va = sortVal(a, k), vb = sortVal(b, k);
if (va == null && vb == null) return 0;
if (va == null) return 1;
if (vb == null) return -1;
if (typeof va === "string") return va.localeCompare(vb) * d;
return (va - vb) * d;
});
if (S.entries.length) {
/* группа всегда последней */
out.sort(function (a, b) { return (a.group ? 1 : 0) - (b.group ? 1 : 0); });
}
return out;
}
function sortVal(e, k) {
if (k === "name") return e.name;
if (k === "debt") return e.debt == null ? null : e.debt;
if (k === "other") return e.other;
if (k === "ebitda") return e.ebitda;
if (k === "intexp") return e.intExp;
if (k === "equity") return e.equity;
if (k === "k1") return e.k1 == null ? null : e.k1;
if (k === "k2") return e.k2 == null ? null : e.k2;
if (k === "k3") return e.k3 == null ? null : e.k3;
if (k === "k1n") return (e.norms && e.norms.k1t) ? 1 : 0;
if (k === "k2n") return (e.norms && e.norms.k2t) ? 1 : 0;
if (k === "k3n") return (e.norms && e.norms.k3t) ? 1 : 0;
return 0;
}
function pill(e, key) {
var zn = e[key + "Zone"];
if (!zn) return '<span class="zone-pill z-nodata">—</span>';
var cls = { green: "z-green", yellow: "z-yellow", red: "z-red", nodefault: "z-nodefault", nodata: "z-nodata" }[zn];
return "<span class=\"zone-pill " + cls + "\" title=\"" + esc(e[key + "ZoneLabel"] || "") + "\">" + zoneLabel(zn) + "</span>";
}
function normCell(e, key) {
var n = e.norms;
if (!n) return '<span class="none">—</span>';
var t = n[key + (key === "k1" ? "t" : key === "k2" ? "t" : "t")];
var th = n[key + "th"];
var tv = normNum(t), hv = normNum(th);
function f(x) { return x == null ? "N/A" : nfmt(x, 2); }
return esc(f(tv) + " / " + f(hv));
}
function render() {
var body = $("#tbody");
if (!S.source && !S.isMock) {
$("#kpis").hidden = true; $("#toolbar").hidden = true; $("#tableWrap").hidden = true; $("#btnExport").hidden = true;
$("#mSource").textContent = "";
$("#mockBanner").hidden = true;
return;
}
$("#kpis").hidden = false; $("#toolbar").hidden = false; $("#tableWrap").hidden = false; $("#btnExport").hidden = false;
$("#btnExport").title = "Снимок текущего расчёта: коэффициенты, нормативы, сверка и источники";
$("#mSource").textContent = S.isMock ? "учебный пример (синтетика)" : (S.source || "");
$("#mockBanner").hidden = !S.isMock;
renderDiag();
renderKpis();
var rows = visibleEntries();
if (!rows.length) { body.innerHTML = ""; $("#emptyState").hidden = false; return; }
$("#emptyState").hidden = true;
var html = "";
for (var i = 0; i < rows.length; i++) {
var e = rows[i];
var wzone = worst(e);
var wcls = { green: "z-green", yellow: "z-yellow", red: "z-red", nodefault: "z-nodefault", nodata: "z-nodata" }[wzone] || "z-nodata";
html += "<tr data-name=\"" + esc(e._name || e.name) + "\" class=\"" + (e.group ? "is-group" : "") + "\">" +
"<td title=\"" + esc(e._name || e.name) + "\">" + esc(e._name || e.name) + (e.group ? " <span class='kt-ai-chip' data-tone='blue' style='margin-left:6px'>группа</span>" : "") + "</td>" +
"<td><span class='zone-pill " + wcls + "'></span> " + zoneLabel(wzone) + "</td>" +
"<td>" + fmtR(e.k1) + " " + pill(e, "k1") + "</td>" +
"<td>" + fmtR(e.k2) + " " + pill(e, "k2") + "</td>" +
"<td>" + fmtR(e.k3) + " " + pill(e, "k3") + "</td>" +
"</tr>";
}
body.innerHTML = html;
/* сортировка */
var ths = document.querySelectorAll(".kfu-table th[data-k]");
for (var t = 0; t < ths.length; t++) {
var k = ths[t].getAttribute("data-k");
ths[t].className = k === S.sort.k ? (S.sort.d === 1 ? "sorted-asc" : "sorted-desc") : "";
}
}
function renderKpis() {
var kpis = $("#kpis");
var co = 0, grp = 0;
var g = 0, y = 0, r = 0, nd = 0, nn = 0;
function hasNorm(e) {
var n = e.norms; if (!n) return false;
var keys = ["k1t", "k1th", "k2t", "k2th", "k3t", "k3th"];
for (var i = 0; i < keys.length; i++) {
var v = n[keys[i]];
if (typeof v === "number") return true;
}
return false;
}
var grpZone = "";
for (var i = 0; i < S.entries.length; i++) {
var e = S.entries[i];
var sc = screenEntry(e);
if (e.group) { grp++; grpZone = worst(sc); continue; }
co++;
var w = worst(sc);
if (w === "red") r++;
else if (w === "yellow") y++;
else if (w === "nodata") nd++;
else if (w === "nodefault") {
if (hasNorm(e)) g++; else nn++;
}
else g++;
}
var grpCls = { green: "var(--kt-ai-status-ok-fg)", yellow: "var(--kt-ai-status-warn-fg)", red: "var(--kt-ai-status-risk-fg)", nodata: "var(--kt-ai-fg-muted)", nodefault: "var(--kt-ai-fg-muted)" }[grpZone] || "";
var reconOk = S.recon ? S.recon.ok + " / " + S.recon.total : "—";
kpis.innerHTML =
"<div class='kt-ai-kpi'><span class='label'>Компании</span><span class='value'>" + co + "</span><span class='hint'>из файла</span></div>" +
"<div class='kt-ai-kpi'><span class='label'>Группа</span><span class='value'>" + grp + "</span><span class='hint'>консолидированная</span></div>" +
"<div class='kt-ai-kpi'><span class='label'>Зелёные</span><span class='value' style='color:var(--kt-ai-status-ok-fg)'>" + g + "</span><span class='hint'>все зоны в норме</span></div>" +
"<div class='kt-ai-kpi'><span class='label'>Жёлтые</span><span class='value' style='color:var(--kt-ai-status-warn-fg)'>" + y + "</span><span class='hint'>между целью и порогом</span></div>" +
"<div class='kt-ai-kpi'><span class='label'>Красные</span><span class='value' style='color:var(--kt-ai-status-risk-fg)'>" + r + "</span><span class='hint'>нарушен порог или EBITDA≤0</span></div>" +
(nn ? "<div class='kt-ai-kpi'><span class='label'>Без норматива</span><span class='value' style='color:var(--kt-ai-fg-muted)'>" + nn + "</span><span class='hint'>нет числовых норм</span></div>" : "") +
(nd ? "<div class='kt-ai-kpi'><span class='label'>Нет данных</span><span class='value' style='color:var(--kt-ai-fg-muted)'>" + nd + "</span><span class='hint'>отсутствуют исходные</span></div>" : "") +
(S.recon ? "<div class='kt-ai-kpi'><span class='label'>Сверка (G5:J20)</span><span class='value'>" + reconOk + "</span><span class='hint'>допуск 0,01 млн ₸</span></div>" : "");
}
/* ---------- drawer ---------- */
function openDrawer(name) {
var en = null;
for (var i = 0; i < S.entries.length; i++) if (S.entries[i].name === name) en = S.entries[i];
if (!en) return;
S.scen = S.scen || {};
if (S.scen[name] == null) S.scen[name] = 0;
renderDrawer(en);
$("#drawer").classList.add("open");
}
function closeDrawer() { $("#drawer").classList.remove("open"); }
function metricHtml(e, key, title, formula) {
var v = e[key];
var zn = e[key + "Zone"] || "nodata";
var cls = { green: "var(--kt-ai-status-ok-fg)", yellow: "var(--kt-ai-status-warn-fg)", red: "var(--kt-ai-status-risk-fg)", nodefault: "var(--kt-ai-fg-muted)", nodata: "var(--kt-ai-fg-muted)" }[zn];
return "<div class='kfu-metric'><div class='mlabel'>" + title + " " + pill(e, key) + "</div>" +
"<div class='mval' style='color:" + cls + "'>" + fmtR(v).replace(/<[^>]+>/g, function (m) { return m === "<span class=\"none\">нет данных<\/span>" ? "<span class='none'>нет данных</span>" : m; }) + "</div>" +
"<div class='mhint'>" + normLine(e, key) + "</div>" +
(e.why && e.why[key] ? "<div class='mhint' style='color:var(--kt-ai-status-warn-fg)'>" + esc(e.why[key]) + "</div>" : "") +
"<div class='kfu-formula'>" + formula + "</div></div>";
}
function normLine(e, key) {
var n = e.norms;
if (!n) return "Норматив не задан → «Без норматива»";
var t = normNum(n[key + "t"]), th = normNum(n[key + "th"]);
var s = "цель " + (t == null ? "N/A" : t) + " · порог " + (th == null ? "N/A" : th);
return s;
}
function renderDrawer(en) {
var delta = S.scen[en.name] || 0;
/* экранная копия — coefFor с дельтой, база en не меняется */
var sc = recomputeFor(en, delta);
var baseOther = en.other;
var newOther = (baseOther == null ? null : baseOther + delta);
var body = "";
body += "<div class='kfu-drawer-head'><div><h2 id='drawerTitle'>" + esc(en.name) + "</h2>" +
"<span class='kt-ai-meta'>" + (en.group ? "Консолидированная группа — считается отдельно, не складывается с компаниями" : "Портфельная компания") + "</span></div>" +
"<button class='kt-ai-icon-button' id='btnCloseDrawer' aria-label='Закрыть'><svg class='kt-icon' aria-hidden='true'><use href='#x'></use></svg></button></div>";
body += "<div class='kfu-drawer-section'><h3>Коэффициенты</h3><div class='kfu-metrics'>" +
metricHtml(sc, "k1", "К1", "(долг + прочие/гарантии) / EBITDA · меньше лучше") +
metricHtml(sc, "k2", "К2", "EBITDA / процентные расходы · больше лучше") +
metricHtml(sc, "k3", "К3", "(долг + прочие/гарантии) / капитал · меньше лучше") +
"</div></div>";
body += "<div class='kfu-drawer-section'><h3>Исходные данные (млн ₸)</h3><table class='kfu-sources'>" +
srcRow("Долг", fmtMoney(en.debt), addrVal(en, "debt")) +
srcRow("Прочие, в т.ч. гарантии", fmtMoney(newOther), addrVal(en, "other") + "<br><span class='kt-ai-meta'>в т.ч. сценарий " + pctStr(delta) + "</span>") +
srcRow("EBITDA", fmtMoney(en.ebitda), addrVal(en, "ebitda")) +
srcRow("Процентные расходы", fmtMoney(en.intExp), addrVal(en, "intexp")) +
srcRow("Капитал", fmtMoney(en.equity), addrVal(en, "equity")) +
"</table></div>";
if (en.issues && en.issues.length) {
body += "<div class='kfu-drawer-section'><h3>Внимание</h3>" + en.issues.map(function (s) {
return "<div class='kfu-delta' style='margin-bottom:6px'>⚠ " + esc(s) + "</div>";
}).join("") + "</div>";
}
body += "<div class='kfu-drawer-section'><h3>Сценарий: изменение прочих/гарантий</h3>" +
"<div class='kfu-scenario-lbl'>Первичные прочие/гарантии: <b>" + fmtMoney(baseOther) + "</b> млн ₸ · изменение: <b>" + pctStr(delta) + "</b> млн ₸</div>" +
"<div class='kfu-scenario-row'><input class='kt-ai-input' id='scenInput' type='text' inputmode='decimal' value='" + pctStr(delta) + "'>" +
"<button class='kt-ai-btn' id='btnScenReset' data-variant='ghost'>Вернуть</button></div>" +
"<div class='kfu-delta'>Сценарий влияет на коэффициенты и выгрузку. Сверка показывает исходные данные.</div>" +
"</div>";
body += "<div class='kfu-drawer-actions'><button class='kt-ai-btn' data-variant='primary' id='btnCloseDrawer2'>Понятно</button></div>";
$("#drawerBody").innerHTML = body;
$("#btnCloseDrawer").onclick = closeDrawer;
$("#btnCloseDrawer2").onclick = closeDrawer;
$("#btnScenReset").onclick = function () {
S.scen[en.name] = 0;
document.getElementById("scenInput").value = "0";
renderDrawer(en);
render();
};
var inp = document.getElementById("scenInput");
inp.addEventListener("input", function () {
var r = parseDelta(inp.value);
inp.dataset.v = r.v;
inp.style.borderColor = r.ok ? "" : "var(--kt-ai-status-risk-fg)";
if (!r.ok) toast("Введите число (допускаются −, +, запятая)", true);
clearTimeout(inp._t);
inp._t = setTimeout(function () {
S.scen[en.name] = r.v;
renderDrawer(en);
render();
var ni = document.getElementById("scenInput");
if (ni) { ni.focus(); }
}, 350);
});
}
/* возвращает { v: число, ok: true } или { v: 0, ok: false } для нечислового */
function parseDelta(s) {
if (s == null) return { v: 0, ok: true };
var t = String(s).trim().replace(",", ".");
if (t === "") return { v: 0, ok: true };
var sign = 1;
if (/^[-–—−]/.test(t)) { sign = -1; t = t.slice(1); }
else if (/^\+/.test(t)) t = t.slice(1);
t = t.replace(/\s/g, "");
if (t === "" || !/^\d*\.?\d*$/.test(t) || t === ".") return { v: 0, ok: false };
var parts = t.split(".");
var ip = parseInt(parts[0] || "0", 10);
var fp = parseInt((parts[1] || "").slice(0, 4), 10);
if (isNaN(ip)) return { v: 0, ok: false };
return { v: sign * (ip + (fp ? fp / Math.pow(10, Math.min((parts[1] || "").length, 4)) : 0)), ok: true };
}
function pctStr(v) {
if (!v) return "0";
var s = Math.abs(v) < 0.001 ? 0 : v;
var parts = String(Math.abs(s)).replace(".", ",").split(".");
var a = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
var f = parts[1] ? "." + parts[1] : "";
return (s < 0 ? "−" : "") + a + f;
}
function addrVal(e, key) {
if (!e.addr) return "";
var a = e.addr;
var sheet = a[key + "Sheet"];
var val = a[key] || "";
return esc(val) + (sheet ? "<br><span class='kt-ai-meta'>лист «" + esc(sheet) + "»</span>" : "");
}
function srcRow(label, val, addr) {
return "<tr><td>" + label + "</td><td>" + val + "<br>" + (addr || "") + "</td></tr>";
}
/* ---------- выгрузка ---------- */
function exportXlsx() {
var wb = XLSX.utils.book_new();
var deltaOf = function (n) { return S.scen[n] || 0; };
var rows = [];
S.entries.forEach(function (e) {
var d = deltaOf(e.name);
var en = recomputeFor(e, d);
rows.push({
"Объект": e.name, "Группа": e.group ? "да" : "нет",
"Долг, млн ₸": e.debt, "Прочие/гарантии, млн ₸": (e.other || 0), "Изменение прочих (сценарий), млн ₸": d,
"Долг+прочие, млн ₸": e.debt == null ? null : e.debt + (e.other || 0) + d,
"EBITDA, млн ₸": e.ebitda, "Проценты, млн ₸": e.intExp, "Капитал, млн ₸": e.equity,
"К1 (Debt/EBITDA)": fmtNum(en.k1), "Зона К1": zoneLabel(en.k1Zone) + (en.k1Zone === "nodata" && en.why.k1 ? " (" + en.why.k1 + ")" : ""),
"Цель К1": normNum(e.norms && e.norms.k1t), "Порог К1": normNum(e.norms && e.norms.k1th),
"К2 (EBITDA/Int)": fmtNum(en.k2), "Зона К2": zoneLabel(en.k2Zone) + (en.k2Zone === "nodata" && en.why.k2 ? " (" + en.why.k2 + ")" : ""),
"Цель К2": normNum(e.norms && e.norms.k2t), "Порог К2": normNum(e.norms && e.norms.k2th),
"К3 (Debt/Equity)": fmtNum(en.k3), "Зона К3": zoneLabel(en.k3Zone) + (en.k3Zone === "nodata" && en.why.k3 ? " (" + en.why.k3 + ")" : ""),
"Цель К3": normNum(e.norms && e.norms.k3t), "Порог К3": normNum(e.norms && e.norms.k3th)
});
});
var ws1 = XLSX.utils.json_to_sheet(rows);
ws1["!cols"] = rows.length ? Object.keys(rows[0]).map(function (k) { return { wch: 16 }; }) : [];
XLSX.utils.book_append_sheet(wb, ws1, "КФУ");
/* нормативы и источники */
var src = [];
S.entries.forEach(function (e) {
["debt", "other", "ebitda", "intexp", "equity"].forEach(function (k) {
var sheet = e.addr[k + "Sheet"];
if (e.addr[k]) src.push({ "Объект": e.name, "Показатель": k, "Формула/адрес": e.addr[k], "Лист": sheet || (S.isMock ? "<демо>" : "") });
});
});
var ws2 = XLSX.utils.json_to_sheet(src);
ws2["!cols"] = [{ wch: 30 }, { wch: 12 }, { wch: 26 }, { wch: 22 }];
XLSX.utils.book_append_sheet(wb, ws2, "Источники");
if (S.diag.length || (S.diagErrors && S.diagErrors.length)) {
var diagRows = [["Раздел", "Позиция", "Значение"]];
S.diag.forEach(function (d) { diagRows.push(["Импорт", d, ""]); });
(S.diagErrors || []).forEach(function (d) { diagRows.push(["Ошибки ячеек", d.sheet + "!" + d.addr, d.val]); });
var ws3 = XLSX.utils.aoa_to_sheet(diagRows);
ws3["!cols"] = [{ wch: 20 }, { wch: 80 }, { wch: 30 }];
XLSX.utils.book_append_sheet(wb, ws3, "Диагностика");
}
/* сверка */
if (S.recon) {
var rr = [];
S.recon.rows.forEach(function (x) {
rr.push({ "Объект": x.name, "Показатель": x.field,
"Контрольный свод (План!G:J)": x.ref == null ? "—" : x.ref,
"Расчёт приложения": x.val == null ? "—" : x.val,
"Совпадение (≤0,01)": x.pass == null ? "нет ссылок" : (x.pass ? "да" : "НЕТ") });
});
var ws4 = XLSX.utils.json_to_sheet(rr);
ws4["!cols"] = [{ wch: 30 }, { wch: 16 }, { wch: 18 }, { wch: 16 }, { wch: 18 }];
XLSX.utils.book_append_sheet(wb, ws4, "Сверка");
}
/* примечание о снапшоте */
var note = [[
"Отчёт — снимок текущего расчёта КФУ.",
"Сформирован: " + new Date().toLocaleString("ru-RU"),
"Источник: " + (S.isMock ? "Учебный пример (синтетические данные)" : (S.source || "файл")),
"Зоны: зелёная — до цели (включая), жёлтая — между целью и порогом, красная — при нарушении порога или EBITDA≤0.",
"При отсутствии нормативов — «Без норматива». При отсутствии долга/капитала коэффициент не вычисляется (не заменяется нулём)."
]];
var ws5 = XLSX.utils.aoa_to_sheet(note);
ws5["!cols"] = [{ wch: 100 }];
XLSX.utils.book_append_sheet(wb, ws5, "Пояснения");
try {
XLSX.writeFile(wb, "КФУ_финансовая_устойчивость_2025.xlsx");
toast("Отчёт .xlsx сохранён (снимок текущего расчёта)");
} catch (err) {
toast("Не удалось сохранить файл: " + err.message, true);
}
}
function fmtNum(v) {
if (v == null) return null;
if (v === Infinity) return "∞";
return Math.round(v * 10000) / 10000;
}
function recomputeFor(e, delta) {
var clone = JSON.parse(JSON.stringify(e));
return coefFor(clone, delta);
}
/* ---------- загрузка файла ---------- */
function onFile(file) {
if (!file) return;
var r = new FileReader();
r.onload = function (ev) {
try {
var wb = XLSX.read(new Uint8Array(ev.target.result), { type: "array" });
var res = analyzeWorkbook(wb);
applyResult(res, file.name);
if (res.diag.length) {
toast("Импорт завершён с замечаниями: " + res.diag.length, true);
} else {
toast("Импорт завершён: компаний " + res.entries.length);
}
if (res.recon && res.recon.ok < res.recon.total) {
toast("Сверка: " + res.recon.ok + " из " + res.recon.total + " показателей совпали", true);
}
} catch (err) {
toast("Ошибка чтения файла: " + err.message, true);
}
};
r.onerror = function () { toast("Не удалось прочитать файл", true); };
r.readAsArrayBuffer(file);
}
function renderDiag() {
if (S.diagErrors && S.diagErrors.length) {
$("#footnote").innerHTML = "<details class='kfu-diag'><summary>В книге " + S.diagErrors.length + " ошибок в посторонних ячейках — раскрыть</summary>" +
"<ul>" + S.diagErrors.map(function (d) {
return "<li>«" + esc(d.sheet) + "» " + esc(d.addr) + " → " + esc(d.val || "#err") + "</li>";
}).join("") + "</ul></details>";
} else {
$("#footnote").innerHTML = "";
}
}
function applyResult(res, sourceName) {
S.source = sourceName || null;
S.sheetNames = res.sheetNames || [];
S.diag = res.diag || [];
S.diagErrors = res.diagErrors || [];
S.entries = res.entries || [];
S.recon = res.recon || null;
S.isMock = !!(res.isMock);
S.scen = {};
render();
}
/* ---------- инициализация ---------- */
function init() {
$("#fileInput").addEventListener("change", function (e) {
if (e.target.files && e.target.files[0]) onFile(e.target.files[0]);
e.target.value = "";
});
var dz = $("#dropzone");
dz.addEventListener("dragover", function (e) { e.preventDefault(); dz.style.borderColor = "var(--kt-ai-primary)"; });
dz.addEventListener("dragleave", function () { dz.style.borderColor = ""; });
dz.addEventListener("drop", function (e) {
e.preventDefault(); dz.style.borderColor = "";
if (e.dataTransfer.files && e.dataTransfer.files[0]) onFile(e.dataTransfer.files[0]);
});
$("#btnSample").addEventListener("click", function () {
var res = syntheticDemo();
applyResult(res, null);
toast("Загружен учебный пример (синтетические данные)");
});
$("#btnClearMock").addEventListener("click", function () {
S = { source: null, sheetNames: [], diag: [], entries: [], isMock: false, sort: { k: "name", d: 1 }, q: "", zone: "", norm: "", onlyRisk: false, scen: {} };
render();
});
$("#btnExport").addEventListener("click", exportXlsx);
$("#q").addEventListener("input", function (e) { S.q = e.target.value; render(); });
$("#zoneFilter").addEventListener("change", function (e) { S.zone = e.target.value; render(); });
$("#normFilter").addEventListener("change", function (e) { S.norm = e.target.value; render(); });
$("#onlyRisk").addEventListener("change", function (e) { S.onlyRisk = e.target.checked; render(); });
document.querySelectorAll(".kfu-table th[data-k]").forEach(function (th) {
th.addEventListener("click", function () {
var k = th.getAttribute("data-k");
if (S.sort.k === k) S.sort.d *= -1;
else S.sort = { k: k, d: k === "name" ? 1 : (["debt", "k1", "k3"].indexOf(k) >= 0 ? -1 : 1) };
render();
});
});
$("#tbody").addEventListener("click", function (e) {
var tr = e.target.closest("tr[data-name]");
if (tr) openDrawer(tr.getAttribute("data-name"));
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeDrawer();
});
render();
}
document.addEventListener("DOMContentLoaded", init);
})();