417 lines
25 KiB
JavaScript
417 lines
25 KiB
JavaScript
/* Свод отчётности — статичный макет. Данные: 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 = '<div class="modal-title">' + esc(title) + "</div>" + bodyHtml +
|
||
'<div class="modal-actions">' + (actions || "") + "</div>";
|
||
$("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 += '<div class="kt-ai-doc"><div class="doc-ic"><svg class="kt-icon"><use href="#spreadsheet"></use></svg></div>' +
|
||
'<div class="doc-meta"><span class="doc-name">' + esc(org.name) + " — файл " + i + ".xlsx</span>" +
|
||
'<span class="doc-sub">' + i + ".xlsx · " + (0.4 + i * 0.3).toFixed(1) + " МБ</span></div>" +
|
||
'<button class="kt-ai-btn" data-size="sm"><svg class="kt-icon"><use href="#download"></use></svg></button></div>';
|
||
}
|
||
var issues = org.status === "mismatch"
|
||
? '<div class="insight" data-tone="warn" style="margin-top: var(--kt-ai-space-5)"><svg class="kt-icon"><use href="#warning"></use></svg><div><div class="t">Нужна сверка</div><div class="d">ИИ обнаружил расхождение показателей между файлами. Строки подсвечены в сводном файле, пока статус не станет «свёл».</div></div></div>'
|
||
: "";
|
||
openDrawer(
|
||
'<div style="display:flex; align-items:center; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6)">' +
|
||
'<h2 style="margin:0; font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold)">' + esc(org.name) + "</h2>" +
|
||
'<span class="kt-ai-chip" data-tone="' + st.tone + '">' + st.label + "</span></div>" +
|
||
'<div class="kv" style="display:flex; gap: var(--kt-ai-space-5)"><div class="kv"><span class="k">Файлов получено</span><span class="v">' + org.files + "</span></div>" +
|
||
'<div class="kv"><span class="k">Показателей</span><span class="v">' + org.kpi + "</span></div>" +
|
||
'<div class="kv"><span class="k">Объём</span><span class="v">' + fmt(org.volume) + " млн</span></div></div>" +
|
||
'<h3 style="margin: var(--kt-ai-space-7) 0 var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold)">Файлы из письма</h3>' +
|
||
'<div class="kt-ai-doc-list" style="display:flex; flex-direction:column; gap: var(--kt-ai-space-3)">' + files + "</div>" +
|
||
issues +
|
||
'<div class="kt-ai-drawer-actions" style="display:flex; justify-content:flex-end; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-8)">' +
|
||
(org.status === "pending" ? '<button class="kt-ai-btn" data-act="open">Открыть письмо</button>' : "") +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="App.goSummary(\'' + org.id + '\')">К своду</button></div>'
|
||
);
|
||
}
|
||
|
||
/* ================= ВХОДЯЩИЕ ================= */
|
||
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 '<button class="kt-ai-filter-chip" data-f="' + c[0] + '" data-active="' + (view.mailFilter === c[0]) + '"><span class="k">' + c[1] + "</span> " + c[2] + "</button>";
|
||
}).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 '<tr class="row-click" data-id="' + o.id + '" style="' + (sel ? "background: var(--kt-ai-bg-active)" : "") + '">' +
|
||
'<td><input type="checkbox"' + (sel ? " checked" : "") + ' aria-label="Выбрать ' + esc(o.name) + '"></td>' +
|
||
'<td>' + (o.status === "pending" ? '<svg class="kt-icon" style="width:16px;height:16px;color:var(--kt-ai-status-warn-fg)"><use href="#warning"></use></svg>' : '<svg class="kt-icon" style="width:16px;height:16px;color:var(--kt-ai-fg-faint)"><use href="#spreadsheet"></use></svg>') + "</td>" +
|
||
'<td><div style="font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium)">' + esc(o.name) + "</div></td>" +
|
||
'<td style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs)">' + esc(m.subject) + "</td>" +
|
||
'<td class="num">' + o.files + "</td>" +
|
||
'<td style="font-size: var(--kt-ai-text-xs); font-family: var(--kt-ai-font-mono); color: var(--kt-ai-fg-muted)">' + esc(o.date) + " " + m.time + (o.status === "pending" ? ' <span style="color:var(--kt-ai-status-warn-fg)">· недавно</span>' : "") + "</td>" +
|
||
'<td><span class="kt-ai-chip" data-tone="' + st.tone + '">' + st.label + "</span></td></tr>";
|
||
}).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 '<div class="kt-ai-kpi"><span class="value" data-long="true">' + v + "</span><span class="label\">" + l + '</span><span class="hint">' + h + "</span></div>"; }
|
||
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("Свести файлы",
|
||
"<p style='margin:0 0 var(--kt-ai-space-4); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm)'>ИИ разберёт файлы из " + names.length + " писем, обновит сводные показатели и отметит расхождения:</p>" +
|
||
"<div style='display:flex; flex-direction:column; gap: var(--kt-ai-space-2);'>" +
|
||
names.map(function (n) { return '<div style="display:flex; gap: var(--kt-ai-space-3); align-items:center; font-size: var(--kt-ai-text-sm)"><svg class="kt-icon" style="width:15px;height:15px;color:var(--kt-ai-primary)"><use href="#sparkles"></use></svg>' + esc(n) + "</div>"; }).join("") +
|
||
"</div>",
|
||
'<button class="kt-ai-btn" data-close>Отмена</button><button class="kt-ai-btn" data-variant="primary" id="btnConfirmProcess">Свести</button>');
|
||
$("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 '<button class="kt-ai-filter-chip" data-f="' + c[0] + '" data-active="' + (view.sumFilter === c[0]) + '"><span class="k">' + c[1] + "</span> " + c[2] + "</button>";
|
||
}).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" ? ' <span style="color:var(--kt-ai-status-warn-fg); font-size: var(--kt-ai-text-xs)">' + "проверить" + "</span>" : "";
|
||
return '<tr class="row-click" data-id="' + o.id + '"><td style="font-weight: var(--kt-ai-weight-medium)">' + esc(o.name) + "</td>" +
|
||
'<td class="num">' + o.files + "</td>" +
|
||
'<td class="num">' + o.kpi + "</td>" +
|
||
'<td class="num">' + fmt(o.volume) + hl + "</td>" +
|
||
'<td style="font-size: var(--kt-ai-text-xs); font-family: var(--kt-ai-font-mono); color: var(--kt-ai-fg-muted)">' + esc(o.date) + "</td>" +
|
||
'<td><span class="kt-ai-chip" data-tone="' + st.tone + '">' + st.label + "</span></td></tr>";
|
||
}).join("");
|
||
rows += "<tr style='background: var(--kt-ai-bg-soft)'><td style='font-weight: var(--kt-ai-weight-semibold)'>Всего</td>" +
|
||
'<td class="num" style="font-weight: var(--kt-ai-weight-semibold)">' + tot.f + "</td>" +
|
||
'<td class="num" style="font-weight: var(--kt-ai-weight-semibold)">' + tot.k + "</td>" +
|
||
'<td class="num" style="font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-primary)">' + fmt(tot.v) + "</td>" +
|
||
"<td></td><td></td></tr>";
|
||
$("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 '<button class="kt-ai-filter-chip" data-f="' + c[0] + '" data-active="' + (view.insightFilter === c[0]) + '"><span class="k">' + c[1] + "</span> " + c[2] + "</button>";
|
||
}).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 '<div class="insight" data-tone="' + i.tone + '"><svg class="kt-icon"><use href="#' + icon + '"></use></svg>' +
|
||
'<div><div class="t">' + esc(i.org) + "</div><div class=\"d\">" + esc(i.text) + "</div></div></div>";
|
||
}).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('<rect class="cc-bar" x="' + x + '" y="' + y + '" width="' + barW + '" height="' + h + '" rx="3" tabindex="0" data-tip="' + tip + '"></rect>');
|
||
out.push('<text class="cc-lbl" x="' + (x + barW / 2) + '" y="' + (H - 8) + '" text-anchor="middle">' + label + "</text>");
|
||
out.push('<text class="cc-val" x="' + (x + barW / 2) + '" y="' + (y - 6) + '" text-anchor="middle">' + Math.round(v) + "</text>");
|
||
});
|
||
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 готов",
|
||
"<div class=\"kt-ai-doc\" style=\"margin-bottom: var(--kt-ai-space-5)\"><div class=\"doc-ic\"><svg class=\"kt-icon\"><use href=\"#spreadsheet\"></use></svg></div>" +
|
||
"<div class=\"doc-meta\"><span class=\"doc-name\">SWAT_дочки_14-20.09.2026.xlsx</span>" +
|
||
'<span class="doc-sub">' + state.orgs.length + " организаций · " + state.orgs.reduce(function (a, o) { return a + o.kpi; }, 0) + " показателей</span></div></div>" +
|
||
"<p style='margin:0; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted)'>Свод по всем дочерним организациям с анализом. Остаток: " +
|
||
state.orgs.filter(function (o) { return o.status === "mismatch" || o.status === "pending"; }).length + " позиций требуют внимания.</p>",
|
||
'<button class="kt-ai-btn" data-close>Закрыть</button><button class="kt-ai-btn" data-variant="primary" data-act="dl">Скачать файл</button>');
|
||
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");
|
||
})();
|