651 lines
42 KiB
JavaScript
651 lines
42 KiB
JavaScript
/* Сведение отчётов дочерних компаний — статичный интерфейс, состояние в 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||
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);
|
||
})();
|