585 lines
48 KiB
JavaScript
585 lines
48 KiB
JavaScript
/* Сводный финансовый анализ дочерних компаний — статичный макет, состояние в localStorage */
|
||
(function () {
|
||
"use strict";
|
||
var LS = "sfa-do-v1";
|
||
function loadState() {
|
||
var base = { screen: "upload", fileStatus: {}, fileManual: false, mappingMode: "auto", unitFixed: [], deviations: [], aiAccepted: false, sent: false, analysis: {}, done: { recognize: true, check: false, analysis: false, result: false }, theme: "light" };
|
||
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) { if (!localStorage.getItem(LS)) base.resetFlag = true; }
|
||
return base;
|
||
}
|
||
function save() { try { localStorage.setItem(LS, JSON.stringify(S)); } catch (e) {} }
|
||
var S = loadState();
|
||
|
||
var COMPANIES = [
|
||
["ООО А", 12500, 11800, 3100, 3050, 420, 510, 640, 590, 1840, 2110, 1240, 1215],
|
||
["ООО Б", 8200, 7950, 2100, 1980, 280, 310, 410, 390, 1220, 1150, 860, 842],
|
||
["ООО В", 15400, 13100, 4100, 3480, 530, 470, 880, 720, 2450, 2610, 1580, 1595],
|
||
["ООО Г", 9800, 10050, 2500, 2560, 340, 355, 520, 480, 1480, 1390, 1010, 998],
|
||
["ООО Д", 11300, 10900, 2850, 2700, 390, 430, 610, 585, 1620, 1705, 1130, 1128],
|
||
["ООО Е", 7600, 7420, 1900, 1830, 250, 270, 380, 350, 1090, 1160, 780, 775],
|
||
["ООО Ж", 13900, 14350, 3600, 3720, 480, 455, 760, 730, 2180, 2040, 1420, 1408],
|
||
["ООО З", 6700, 6510, 1680, 1610, 220, 240, 330, 300, 960, 1015, 690, 682],
|
||
["ООО И", 10400, 9980, 2640, 2490, 360, 385, 570, 545, 1490, 1565, 1040, 1035],
|
||
["ООО К", 8900, 8730, 2280, 2210, 300, 325, 450, 420, 1270, 1335, 915, 908],
|
||
["ООО Л", 12100, 12490, 3200, 3310, 430, 415, 690, 660, 1740, 1650, 1265, 1258],
|
||
["ООО М", 7100, 6890, 1780, 1720, 260, 280, 400, 365, 1030, 1085, 745, 740],
|
||
["ООО Н", 14600, 13850, 3850, 3640, 505, 525, 830, 790, 2320, 2415, 1495, 1488],
|
||
["ООО О", 9300, 8910, 2380, 2240, 330, 350, 510, 480, 1360, 1425, 965, 958],
|
||
["ООО П", 11700, 11980, 3000, 3080, 405, 395, 640, 615, 1580, 1495, 1175, 1168],
|
||
["ООО Р", 8400, 8160, 2150, 2060, 290, 310, 430, 400, 1180, 1240, 835, 830],
|
||
["ООО С", 13200, 12840, 3400, 3280, 460, 485, 710, 680, 1950, 2020, 1355, 1348]
|
||
];
|
||
var CO = COMPANIES.map(function (r) { return { name: r[0], vp: r[1], vf: r[2], ep: r[3], ef: r[4], cap: r[5], caf: r[6], dp: r[7], df: r[8], cp: r[9], cf: r[10], hp: r[11], hf: r[12] }; });
|
||
function dev(o, f, v) { return v > 0 ? Math.round(Math.abs(f - v) / v * 1000) / 10 : 0; }
|
||
CO.forEach(function (c) {
|
||
c.rDev = dev(c, c.vf, c.vp); c.eDev = dev(c, c.ef, c.ep); c.cDev = dev(c, c.caf, c.cap);
|
||
c.dDev = dev(c, c.df, c.dp); c.kDev = dev(c, c.cf, c.cp); c.maxDev = Math.max(c.rDev, c.eDev, c.cDev, c.dDev, c.kDev);
|
||
});
|
||
|
||
var FILES = [
|
||
["Отчет_ОАОА_июнь.xlsx", 0], ["Fin_report_ООО-Б_0626.xlsx", 1], ["Свод_В_план_факт.xlsx", 2],
|
||
["Отчет_Г_июнь_2026.xlsx", 3], ["Fin_Д_06-2026.xlsx", 4], ["Финансы_Е_июнь.xlsx", 5],
|
||
["Отчет_Ж.xlsx", 6], ["Годовой_отчет_З.xlsx", 7], ["Данные_И_июнь.xlsx", 8],
|
||
["Report_K_2026-06.xlsx", 9], ["Свод_Л_июнь.xlsx", 10], ["Fin_М_06.xlsx", 11],
|
||
["Отчет_Н_июнь_2026.xlsx", 12], ["Data_O_2026.06.xlsx", 13], ["Отчет_П.xlsx", 14],
|
||
["Фин_Р_июнь.xlsx", 15], ["Summary_S_06.2026.xlsx", 16]
|
||
];
|
||
|
||
var MAPPING = [
|
||
["Выручка", { 0: "Раздел_1_Оборот", 1: "Выручка_в_тыс", 2: "Выручка", 3: "ОБорот_за_отчетный_период", 4: "Revenue", 5: "ОП_Выручка", 6: "Выручка(нетто)", 7: "Выручка год", 8: "ОП-1 Выручка", 9: "REV_06", 10: "Выручка", 11: "Rev", 12: "Выручка", 13: "Sales", 14: "О_п_факт", 15: "Выручка_итоги", 16: "Revenue_06" }],
|
||
["EBITDA", { 0: "EBITDA", 1: "Прибыль_до_аморт", 2: "EBITDA_план", 3: "EBITDA факт", 4: "EBITDA", 5: "EBITDA_итого", 6: "Прибыль_EBITDA", 7: "EBITDA_год", 8: "EBITDA факт", 9: "EBITDA_06", 10: "EBITDA", 11: "eBITDA", 12: "EBITDA факт", 13: "EBITDA", 14: "EBITDA_факт", 15: "EBITDA", 16: "EBITDA_06" }],
|
||
["CAPEX", { 0: "Инвест_затраты", 1: "Капзатраты_тыс", 2: "CAPEX", 3: "Инвестпроект_затраты", 4: "Capex", 5: "CAPEX_итоги", 6: "Инвест.затраты", 7: "CAPEX_год", 8: "CAPEX факт", 9: "CAPEX_06", 10: "CAPEX", 11: "capex", 12: "CAPEX факт", 13: "CAPEX", 14: "ИЗ_факт", 15: "Капзатраты", 16: "Capex_06" }],
|
||
["Дебиторка", { 0: "Дебиторка_итог", 1: "ДК_в_сальдо", 2: "Дебиторская", 3: "Д-ка_на_конец_мес", 4: "AR", 5: "Дебиторка_кон", 6: "ДК(кон.ост.)", 7: "Дебиторка_год", 8: "ДК факт", 9: "AR_06", 10: "Дебиторская", 11: "AR", 12: "ДК к/мес", 13: "AR", 14: "ДК_факт", 15: "Дебиторка_итого", 16: "AR_06" }],
|
||
["Кредиторка", { 0: "Кредиторка_итог", 1: "КК_в_сальдо", 2: "Кредиторская", 3: "К-ка_на_конец_мес", 4: "AP", 5: "Кредиторка_кон", 6: "КК(кон.ост.)", 7: "Кредиторка_год", 8: "КК факт", 9: "AP_06", 10: "Кредиторская", 11: "AP", 12: "КК к/мес", 13: "AP", 14: "КК_факт", 15: "Кредиторка_итого", 16: "AP_06" }],
|
||
["Численность", { 0: "Штат_ФТ", 1: "Числ._по_окладу", 2: "Численность", 3: "Штатное_число", 4: "Heads", 5: "Численность_кон", 6: "ФТ_кон.оот.", 7: "Численность_год", 8: "Штат факт", 9: "HC_06", 10: "Численность", 11: "FTE", 12: "Штат к/мес", 13: "HC", 14: "Числ_факт", 15: "Численность_итого", 16: "Headcount_06" }]
|
||
];
|
||
|
||
var LOGS = [
|
||
["10:02:14", "ok", "Получено 17 вложений из общего ящика Outlook"],
|
||
["10:02:15", "info", "Форматы: 17 Excel, 3 нестандартных шаблона"],
|
||
["10:02:18", "ok", "ООО А · сопоставлено 6 столбцов (шаблон 2024)"],
|
||
["10:02:21", "warn", "ООО Б · «Выручка_в_тыс» → «Выручка», множитель 1/1000"],
|
||
["10:02:23", "ok", "ООО В..И · автоматическое сопоставление (6/6)"],
|
||
["10:02:27", "warn", "ООО О · пустые строки в файле, пропущены"],
|
||
["10:02:29", "ok", "ООО Л..С · автоматическое сопоставление (6/6)"],
|
||
["10:02:31", "info", "1 расхождение единиц измерения, отклонения свыше 5% — 3 компании"]
|
||
];
|
||
|
||
var ANALYSIS = {
|
||
0: "Выручка 11 800 млн тг — 94% плана (−6%). EBITDA фактически выше плана (101%), операционная эффективность сохранена: CAPEX 510 млн тг при плане 420. Дебиторка +8%, кредиторка +3% — оборотный капитал в пределах нормы. Численность 1215 чел. при плане 1240.",
|
||
1: "Выручка 7 950 млн тг — 97% плана. Обнаружено расхождение единиц: в файле выручка в тысячах тенге, при переносе применён множитель 1/1000. EBITDA 94% плана, отклонение в пределах допустимого. Численность 842 чел.",
|
||
2: "Выручка 13 100 млн тг — −15% к плану, крупнейшее отклонение в группе. EBITDA 3 480 млн тг при плане 4 100 (−15%), доля в выручке снизилась с 27% до 27%. Дебиторка 1 720 при 880 — рост 96% требует разбора. Кредиторка 2 610 (96% плана).",
|
||
3: "Выручка 10 050 млн тг — 103% плана, опережение. EBITDA 102% плана. CAPEX 355 (104%). Дебиторка 480 (92% плана). Компенсация плановых отставок, численность 998 чел. при плане 1 010.",
|
||
4: "Выручка 10 900 млн тг — 96% плана. EBITDA 2 700 при 2 850 (95%). CAPEX 430 (110% плана) — опережение инвестиционной программы. Численность 1 128 чел.",
|
||
5: "Выручка 7 420 млн тг — 98% плана. EBITDA 1 830 (96%). Отклонения в пределах 5% по всем статьям. Численность 775 чел.",
|
||
6: "Выручка 14 350 млн тг — 103% плана, опережение. EBITDA 3 720 (103%). Дебиторка 730 (96% плана). Рост органический, численность 1 408 чел.",
|
||
7: "Выручка 6 510 млн тг — 97% плана. EBITDA 1 610 (96%). Отклонений свыше 5% нет. Численность 682 чел.",
|
||
8: "Выручка 9 980 млн тг — 96% плана, снижение 4% к плану. EBITDA 2 490 (94%). Кредиторка 1 565 (106% плана). Численность 1 035 чел.",
|
||
9: "Выручка 8 730 млн тг — 98% плана. EBITDA 2 210 (97%). Отклонения минимальные. Численность 908 чел.",
|
||
10: "Выручка 12 490 млн тг — 103% плана, опережение. EBITDA 3 310 (103%). CAPEX 415 (97%). Численность 1 258 чел.",
|
||
11: "Выручка 6 890 млн тг — 97% плана. EBITDA 1 720 (97%). Отклонений свыше 5% нет. Численность 740 чел.",
|
||
12: "Выручка 13 850 млн тг — 95% плана. EBITDA 3 640 (95%). Дебиторка 790 (95% плана), кредиторка 2 415 (104%). Численность 1 488 чел.",
|
||
13: "Выручка 8 910 млн тг — 96% плана. EBITDA 2 240 (94%). Дебиторка 480 (94%). Численность 958 чел.",
|
||
14: "Выручка 11 980 млн тг — 103% плана, опережение. EBITDA 3 080 (103%). Кредиторка 1 495 (95% плана). Численность 1 168 чел.",
|
||
15: "Выручка 8 160 млн тг — 97% плана. EBITDA 2 060 (96%). Отклонения в пределах 5%. Численность 830 чел.",
|
||
16: "Выручка 12 840 млн тг — 97% плана. EBITDA 3 280 (96%). Дебиторка 680 (96%). Численность 1 348 чел."
|
||
};
|
||
CO.forEach(function (c, i) { c.analysis = ANALYSIS[i]; c.aStatus = 0; });
|
||
CO[2].aStatus = 2; var A_ERR = CO[2];
|
||
|
||
var STEPS = [
|
||
{ id: "upload", n: 1, t: "Загрузка", sub: "распознавание файлов" },
|
||
{ id: "check", n: 2, t: "Проверка", sub: "данные и отклонения" },
|
||
{ id: "analysis", n: 3, t: "Черновик", sub: "анализа" },
|
||
{ id: "result", n: 4, t: "Итог", sub: "результат и отправка" }
|
||
];
|
||
|
||
/* ---------- utils ---------- */
|
||
var T = "light";
|
||
function $(id) { return document.getElementById(id); }
|
||
function esc(s) { return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||
var NLC = new Intl.NumberFormat("ru-RU");
|
||
function fmt(n) { return n == null ? "—" : NLC.format(n); }
|
||
function pct(d) { if (!d) return "0"; return "−" + String(d).replace(".", ","); }
|
||
function pctSign(d) { return (d > 0 ? "+" : "−") + String(d).replace(".", ","); }
|
||
function ic(name) { return '<svg class="kt-icon" aria-hidden="true"><use href="#' + name + '"></use></svg>'; }
|
||
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);
|
||
return true;
|
||
}
|
||
function sum(arr) { return arr.reduce(function (a, b) { return a + b; }, 0); }
|
||
function fileStatus(i) {
|
||
if (S.fileManual) return ["info", "Вручную"];
|
||
if (S.aiAccepted) return ["ok", "Сопоставлено"];
|
||
return S.fileStatus[i] || (i === 1 ? ["warn", "Единицы: ×1/1000"] : ["ok", "Сопоставлено"]);
|
||
}
|
||
|
||
/* ---------- shell ---------- */
|
||
function renderSidebar() {
|
||
var items = STEPS.map(function (st) {
|
||
var done = !!S.done[st.id]; var cur = S.screen === st.id;
|
||
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>' +
|
||
(done ? '<span class="kt-ai-count-split done">' + esc("17") + '</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>';
|
||
}
|
||
function renderTopbar() {
|
||
var t = "";
|
||
if (S.screen === "upload") t = "Загрузка и распознавание · " + "пятница, 10:02";
|
||
else if (S.screen === "check") t = "Проверка данных и отклонений";
|
||
else if (S.screen === "analysis") t = "Черновик анализа";
|
||
else if (S.screen === "result") t = "Итоговый результат";
|
||
else t = "О решении";
|
||
$("topTitle").innerHTML = '<span class="t">' + t + '</span><span class="sub">пакет на пн, 15:00</span>';
|
||
}
|
||
function showScreen(id) {
|
||
if (id !== "about" && !canOpen(id)) { toast("Сначала завершите предыдущий шаг", "warn"); return; }
|
||
S.screen = id; save();
|
||
renderSidebar(); renderTopbar();
|
||
var r = { upload: viewUpload, check: viewCheck, analysis: viewAnalysis, result: viewResult, about: viewAbout }[id];
|
||
var el = $("view");
|
||
el.innerHTML = r();
|
||
el.scrollTop = 0;
|
||
document.querySelector(".kt-ai-main").scrollTop = 0;
|
||
bind(id);
|
||
closeMobile();
|
||
}
|
||
function canOpen(id) {
|
||
if (id === "upload") return true;
|
||
if (id === "check") return !!S.done.recognize;
|
||
if (id === "analysis") return !!S.done.check;
|
||
if (id === "result") return !!S.done.analysis || S.screen === "result";
|
||
return true;
|
||
}
|
||
function closeMobile() { var sb = $("sidebar"); if (sb) sb.removeAttribute("data-open"); $("scrim").classList.remove("on"); }
|
||
|
||
/* ---------- stepper ---------- */
|
||
function stepHtml(curId) {
|
||
var h = "";
|
||
STEPS.forEach(function (st, i) {
|
||
var done = !!S.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 (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" style="margin-bottom:var(--kt-ai-space-6)">' + h + '</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) + ' · получен 10 июн, ' + (9 + i % 4) + ':' + String((5 + i * 7) % 60).padStart(2, "0") + '</div></div>' +
|
||
'<span class="kt-ai-status-pill" data-status="' + st[0] + '">' + st[1] + '</span></div>';
|
||
}).join("");
|
||
var mapRows = MAPPING.map(function (m) {
|
||
var orig = Object.keys(m[1]).map(function (k) { return m[1][k]; });
|
||
return '<tr><td><b style="font-weight:var(--kt-ai-weight-medium)">' + m[0] + '</b></td>' +
|
||
'<td class="cell-orig">' + orig.map(function (o) { return esc(o); }).join(" · ") + "</td>" +
|
||
'<td><span class="kt-ai-chip" data-tone="blue">' + m[0] + '</span></td></tr>';
|
||
}).join("");
|
||
var progress = S.fileManual ? 100 : S.aiAccepted ? 100 : 82;
|
||
var banner, tone;
|
||
if (S.fileManual) { tone = "warn"; banner = "Распознавание остановлено: 1 файл обработан в ручном режиме. Проверьте столбцы и верните автоматический режим."; }
|
||
else if (S.aiAccepted) { tone = "ok"; banner = "Все 17 файлов сопоставлены, расхождения единиц отмечены. Можно переходить к проверке данных."; }
|
||
else { tone = "info"; banner = "ИИ сопоставляет столбцы по 17 файла. Отклонения и единицы будут отмечены после завершения."; }
|
||
banner = '<div class="kt-ai-banner" data-tone="' + tone + '" style="margin-bottom:var(--kt-ai-space-5)">' + banner + '</div>';
|
||
return pageHead("Загрузка и распознавание", "Вложения из общего ящика Outlook · 17 дочерних компаний, отчёт за июнь",
|
||
'', '<button class="kt-ai-btn" data-act="outlook">' + ic("mail") + ' Загрузить из Outlook</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" data-act="recognize">' + (S.aiAccepted ? ic("check-circle") + " Подтверждено" : ic("sparkles") + " Запустить распознавание") + '</button>') +
|
||
banner + stepHtml("upload") +
|
||
'<div class="grid-2">' +
|
||
'<div class="kt-ai-card block"><div class="block-head"><h2>Файлы</h2><span class="cap">17 шт. · июнь 2026</span><span class="spacer"></span>' +
|
||
(S.aiAccepted ? '<button class="kt-ai-btn" data-size="sm" data-act="manual">' + ic("tool") + ' Ручной режим</button>' : "") + '</div>' +
|
||
'<div class="file-list">' + fileRows + '</div></div>' +
|
||
'<div class="kt-ai-card block"><div class="block-head"><h2>Прогресс распознавания</h2></div>' +
|
||
'<div class="progress-head"><b>' + progress + '%</b><span class="cap">' + (S.aiAccepted ? "17/17 файлов" : "14/17 файлов") + '</span><span class="spacer" 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)">Сопоставление: 6 целевых показателей во всех файлах</div>' +
|
||
'<div class="block-head" style="margin-top:var(--kt-ai-space-6)"><h2>Журнал действий ИИ</h2></div>' +
|
||
'<div class="log-list">' + LOGS.map(function (l) {
|
||
return '<div class="log-item"><span class="log-time">' + l[0] + '</span>' +
|
||
'<span class="kt-ai-status-pill log-tag" data-status="' + l[1] + '">' + (l[1] === "ok" ? "готово" : l[1] === "warn" ? "внимание" : "инфо") + '</span>' +
|
||
'<p>' + l[2] + '</p></div>';
|
||
}).join("") + '</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">исходный формат → единый шаблон</span></div>' +
|
||
'<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr><th style="width:160px">Показатель</th><th>Исходный столбец (варианты)</th><th style="width:170px">Целевой столбец</th></tr></thead><tbody>' +
|
||
mapRows + '</tbody></table></div></div>';
|
||
}
|
||
|
||
/* ================= ЭКРАН 2 — проверка ================= */
|
||
var chk = { company: "all", only: "all" };
|
||
function viewCheck() {
|
||
var opts = "<option value='all'>Все компании (17)</option>" + CO.map(function (c, i) { return "<option value='" + i + "'>" + esc(c.name) + "</option>"; }).join("");
|
||
var devs = CO.filter(function (c) { return c.maxDev > 5; }).length;
|
||
var kpis =
|
||
kpi("Компаний в сводном файле", "17 / 17", "план и факт, июнь") +
|
||
kpi("Отклонения плана > 5%", devs, "из 17 компаний") +
|
||
kpi("Единицы измерения", S.unitFixed.length ? "исправлено" : "1 / 17", "ООО Б · ×1/1000") +
|
||
kpi("Структура", "6 показателей", "итоговый лист + 17 вкладок");
|
||
return pageHead("Проверка данных и отклонения", "Сводный файл сформирован ИИ · проверьте единицы измерения и отклонения от плана",
|
||
'', '<button class="kt-ai-btn" data-variant="primary" data-act="to-check">' + ic("arrowRight") + " Перейти к черновику анализа</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-select-wrap"><select class="kt-ai-select" id="fCompany">' + opts + '</select></div>' +
|
||
'<div class="kt-ai-segmented" id="fOnly"><button class="seg" data-v="all" data-active="true">Все</button><button class="seg" data-v="dev">С отклонением</button><button class="seg" data-v="unit">Единицы</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"><p class="text">Отклонения подсвечены: двухсегментная шкала — 2% и 5%. Строка с ошибками единиц помечена красным; исправление — по клику на ячейку. Итоговая строка — сумма по выбранным компаниям.</p></div>' +
|
||
'</div>' +
|
||
unitBanner();
|
||
}
|
||
function kpi(label, value, hint) { return '<div class="kt-ai-kpi"><span class="label">' + label + '</span><span class="value">' + value + '</span><span class="hint">' + hint + '</span></div>'; }
|
||
function unitBanner() {
|
||
var fixed = S.unitFixed.indexOf(1) !== -1;
|
||
return fixed
|
||
? '<div class="kt-ai-banner" data-tone="ok" style="margin-top:var(--kt-ai-space-5)">Единицы ООО Б исправлены: выручка переведена из тысяч в миллионы тенге.</div>'
|
||
: '<div class="kt-ai-banner" data-tone="risk" style="margin-top:var(--kt-ai-space-5)"><div><b>Расхождение единиц: ООО Б.</b> В файле выручка в тысячах тенге, в шаблоне — в миллионах. Клик по ячейке выручки — исправить вручную.</div></div>';
|
||
}
|
||
function totalRow() {
|
||
var r = ["Всего", sum(CO.map(function (c) { return c.vp; })), sum(CO.map(function (c) { return c.vf; })), sum(CO.map(function (c) { return c.ep; })), sum(CO.map(function (c) { return c.ef; })), sum(CO.map(function (c) { return c.cap; })), sum(CO.map(function (c) { return c.caf; })), sum(CO.map(function (c) { return c.dp; })), sum(CO.map(function (c) { return c.df; })), sum(CO.map(function (c) { return c.cp; })), sum(CO.map(function (c) { return c.cf; })), sum(CO.map(function (c) { return c.hp; })), sum(CO.map(function (c) { return c.hf; }))];
|
||
return r;
|
||
}
|
||
function renderSumTable() {
|
||
var box = $("sumTable"); if (!box) return;
|
||
var filtered = CO.filter(function (c, i) { return chk.company === "all" || chk.company === String(i); });
|
||
var rows = CO.map(function (c, i) {
|
||
var show = chk.company === "all" || chk.company === String(i);
|
||
if (!show) return "";
|
||
if (chk.only === "dev" && c.maxDev <= 5) return "";
|
||
if (chk.only === "unit" && c.name !== "ООО Б") return "";
|
||
var unitFix = S.unitFixed.indexOf(1) !== -1;
|
||
var dR = c.rDev, dE = c.eDev, dC = c.cDev, dD = c.dDev, dK = c.kDev;
|
||
function cell(plan, fact, d, extra) {
|
||
var cls = "num cell-num" + (d >= 5 ? " cell-dev-3" : d > 2 ? " cell-dev-2" : "");
|
||
return '<td class="' + cls + (extra ? " " + extra : "") + '">' + fmt(plan) + ' / ' + fmt(fact) + ' <span class="' + (d >= 5 ? "delta-neg" : "delta-pos") + '">(' + (d > 0 ? pct(d) : "0") + '%)</span></td>';
|
||
}
|
||
var vfCell = c.name === "ООО Б"
|
||
? '<td class="num cell-num ' + (unitFix ? "cell-unit-fixed" : "cell-unit-w") + '" data-unit="1">' + fmt(c.vp) + ' / ' + fmt(c.vf) + ' <span class="' + (dR >= 5 ? "delta-neg" : "delta-pos") + '">(' + pct(dR) + '%)</span> ' + (unitFix ? '<span class="kt-ai-chip" data-tone="green">исправлено</span>' : '<span class="kt-ai-chip" data-tone="red">×1 000</span>') + '</td>'
|
||
: cell(c.vp, c.vf, dR);
|
||
return '<tr data-clickable="true" data-oi="' + i + '"><td><b style="font-weight:var(--kt-ai-weight-medium)">' + esc(c.name) + '</b></td>' + vfCell +
|
||
cell(c.ep, c.ef, dE) + cell(c.cap, c.caf, dC) + cell(c.dp, c.df, dD) + cell(c.cp, c.cf, dK) +
|
||
'<td class="num">' + fmt(c.hp) + ' / ' + fmt(c.hf) + '</td></tr>';
|
||
}).join("");
|
||
var totalN = filtered.length;
|
||
var tsum = filtered.map(function (c) {
|
||
return { vp: c.vp, vf: c.vf, ep: c.ep, ef: c.ef, cap: c.cap, caf: c.caf, dp: c.dp, df: c.df, cp: c.cp, cf: c.cf, hp: c.hp, hf: c.hf };
|
||
}).reduce(function (a, b) { for (var k in a) a[k] += b[k]; return a; }, { vp: 0, vf: 0, ep: 0, ef: 0, cap: 0, caf: 0, dp: 0, df: 0, cp: 0, cf: 0, hp: 0, hf: 0 });
|
||
var dV = dev({}, tsum.vf, tsum.vp), dE = dev({}, tsum.ef, tsum.ep), dC = dev({}, tsum.caf, tsum.cap), dD = dev({}, tsum.df, tsum.dp), dK = dev({}, tsum.cf, tsum.cp);
|
||
function t(p, f, d) { return '<td class="num cell-num">' + fmt(p) + ' / ' + fmt(f) + ' <span class="' + (d >= 5 ? "delta-neg" : "delta-pos") + '">(' + (d > 0 ? pct(d) : "0") + '%)</span></td>'; }
|
||
var totalRowHtml = '<tr style="background:var(--kt-ai-bg-soft)"><td><b>Итого, ' + (chk.company === "all" ? "17 компаний" : esc(CO[+chk.company].name)) + '</b></td>' +
|
||
t(tsum.vp, tsum.vf, dV) + t(tsum.ep, tsum.ef, dE) + t(tsum.cap, tsum.caf, dC) + t(tsum.dp, tsum.df, dD) + t(tsum.cp, tsum.cf, dK) +
|
||
'<td class="num cell-num">' + fmt(tsum.hp) + ' / ' + fmt(tsum.hf) + '</td></tr>';
|
||
var any = rows.indexOf("<tr") !== -1;
|
||
box.innerHTML = '<table class="kt-ai-table" data-density="compact"><thead><tr><th>Компания</th>' +
|
||
"<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'>Численность, чел.</th></tr></thead><tbody>" +
|
||
(any ? rows + totalRowHtml : '<tr><td colspan="7" style="text-align:center;padding:var(--kt-ai-space-8);color:var(--kt-ai-fg-faint)">Под фильтр не попали компании</td></tr>') +
|
||
"</tbody></table>";
|
||
var note = $("fNote"); if (note) note.textContent = any ? totalN + " из 17 · план / факт, откл. в %" : "";
|
||
}
|
||
function pageHead(title, sub, left, 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>';
|
||
}
|
||
|
||
/* ================= ЭКРАН 3 — черновик анализа ================= */
|
||
var curA = 0;
|
||
function analysisText(i) {
|
||
var o = S.analysis[i];
|
||
if (o && typeof o === "string") return o;
|
||
return CO[i].analysis;
|
||
}
|
||
function aStatus(i) { return (S.analysis[i] && S.analysis[i].status) || CO[i].aStatus || 0; }
|
||
function viewAnalysis() {
|
||
var list = CO.map(function (c, i) {
|
||
var st = aStatus(i);
|
||
var dot = st === 1 ? 'dot-ok' : (st === 2 ? 'dot-err' : 'dot-gen');
|
||
return '<div class="kt-ai-row" data-ana="' + i + '"' + (i === curA ? ' data-active="true"' : "") + '>' +
|
||
'<span class="f-ic" style="width:26px;height:26px">' + ic("file") + '</span>' +
|
||
'<div style="min-width:0;flex:1"><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block">' + esc(c.name) + (S.analysis[i] && S.analysis[i].edited ? ' <span class="kt-ai-chip" data-tone="blue" style="margin-left:6px">правка</span>' : "") + '</span></div>' +
|
||
'<span class="dot ' + dot + '" title=""></span></div>';
|
||
}).join("");
|
||
var words = CO.reduce(function (a, c, i) { return a + analysisText(i).split(/\s+/).filter(Boolean).length; }, 0);
|
||
var ready = CO.filter(function (c, i) { return aStatus(i) === 1; }).length;
|
||
var memoKpi = kpi("Компаний", CO.length, "по одному выводу") +
|
||
kpi("Принято", ready, "из " + CO.length) +
|
||
kpi("Объём записки", fmt(words) + " слов", "≈ " + (words > 640 ? "2" : "1") + " стр.") +
|
||
kpi("Готовность", ready === CO.length ? "100%" : Math.round(ready / CO.length * 100) + "%", "к формированию записки");
|
||
return pageHead("Черновик анализа", "ИИ сгенерировал выводы по каждой компании · отредактируйте при необходимости",
|
||
'', '<button class="kt-ai-btn" data-act="accept-all">' + ic("check-circle") + ' Принять черновик</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" data-act="make-memo">' + ic("send") + ' Сформировать записку</button>') +
|
||
stepHtml("analysis") +
|
||
'<div class="kt-ai-kpi-strip">' + memoKpi + '</div>' +
|
||
'<div class="analysis-body">' +
|
||
'<div class="analysis-list">' + list + '</div>' +
|
||
'<div><div class="ed-head"><span class="cap">Черновик · ' + esc(CO[curA].name) + '</span><span class="ed-stats">' + words + ' слов</span><span style="flex:1"></span>' +
|
||
'<button class="kt-ai-btn" data-size="sm" data-act="a-prev">' + ic("chevronLeft") + '</button>' +
|
||
'<button class="kt-ai-btn" data-size="sm" data-act="a-next">' + ic("chevronRight") + '</button></div>' +
|
||
'<div class="ed-wrap"><textarea class="kt-ai-analysis" id="anaText" spellcheck="false"></textarea></div>' +
|
||
'<div style="display:flex;gap:var(--kt-ai-space-4);align-items:center;margin-top:var(--kt-ai-space-4);flex-wrap:wrap">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" data-size="sm" data-act="a-accept">' + ic("check") + ' Принять выводы</button>' +
|
||
'<span class="ed-stats">Статус: ' + (aStatus(curA) === 1 ? "принято" : aStatus(curA) === 2 ? "есть пометка о данных" : "черновик") + '</span>' +
|
||
(aStatus(curA) === 2 ? '<span class="kt-ai-chip" data-tone="orange">проверить дебиторку</span>' : "") +
|
||
'</div></div>' +
|
||
'</div>';
|
||
}
|
||
|
||
/* ================= ЭКРАН 4 — итоговый результат ================= */
|
||
function viewResult() {
|
||
var tv = totalRow();
|
||
var dV = dev({}, tv[2], tv[1]), dE = dev({}, tv[4], tv[3]);
|
||
var memoRows = CO.filter(function (c) { return c.maxDev > 5 || c.name === "ООО Б"; }).map(function (c) {
|
||
return "<p>• <b>" + esc(c.name) + ".</b> " + analysisTextShort(c) + "</p>";
|
||
}).join("");
|
||
var status = S.sent ? ['data-status="ok"', "Отправлено директору 09:58", ""]
|
||
: S.done.analysis ? ['data-status="ok"', "Готово к отправке", "дедлайн: пн 15:00"]
|
||
: ['data-status="warn"', "Ожидает подтверждения анализа", "дедлайн: пн 15:00"];
|
||
var xTable = '<table class="kt-ai-table" data-density="compact"><thead><tr><th>Организация</th><th class="num">Выручка, млн тг</th><th class="num">EBITDA, млн тг</th><th class="num">CAPEX, млн тг</th><th class="num">Дебит. / Кредит., млн тг</th></tr></thead><tbody>' +
|
||
CO.map(function (c) { return '<tr><td>' + esc(c.name) + '</td><td class="num cell-num">' + fmt(c.vf) + '</td><td class="num cell-num">' + fmt(c.ef) + '</td><td class="num cell-num">' + fmt(c.caf) + '</td><td class="num cell-num">' + fmt(c.df) + ' / ' + fmt(c.cf) + '</td></tr>'; }).join("") +
|
||
'</tbody></table>';
|
||
return pageHead("Итоговый результат", "Финальная проверка пакета перед передачей финансовому директору",
|
||
'', '<button class="kt-ai-btn" data-act="dl-xlsx">' + ic("download") + ' Скачать Excel</button>' +
|
||
'<button class="kt-ai-btn" data-act="dl-memo">' + ic("download") + ' Скачать записку</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" data-act="send-pkg" ' + (S.sent ? "disabled" : "") + '>' + (S.sent ? ic("check-circle") + " Отправлено" : ic("send") + " Отправить пакет директору") + '</button>') +
|
||
stepHtml("result") +
|
||
'<div class="kt-ai-banner" ' + status[0] + ' style="margin-bottom:var(--kt-ai-space-5)">' +
|
||
'<b>' + status[1] + '</b> · ' + status[2] + (S.sent ? ' · адресат: финансовый директор' : "") + '</div>' +
|
||
'<div class="kt-ai-kpi-strip">' +
|
||
kpi("Сводный файл", "1 итог + 17 листов", "сводный_файл_июнь.xlsx") +
|
||
kpi("Записка", "2 страницы", "выводы по компаниям") +
|
||
kpi("Сверка", "17 / 17", "единицы согласованы") +
|
||
kpi("Отклонения", dV > 0 ? "−" + pctSign(dV) + "%" : pctSign(dV) + "%", "выручка к плану · EBITDA " + pctSign(dE) + "%") +
|
||
'</div>' +
|
||
'<div class="grid-2">' +
|
||
'<div class="kt-ai-card block"><div class="block-head"><h2>Сводный Excel</h2><span class="cap">sheet 1 из 18: Итог, затем по организации</span></div>' +
|
||
'<div class="table-wrap" style="max-height:430px">' + xTable + '</div></div>' +
|
||
'<div class="kt-ai-card block"><div class="block-head"><h2>Записка к отчёту</h2><span class="kt-ai-chip" data-tone="gray">2 стр.</span></div>' +
|
||
'<div class="memo"><div class="memo-meta">На: финансовый директор · От: аналитик · Пн, 15:00</div>' +
|
||
'<h3>Свод по дочерним компаниям, июнь</h3>' +
|
||
'<p>По всем 17 компаниям факт к плану по выручке ' + (dV > 0 ? "−" + pctSign(dV) : pctSign(dV)) + "%, по EBITDA — " + pctSign(dE) + "% (млн тг). Единицы измерения 1 компании (ООО Б) исправлены и пересчитаны.</p>" +
|
||
memoRows +
|
||
'<p>Рекомендации: разобрать дебиторку ООО В (рост 96% к плану) и CAPEX ООО Д (110% к программе).</p>' +
|
||
'</div></div>' +
|
||
'</div>';
|
||
}
|
||
function analysisTextShort(c) {
|
||
var t = c.analysis;
|
||
return t.length > 200 ? t.slice(0, 197).replace(/\s+\S*$/, "") + "…" : t;
|
||
}
|
||
|
||
/* ================= действия ================= */
|
||
function setUnitFix(i, fixed) {
|
||
var idx = S.unitFixed.indexOf(1);
|
||
if (fixed && idx === -1) { S.unitFixed.push(1); }
|
||
if (!fixed) S.unitFixed = S.unitFixed.filter(function (x) { return x !== 1; });
|
||
save();
|
||
if (S.screen === "check") renderSumTable(); updateUnitBanner();
|
||
toast(fixed ? "Единицы ООО Б исправлены: ×1/1000 применён" : "Единицы ООО Б возвращены в исходные", fixed ? "ok" : "warn");
|
||
}
|
||
function updateUnitBanner() {
|
||
if (S.screen !== "check") return;
|
||
var banner = document.querySelector("#view .kt-ai-banner:not([data-tone='info']):not([data-tone='warn'])");
|
||
if (!banner) return;
|
||
var fixed = S.unitFixed.indexOf(1) !== -1;
|
||
banner.setAttribute("data-tone", fixed ? "ok" : "risk");
|
||
banner.innerHTML = fixed
|
||
? '<b>Единицы ООО Б исправлены:</b> выручка переведена из тысяч в миллионы тенге.'
|
||
: '<b>Расхождение единиц: ООО Б.</b> В файле выручка в тысячах тенге, в шаблоне — в миллионах. Клик по ячейке выручки — исправить вручную.';
|
||
var k = document.querySelectorAll(".kt-ai-kpi");
|
||
if (k[2]) k[2].querySelector(".value").textContent = fixed ? "исправлено" : "1 / 17";
|
||
}
|
||
function acceptAllAnalysis() {
|
||
CO.forEach(function (c, i) { S.analysis[i] = { status: 1 }; });
|
||
S.done.analysis = true; save();
|
||
toast("Черновик принят по всем 17 компаниям", "ok");
|
||
showScreen("analysis");
|
||
}
|
||
function makeMemo() {
|
||
if (!S.done.analysis) { toast("Сначала примите черновик анализа по всем компаниям", "warn"); return; }
|
||
showScreen("result");
|
||
toast("Записка сформирована", "ok");
|
||
}
|
||
function download(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 xlsxText() {
|
||
return "Сводный файл, июнь 2026, млн тг (дебиторка/кредиторка/численность — свои единицы)\n\nКомпания;Выручка (план);Выручка (факт);EBITDA (план);EBITDA (факт);CAPEX (план);CAPEX (факт);Дебиторка (план);Дебиторка (факт);Кредиторка (план);Кредиторка (факт);Численность (план);Численность (факт)\n" +
|
||
CO.map(function (c) { return [c.name, c.vp, c.vf, c.ep, c.ef, c.cap, c.caf, c.dp, c.df, c.cp, c.cf, c.hp, c.hf].join(";"); }).join("\n") +
|
||
"\n\n" + CO.map(function (c) { return "== " + c.name + " ==\n" + analysisText(CO.indexOf(c)); }).join("\n\n");
|
||
}
|
||
function memoText() {
|
||
var tv = totalRow(); var dV = dev({}, tv[2], tv[1]), dE = dev({}, tv[4], tv[3]);
|
||
return "ЗАПИСКА — свод по дочерним компаниям, июнь 2026\nНа: финансовый директор\n\nПо всем 17 компаниям факт к плану: выручка " + pctSign(dV) + "%, EBITDA " + pctSign(dE) + "% (млн тг).\nЕдиницы измерения ООО Б исправлены (тысячи → миллионы тенге).\n\n" + CO.filter(function (c) { return c.maxDev > 5 || c.name === "ООО Б"; }).map(function (c) { return "• " + c.name + ". " + analysisTextShort(c); }).join("\n") +
|
||
"\n\nРекомендации: разобрать дебиторку ООО В (рост 96% к плану) и CAPEX ООО Д (110% к программе).\n";
|
||
}
|
||
|
||
/* ---------- 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='outlook']").forEach(function (b) { b.addEventListener("click", function () { toast("Обнаружено 17 вложений в общем ящике · загрузка завершена", "ok"); }); });
|
||
v.querySelectorAll("[data-act='recognize']").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
if (S.aiAccepted) { if (S.done.recognize) showScreen("check"); return; }
|
||
S.aiAccepted = true; S.done.recognize = true; save();
|
||
toast("Сопоставление завершено: 17/17 файлов, 1 расхождение единиц", "ok");
|
||
showScreen("upload");
|
||
});
|
||
});
|
||
v.querySelectorAll("[data-act='manual']").forEach(function (b) { b.addEventListener("click", function () {
|
||
S.fileManual = true; save(); toast("Открыт ручной режим для 1 файла. Проверьте столбцы и верните автоматический режим.", "warn"); showScreen("upload");
|
||
}); });
|
||
v.querySelectorAll("[data-file]").forEach(function (r) { r.addEventListener("click", function () { toast(esc(CO[+r.getAttribute("data-file")].name) + " · формат распознан, 6/6 столбцов", "ok"); }); });
|
||
}
|
||
// проверка
|
||
if (screen === "check") {
|
||
renderSumTable();
|
||
var fc = $("fCompany");
|
||
if (fc) { fc.value = chk.company; fc.addEventListener("change", function () { chk.company = fc.value; renderSumTable(); }); }
|
||
var fo = $("fOnly");
|
||
if (fo) fo.addEventListener("click", function (e) {
|
||
var b = e.target.closest(".seg"); if (!b) return;
|
||
chk.only = b.getAttribute("data-v");
|
||
fo.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(1, S.unitFixed.indexOf(1) === -1); return; }
|
||
var tr = e.target.closest("tr[data-oi]");
|
||
if (tr) {
|
||
var i = +tr.getAttribute("data-oi");
|
||
toast(CO[i].name + ": " + (CO[i].maxDev > 2 ? "макс. отклонение " + CO[i].maxDev + "%" : "в пределах нормы") + (CO[i].name === "ООО Б" ? " · расхождение единиц: ×1/1000" : ""), CO[i].maxDev > 5 ? "warn" : "ok");
|
||
}
|
||
});
|
||
v.querySelectorAll("[data-act='to-check']").forEach(function (b) { b.addEventListener("click", function () {
|
||
if (S.unitFixed.indexOf(1) === -1) { toast("Сначала исправьте единицы ООО Б (клик по ячейке выручки)", "warn"); return; }
|
||
if (!S.done.check) { S.done.check = true; save(); }
|
||
showScreen("analysis");
|
||
}); });
|
||
}
|
||
// анализ
|
||
if (screen === "analysis") {
|
||
var ta = $("anaText"); if (ta) ta.value = analysisText(curA);
|
||
v.querySelectorAll("[data-ana]").forEach(function (r) { r.addEventListener("click", function () {
|
||
curA = +r.getAttribute("data-ana"); save(); showScreen("analysis");
|
||
}); });
|
||
if (ta) ta.addEventListener("input", function () {
|
||
var cur = S.analysis[curA] || {};
|
||
S.analysis[curA] = Object.assign(cur, { text: ta.value, edited: true });
|
||
save();
|
||
var st = ta.closest(".analysis-body").querySelector(".ed-stats");
|
||
if (st) st.textContent = ta.value.split(/\s+/).filter(Boolean).length + " слов";
|
||
});
|
||
v.querySelectorAll("[data-act='a-prev'],[data-act='a-next']").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
curA = (curA + (b.getAttribute("data-act") === "a-next" ? 1 : CO.length - 1)) % CO.length;
|
||
save(); showScreen("analysis");
|
||
});
|
||
});
|
||
v.querySelectorAll("[data-act='a-accept']").forEach(function (b) { b.addEventListener("click", function () {
|
||
var st = S.analysis[curA] || {}; st.status = 1; S.analysis[curA] = st; save();
|
||
toast(CO[curA].name + ": выводы приняты", "ok"); showScreen("analysis");
|
||
}); });
|
||
v.querySelectorAll("[data-act='accept-all']").forEach(function (b) { b.addEventListener("click", acceptAllAnalysis); });
|
||
v.querySelectorAll("[data-act='make-memo']").forEach(function (b) { b.addEventListener("click", makeMemo); });
|
||
}
|
||
// итог
|
||
if (screen === "result") {
|
||
v.querySelectorAll("[data-act='dl-xlsx']").forEach(function (b) { b.addEventListener("click", function () { download(xlsxText(), "Сводный_файл_июнь_2026.txt"); }); });
|
||
v.querySelectorAll("[data-act='dl-memo']").forEach(function (b) { b.addEventListener("click", function () { download(memoText(), "Записка_июнь_2026.txt"); }); });
|
||
var sb2 = v.querySelector("[data-act='send-pkg']");
|
||
if (sb2 && !S.sent) sb2.addEventListener("click", function () {
|
||
S.sent = true; S.done.result = true; save();
|
||
toast("Пакет отправлен финансовому директору · 09:58", "ok");
|
||
showScreen("result");
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ---------- инициализация ---------- */
|
||
function init() {
|
||
var html = document.documentElement;
|
||
T = S.theme || "light";
|
||
html.setAttribute("data-theme", T);
|
||
var ti = $("themeIcon"); if (ti) ti.setAttribute("href", T === "light" ? "#moon" : "#sun");
|
||
$("themeToggle").addEventListener("click", function () {
|
||
T = T === "light" ? "dark" : "light";
|
||
S.theme = T; save();
|
||
html.setAttribute("data-theme", T);
|
||
if (ti) ti.setAttribute("href", T === "light" ? "#moon" : "#sun");
|
||
});
|
||
$("burger").hidden = window.innerWidth >= 832;
|
||
$("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"); }
|
||
$("burger").hidden = window.innerWidth >= 832;
|
||
});
|
||
$("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"));
|
||
});
|
||
if (S.resetFlag) { delete S.resetFlag; save(); toast("Предыдущее состояние не восстановилось · начат новый цикл", "warn"); }
|
||
renderSidebar();
|
||
showScreen(S.screen || "upload");
|
||
}
|
||
/* ================= ЭКРАН 5 — о решении ================= */
|
||
function viewAbout() {
|
||
return pageHead("О решении", "Сводный финансовый анализ дочерних компаний", "",
|
||
'') +
|
||
'<div class="pass-grid">' +
|
||
card("Паспорт решения", dl([
|
||
["Наименование", "Сводный финансовый анализ дочерних компаний"],
|
||
["Исполнитель", "владелец процесса — сборка на платформе Vibe AI"],
|
||
["Пользователи", "аналитик, финансовый директор"],
|
||
["Частота", "еженедельно, вложения по пятницам"],
|
||
["Дедлайн", "понедельник, 15:00"]
|
||
])) +
|
||
card("Предпосылки", '<ul><li>Ручная обработка данных 17 дочерних компаний занимает 3–4 часа в неделю</li><li>Разные форматы Excel приводят к путанице в столбцах и потере данных</li></ul>') +
|
||
card("Входные данные", '<ul><li>Выручка, EBITDA, CAPEX, дебиторка, кредиторка, численность — план и факт</li><li>Вложения в общем ящике Outlook, приходят по пятницам</li><li>У каждой компании свой шаблон листов и названий столбцов</li></ul>') +
|
||
card("Что делает ИИ", '<ol><li>Распознаёт формат файла и сопоставляет столбцы</li><li>Переносит данные в единый сводный файл</li><li>Подсвечивает отклонения от плана</li><li>Генерирует черновик анализа по каждой компании</li></ol>') +
|
||
card("Что решает человек", '<ul><li>Аналитик проверяет единицы измерения и расхождения</li><li>Аналитик редактирует и утверждает черновик анализа</li><li>Руководитель выборочно просматривает итоговый результат</li></ul>') +
|
||
card("Результат и эффект", '<ul><li>Сводный Excel: итоговый лист + листы по организациям</li><li>Записка на 1–2 страницы с выводами</li><li>Экономия 3–4 часов в неделю</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>'; }
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
})();
|