/* КФУ — финансовая устойчивость. Статическое приложение, весь анализ — в браузере. */ (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 'нет данных'; return nfmt(v, 1); } function fmtR(v) { if (v == null) return 'нет данных'; if (v === Infinity) return '∞'; return nfmt(v, 2); } /* для отображения: EBITDA ≤ 0 вместо «нет данных» у К1/К2 */ function coefDisplay(e, key) { var v = e[key]; if (v == null && e.why && e.why[key] && /EBITDA/i.test(e.why[key]) && e.ebitda != null && e.ebitda <= 0) { return "EBITDA ≤ 0"; } return typeof v === "number" ? nfmt(v, 2) : fmtR(v); } 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, "&").replace(//g, ">").replace(/"/g, """); } 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 zonePriority(z) { var p = { red: 0, yellow: 1, nodata: 2, green: 3, nodefault: 4 }[z]; return p === undefined ? 9 : p; } function filterSortList(list) { var k = S.sort.k, d = S.sort.d; list.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; }); list.sort(function (a, b) { return zonePriority(worst(a)) - zonePriority(worst(b)); }); return list; } 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 hasNumericNorm(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++) if (typeof n[keys[i]] === "number") return true; return false; } function statusHtml(zone) { var cls = { green: "s-green", yellow: "s-yellow", red: "s-red", nodefault: "s-nodefault", nodata: "s-nodata" }[zone] || "s-nodata"; var screenLabel = { green: "В норме", yellow: "Наблюдение", red: "Нарушение", nodefault: "Без норматива", nodata: "Нет данных" }[zone] || zone; return "" + screenLabel + ""; } function coefCell(e, key) { var v = e[key]; var zn = e[key + "Zone"] || "nodata"; var dotCls = { green: "c-green", yellow: "c-yellow", red: "c-red", nodefault: "c-nodefault", nodata: "c-nodata" }[zn] || "c-nodata"; var valHtml; if (v == null) { if (e.why && e.why[key] && /EBITDA/i.test(e.why[key]) && e.ebitda != null && e.ebitda <= 0) { valHtml = "EBITDA ≤ 0"; } else { valHtml = "—"; } } else { var cvCls = zn === "red" ? " cv-red" : (zn === "yellow" ? " cv-yellow" : ""); valHtml = "" + nfmt(v, 2) + ""; } return "" + valHtml + ""; } function causeFor(e) { var parts = []; /* «Без норматива» — отдельная фраза, без числовых сравнений */ if (worst(e) === "nodefault" && !hasNumericNorm(e)) { return "Нормативы не установлены"; } if (e.ebitda != null && e.ebitda <= 0 && (e.k1Zone === "red" || e.k2Zone === "red")) { parts.push("Отрицательная EBITDA"); } var keys = ["k1", "k2", "k3"]; var missOrder = ["debt", "other", "ebitda", "intExp", "equity"]; var missName = { debt: "долг", other: "прочие/гарантии", ebitda: "EBITDA", intExp: "проценты", equity: "капитал" }; for (var i = 0; i < keys.length; i++) { var k = keys[i]; var zone = e[k + "Zone"] || "nodata"; if (zone === "green" || zone === "nodefault") continue; if (zone === "nodata" || e[k] == null) { /* конкретный недостающий показатель */ var fieldMiss = k === "k1" ? "debt" : k === "k2" ? "intExp" : "equity"; if (fieldMiss === "intExp" && e.ebitda != null && e.ebitda <= 0) continue; /* уже фразой выше */ if (fieldMiss === "debt" && e.debt == null) parts.push("нет долга"); else if (fieldMiss === "debt" && e.other == null) parts.push("нет прочих/гарантий"); else if (fieldMiss === "intExp") parts.push("нет процентов"); else if (fieldMiss === "equity") parts.push("нет капитала"); continue; } var th = normNum(e.norms && e.norms[k + "th"]); var tg = normNum(e.norms && e.norms[k + "t"]); var val = nfmt(e[k], 2); if (zone === "red") { if (k === "k2") parts.push("К2 " + val + " ниже порога " + nfmt(th, 2)); else parts.push("К" + (i + 1) + " " + val + " выше порога " + nfmt(th, 2)); } else if (zone === "yellow") { if (k === "k2") parts.push("К2 " + val + " ниже цели " + nfmt(tg, 2)); else parts.push("К" + (i + 1) + " " + val + " выше цели " + nfmt(tg, 2)); } } if (!parts.length) return "—"; var w = worst(e); var cls = w === "red" ? "danger" : (w === "yellow" ? "caution" : ""); return "" + esc(parts.join("; ")) + ""; } function tableRow(e, isGroup) { var wzone = worst(e); var name = e._name || e.name; return "" + "
" + esc(name) + "
" + "" + statusHtml(wzone) + "" + "" + coefCell(e, "k1") + "" + "" + coefCell(e, "k2") + "" + "" + coefCell(e, "k3") + "" + "" + (wzone === "green" ? "—" : causeFor(e)) + "" + ""; } /* ---------- основной рендер ---------- */ function render() { var hasData = !!(S.source || S.isMock); /* первая загрузка */ if (!hasData) { $("#headerSub").textContent = ""; $("#reconChip").hidden = true; $("#brief").hidden = true; $("#toolbar").hidden = true; $("#tableWrap").hidden = true; $("#groupBlock").hidden = true; $("#diagBlock").hidden = true; $("#uploadPanel").hidden = false; $("#fileBadge").hidden = true; $("#btnReplace").hidden = true; $("#btnExport").hidden = true; $("#mockChip").hidden = true; return; } $("#uploadPanel").hidden = true; $("#headerSub").hidden = false; $("#brief").hidden = false; $("#toolbar").hidden = false; $("#tableWrap").hidden = false; $("#btnReplace").hidden = false; $("#btnExport").hidden = false; $("#mockChip").hidden = !S.isMock; /* файл-бейдж: реальное имя + постоянная метка учебного режима */ var shortName = S.source || "учебный пример"; if (shortName.length > 28) shortName = shortName.slice(0, 26) + "…"; $("#fileBadge").hidden = false; $("#fileBadgeText").textContent = S.isMock ? shortName + " · учебный пример (синтетика)" : shortName; /* данные: компании + группа, по экрану (сценарий) */ var comps = [], grp = null; for (var i = 0; i < S.entries.length; i++) { var se = screenEntry(S.entries[i]); if (S.entries[i].group) grp = se; else comps.push(se); } /* заголовок + счётчики: зелёные только при worst === "green" */ var r = 0, y = 0, g = 0, nn = 0, nd = 0, co = comps.length; for (var ci = 0; ci < comps.length; ci++) { var c = comps[ci]; var w = worst(c); if (w === "red") r++; else if (w === "yellow") y++; else if (w === "green") g++; else if (w === "nodefault") nn++; else nd++; } var subParts = []; if (r > 0) subParts.push("" + r + " " + plural(r, "компании", "компании", "компаний") + " нарушают нормативы"); if (y > 0) subParts.push("" + y + " " + plural(y, "компания требует", "компании требуют", "компаний требуют") + " внимания"); if (r === 0 && y === 0 && g === co) subParts.push("Все компании в норме"); else if (!subParts.length) subParts.push("Нарушений нет"); if (nn > 0) subParts.push("Без нормативов: " + nn + ""); if (nd > 0) subParts.push("Неполные данные: " + nd + ""); $("#headerSub").innerHTML = subParts.join(", "); /* сверка — исходные данные */ if (S.recon) { $("#reconChip").hidden = false; $("#reconChipText").textContent = "Сверка " + S.recon.ok + "/" + S.recon.total + " по исходным"; } else { $("#reconChip").hidden = true; } /* сводка: зелёные = только в норме; без нормативов и нет данных — отдельно */ $("#briefRed").textContent = r; $("#briefYellow").textContent = y; $("#briefGreen").textContent = g; $("#briefGreenSub").textContent = nd > 0 ? " + " + nd + " нет данных" : ""; $("#briefNoNorm").textContent = nn; renderDiag(); /* компании (без группы): поиск и зона комбинируются независимо */ var q = (S.q || "").toLowerCase(); var filtered = comps.filter(function (e) { if (q && e.name.toLowerCase().indexOf(q) === -1) return false; if (S.zone) { var wz = worst(e); if (S.zone === "nodefault") return wz === "nodefault"; return wz === S.zone; } return true; }); filtered = filterSortList(filtered); var body = $("#tbody"); if (!filtered.length) { body.innerHTML = ""; $("#tbody").hidden = true; $("#emptyState").hidden = false; } else { $("#tbody").hidden = false; $("#emptyState").hidden = true; var html = ""; for (var j = 0; j < filtered.length; j++) html += tableRow(filtered[j], false); body.innerHTML = html; } $("#tableCount").textContent = filtered.length + " из " + co; /* блок группы: отдельно из S.entries, скрывается только под фильтром/поиском, несовместимым с её зоной */ var showGroup = !!grp && (!S.q || (grp.name.toLowerCase().indexOf(S.q.toLowerCase()) !== -1)) && (!S.zone || worst(grp) === S.zone); if (showGroup) { $("#groupBlock").hidden = false; $("#groupTbody").innerHTML = tableRow(grp, true); } else { $("#groupBlock").hidden = true; } /* сортировка */ var ths = document.querySelectorAll("#tableWrap .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") : ""; } /* активный фильтр-чип */ var chips = document.querySelectorAll(".kfu-chip-filter"); for (var c = 0; c < chips.length; c++) { chips[c].classList.toggle("active", (chips[c].getAttribute("data-zone") || "") === S.zone); } } function plural(n, a, b, c) { var n10 = n % 10, n100 = n % 100; if (n10 === 1 && n100 !== 11) return a; if (n10 >= 2 && n10 <= 4 && (n100 < 12 || n100 > 14)) return b; return c; } /* ---------- 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 dotCls = { green: "c-green", yellow: "c-yellow", red: "c-red", nodefault: "c-nodefault", nodata: "c-nodata" }[zn] || "c-nodata"; var valCls = zn === "red" ? " mval-red" : (zn === "yellow" ? " mval-yellow" : ""); var valHtml; if (v == null) { if (e.why && e.why[key] && /EBITDA/i.test(e.why[key]) && e.ebitda != null && e.ebitda <= 0) { valHtml = "
EBITDA ≤ 0
"; } else { valHtml = "
—
"; } } else { valHtml = "
" + nfmt(v, 2) + "
"; } return "
" + "
" + title + "
" + valHtml + "
" + normLine(e, key) + "
" + (e.why && e.why[key] ? "
" + esc(e.why[key]) + "
" : "") + "
" + formula + "
"; } 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; var sc = recomputeFor(en, delta); var baseOther = en.other; var newOther = (baseOther == null ? null : baseOther + delta); var wzone = worst(sc); var body = ""; /* заголовок: имя + статус */ body += "

" + esc(en.name) + "

" + statusHtml(wzone) + "
" + "
"; /* коэффициенты */ body += "

Коэффициенты

" + metricHtml(sc, "k1", "К1", "(долг + прочие/гарантии) / EBITDA") + metricHtml(sc, "k2", "К2", "EBITDA / процентные расходы") + metricHtml(sc, "k3", "К3", "(долг + прочие/гарантии) / капитал") + "
"; /* сценарий */ body += "

Сценарий

" + "
Прочие/гарантии: " + fmtMoney(baseOther) + " млн ₸ · изменение: " + pctStr(delta) + " млн ₸
" + "
" + "
" + "
Сценарий обновляет коэффициенты, таблицу, счётчики и выгрузку. Сверка остаётся базовой.
" + "
"; /* источники — раскрываемый блок */ body += "
Исходные данные и формулы" + "" + srcRow("Долг", fmtMoney(en.debt), addrVal(en, "debt")) + srcRow("Прочие, в т.ч. гарантии", fmtMoney(newOther), addrVal(en, "other")) + srcRow("EBITDA", fmtMoney(en.ebitda), addrVal(en, "ebitda")) + srcRow("Процентные расходы", fmtMoney(en.intExp), addrVal(en, "intexp")) + srcRow("Капитал", fmtMoney(en.equity), addrVal(en, "equity")) + "
" + "
" + "К1 = (долг + прочие/гарантии) / EBITDA
" + "К2 = EBITDA / процентные расходы
" + "К3 = (долг + прочие/гарантии) / капитал
" + (en.group ? "Группа считается отдельно, не складывается с компаниями." : "Портфельная компания.") + "
"; if (en.issues && en.issues.length) { body += "

Внимание

" + en.issues.map(function (s) { return "
" + esc(s) + "
"; }).join("") + "
"; } $("#drawerBody").innerHTML = body; $("#btnCloseDrawer").onclick = closeDrawer; $("#btnScenReset").onclick = function () { S.scen[en.name] = 0; var si = document.getElementById("scenInput"); if (si) si.value = "0"; renderDrawer(en); render(); }; var inp = document.getElementById("scenInput"); if (inp) { inp.addEventListener("input", function () { var r = parseDelta(inp.value); inp.style.borderColor = r.ok ? "" : "var(--kfu-risk)"; if (!r.ok) { /* нечисловой ввод: последнее допустимое значение остаётся в расчёте, показываем ошибку */ if (!inp._err) { inp._err = true; toast("Введите число (допускаются −, +, запятая)", true); } return; } inp._err = false; 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); }); inp.addEventListener("change", function () { var r = parseDelta(inp.value); if (!r.ok) { /* при переводе фокуса восстанавливаем последнее допустимое значение */ inp.value = (S.scen[en.name] != null && S.scen[en.name] !== 0) ? pctStr(S.scen[en.name]) : "0"; inp.style.borderColor = ""; if (S.scen[en.name] != null && S.scen[en.name] !== 0) { renderDrawer(en); render(); } } }); } } /* возвращает { 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 ? " · " + esc(sheet) : "") + ""; } function srcRow(label, val, addr) { return "" + label + "" + val + "
" + (addr || "") + ""; } /* ---------- выгрузка ---------- */ 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) { $("#diagBlock").hidden = false; $("#diagSummary").textContent = "В книге " + S.diagErrors.length + " ошибок в посторонних ячейках — раскрыть"; $("#diagList").innerHTML = S.diagErrors.map(function (d) { return "
  • «" + esc(d.sheet) + "» " + esc(d.addr) + " → " + esc(d.val || "#err") + "
  • "; }).join(""); } else { $("#diagBlock").hidden = true; } } 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(); } window.KfuWorkbook = { analyzeWorkbook: analyzeWorkbook }; /* ---------- инициализация ---------- */ function init() { var fileInput = $("#fileInput"); fileInput.addEventListener("change", function (e) { if (e.target.files && e.target.files[0]) onFile(e.target.files[0]); e.target.value = ""; }); /* загрузить (первый экран) + заменить файл --> */ var btnUpload = $("#btnUpload"); if (btnUpload) btnUpload.addEventListener("click", function () { fileInput.click(); }); var btnReplace = $("#btnReplace"); if (btnReplace) btnReplace.addEventListener("click", function () { fileInput.click(); }); /* drag-and-drop на страницу --> */ document.addEventListener("dragover", function (e) { e.preventDefault(); }); document.addEventListener("drop", function (e) { e.preventDefault(); if (e.dataTransfer.files && e.dataTransfer.files[0]) onFile(e.dataTransfer.files[0]); }); $("#btnSample").addEventListener("click", function () { var res = syntheticDemo(); applyResult(res, null); toast("Загружен учебный пример (синтетические данные)"); }); /* выгрузка отчёта (topbar, hidden до загрузки) */ var be = $("#btnExport"); if (be) be.addEventListener("click", exportXlsx); $("#q").addEventListener("input", function (e) { S.q = e.target.value; render(); }); /* фильтр-чипы */ var chips = document.querySelectorAll(".kfu-chip-filter"); for (var c = 0; c < chips.length; c++) { chips[c].addEventListener("click", function () { var zone = this.getAttribute("data-zone"); S.zone = zone || ""; for (var j = 0; j < chips.length; j++) chips[j].classList.remove("active"); this.classList.add("active"); render(); }); } /* сортировка в таблице компаний */ document.querySelectorAll("#tableWrap .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")); }); $("#groupTbody").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", function () { if (!document.getElementById("uploadPanel")) return; init(); }); })();