svedenie-otchetov-dochernih/script.js

651 lines
42 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.

/* Сведение отчётов дочерних компаний — статичный интерфейс, состояние в localStorage */
(function () {
"use strict";
var LS = "svod-otchetov-dochernih-v1";
function loadState() {
var base = { screen: "upload", recognize: false, manual: false, unitFixed: false, checked: {}, memoText: "", memoSent: false };
try {
var raw = localStorage.getItem(LS);
if (raw) {
var s = JSON.parse(raw);
for (var k in base) { if (s && k in s) base[k] = s[k]; }
}
} catch (e) {}
return base;
}
function save() { try { localStorage.setItem(LS, JSON.stringify(S)); } catch (e) {} }
var S = loadState();
/* 15 компаний: [название, выручка план, выручка факт, EBITDA план, EBITDA факт, CAPEX план, CAPEX факт, ДЗ план, ДЗ факт] — млн тг */
var CO = [
["ООО «Телеком-1»", 12500, 11900, 3100, 2950, 420, 445, 640, 610],
["ООО «Телеком-2»", 8200, 7400, 2100, 1890, 280, 295, 410, 455],
["ООО «Телеком-3»", 15400, 15810, 4100, 4050, 530, 600, 880, 940],
["ООО «Телеком-4»", 9800, 9950, 2500, 2480, 340, 350, 520, 495],
["ООО «Телеком-5»", 11300, 9900, 2850, 2780, 390, 360, 610, 585],
["ООО «Телеком-6»", 7600, 7450, 1900, 1840, 250, 265, 380, 370],
["ООО «Телеком-7»", 13900, 14650, 3600, 3380, 480, 510, 760, 735],
["ООО «Телеком-8»", 6700, 6500, 1680, 1620, 220, 230, 330, 315],
["ООО «Телеком-9»", 10400, 9050, 2640, 2590, 360, 380, 570, 605],
["ООО «Телеком-10»", 8900, 8700, 2280, 2210, 300, 285, 450, 470],
["ООО «Телеком-11»", 12100, 12450, 3200, 3260, 430, 445, 690, 660],
["ООО «Телеком-12»", 7100, 6900, 1780, 1720, 260, 275, 400, 385],
["ООО «Телеком-13»", 14600, 13750, 3850, 3780, 505, 490, 830, 795],
["ООО «Телеком-14»", 9300, 8200, 2380, 2330, 330, 310, 510, 480],
["ООО «Телеком-15»", 11700, 12000, 3000, 2960, 640, 700, 580, 555]
];
/* 15 дочерних компаний: выручка / EBITDA / CAPEX / ДЗ — план и факт, млн тг.
Сценарий: 7 отклонений >10% (2/3/5/7/9/13/14) + ошибка единиц у 4 (выручка в тыс. тг).
В пределах нормы: 1 (Выр −4.8), 6 (Выр −2), 8 (Выр −3), 11 (Все ≤7.5), 12 (Выр −2.8).
*/
var _CO = CO;
function _dev(f, p) { return p > 0 ? Math.abs(f - p) / p * 100 : 0; }
CO = _CO.map(function (r) {
var c = { name: r[0], vp: r[1], vf: r[2], ep: r[3], ef: r[4], cp: r[5], cf: r[6], dp: r[7], df: r[8] };
c.dRev = _dev(c.vf, c.vp); c.dEbit = _dev(c.ef, c.ep); c.dCap = _dev(c.cf, c.cp); c.dDz = _dev(c.df, c.dp);
c.maxDev = Math.max(c.dRev, c.dEbit, c.dCap, c.dDz);
c.revOk = c.dRev > 10; c.ebitOk = c.dEbit > 10; c.capOk = c.dCap > 10; c.dzOk = c.dDz > 10;
return c;
});
function dev(f, p) { return p > 0 ? Math.abs(f - p) / p * 100 : 0; }
function hasDeviations(c) { return c.revOk || c.ebitOk || c.capOk || c.dzOk; }
var DEVS = CO.filter(hasDeviations);
var REV_ISSUE = 3; // индекс компании с ошибкой единиц (выручка в тыс. тг в файле)
(function () {
var c = CO[REV_ISSUE];
c.vp *= 1000; c.vf *= 1000;
c.dRev = _dev(c.vf, c.vp);
c.maxDev = Math.max(c.dRev, c.dEbit, c.dCap, c.dDz);
c.revOk = c.dRev > 10;
c.vp1000 = true;
})();
var FILES = [
["Отчет_TV-1_агуст.xlsx", "2024", "09:42"],
["Fin_report_TV-2.xlsx", "2025", "09:45"],
["Свод_TV-3_план_факт.xlsx", "2026", "09:47"],
["Отчет_TV-4_август.xlsx", "2024", "09:49"],
["Fin_TV-5_08-2026.xlsx", "2025", "09:51"],
["Финансы_TV-6_август.xlsx", "2024", "09:54"],
["Отчет_TV-7.xlsx", "2026", "09:56"],
["Годовой_отчет_TV-8.xlsx", "2025", "09:58"],
["Данные_TV-9_август.xlsx", "2024", "10:01"],
["Report_TV-10_2026-08.xlsx", "2026", "10:03"],
["Свод_TV-11_август.xlsx", "2025", "10:05"],
["Fin_TV-12_08.xlsx", "2024", "10:07"],
["Отчет_TV-13_август_2026.xlsx", "2026", "10:09"],
["Data_TV-14_2026.08.xlsx", "2025", "10:12"],
["Отчет_TV-15.xlsx", "2024", "10:14"]
];
var TPL = { "2024": "шаблон 2024", "2025": "шаблон 2025", "2026": "шаблон 2026" };
var BASE_LOGS = [
["10:02:14", "ok", "Получено 15 вложений из общего ящика"],
["10:02:15", "info", "Форматы: 15 Excel · 3 варианта шаблонов (2024 / 2025 / 2026)"]
];
var DONE_LOGS = [
["10:02:18", "ok", "ООО «Телеком-1» · сопоставлено 8 столбцов (выручка, EBITDA, CAPEX, ДЗ — план и факт)"],
["10:02:21", "warn", "ООО «Телеком-4» · выручка в файле в тысячах тенге → множитель 1/1000 при переносе"],
["10:02:23", "ok", "ООО «Телеком-2»…«Телеком-15» · автоматическое сопоставление колонок 8/8"],
["10:02:29", "ok", "Данные перенесены в единый свод: 15 компаний, 120 показателей план/факт"],
["10:02:30", "warn", "Отклонения от плана более 10% подсвечены: 7 компаний"]
];
var STEPS = [
{ id: "upload", n: 1, t: "Загрузка", sub: "распознавание" },
{ id: "check", n: 2, t: "Проверка", sub: "данные и отклонения" },
{ id: "memo", n: 3, t: "Записка", sub: "для финансового директора" }
];
/* ---------- utils ---------- */
function $(id) { return document.getElementById(id); }
function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
var NLC = new Intl.NumberFormat("ru-RU");
function fmt(n) { return n == null ? "—" : NLC.format(Math.round(n)); }
function pct(s) { return String(Math.round(s)).replace(".", ","); }
function fmtPct(s) { return String(Math.round(s)).replace(".", ","); }
function signPct(s) { return (s >= 0 ? "+" : "−") + fmtPct(parseFloat(s.toFixed(1))); }
function ic(name) { return '<svg class="kt-icon" aria-hidden="true"><use href="#' + name + '"></use></svg>'; }
function chip(tone, text) { return '<span class="kt-ai-chip" data-tone="' + tone + '">' + text + '</span>'; }
function checkedCount() { var n = 0; for (var k in S.checked) { if (S.checked[k]) n++; } return n; }
function allChecked() { return CO.every(function (c, i) { return !!S.checked[i]; }); }
function kpi(label, value, hint) {
return '<div class="kt-ai-kpi"><span class="label">' + label + '</span>' +
'<span class="value" style="font-size:inherit;font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg)">' + value + '</span>' +
'<span class="hint">' + (hint || "") + '</span></div>';
}
function toast(msg, tone) {
var d = document.createElement("div");
d.className = "toast"; if (tone) d.setAttribute("data-tone", tone);
d.textContent = msg;
$("toasts").appendChild(d);
setTimeout(function () { d.style.transition = "opacity .3s"; d.style.opacity = "0"; setTimeout(function () { d.remove(); }, 320); }, 3600);
}
function fileStatus(i) {
if (!S.recognize && !S.manual) return ["info", "В очереди"];
if (S.manual) return ["warn", "Вручную"];
var tpl = FILES[i][1];
if (tpl === "2024") return ["ok", "Т 2024 · 8/8"];
if (tpl === "2025") return ["ok", "Т 2025 · 8/8"];
return ["ok", "Т 2026 · 8/8"];
}
function logs() {
var l = BASE_LOGS.slice();
if (S.recognize || S.manual) l = l.concat(DONE_LOGS);
return l;
}
/* ---------- shell ---------- */
function renderSidebar() {
var items = STEPS.map(function (st) {
var cur = S.screen === st.id;
var badge = "";
if (st.id === "check") badge = allChecked() ? "15/15" : checkedCount() + "/15";
return '<button class="kt-ai-nav-item" data-screen="' + st.id + '"' + (cur ? ' data-active="true"' : "") + '>' +
'<span class="lbl"><b>' + st.n + '. ' + st.t + '</b><small>' + st.sub + '</small></span>' +
(badge ? '<span class="kt-ai-count-split">' + badge + '</span>' : "") +
'</button>';
});
items.push('<button class="kt-ai-nav-item" data-screen="about"' + (S.screen === "about" ? ' data-active="true"' : "") + '>' +
'<span class="lbl"><b>О решении</b><small>паспорт и эффект</small></span></button>');
$("sbNav").innerHTML = '<div class="kt-ai-nav-group">' + items.join("") + '</div>';
var f = $("sbFootMeta");
if (f) f.textContent = S.memoSent ? "отправлено" : "15 компаний";
}
function renderTopbar() {
var t = "";
if (S.screen === "upload") t = "Загрузка и распознавание";
else if (S.screen === "check") t = "Проверка данных и отклонений";
else if (S.screen === "memo") t = "Формирование записки";
else t = "О решении";
$("topTitle").innerHTML = '<span class="t">' + t + '</span><span class="sub">пакет к 15:00 понедельника</span>';
}
function showScreen(id) {
var ok = canOpen(id);
if (!ok) { toast("Сначала завершите предыдущий шаг", "warn"); return; }
var need = id !== S.screen;
if (need && id === "memo" && !allChecked()) {
toast("Сначала отметьте все 15 компаний как проверенные", "warn"); return;
}
if (need && id === "memo" && !S.unitFixed) {
toast("Сначала исправьте расхождения единиц измерения", "warn"); return;
}
S.screen = id; save();
renderSidebar(); renderTopbar();
var r = { upload: viewUpload, check: viewCheck, memo: viewMemo, about: viewAbout }[id];
var el = $("view");
el.innerHTML = r();
document.querySelector(".kt-ai-main").scrollTop = 0;
bind(id);
closeMobile();
}
function canOpen(id) {
if (id === "upload") return true;
if (id === "check") return !!S.recognize;
if (id === "memo") return !!S.recognize;
return true;
}
function closeMobile() { var sb = $("sidebar"); if (sb) sb.removeAttribute("data-open"); $("scrim").classList.remove("on"); }
function stepHtml(curId) {
var h = "";
STEPS.forEach(function (st, i) {
var open = canOpen(st.id);
var done = canOpen({ upload: "check", check: "memo", memo: "done" }[st.id]);
var cur = curId === st.id;
var cls = [];
if (cur) cls.push('data-cur="true"');
if (done) cls.push('data-done="true"');
if (!open && !cur) cls.push('data-disabled="true"');
if (i > 0) h += '<span class="step-sep"></span>';
h += '<span class="step" data-goto="' + st.id + '" ' + cls.join(" ") + '><span class="ic">' + (done ? ic("check") : st.n) + "</span>" + st.t + "</span>";
});
return '<div class="steps">' + h + "</div>";
}
function pageHead(title, sub, right) {
return '<div class="page-head"><div class="ph-text"><h1>' + title + "</h1><p class=\"sub\">" + (sub || "") + "</p></div>" +
(right ? '<div class="ph-action">' + right + "</div>" : "") + "</div>";
}
/* ================= ЭКРАН 1 — загрузка и распознавание ================= */
function viewUpload() {
var fileRows = FILES.map(function (f, i) {
var st = fileStatus(i);
return '<div class="kt-ai-row" data-file="' + i + '">' +
'<span class="f-ic">' + ic("spreadsheet") + '</span>' +
'<div style="min-width:0;flex:1"><div class="f-name">' + esc(f[0]) + '</div>' +
'<div class="f-meta">' + esc(CO[i].name) + " · получен 21 авг, " + f[2] + "</div></div>" +
chip(st[0], st[1]) + "</div>";
}).join("");
var progress = S.recognize || S.manual ? 100 : 0;
var banner, tone;
if (S.manual) { tone = "warn"; banner = "Распознавание остановлено: данные перенесены в ручной режим. Проверьте столбцы и верните автоматический режим."; }
else if (S.recognize) { tone = "ok"; banner = "Рассмотрены все форматы, все 15 файлов сопоставлены, данные перенесены в единый свод. Расхождение единиц отмечено у ООО «Телеком-4»."; }
else { tone = "info"; banner = "15 файлов получено из одного ящика. Запустите распознавание — система сопоставит форматы и извлечёт показатели."; }
var bannerHtml = '<div class="kt-ai-banner" data-tone="' + tone + '" style="margin-bottom:var(--kt-ai-space-5)">' + banner + "</div>";
var logHtml = logs().map(function (l) {
var tag = l[1] === "ok" ? "ok" : (l[1] === "warn" ? "warn" : "info");
var label = l[1] === "ok" ? "готово" : (l[1] === "warn" ? "внимание" : "инфо");
return '<div class="log-item"><span class="log-time">' + l[0] + '</span>' +
'<span class="log-tag">' + chip(tag, label) + "</span>" +
"<p>" + l[2] + "</p></div>";
}).join("");
var extractedHead = CO.map(function (c) {
return "<th>" + esc(c.name) + "</th>";
}).join("");
var rowNames = ["Выручка, план", "Выручка, факт", "EBITDA, план", "EBITDA, факт", "CAPEX, план", "CAPEX, факт", "ДЗ, план", "ДЗ, факт"];
var extractedRows = [];
extractedRows = rowNames.map(function (label, ri) {
var key = ["vp", "vf", "ep", "ef", "cp", "cf", "dp", "df"][ri];
var cells = CO.map(function (c) {
var cls = "num cell-num";
if (key === "vf" && c.name === CO[REV_ISSUE].name && !S.unitFixed) cls += " cell-unit-w";
return '<td class="' + cls + '">' + (key === "vf" && c.name === CO[REV_ISSUE].name && S.unitFixed ? fmt(c.vf / 1000) : fmt(c[key])) + "</td>";
}).join("");
return "<tr><td><b style=\"font-weight:var(--kt-ai-weight-medium)\">" + label + "</b></td>" + cells + "</tr>";
}).join("");
return pageHead("Загрузка и распознавание", "Вложения из общего ящика · отчёт 15 дочерних компаний за август",
'<button class="kt-ai-btn" data-act="fetch">' + ic("download") + ' Загрузить с общего ящика</button>' +
(S.recognize || S.manual
? '<button class="kt-ai-btn" data-variant="primary" data-act="to-check">' + ic("check-circle") + " Перейти к проверке</button>"
: '<button class="kt-ai-btn" data-variant="primary" data-act="recognize">' + ic("sparkles") + ' Запустить распознавание</button>') +
(S.recognize ? '<button class="kt-ai-btn" data-act="manual">' + ic("tool") + ' Вернуть в ручной режим</button>' : "")) +
bannerHtml + stepHtml("upload") +
'<div class="kt-ai-kpi-strip">' +
kpi("Файлов получено", "15 / 15", "общий ящик · 21 августа") +
kpi("Форматов распознано", "3 / 3", "шаблоны 2024, 2025, 2026") +
kpi("Показателей извлечено", "120", "8 показателей × 15 компаний") +
kpi("Прогресс", progress + "%", S.recognize || S.manual ? "распознавание завершено" : "ожидание запуска") +
"</div>" +
'<div class="grid-2">' +
'<div class="kt-ai-card block"><div class="block-head"><h2>Файлы</h2><span class="cap">15 шт. · август 2026</span></div>' +
'<div class="file-list">' + fileRows + "</div></div>" +
'<div class="kt-ai-card block"><div class="block-head"><h2>Прогресс распознавания</h2><span class="cap">формы → единый шаблон</span></div>' +
'<div class="progress-head"><b>' + progress + "%</b><span class=\"cap\">" + (S.recognize || S.manual ? "15/15 файлов" : "0/15 файлов") + '</span><span style="flex:1"></span><span class="kt-ai-chip" data-tone="blue">автоматически</span></div>' +
'<div class="kt-ai-progress"><div style="width:' + progress + '%"></div></div>' +
'<div class="block-sub" style="margin-top:var(--kt-ai-space-5)">Показатели: выручка, EBITDA, CAPEX, дебиторская задолженность — план и факт. Сопоставление по 3 вариантам шаблонов.</div>' +
'<div class="block-head" style="margin-top:var(--kt-ai-space-6)"><h2>Журнал действий ИИ</h2></div>' +
'<div class="log-list">' + logHtml + "</div></div>" +
"</div>" +
'<div class="kt-ai-card block" style="margin-top:var(--kt-ai-space-6)"><div class="block-head"><h2>Извлечённые показатели</h2><span class="cap">план и факт по 15 компаниям, млн тг</span></div>' +
'<div class="table-wrap"><table class="kt-ai-table" data-density="compact"><thead><tr><th style="min-width:130px">Показатель</th>' + extractedHead + "</tr></thead><tbody>" +
extractedRows + "</tbody></table></div></div>";
}
/* ================= ЭКРАН 2 — проверка данных и отклонений ================= */
var chk = { metric: "all" };
function viewCheck() {
var cc = checkedCount();
var devCount = DEVS.length;
var unitFixOk = S.unitFixed;
var kpis =
kpi("Компаний в сводном файле", "15 / 15", "план и факт, август") +
kpi("Отклонения от плана более 10%", devCount, "Телеком-2, -3, -5, -7, -9, -13, -14") +
kpi("Единицы измерения", unitFixOk ? "исправлены" : "1 требует проверки", "ООО «Телеком-4» · выручка в файле в тыс. тг") +
kpi("Статус проверки", cc + " / 15", (unitFixOk && allChecked()) ? "можно переходить к записке" : "отмечайте строки по одной");
return pageHead("Проверка данных и отклонений", "Единый свод, созданный ИИ · проверьте перенос данных и отклонения от плана более 10%",
'<button class="kt-ai-btn" data-act="excel">' + ic("spreadsheet") + ' Сформировать Excel</button>' +
'<button class="kt-ai-btn" data-variant="primary" data-act="to-memo"' + ((unitFixOk && allChecked()) ? "" : " disabled") + ">" + ic("send") + " Перейти к подготовке записки</button>") +
stepHtml("check") +
'<div class="kt-ai-kpi-strip">' + kpis + "</div>" +
'<div class="kt-ai-card block"><div class="block-head"><h2>Единый свод</h2><span class="spacer"></span>' +
'<div class="filter-row" style="margin:0">' +
'<div class="kt-ai-segmented" id="fMetric">' +
'<button class="seg" data-v="all" data-active="true">Все отклонения</button>' +
'<button class="seg" data-v="rev">Выручка</button>' +
'<button class="seg" data-v="ebit">EBITDA</button>' +
'<button class="seg" data-v="cap">CAPEX</button>' +
'<button class="seg" data-v="dz">ДЗ</button>' +
"</div>" +
'<span class="filter-note" id="fNote"></span>' +
"</div></div>" +
'<div class="table-wrap kt-ai-table-wrap" id="sumTable"></div>' +
'<div class="kt-ai-conclusion" style="margin-top:var(--kt-ai-space-5)"><p class="text">Подсветка: «×1 000» — расхождение единиц измерения, «+N%» — отклонение факта от плана. Строки с отклонениями более 10% помечены красным. Клик по строке — отметить данные как проверенные; клик по ячейке с ошибкой — исправить вручную.</p></div>' +
"</div>" + unitBanner;
}
function metricKey(m) {
if (m === "rev") return { plan: "vp", fact: "vf", dev: "dRev", ok: "revOk" };
if (m === "ebit") return { plan: "ep", fact: "ef", dev: "dEbit", ok: "ebitOk" };
if (m === "cap") return { plan: "cp", fact: "cf", dev: "dCap", ok: "capOk" };
if (m === "dz") return { plan: "dp", fact: "df", dev: "dDz", ok: "dzOk" };
return null;
}
function metricLabel(m) {
return { rev: "Выручка, млн тг", ebit: "EBITDA, млн тг", cap: "CAPEX, млн тг", dz: "Дебиторская задолженность, млн тг" }[m] || "";
}
function rowStatusHtml(c, i) {
if (S.checked[i]) return chip("ok", "Проверено");
return '<button class="kt-ai-btn" data-size="sm" data-check="' + i + '">' + ic("check") + " Проверено</button>";
}
function cellHtml(c, planKey, factKey, devKey, isRevUnit) {
if (isRevUnit && c.vp1000 && !S.unitFixed) {
return '<td class="num cell-num cell-unit-w" data-unit="1">' + fmt(c[planKey]) + " / " + fmt(c[factKey]) +
' <span class="delta-neg">(' + signPct(c[devKey]) + "%)</span> " + chip("red", "×1 000") + "</td>";
}
var d = c[devKey];
var cls = "num cell-num" + (d > 10 ? " cell-dev" : "");
return '<td class="' + cls + '">' + fmt(c[planKey]) + " / " + fmt(c[factKey]) +
' <span class="' + (d > 10 ? "delta-neg" : "delta-pos") + '">(' + signPct(d) + "%)</span></td>";
}
function coRows() {
var m = chk.metric;
return CO.map(function (c, i) {
var st = rowStatusHtml(c, i);
if (m === "all") {
return '<tr data-oi="' + i + '">' +
'<td><b style="font-weight:var(--kt-ai-weight-medium)">' + esc(c.name) + "</b></td>" +
cellHtml(c, "vp", "vf", "dRev", true) +
cellHtml(c, "ep", "ef", "dEbit") +
cellHtml(c, "cp", "cf", "dCap") +
cellHtml(c, "dp", "df", "dDz") +
'<td style="white-space:nowrap">' + st + "</td></tr>";
}
var mk = metricKey(m);
var rowDev = (m === "rev" && c.revOk) || (m === "ebit" && c.ebitOk) || (m === "cap" && c.capOk) || (m === "dz" && c.dzOk);
if (!rowDev) return "";
if (S.checked[i]) return "";
var unit = m === "rev" && c.vp1000 && !S.unitFixed;
return '<tr data-oi="' + i + '">' +
'<td><b style="font-weight:var(--kt-ai-weight-medium)">' + esc(c.name) + "</b></td>" +
'<td class="num cell-num">' + fmt(c.vp / (c.vp1000 ? 1000 : 1)) + " / " + fmt(c.vf / (c.vp1000 ? 1000 : 1)) + " (" + signPct(c.dRev) + "%)</td>" +
'<td class="num cell-num">' + fmt(c.ep) + " / " + fmt(c.ef) + " (" + signPct(c.dEbit) + "%)</td>" +
cellHtml(c, mk.plan, mk.fact, mk.dev, unit) +
'<td style="white-space:nowrap">' + st + "</td></tr>";
}).join("");
}
function totalRowHtml() {
var tv = CO.map(function (c) { return { vp: c.vp / (c.vp1000 ? 1000 : 1), vf: c.vf / (c.vp1000 ? 1000 : 1), ep: c.ep, ef: c.ef, cp: c.cp, cf: c.cf, dp: c.dp, df: c.df }; });
var s = tv.reduce(function (a, b) { a.vp += b.vp; a.vf += b.vf; a.ep += b.ep; a.ef += b.ef; a.cp += b.cp; a.cf += b.cf; a.dp += b.dp; a.df += b.df; return a; }, { vp: 0, vf: 0, ep: 0, ef: 0, cp: 0, cf: 0, dp: 0, df: 0 });
function t(p, f, d) { return '<td class="num cell-num">' + fmt(p) + " / " + fmt(f) + ' <span class="' + (d > 10 ? "delta-neg" : "delta-pos") + '">(' + signPct(d) + "%)</span></td>"; }
return '<tr style="background:var(--kt-ai-bg-soft)"><td><b>Итого, 15 компаний</b></td>' +
t(s.vp, s.vf, dev(s.vf, s.vp)) + t(s.ep, s.ef, dev(s.ef, s.ep)) + t(s.cp, s.cf, dev(s.cf, s.cp)) + t(s.dp, s.df, dev(s.df, s.dp)) + "<td></td></tr>";
}
function emptyRowHtml() {
return '<tr><td colspan="6" style="text-align:center;padding:var(--kt-ai-space-8);color:var(--kt-ai-fg-faint)">Все отклонения в этом показателе проверены</td></tr>';
}
function renderSumTable() {
var box = $("sumTable");
if (!box) return;
var m = chk.metric;
var headExtra = m === "all"
? "<th class='num'>Выручка, млн тг</th><th class='num'>EBITDA, млн тг</th><th class='num'>CAPEX, млн тг</th><th class='num'>ДЗ, млн тг</th>"
: "<th class='num'>Выручка</th><th class='num'>EBITDA</th><th class='num'>" + metricLabel(m) + "</th>";
var rows = coRows();
var any = rows.indexOf("<tr") !== -1;
var devCount = m === "all" ? DEVS.length : CO.filter(function (c, i) {
var mk = metricKey(m);
return c[mk.ok] && !S.checked[i];
}).length;
var note = m === "all" ? "все компании · отклонения подсвечены" : "показатель: " + metricLabel(m) + " · " + devCount + " не проверено";
box.innerHTML = '<table class="kt-ai-table" data-density="compact"><thead><tr><th>Компания</th>' + headExtra + "<th>Статус</th></tr></thead><tbody>" +
(any ? rows + (m === "all" ? totalRowHtml() : "") : emptyRowHtml()) +
"</tbody></table>";
var n = $("fNote");
if (n) n.textContent = note;
}
/* ================= ЭКРАН 3 — формирование записки ================= */
function memoBody() {
var s = CO.reduce(function (a, b) { a.vp += b.vp / (b.vp1000 ? 1000 : 1); a.vf += b.vf / (b.vp1000 ? 1000 : 1); a.ep += b.ep; a.ef += b.ef; return a; }, { vp: 0, vf: 0, ep: 0, ef: 0 });
var dV = dev(s.vf, s.vp), dE = dev(s.ef, s.ep);
var devLines = DEVS.map(function (c) {
var parts = [];
if (c.revOk) parts.push("выручка " + signPct(c.dRev) + "%");
if (c.ebitOk) parts.push("EBITDA " + signPct(c.dEbit) + "%");
if (c.capOk) parts.push("CAPEX " + signPct(c.dCap) + "%");
if (c.dzOk) parts.push("ДЗ " + signPct(c.dDz) + "%");
return "• " + c.name + " — " + parts.join(", ");
}).join("\n");
var unitLine = S.unitFixed
? "Единицы измерения ООО «Телеком-4» исправлены: выручка переведена из тысяч в миллионы тенге."
: "Внимание: у ООО «Телеком-4» в файле выручка в тысячах тенге — требуется ручная проверка единиц.";
return "Свод по 15 дочерним компаниям за август 2026 г.\n\n" +
"По группе: выручка " + fmt(s.vf) + " млн тг (" + signPct(dV) + "% к плану), EBITDA " + fmt(s.ef) + " млн тг (" + signPct(dE) + "% к плану).\n" + unitLine + "\n" +
"Компании с отклонениями от плана более 10% (" + DEVS.length + "):\n" + devLines + "\n\n" +
"Рекомендации: разобрать причины отклонений по компаниям из списка выше.\n";
}
function viewMemo() {
var kpis =
kpi("Показателей в записке", "8", "выручка, EBITDA, CAPEX, ДЗ — план/факт") +
kpi("Отклонений более 10%", String(DEVS.length), "компаний в списке") +
kpi("Статус проверки", checkedCount() + " / 15", allChecked() ? "все компании проверены" : "осталось " + (15 - checkedCount())) +
kpi("Готовность", S.memoSent ? "отправлено" : "к отправке", "дедлайн: пн 15:00");
var devLinesHtml = DEVS.map(function (c) {
var parts = [];
if (c.revOk) parts.push('выручка <b>' + signPct(c.dRev) + "%</b>");
if (c.ebitOk) parts.push('EBITDA <b>' + signPct(c.dEbit) + "%</b>");
if (c.capOk) parts.push('CAPEX <b>' + signPct(c.dCap) + "%</b>");
if (c.dzOk) parts.push('ДЗ <b>' + signPct(c.dDz) + "%</b>");
return "<p>• " + esc(c.name) + " — " + parts.join(", ") + "</p>";
}).join("");
var sentBanner = S.memoSent
? '<div class="kt-ai-banner" data-tone="ok" style="margin-top:var(--kt-ai-space-5)"><b>Записка отправлена на согласование</b> · финансовый директор · сегодня</div>'
: '<div class="kt-ai-banner" data-tone="info" style="margin-top:var(--kt-ai-space-5)"><b>Дедлайн: понедельник, 15:00</b> · записка уходит финансовому директору после согласования текста</div>';
return pageHead("Формирование записки", "Итоговый документ для финансового директора · показатели подставлены из проверенного свода",
'<button class="kt-ai-btn" data-act="memo-download">' + ic("download") + ' Скачать записку</button>' +
'<button class="kt-ai-btn" data-act="memo-regen">' + ic("sparkles") + ' Пересобрать из данных</button>' +
'<button class="kt-ai-btn" data-variant="primary" data-act="memo-send" ' + (S.memoSent ? "disabled" : "") + ">" + (S.memoSent ? ic("check-circle") + " Отправлено" : ic("send") + " Отправить на согласование") + "</button>") +
stepHtml("memo") +
'<div class="kt-ai-kpi-strip">' + kpis + "</div>" +
'<div class="grid-2-rev">' +
'<div class="kt-ai-card block" style="margin-bottom:0"><div class="block-head"><h2>Предпросмотр документа</h2></div>' +
'<div class="memo"><div class="memo-meta">На: финансовый директор · От: сводящий · август 2026</div>' +
"<h3>Свод по 15 дочерним компаниям</h3>" +
"<p>Выручка группы <b>" + fmt(CO.reduce(function (a, b) { return a + b.vf / (b.vp1000 ? 1000 : 1); }, 0)) + " млн тг</b>, EBITDA <b>" + fmt(CO.reduce(function (a, b) { return a + b.ef; }, 0)) + " млн тг</b>. Отклонения от плана более 10% — у " + DEVS.length + " компаний.</p>" +
'<div class="memo-dev">' + devLinesHtml + "</div>" +
"<p style=\"margin-top:var(--kt-ai-space-5)\">" + (S.unitFixed ? "Единицы измерения всех компаний согласованы." : "У ООО «Телеком-4» требуют проверки единицы измерения выручки.") + "</p>" +
"</div></div>" +
'<div class="kt-ai-card block" style="margin-bottom:0"><div class="block-head"><h2>Компании с отклонениями более 10%</h2><span class="cap">подставлено автоматически</span></div>' +
chip("warn", "требуют внимания") +
'<ul style="margin:var(--kt-ai-space-4) 0 0;padding-left:var(--kt-ai-space-6);display:flex;flex-direction:column;gap:var(--kt-ai-space-3);font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted)">' +
DEVS.map(function (c) { return "<li>" + esc(c.name) + "</li>"; }).join("") +
"</ul></div>" +
"</div>" +
'<div class="kt-ai-card block" style="margin-top:var(--kt-ai-space-6)"><div class="block-head"><h2>Текст записки</h2><span class="cap">редактируется вручную</span></div>' +
'<textarea class="kt-ai-textarea" id="memoText" spellcheck="false" style="min-height:300px"></textarea>' +
"</div>" + sentBanner;
}
/* ================= ЭКРАН 4 — о решении ================= */
function viewAbout() {
return pageHead("О решении", "Сведение отчётов дочерних компаний", "") +
'<div class="pass-grid">' +
card("Паспорт решения", dl([
["Наименование", "Сведение отчётов дочерних компаний"],
["Исполнитель", "владелец процесса — сборка на платформе Vibe AI"],
["Пользователи", "сводящий, финансовый директор"],
["Дедлайн", "понедельник, 15:00"]
])) +
card("Предпосылки", '<ul><li>Сведение отчётов 15 дочерних компаний вручную раз в месяц</li><li>Использование разных шаблонов отчётности</li><li>Длительный процесс с высоким риском ошибок</li></ul>') +
card("Входные данные", '<ul><li>Файлы Excel: выручка, EBITDA, CAPEX, дебиторская задолженность, план и факт</li><li>Доставка писем на общий ящик в пятницу</li></ul>') +
card("Что делает ИИ", '<ol><li>Собирает файлы и распознаёт форматы</li><li>Переносит данные в единый свод</li><li>Подсвечивает отклонения от плана более 10%</li></ol>') +
card("Что решает человек", '<ul><li>Сводящий проверяет корректность извлечённых данных и единиц измерения</li><li>Сводящий принимает решение об ошибках и при необходимости включает ручной режим</li><li>Сводящий формулирует и редактирует текст записки</li></ul>') +
card("Результат и эффект", '<ul><li>Сводный файл Excel</li><li>Записка финансовому директору к 15:00 понедельника</li><li>Сокращение времени на выполнение процесса</li><li>Снижение количества ошибок</li><li>Экономия 12–16 часов в месяц</li></ul>') +
card("Риски и меры", '<ul><li>Возможность ошибок при обработке нестандартных файлов</li><li>Риск ошибок в единицах измерения</li><li>Контроль результата осуществляет сводящий</li><li>Журнал действий ИИ и возможность вернуть ручной режим</li></ul>') +
"</div>";
}
function card(title, body) {
return '<div class="kt-ai-card block pass-card" style="margin:0"><div class="block-head" style="margin-bottom:var(--kt-ai-space-4)"><h2>' + title + "</h2></div>" + body + "</div>";
}
function dl(rows) {
return '<dl class="kv">' + rows.map(function (r) { return "<dt>" + r[0] + "</dt><dd>" + r[1] + "</dd>"; }).join("") + "</dl>";
}
/* ================= действия ================= */
function downloadText(text, filename) {
try {
var b = new Blob([text], { type: "text/plain;charset=utf-8" });
var a = document.createElement("a");
a.href = URL.createObjectURL(b);
a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
toast("Скачивание: " + filename, "ok");
} catch (e) { toast("Не удалось скачать файл", "warn"); }
}
function excelText() {
var head = "Компания;Выручка план;Выручка факт;EBITDA план;EBITDA факт;CAPEX план;CAPEX факт;ДЗ план;ДЗ факт";
var body = CO.map(function (c) {
return [c.name, c.vp / (c.vp1000 ? 1000 : 1), c.vf / (c.vp1000 ? 1000 : 1), c.ep, c.ef, c.cp, c.cf, c.dp, c.df].join(";");
}).join("\n");
return "Единый свод, август 2026 (млн тг)\n" + head + "\n" + body + "\n";
}
function downloadExcel() {
downloadText(excelText(), "svod_otchetov_avgust_2026.csv");
S.excelMade = true; save();
}
function markChecked(i, on) {
if (on) S.checked[i] = true; else delete S.checked[i];
save();
if (on && checkedCount() === CO.length) {
toast("Все 15 компаний проверены — доступно формирование записки", "ok");
}
var row = document.querySelector('tr[data-oi="' + i + '"]');
if (row) {
var stTd = row.querySelector("td:last-child");
if (stTd) stTd.innerHTML = rowStatusHtml(CO[i], i);
}
renderSidebar();
var k = document.querySelectorAll(".kt-ai-kpi");
var kk = k.length > 3 ? k[3] : null;
if (kk) {
var vEl = kk.querySelector(".value");
if (vEl) vEl.textContent = checkedCount() + " / 15";
}
var toMemo = document.querySelector("[data-act='to-memo']");
if (toMemo) toMemo.disabled = !(allChecked() && S.unitFixed);
toast(CO[i].name + ": " + (on ? "отмечено как проверенное" : "вернуто в работу"), on ? "ok" : "warn");
}
function setUnitFix(fixed) {
S.unitFixed = fixed; save();
toast(fixed ? "Единицы ООО «Телеком-4» исправлены: выручка переведена в млн тг" : "Единицы ООО «Телеком-4» возвращены в исходные", fixed ? "ok" : "warn");
if (S.screen === "check") showScreen("check");
/* частичное обновление: ячейка, баннер, KPI, кнопка */
var box = $("sumTable");
if (box && S.screen === "check") renderSumTable();
var b = $("unitBanner");
if (b) {
b.setAttribute("data-tone", fixed ? "ok" : "risk");
b.innerHTML = fixed
? "Единицы ООО «Телеком-4» исправлены: выручка переведена из тысяч в миллионы тенге."
: "<b>Расхождение единиц: ООО «Телеком-4».</b> В файле выручка в тысячах тенге, в сводном — в миллионах. Клик по ячейке выручки — исправить вручную.";
}
var k = document.querySelectorAll(".kt-ai-kpi");
var kk = k.length > 2 ? k[2] : null;
if (kk) {
var vEl = kk.querySelector(".value");
if (vEl) vEl.textContent = fixed ? "исправлены" : "1 требует проверки";
}
var toMemo = document.querySelector("[data-act='to-memo']");
if (toMemo) toMemo.disabled = !(allChecked() && S.unitFixed);
}
/* ---------- bind ---------- */
function bind(screen) {
var v = $("view");
v.querySelectorAll("[data-goto]").forEach(function (b) {
b.addEventListener("click", function () { showScreen(b.getAttribute("data-goto")); });
});
if (screen === "upload") {
v.querySelectorAll("[data-act='fetch']").forEach(function (b) {
b.addEventListener("click", function () { toast("15 вложений получены из общего ящика", "ok"); });
});
v.querySelectorAll("[data-act='recognize']").forEach(function (b) {
b.addEventListener("click", function () {
S.recognize = true; save();
toast("Распознавание завершено: 15/15 файлов, 3 варианта шаблонов", "ok");
showScreen("upload");
});
});
v.querySelectorAll("[data-act='manual']").forEach(function (b) {
b.addEventListener("click", function () {
S.manual = true; save();
toast("Ручной режим включён: проверьте столбцы и верните автоматическое распознавание", "warn");
showScreen("upload");
});
});
v.querySelectorAll("[data-act='to-check']").forEach(function (b) {
b.addEventListener("click", function () { showScreen("check"); });
});
v.querySelectorAll("[data-file]").forEach(function (r) {
r.addEventListener("click", function () {
var i = +r.getAttribute("data-file");
toast(CO[i].name + " · распознан, 8/8 показателей " + (S.recognize || S.manual ? "извлечены" : "в очереди"), "ok");
});
});
}
if (screen === "check") {
renderSumTable();
var fm = $("fMetric");
if (fm) fm.addEventListener("click", function (e) {
var b = e.target.closest(".seg"); if (!b) return;
chk.metric = b.getAttribute("data-v");
fm.querySelectorAll(".seg").forEach(function (x) { x.setAttribute("data-active", x === b ? "true" : "false"); });
renderSumTable();
});
var box = $("sumTable");
if (box) box.addEventListener("click", function (e) {
var cell = e.target.closest("[data-unit]");
if (cell) { setUnitFix(!S.unitFixed); return; }
var cb = e.target.closest("[data-check]");
if (cb) { markChecked(+cb.getAttribute("data-check"), true); return; }
var tr = e.target.closest("tr[data-oi]");
if (tr) { markChecked(+tr.getAttribute("data-oi"), true); }
});
v.querySelectorAll("[data-act='excel']").forEach(function (b) {
b.addEventListener("click", downloadExcel);
});
v.querySelectorAll("[data-act='to-memo']").forEach(function (b) {
b.addEventListener("click", function () {
if (!b.disabled) showScreen("memo");
});
});
}
if (screen === "memo") {
var ta = $("memoText");
if (ta) {
ta.value = S.memoText || memoBody();
ta.addEventListener("input", function () { S.memoText = ta.value; save(); });
}
v.querySelectorAll("[data-act='memo-regen']").forEach(function (b) {
b.addEventListener("click", function () {
var ta2 = $("memoText");
if (ta2 && S.memoText && S.memoText !== memoBody() && !confirm("Текст был отредактирован. Пересобрать записку из данных свода и записать изменения?")) return;
S.memoText = memoBody(); save();
if (ta2) ta2.value = S.memoText;
toast("Записка пересобрана из данных свода", "ok");
});
});
v.querySelectorAll("[data-act='memo-download']").forEach(function (b) {
b.addEventListener("click", function () { downloadText(S.memoText || memoBody(), "zapiska_ugotov_avgust_2026.txt"); });
});
v.querySelectorAll("[data-act='memo-send']").forEach(function (b) {
b.addEventListener("click", function () {
S.memoSent = true; save();
toast("Записка отправлена на согласование финансовому директору", "ok");
showScreen("memo");
});
});
}
}
/* ---------- инициализация ---------- */
function init() {
$("burger").addEventListener("click", function () {
var sb = $("sidebar");
var open = sb.hasAttribute("data-open");
if (open) closeMobile();
else { sb.setAttribute("data-open", "true"); $("scrim").classList.add("on"); }
});
$("scrim").addEventListener("click", closeMobile);
window.addEventListener("resize", function () { $("burger").hidden = window.innerWidth >= 832; });
document.querySelector(".kt-ai-shell").addEventListener("click", function (e) {
var n = e.target.closest(".kt-ai-nav-item[data-screen]");
if (n) showScreen(n.getAttribute("data-screen"));
});
renderSidebar();
showScreen(S.screen || "upload");
}
document.addEventListener("DOMContentLoaded", init);
})();