diff --git a/index.html b/index.html index 37a2be8..2037b19 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,6 @@ - + + @@ -13,243 +14,428 @@ -
- +
+
+ + + +
+ -
-
- КФУ · факт 2025 -
- + + + + + + + + + + + + + +
+
+ +
+

Загрузите сводную книгу

+

Получите коэффициенты, зоны риска и отчёт в .xlsx

+
+ + -
- -
-
-

Финансовая устойчивость

- файл не загружен -
- -
- - -
- - - - - - - - - -

-
+

Файл обрабатывается в браузере и не отправляется на сервер

+ + + + + +
+ + diff --git a/kfu-client.js b/kfu-client.js index 7a7aeaf..e21a188 100644 --- a/kfu-client.js +++ b/kfu-client.js @@ -550,31 +550,13 @@ 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); - } + 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; - out.sort(function (a, b) { + 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; @@ -582,11 +564,8 @@ 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; + list.sort(function (a, b) { return zonePriority(worst(a)) - zonePriority(worst(b)); }); + return list; } function sortVal(e, k) { if (k === "name") return e.name; @@ -603,99 +582,218 @@ 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 '—'; - var cls = { green: "z-green", yellow: "z-yellow", red: "z-red", nodefault: "z-nodefault", nodata: "z-nodata" }[zn]; - return "" + zoneLabel(zn) + ""; + /* ---------- отображение: статус, коэффициенты, причина ---------- */ + 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 normCell(e, key) { - var n = e.norms; - if (!n) return '—'; - 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 statusHtml(zone) { + var cls = { green: "s-green", yellow: "s-yellow", red: "s-red", nodefault: "s-nodefault", nodata: "s-nodata" }[zone] || "s-nodata"; + return "" + zoneLabel(zone) + ""; } + 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 body = $("#tbody"); - if (!S.source && !S.isMock) { - $("#kpis").hidden = true; $("#toolbar").hidden = true; $("#tableWrap").hidden = true; $("#btnExport").hidden = true; - $("#mSource").textContent = ""; - $("#mockBanner").hidden = true; + 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; } - $("#kpis").hidden = false; $("#toolbar").hidden = false; $("#tableWrap").hidden = false; $("#btnExport").hidden = false; - $("#btnExport").title = "Снимок текущего расчёта: коэффициенты, нормативы, сверка и источники"; - $("#mSource").textContent = S.isMock ? "учебный пример (синтетика)" : (S.source || ""); - $("#mockBanner").hidden = !S.isMock; + $("#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 + " — " + plural(nn, "компания без нормативов", "компании без нормативов", "компаний без нормативов")); + if (nd > 0) subParts.push("" + nd + " — " + plural(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(); - 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 += "" + - "" + esc(e._name || e.name) + (e.group ? " группа" : "") + "" + - " " + zoneLabel(wzone) + "" + - "" + coefDisplay(e, "k1") + " " + pill(e, "k1") + "" + - "" + coefDisplay(e, "k2") + " " + pill(e, "k2") + "" + - "" + coefDisplay(e, "k3") + " " + pill(e, "k3") + "" + - ""; + + /* компании (без группы) */ + var filtered = S.q ? comps.filter(function (e) { + if (e.name.toLowerCase().indexOf(S.q.toLowerCase()) === -1) return false; + var wz = worst(e); + if (!S.zone) return true; + if (S.zone === "nodefault") return wz === "nodefault"; + return wz === S.zone; + }) : comps; + 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; } - 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(".kfu-table th[data-k]"); + 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 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 = - "
Компании" + co + "из файла
" + - "
Группа" + grp + "консолидированная
" + - "
Зелёные" + g + "все зоны в норме
" + - "
Жёлтые" + y + "между целью и порогом
" + - "
Красные" + r + "нарушен порог или EBITDA≤0
" + - (nn ? "
Без норматива" + nn + "нет числовых норм
" : "") + - (nd ? "
Нет данных" + nd + "отсутствуют исходные
" : "") + - (S.recon ? "
Сверка (G5:J20)" + reconOk + "допуск 0,01 млн ₸
" : ""); + 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 ---------- */ @@ -712,12 +810,24 @@ 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 "
" + title + " " + pill(e, key) + "
" + - "
" + (v == null && e.why && e.why[key] && /EBITDA/i.test(e.why[key]) && e.ebitda != null && e.ebitda <= 0 ? "EBITDA ≤ 0" : fmtR(v).replace(/<[^>]+>/g, function (m) { return m === "нет данных<\/span>" ? "нет данных" : m; })) + "
" + + 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 + "
"; + (e.why && e.why[key] ? "
" + esc(e.why[key]) + "
" : "") + + "
" + formula + "
"; } function normLine(e, key) { var n = e.norms; @@ -728,68 +838,93 @@ } 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 wzone = worst(sc); var body = ""; - body += "

" + esc(en.name) + "

" + - "" + (en.group ? "Консолидированная группа — считается отдельно, не складывается с компаниями" : "Портфельная компания") + "
" + - "
"; + /* заголовок: имя + статус */ + body += "

" + esc(en.name) + "

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

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

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

Исходные данные (млн ₸)

" + + /* сценарий */ + body += "

Сценарий

" + + "
Прочие/гарантии: " + fmtMoney(baseOther) + " млн ₸ · изменение: " + pctStr(delta) + " млн ₸
" + + "
" + + "
" + + "
Сценарий обновляет коэффициенты, таблицу, счётчики и выгрузку. Сверка остаётся базовой.
" + + "
"; + + /* источники — раскрываемый блок */ + body += "
Исходные данные и формулы" + + "
" + srcRow("Долг", fmtMoney(en.debt), addrVal(en, "debt")) + - srcRow("Прочие, в т.ч. гарантии", fmtMoney(newOther), addrVal(en, "other") + "
в т.ч. сценарий " + pctStr(delta) + "") + + 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) + "
"; + return "
" + esc(s) + "
"; }).join("") + "
"; } - body += "

Сценарий: изменение прочих/гарантий

" + - "
Первичные прочие/гарантии: " + fmtMoney(baseOther) + " млн ₸ · изменение: " + pctStr(delta) + " млн ₸
" + - "
" + - "
" + - "
Сценарий влияет на коэффициенты и выгрузку. Сверка показывает исходные данные.
" + - "
"; - - body += "
"; - $("#drawerBody").innerHTML = body; $("#btnCloseDrawer").onclick = closeDrawer; - $("#btnCloseDrawer2").onclick = closeDrawer; $("#btnScenReset").onclick = function () { S.scen[en.name] = 0; - document.getElementById("scenInput").value = "0"; + var si = document.getElementById("scenInput"); + if (si) si.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); - }); + 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) { @@ -820,7 +955,7 @@ var a = e.addr; var sheet = a[key + "Sheet"]; var val = a[key] || ""; - return esc(val) + (sheet ? "
лист «" + esc(sheet) + "»" : ""); + return "" + esc(val) + (sheet ? " · " + esc(sheet) : "") + ""; } function srcRow(label, val, addr) { return "" + label + "" + val + "
" + (addr || "") + ""; @@ -938,12 +1073,13 @@ function renderDiag() { if (S.diagErrors && S.diagErrors.length) { - $("#footnote").innerHTML = "
В книге " + 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 { - $("#footnote").innerHTML = ""; + $("#diagBlock").hidden = true; } } function applyResult(res, sourceName) { @@ -960,35 +1096,52 @@ /* ---------- инициализация ---------- */ function init() { + var fileInput = $("#fileInput"); - $("#fileInput").addEventListener("change", function (e) { + 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 = ""; + + /* загрузить (первый экран) + заменить файл --> */ + 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("Загружен учебный пример (синтетические данные)"); }); - $("#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); + + /* выгрузка отчёта (topbar, hidden до загрузки) */ + var be = $("#btnExport"); + if (be) be.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) { + /* фильтр-чипы */ + 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; @@ -997,13 +1150,20 @@ }); }); + /* клик по строке --> */ $("#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", init);