/* Свод отчётности — статичный макет. Данные: localStorage с seed-набором. */
(function () {
"use strict";
var KEY = "ii-swot-state-v1";
var WEEK = ["08.09", "15.09", "22.09", "29.09", "06.10", "13.10", "20.10", "27.10"];
var SEED = {
orgs: [
{ id: "o1", name: "ДО «Север»", files: 5, kpi: 14, volume: 128.4, date: "16.09.2026", status: "ready" },
{ id: "o2", name: "ДО «Восток»", files: 4, kpi: 11, volume: 86.2, date: "16.09.2026", status: "ready" },
{ id: "o3", name: "ДО «Юг-Транс»",files: 5, kpi: 13, volume: 112.7, date: "17.09.2026", status: "mismatch" },
{ id: "o4", name: "ДО «Запад»", files: 3, kpi: 9, volume: 64.1, date: "17.09.2026", status: "ready" },
{ id: "o5", name: "ДО «Центр»", files: 5, kpi: 14, volume: 143.9, date: "18.09.2026", status: "checked" },
{ id: "o6", name: "ДО «Приозерье»",files: 4, kpi: 10, volume: 71.5, date: "18.09.2026", status: "ready" },
{ id: "o7", name: "ДО «Степь»", files: 4, kpi: 12, volume: 95.3, date: "19.09.2026", status: "mismatch" },
{ id: "o8", name: "ДО «Речной»", files: 3, kpi: 8, volume: 58.6, date: "19.09.2026", status: "pending" }
],
week: [612.0, 648.5, 631.2, 670.8, 655.4, 689.1, 672.6, 760.7],
weekDates: ["10.02.2026", "16.03.2026", "22.04.2026", "29.05.2026", "26.06.2026", "24.07.2026", "21.08.2026", "27.09.2026"],
insights: [
{ org: "ДО «Центр»", tone: "ok", text: "Самый стабильный показатель за период: отклонение от среднего −3,1%." },
{ org: "ДО «Север»", tone: "ok", text: "Объём вырос на 4,2% к предыдущей неделе — выше среднего по своду." },
{ org: "ДО «Речной»", tone: "warn", text: "Файлы получены в 18:42, свод не завершён — организация в статусе «ожидаемо»." },
{ org: "ДО «Юг-Транс»", tone: "warn", text: "Расхождение: показатель за период в файле № 3 не сходится с итоговым на 2,3%." },
{ org: "ДО «Степь»", tone: "warn", text: "Расхождение: в выгрузке отсутствует столбец «итого», требуется уточнение у организации." },
{ org: "ДО «Восток»", tone: "info", text: "Динамика в пределах нормы, изменений нет." }
]
};
var MAIL_SUBJECTS = ["Отчёт за неделю №37", "Выгрузка показателей", "Еженедельный отчёт", "Письмо со вложениями"];
var MAIL_TIMES = ["08:12", "09:47", "11:03", "14:26", "16:58", "18:42"];
var state = load();
var view = { page: "passport", mailFilter: "all", mailSearch: "", mailSel: {}, sumFilter: "all", insightFilter: "all" };
function load() {
try {
var raw = localStorage.getItem(KEY);
if (raw) { var s = JSON.parse(raw); if (s && s.orgs) return s; }
} catch (e) { /* повреждённые данные — пересоздаём */ }
return JSON.parse(JSON.stringify(SEED));
}
function save() { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { /* квота — просто не сохраняем */ } }
function mailFor(org) {
if (org.mail) return org.mail;
var i = org.id.charCodeAt(1) % MAIL_TIMES.length;
org.mail = {
time: MAIL_TIMES[i],
subject: MAIL_SUBJECTS[org.id.charCodeAt(1) % MAIL_SUBJECTS.length],
size: (org.files * 1.4 + 0.3).toFixed(1)
};
for (var f = 1; f < org.files; f++) { /* имена вложения в drawer */ }
return org.mail;
}
var STATUS = {
ready: { label: "свёл", tone: "green" },
checked: { label: "проверено", tone: "blue" },
mismatch: { label: "расхождение", tone: "orange" },
pending: { label: "ожидаемо", tone: "gray" }
};
var STATUS_ORDER = ["ready", "checked", "mismatch", "pending"];
function $(id) { return document.getElementById(id); }
function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
function fmt(n) { return n.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); }
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); }
/* ---------- навигация ---------- */
function setPage(p) {
view.page = p;
document.querySelectorAll("#appNav button").forEach(function (b) {
b.setAttribute("data-active", b.getAttribute("data-page") === p ? "true" : "false");
});
document.querySelectorAll(".page").forEach(function (s) {
s.hidden = s.id !== "page-" + p;
});
if (p === "inbox") renderInbox();
if (p === "summary") renderSummary();
if (p === "analysis") renderAnalysis();
window.scrollTo(0, 0);
}
/* ---------- тосты ---------- */
function toast(text, tone, action, cb) {
var t = el("div", "kt-ai-toast", esc(text));
if (tone) t.dataset.tone = tone;
if (action) {
var b = el("button", "kt-ai-btn", action);
b.dataset.size = "sm";
b.style.marginLeft = "var(--kt-ai-space-3)";
b.onclick = function () { cb && cb(); t.remove(); };
t.appendChild(b);
}
$("toasts").appendChild(t);
setTimeout(function () { t.style.transition = "opacity .3s"; t.style.opacity = "0"; setTimeout(function () { t.remove(); }, 320); }, 4200);
}
/* ---------- модалка ---------- */
function modal(title, bodyHtml, actions) {
var box = $("modalBox");
box.innerHTML = '
' + esc(title) + "
" + bodyHtml +
'' + (actions || "") + "
";
$("modalOvl").style.display = "flex";
box.querySelectorAll("button[data-close]").forEach(function (b) { b.onclick = closeModal; });
}
function closeModal() { $("modalOvl").style.display = "none"; }
$("modalOvl").addEventListener("click", function (e) { if (e.target === this) closeModal(); });
document.addEventListener("keydown", function (e) { if (e.key === "Escape") { closeModal(); closeDrawer(); } });
/* ---------- drawer ---------- */
function openDrawer(html) { $("drawer").innerHTML = html; $("ovl").style.display = "block"; }
function closeDrawer() { $("ovl").style.display = "none"; }
$("ovlBg").addEventListener("click", closeDrawer);
function orgDrawer(org) {
var st = STATUS[org.status];
var files = "";
for (var i = 1; i <= org.files; i++) {
files += '' +
'
' + esc(org.name) + " — файл " + i + ".xlsx" +
'' + i + ".xlsx · " + (0.4 + i * 0.3).toFixed(1) + " МБ
" +
'
';
}
var issues = org.status === "mismatch"
? 'Нужна сверка
ИИ обнаружил расхождение показателей между файлами. Строки подсвечены в сводном файле, пока статус не станет «свёл».
'
: "";
openDrawer(
'' +
'
' + esc(org.name) + "
" +
'' + st.label + "" +
'Файлов получено' + org.files + "
" +
'
Показателей' + org.kpi + "
" +
'
Объём' + fmt(org.volume) + " млн
" +
'Файлы из письма
' +
'' + files + "
" +
issues +
'' +
(org.status === "pending" ? '' : "") +
'
'
);
}
/* ================= ВХОДЯЩИЕ ================= */
function filteredMails() {
var q = view.mailSearch.toLowerCase();
return state.orgs.filter(function (o) {
if (view.mailFilter === "new" && o.status !== "pending" && o.status !== "ready") return false;
if (view.mailFilter === "attention" && o.status !== "mismatch" && o.status !== "pending") return false;
if (view.mailFilter === "done" && o.status !== "checked") return false;
if (q && o.name.toLowerCase().indexOf(q) === -1 && mailFor(o).subject.toLowerCase().indexOf(q) === -1) return false;
return true;
});
}
function renderInbox() {
var orgs = filteredMails();
var totalFiles = state.orgs.reduce(function (a, o) { return a + o.files; }, 0);
var filesIn = orgs.reduce(function (a, o) { return a + o.files; }, 0);
var pending = state.orgs.filter(function (o) { return o.status === "pending"; }).length;
var mismatch = state.orgs.filter(function (o) { return o.status === "mismatch"; }).length;
var checked = state.orgs.filter(function (o) { return o.status === "checked"; }).length;
$("kpiInbox").innerHTML = kpi(fmtCount(state.orgs.length), "организаций", "пришлют отчёты за неделю") +
kpi(totalFiles, "файлов", "из писем за период") +
kpi(pending + mismatch, "требуют внимания", "ожидаемо или с расхождением") +
kpi(checked, "проверено", "человеком, готово в отчёт");
var chips = [
["all", "Все", state.orgs.length],
["new", "Новые", state.orgs.filter(function (o) { return o.status === "pending" || o.status === "ready"; }).length],
["attention", "Требуют внимания", pending + mismatch],
["done", "Завершено", checked]
];
$("mailFilters").innerHTML = chips.map(function (c) {
return '";
}).join("");
document.querySelectorAll("#mailFilters .kt-ai-filter-chip").forEach(function (b) {
b.onclick = function () { view.mailFilter = b.dataset.f; renderInbox(); };
});
var rows = orgs.map(function (o) {
var m = mailFor(o), st = STATUS[o.status];
var sel = !!view.mailSel[o.id];
return '' +
' | ' +
'' + (o.status === "pending" ? '' : '') + " | " +
'' + esc(o.name) + " | " +
'' + esc(m.subject) + " | " +
'' + o.files + " | " +
'' + esc(o.date) + " " + m.time + (o.status === "pending" ? ' · недавно' : "") + " | " +
'' + st.label + " |
";
}).join("");
$("mailRows").innerHTML = rows;
$("mailEmpty").hidden = orgs.length > 0;
$("mailCount").textContent = "Показано " + orgs.length + " из " + state.orgs.length + " · файлов: " + filesIn;
$("ctInbox").textContent = state.orgs.length;
document.querySelectorAll("#mailTable tr.row-click").forEach(function (tr) {
tr.onclick = function (e) {
if (e.target.closest("button")) return;
if (e.target.tagName === "INPUT") {
var id = tr.dataset.id;
view.mailSel[id] = e.target.checked;
renderInbox();
return;
}
orgDrawer(byId(tr.dataset.id));
};
});
}
function kpi(v, l, h) { return '' + v + "" + l + '' + h + "
"; }
function fmtCount(n) { return String(n); }
function byId(id) { return state.orgs.filter(function (o) { return o.id === id; })[0]; }
function processMails() {
var pending = state.orgs.filter(function (o) { return o.status === "pending"; });
var mismatch = state.orgs.filter(function (o) { return o.status === "mismatch"; });
if (!pending.length && !mismatch.length) {
toast("Все письма уже обработаны", "ok");
return;
}
var names = pending.concat(mismatch).map(byId).map(function (o) { return o.name; });
modal("Свести файлы",
"ИИ разберёт файлы из " + names.length + " писем, обновит сводные показатели и отметит расхождения:
" +
"" +
names.map(function (n) { return '
' + esc(n) + "
"; }).join("") +
"
",
'');
$("btnConfirmProcess").onclick = function () {
state.orgs.forEach(function (o) {
if (o.status === "pending" || o.status === "mismatch") o.status = "ready";
});
save(); closeModal();
toast("Файлы сведены. Показатели обновлены", "ok", null, null);
renderInbox(); refreshKpiSummary();
setPage("summary");
};
}
/* ================= СВОД ================= */
function renderSummary() {
var orgs = state.orgs.filter(function (o) { return view.sumFilter === "all" || o.status === view.sumFilter; });
var tot = orgs.reduce(function (a, o) { return { f: a.f + o.files, k: a.k + o.kpi, v: a.v + o.volume }; }, { f: 0, k: 0, v: 0 });
var mismatch = orgs.filter(function (o) { return o.status === "mismatch"; }).length;
var checked = orgs.filter(function (o) { return o.status === "checked"; }).length;
$("kpiSum").innerHTML = kpi(state.orgs.length, "организаций в своде", "отчётная неделя 14–20.09") +
kpi(fmt(tot.v), "млн, всего", "сумма показателей") +
kpi(mismatch, "с расхождениями", "нужны перед проверкой") +
kpi(checked, "проверено", "подтверждены человеком");
var filters = [
["all", "Все", state.orgs.length],
["ready", "Сведёно", state.orgs.filter(function (o) { return o.status === "ready"; }).length],
["checked", "Проверено", checked],
["mismatch", "Расхождение", mismatch],
["pending", "Ожидаемо", state.orgs.filter(function (o) { return o.status === "pending"; }).length]
];
$("sumFilters").innerHTML = filters.map(function (c) {
return '";
}).join("");
document.querySelectorAll("#sumFilters .kt-ai-filter-chip").forEach(function (b) {
b.onclick = function () { view.sumFilter = b.dataset.f; renderSummary(); };
});
var rows = orgs.map(function (o) {
var st = STATUS[o.status];
var hl = o.status === "mismatch" ? ' ' + "проверить" + "" : "";
return '| ' + esc(o.name) + " | " +
'' + o.files + " | " +
'' + o.kpi + " | " +
'' + fmt(o.volume) + hl + " | " +
'' + esc(o.date) + " | " +
'' + st.label + " |
";
}).join("");
rows += "| Всего | " +
'' + tot.f + " | " +
'' + tot.k + " | " +
'' + fmt(tot.v) + " | " +
" | |
";
$("sumRows").innerHTML = rows;
$("sumEmpty").hidden = orgs.length > 0;
document.querySelectorAll("#sumTable tr.row-click").forEach(function (tr) {
tr.onclick = function () { orgDrawer(byId(tr.dataset.id)); };
});
}
function refreshKpiSummary() { if (view.page === "summary") renderSummary(); }
/* ================= АНАЛИЗ ================= */
function renderAnalysis() {
var w = state.week;
var last = w[w.length - 1], prev = w[w.length - 2];
var max = Math.max.apply(null, w), maxI = w.indexOf(max);
var avg = w.reduce(function (a, b) { return a + b; }, 0) / w.length;
var delta = (last - prev) / prev * 100;
$("kpiAna").innerHTML = kpi(fmt(last), "млн, текущая неделя", state.weekDates[w.length - 1]) +
kpi(fmt(avg), "млн, среднее", "за 8 недель") +
kpi((delta >= 0 ? "+" : "") + fmt(delta) + "%", "к предыдущей", "неделя vs неделя") +
kpi(state.orgs.length, "организаций", "в сводном файле");
drawChart(w, max);
$("chMaxV").textContent = fmt(max);
$("chMaxD").textContent = state.weekDates[maxI];
$("chAvgV").textContent = fmt(avg);
$("chDeltaV").textContent = (delta >= 0 ? "+" : "") + fmt(delta) + "%";
var ins = state.insights.filter(function (i) {
if (view.insightFilter === "attention") return i.tone === "warn";
if (view.insightFilter === "ok") return i.tone === "ok";
return true;
});
$("insFilter").innerHTML = [
["all", "Все", state.insights.length],
["attention", "Требуют внимания", state.insights.filter(function (i) { return i.tone === "warn"; }).length],
["ok", "В норме", state.insights.filter(function (i) { return i.tone === "ok"; }).length]
].map(function (c) {
return '";
}).join("");
document.querySelectorAll("#insFilter .kt-ai-filter-chip").forEach(function (b) {
b.onclick = function () { view.insightFilter = b.dataset.f; renderAnalysis(); };
});
$("insightsList").innerHTML = ins.map(function (i) {
var icon = i.tone === "warn" ? "warning" : i.tone === "ok" ? "check-circle" : "info";
return '' +
'
' + esc(i.org) + "
" + esc(i.text) + "
";
}).join("");
}
function drawChart(vals, max) {
var svg = $("chartSvg");
var W = 640, H = 210, padL = 8, padB = 24, padT = 18;
var n = vals.length;
var cw = (W - padL * 2) / n;
var barW = 26;
var out = [];
vals.forEach(function (v, i) {
var h = (v / max) * (H - padB - padT);
var x = padL + i * cw + (cw - barW) / 2;
var y = H - padB - h;
var label = WEEK[i];
var tip = state.weekDates[i] + ": " + fmt(v) + " млн";
out.push('');
out.push('' + label + "");
out.push('' + Math.round(v) + "");
});
svg.innerHTML = out.join("");
svg.querySelectorAll(".cc-bar").forEach(function (r) {
r.addEventListener("mouseenter", function () { r.setAttribute("data-hover", "true"); });
r.addEventListener("mouseleave", function () { r.removeAttribute("data-hover"); });
r.addEventListener("focus", function () { r.setAttribute("data-hover", "true"); });
r.addEventListener("blur", function () { r.removeAttribute("data-hover"); });
});
}
/* ================= действия ================= */
function downloadSelected() {
var sel = state.orgs.filter(function (o) { return view.mailSel[o.id]; });
if (!sel.length) { toast("Сначала выберите письма в таблице (клик по строке)", "warn"); return; }
toast("Подготовка " + sel.length + " файлов…", "info");
setTimeout(function () {
toast("Готово: " + sel.reduce(function (a, o) { return a + o.files; }, 0) + " файлов в одну папку", "ok");
}, 900);
}
function exportSwat() {
toast("Формируем единый Excel SWAT…", "info");
setTimeout(function () {
modal("Excel SWAT готов",
"" +
"
SWAT_дочки_14-20.09.2026.xlsx" +
'' + state.orgs.length + " организаций · " + state.orgs.reduce(function (a, o) { return a + o.kpi; }, 0) + " показателей
" +
"Свод по всем дочерним организациям с анализом. Остаток: " +
state.orgs.filter(function (o) { return o.status === "mismatch" || o.status === "pending"; }).length + " позиций требуют внимания.
",
'');
var dl = $("modalBox").querySelector("[data-act=dl]");
if (dl) dl.onclick = function () { closeModal(); toast("Файл сохранён в загрузки", "ok"); };
}, 1100);
}
function refreshMail() {
state.orgs.filter(function (o) { return o.status === "pending"; }).forEach(function (o) {
o.status = "ready";
});
save();
toast("Почта проверена: новые письма не найдены", "ok");
renderInbox();
}
/* ---------- init ---------- */
document.querySelectorAll("#appNav button").forEach(function (b) {
b.onclick = function () { setPage(b.getAttribute("data-page")); };
});
$("btnProcess").onclick = processMails;
$("btnDownload").onclick = downloadSelected;
$("btnRefresh").onclick = refreshMail;
$("btnExport").onclick = exportSwat;
$("mailSearch").addEventListener("input", function (e) {
view.mailSearch = e.target.value;
renderInbox();
});
window.App = {
goSummary: function (id) { closeDrawer(); view.sumFilter = "all"; renderSummary(); setPage("summary"); orgDrawer(byId(id)); },
resetMailFilters: function () {
view.mailFilter = "all"; view.mailSearch = ""; $("mailSearch").value = "";
renderInbox();
}
};
setPage("passport");
})();