946 lines
50 KiB
JavaScript
946 lines
50 KiB
JavaScript
/* Сбор и анализ HR-показателей — статичный интерактивный макет.
|
||
Всё состояние — в localStorage, данные — правдоподобные примеры. */
|
||
(function () {
|
||
"use strict";
|
||
var LS = "hr-analiz-v1";
|
||
var Q = "3 квартал 2026";
|
||
|
||
/* ---------- справочники ---------- */
|
||
var COMPANIES = [
|
||
{ id: "A", name: "АО «СтройИнвест»" },
|
||
{ id: "B", name: "ООО «ЛогМар»" },
|
||
{ id: "C", name: "ТОО «АгроПром»" },
|
||
{ id: "D", name: "АО «СетьРитейл»" },
|
||
{ id: "E", name: "ТОО «МедСервис»" },
|
||
{ id: "F", name: "ООО «ТексДом»" }
|
||
];
|
||
function company(id) {
|
||
for (var i = 0; i < COMPANIES.length; i++) if (COMPANIES[i].id === id) return COMPANIES[i].name;
|
||
return id;
|
||
}
|
||
|
||
/* Показатель, Значение, Тип, Источник, Дата; value — базовое, q3 — факт квартала */
|
||
var METRICS = [
|
||
{ id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "A", date: "10.07.2026", value: "12", q3: 12 },
|
||
{ id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "C", date: "12.07.2026", value: "15", q3: 15, manual: true },
|
||
{ id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "D", date: "08.07.2026", value: "9", q3: 9, formErr: true },
|
||
{ id: "vacancy", name: "Средний срок закрытия вакансии, дн.", type: "vacancy", co: "A", date: "10.07.2026", value: "34", q3: 34 },
|
||
{ id: "vacancy", name: "Открытые вакансии, шт.", type: "vacancy", co: "B", date: "11.07.2026", value: "18", q3: 18 },
|
||
{ id: "vacancy", name: "Открытые вакансии, шт.", type: "vacancy", co: "E", date: "09.07.2026", value: "6", q3: 6, manual: true },
|
||
{ id: "reserve", name: "Доля назначений из резерва, %", type: "reserve", co: "C", date: "12.07.2026", value: "38", q3: 38 },
|
||
{ id: "reserve", name: "Заявки в резерв, шт.", type: "reserve", co: "D", date: "08.07.2026", value: "12", q3: 12 },
|
||
{ id: "reserve", name: "Заявки в резерв, шт.", type: "reserve", co: "F", date: "14.07.2026", value: "9", q3: 9 },
|
||
{ id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "A", date: "10.07.2026", value: "1,8", q3: 1.8 },
|
||
{ id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "B", date: "11.07.2026", value: "2,4", q3: 2.4 },
|
||
{ id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "D", date: "08.07.2026", value: "1,1", q3: 1.1 },
|
||
{ id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "F", date: "14.07.2026", value: "5,6", q3: 5.6 }
|
||
];
|
||
|
||
var TYPE_LABELS = {
|
||
turnover: "Текучесть",
|
||
vacancy: "Вакансии",
|
||
reserve: "Резерв",
|
||
performance: "Производительность"
|
||
};
|
||
var TYPE_CHART_COLOR = {
|
||
turnover: "--kt-ai-chart-blue",
|
||
vacancy: "--kt-ai-chart-green",
|
||
reserve: "--kt-ai-chart-orange",
|
||
performance: "--kt-ai-chart-pink"
|
||
};
|
||
|
||
var FILES_SEED = [
|
||
{ name: "HR_Q3-2026_A.xlsx", co: "A", rows: 6, status: "ok", time: "10 июл 09:12" },
|
||
{ name: "HR_Q3-2026_B.xlsx", co: "B", rows: 5, status: "ok", time: "11 июл 10:05" },
|
||
{ name: "HR_Q3-2026_C.xlsx", co: "C", rows: 4, status: "warn", time: "12 июл 08:47", note: "2 значения без источника" },
|
||
{ name: "HR_Q3-2026_D.xlsx", co: "D", rows: 6, status: "risk", time: "08 июл 15:31", note: "ошибка формулы в листе «вакансии»" },
|
||
{ name: "HR_Q3-2026_E.xlsx", co: "E", rows: 3, status: "ok", time: "09 июл 11:20" },
|
||
{ name: "HR_Q3-2026_F.xlsx", co: "F", rows: 3, status: "ok", time: "14 июл 09:58" }
|
||
];
|
||
|
||
var LOG_SEED = [
|
||
{ time: "10 июл 09:15", who: "ИИ", type: "Загрузка", result: "ok", text: "АО «СтройИнвест»: 6 показателей загружено, ошибок нет" },
|
||
{ time: "11 июл 10:07", who: "ИИ", type: "Загрузка", result: "ok", text: "ООО «ЛогМар»: 5 показателей загружено, ошибок нет" },
|
||
{ time: "12 июл 08:49", who: "ИИ", type: "Загрузка", result: "warn", text: "ТОО «АгроПром»: 4 показатели загружено, 2 значения без источника" },
|
||
{ time: "08 июл 15:33", who: "ИИ", type: "Загрузка", result: "risk", text: "АО «СетьРитейл»: обнаружена ошибка формулы в листе «вакансии»" },
|
||
{ time: "09 июл 11:22", who: "ИИ", type: "Загрузка", result: "ok", text: "ТОО «МедСервис»: 3 показателя загружено, ошибок нет" },
|
||
{ time: "14 июл 10:00", who: "ИИ", type: "Загрузка", result: "ok", text: "ООО «ТексДом»: 3 показателя загружено, ошибок нет" },
|
||
{ time: "14 июл 11:20", who: "ИИ", type: "Анализ", result: "ok", text: "Сводка по 13 показателям из 6 компаний сформирована" },
|
||
{ time: "16 июл 14:05", who: "Аналитик", type: "Правка", result: "ok", text: "Текучесть, ТОО «АгроПром»: значение исправлено вручную" }
|
||
];
|
||
|
||
var REPORT_SEED = [
|
||
"По итогам 3 квартала 2026 по 6 дочерним компаниям проведена сводка по 13 HR-показателям. Общего роста текучести кадров не зафиксировано: значения остаются в пределах 9–15%, при этом по ТОО «АгроПром» наблюдается устойчивый показатель 15%, что выше усреднённого уровня группы.",
|
||
"Средний срок закрытия вакансий — 34 дня по АО «СтройИнвест»; по остальным компаниям показатель существенно ниже. Доля назначений из кадрового резерва у ТОО «АгроПром» составляет 38% — максимальная в группе; остальные компании фиксируют 12–22%.",
|
||
"Производительность труда варьируется от 1,1 до 5,6 млн ₸/чел. в месяц в зависимости от профиля компании. По АО «СетьРитейл» выявлена ошибка в формулах отчёта (лист «вакансии»), значение до исправления не включается в сводку; источник подтверждён экспертом после ручной проверки.",
|
||
"Рекомендация: перед закрытием квартала запросить у АО «СетьРитейл» исправленную выгрузку и повторно провести анализ. В остальном данные дочерних компаний достаточны для принятия решений по управлению персоналом."
|
||
];
|
||
|
||
/* ---------- состояние ---------- */
|
||
function loadState() {
|
||
var base = {
|
||
screen: "upload",
|
||
manual: {},
|
||
files: FILES_SEED.slice(),
|
||
log: LOG_SEED.slice().reverse(),
|
||
overrides: {},
|
||
reportText: REPORT_SEED.join("\n\n"),
|
||
reportStatus: "review",
|
||
expertComment: "Проверено: цифры сверены с исходными выгрузками. Вывод по производительности усреднён по профилю — принять к сведению. Остальное согласовано без замечаний.",
|
||
history: [
|
||
{ time: "16 июл 14:32", text: "ИИ сформировал черновик заключения", by: "ИИ" },
|
||
{ time: "16 июл 14:58", text: "Аналитик отредактировал вывод по производительности", by: "Аналитик" },
|
||
{ time: "17 июл 09:10", text: "Отправлено на проверку эксперту", by: "Аналитик" },
|
||
{ time: "17 июл 11:24", text: "Эксперт оставил комментарий к разделу о производительности", by: "Эксперт" }
|
||
],
|
||
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) { /* повреждённое состояние — начинаем с нуля */ }
|
||
return base;
|
||
}
|
||
function save() {
|
||
try { localStorage.setItem(LS, JSON.stringify(S)); } catch (e) { /* квота */ }
|
||
}
|
||
var S = loadState();
|
||
var EDITING = null;
|
||
|
||
/* ---------- утилиты ---------- */
|
||
function $(id) { return document.getElementById(id); }
|
||
function esc(v) {
|
||
var d = document.createElement("div");
|
||
d.textContent = v == null ? "" : String(v);
|
||
return d.innerHTML;
|
||
}
|
||
function num(n) {
|
||
try { return new Intl.NumberFormat("ru-RU").format(n); } catch (e) { return String(n); }
|
||
}
|
||
function icon(name, size) {
|
||
return '<svg class="kt-icon' + (size ? " kt-icon-" + size : "") + '" aria-hidden="true"><use href="#' + name + '"></use></svg>';
|
||
}
|
||
function toast(msg, tone) {
|
||
var stack = $("toasts");
|
||
if (!stack) return;
|
||
var el = document.createElement("div");
|
||
el.className = "kt-ai-toast";
|
||
if (tone) el.setAttribute("data-tone", tone);
|
||
el.textContent = msg;
|
||
stack.appendChild(el);
|
||
setTimeout(function () {
|
||
el.style.opacity = "0";
|
||
el.style.transition = "opacity .3s";
|
||
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
|
||
}, 3400);
|
||
}
|
||
function nowTime() {
|
||
var d = new Date();
|
||
var m = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
||
var h = String(d.getHours()).padStart(2, "0");
|
||
var mi = String(d.getMinutes()).padStart(2, "0");
|
||
return d.getDate() + " " + m[d.getMonth()] + " " + h + ":" + mi;
|
||
}
|
||
function addLog(who, type, result, text) {
|
||
S.log.unshift({ time: nowTime(), who: who, type: type, result: result, text: text });
|
||
if (S.log.length > 40) S.log.pop();
|
||
save();
|
||
}
|
||
function metric(m) {
|
||
var key = m.id + "_" + m.co;
|
||
var o = S.overrides[key];
|
||
if (o) { m.value = o.value; if (o.status) m.checkStatus = o.status; }
|
||
return m;
|
||
}
|
||
function checkStatus(m) {
|
||
if (m.checkStatus) return m.checkStatus;
|
||
if (S.manual[m.id + "_" + m.co]) return "manual";
|
||
if (m.formErr) return "error";
|
||
return "ok";
|
||
}
|
||
function pill(status, label) {
|
||
return '<span class="kt-ai-status-pill" data-status="' + status + '">' + esc(label || String(status)) + "</span>";
|
||
}
|
||
function statusLabel(m) {
|
||
var st = checkStatus(m);
|
||
if (st === "manual") return pill("warn", "вручную");
|
||
if (st === "error") return pill("risk", "ошибка формулы");
|
||
return pill("ok", "проверено");
|
||
}
|
||
|
||
|
||
/* ---------- каркас экрана ---------- */
|
||
function pageHead(title, sub, actions) {
|
||
return '<div class="page-head">' +
|
||
'<div class="ph-text"><h1>' + esc(title) + "</h1>" +
|
||
(sub ? '<p class="sub">' + esc(sub) + "</p>" : "") +
|
||
'</div><span class="ph-spacer"></span>' + (actions || "") + "</div>";
|
||
}
|
||
function block(title, cap, actions, body) {
|
||
return '<section class="kt-ai-card block"><div class="block-head">' +
|
||
"<h2 class=\"block-title\">" + esc(title) + "</h2>" +
|
||
(cap ? "<span class=\"cap\">" + cap + "</span>" : "") +
|
||
'<span class="spacer"></span>' + (actions || "") +
|
||
"</div>" + body + "</section>";
|
||
}
|
||
|
||
function fileStatusPill(f) {
|
||
if (f.status === "ok") return pill("ok", "успешно");
|
||
if (f.status === "warn") return pill("warn", "с замечаниями");
|
||
return pill("risk", "ошибка");
|
||
}
|
||
|
||
function viewUpload() {
|
||
var totalRows = 0;
|
||
var warnings = 0;
|
||
var errors = 0;
|
||
S.files.forEach(function (f) {
|
||
totalRows += f.rows || 0;
|
||
if (f.status === "warn") warnings++;
|
||
if (f.status === "risk") errors++;
|
||
});
|
||
|
||
var companyOpts = COMPANIES.map(function (c) {
|
||
return '<option value="' + c.id + '">' + esc(c.name) + "</option>";
|
||
}).join("");
|
||
|
||
var fileRows = S.files.map(function (f, i) {
|
||
return '<div class="kt-ai-row" data-file="' + i + '">' +
|
||
'<span class="f-ic">' + icon("spreadsheet") + "</span>" +
|
||
'<div style="min-width:0;flex:1">' +
|
||
'<div class="f-name">' + esc(f.name) + "</div>" +
|
||
'<div class="f-meta">' + esc(company(f.co)) + " · " + (f.rows || 0) + " строк · получен " + esc(f.time) +
|
||
(f.note ? " · " + esc(f.note) : "") + "</div></div>" +
|
||
fileStatusPill(f) + "</div>";
|
||
}).join("");
|
||
|
||
var logItems = S.log.map(function (l) {
|
||
var tone = l.result === "ok" ? "" : (l.result === "warn" ? 'data-status="warn"' : 'data-status="risk"');
|
||
return '<div class="log-item">' +
|
||
'<span class="log-time">' + esc(l.time) + "</span>" +
|
||
'<span class="log-tag">' + (l.who === "ИИ" ? pill("info", "ИИ") : pill("muted", "Пользователь")) + "</span>" +
|
||
"<div style=\"min-width:0;flex:1\">" +
|
||
"<p>" + esc(l.text) + '</p><span class="log-ai">' + esc(l.type) + "</span></div>" +
|
||
"</div>";
|
||
}).join("");
|
||
|
||
var kpi =
|
||
'<div class="kt-ai-kpi"><span class="label">Файлов получено</span><span class="value">' + S.files.length + ' / 6</span><span class="hint">все дочерние компании</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Строк импортировано</span><span class="value">' + num(totalRows) + '</span><span class="hint">из Excel-файлов</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Замечаний</span><span class="value">' + warnings + '</span><span class="hint">требуют внимания</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Ошибок</span><span class="value" data-long="true">' + errors + '</span><span class="hint">ошибка формулы</span></div>';
|
||
|
||
return pageHead("Загрузка данных", "Исходные данные от дочерних компаний, 3 квартал 2026", "") +
|
||
'<div class="kt-ai-kpi-strip">' + kpi + "</div>" +
|
||
'<div class="grid-2">' +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<section class="kt-ai-card block"><div class="block-head"><h2 class="block-title">Дочерние компании</h2>' +
|
||
'<span class="cap">6 источников · ' + Q + "</span></div>" +
|
||
'<div class="dropzone" id="dropzone">' +
|
||
'<span class="dz-ic">' + icon("upload") + "</span>" +
|
||
"<b>Перетащите Excel-файл или выберите его</b>" +
|
||
'<span class="dz-hint">.xlsx, до 25 МБ. ИИ распознает столбцы и перенесёт показатели в сводку.</span>' +
|
||
'<div style="display:flex;gap:var(--kt-ai-space-3);flex-wrap:wrap;justify-content:center;margin-top:var(--kt-ai-space-2)">' +
|
||
'<select class="kt-ai-select" id="srcCompany" style="width:200px" aria-label="Дочерняя компания">' + companyOpts + "</select>" +
|
||
'<button class="kt-ai-btn" data-act="upload-file" data-variant="primary">' + icon("upload") + " Загрузить</button>" +
|
||
"<input type=\"file\" id=\"fileInput\" accept=\".xlsx,.xls\" style=\"display:none\">" +
|
||
"</div></div></section>" +
|
||
block("Файлы", esc(Q), "", '<div class="file-list" id="fileList">' + fileRows + "</div>") +
|
||
"</div>" +
|
||
block("Журнал действий", "ИИ и пользователь",
|
||
'<button class="kt-ai-btn" data-size="sm" data-act="manual-mode">' + icon("tool") + " Вернуть в ручной режим</button>",
|
||
'<div class="log-list" id="logList">' + logItems + "</div>") +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ---------- экран 2: сводка ---------- */
|
||
var CHK = { type: "all", co: "all", only: "all" };
|
||
|
||
function filteredRows() {
|
||
return METRICS.map(metric).filter(function (m) {
|
||
if (CHK.type !== "all" && m.type !== CHK.type) return false;
|
||
if (CHK.co !== "all" && m.co !== CHK.co) return false;
|
||
if (CHK.only === "err") return checkStatus(m) === "error";
|
||
if (CHK.only === "ok") return checkStatus(m) === "ok" || checkStatus(m) === "manual";
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function sumKpis(rows) {
|
||
var ok = 0, wan = 0, err = 0;
|
||
rows.forEach(function (m) {
|
||
var st = checkStatus(m);
|
||
if (st === "error") err++;
|
||
else if (st === "manual") wan++;
|
||
else ok++;
|
||
});
|
||
var kpi =
|
||
'<div class="kt-ai-kpi"><span class="label">Показателей в сводке</span><span class="value">' + rows.length + "</span><span class=\"hint\">из " + METRICS.length + " всего</span></div>" +
|
||
'<div class="kt-ai-kpi"><span class="label">Проверено</span><span class="value">' + ok + '</span><span class="hint" style="color:var(--kt-ai-status-ok-fg)">ок</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Исправлено вручную</span><span class="value">' + wan + "</span><span class=\"hint\">аналитиком</span></div>" +
|
||
'<div class="kt-ai-kpi"><span class="label">Ошибки формул</span><span class="value">' + (err ? '<span style="color:var(--kt-ai-status-risk-fg)">' + err + "</span>" : "0") + "</span><span class=\"hint\">требуют правки</span></div>";
|
||
return kpi;
|
||
}
|
||
|
||
function sumTable(rows) {
|
||
var body = "";
|
||
rows.forEach(function (m) {
|
||
var st = checkStatus(m);
|
||
var val = m.value;
|
||
var valCell;
|
||
if (st === "error") {
|
||
valCell = '<span class="cell-form">не рассчитано</span>';
|
||
} else if (st === "manual") {
|
||
valCell = '<span style="color:var(--kt-ai-status-warn-fg)">' + esc(val) + '</span>';
|
||
} else {
|
||
valCell = esc(val);
|
||
}
|
||
var editing = EDITING && EDITING.id === m.id && EDITING.co === m.co;
|
||
valCell = editing ? '<input class="kt-ai-input" data-edit="' + m.id + "|" + m.co + '" value="' + esc(val) + '">' : valCell;
|
||
body += '<tr data-row="' + m.id + "|" + m.co + '"' + (editing ? ' class="editing"' : "") + ">" +
|
||
'<td data-label="Показатель">' + esc(m.name) + '<div class="cell-sub">' + esc(TYPE_LABELS[m.type]) + "</div></td>" +
|
||
'<td class="num" data-label="Значение">' + valCell + "</td>" +
|
||
'<td data-label="Источники">' + esc(company(m.co)) + "</td>" +
|
||
'<td data-label="Дата">' + esc(m.date) + "</td>" +
|
||
'<td data-label="Статус">' + (st === "error" ? pill("warn", "проверить") : statusLabel(m)) + "</td>" +
|
||
(editing ? '<td><button class="kt-ai-btn" data-size="sm" data-variant="primary" data-act="save-edit" data-save="' + m.id + "|" + m.co + '" style="height:28px">Сохранить</button></td>' : "<td></td>") +
|
||
"</tr>";
|
||
});
|
||
return '<table class="kt-ai-table" data-density="compact">' +
|
||
"<thead><tr><th>Показатель</th><th class=\"num\">Значение</th><th>Источник</th><th>Дата актуальности</th><th>Статус проверки</th><th></th></tr></thead>" +
|
||
"<tbody>" + body + "</tbody></table>";
|
||
}
|
||
|
||
function typeCounts() {
|
||
var c = { all: METRICS.length, turnover: 0, vacancy: 0, reserve: 0, performance: 0 };
|
||
METRICS.forEach(function (m) { c[m.type]++; });
|
||
return c;
|
||
}
|
||
|
||
function viewSummary() {
|
||
var t = filteredRows();
|
||
var coOpts = '<option value="all">Все компании</option>' + COMPANIES.map(function (c) {
|
||
return '<option value="' + c.id + '">' + esc(c.name) + "</option>";
|
||
}).join("");
|
||
var cc = typeCounts();
|
||
|
||
var seg = '<div class="kt-ai-segmented" id="fType">' +
|
||
'<button class="seg" data-v="all" data-active="' + (CHK.type === "all") + '">Все <span class="count-num">' + cc.all + "</span></button>" +
|
||
'<button class="seg" data-v="turnover" data-active="' + (CHK.type === "turnover") + '">Текучесть <span class="count-num">' + cc.turnover + "</span></button>" +
|
||
'<button class="seg" data-v="vacancy" data-active="' + (CHK.type === "vacancy") + '">Вакансии <span class="count-num">' + cc.vacancy + "</span></button>" +
|
||
'<button class="seg" data-v="reserve" data-active="' + (CHK.type === "reserve") + '">Резерв <span class="count-num">' + cc.reserve + "</span></button>" +
|
||
'<button class="seg" data-v="performance" data-active="' + (CHK.type === "performance") + '">Производительность <span class="count-num">' + cc.performance + "</span></button>" +
|
||
"</div>";
|
||
|
||
var toolbar = '<div class="filter-row">' + seg +
|
||
'<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="fCompany">' + coOpts + "</select></div>" +
|
||
'<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="fOnly">' +
|
||
"<option value=\"all\">Все статусы</option><option value=\"err\">Только с ошибками</option><option value=\"ok\">Проверенные</option>" +
|
||
"</select></div>" +
|
||
'<span class="filter-note" id="fNote"></span></div>';
|
||
|
||
return pageHead("Сводка показателей", "Показатели дочерних компаний после загрузки, " + Q,
|
||
'<button class="kt-ai-btn" data-variant="primary" data-act="run-analysis">' + icon("sparkles") + " Запустить анализ</button>") +
|
||
'<div class="kt-ai-kpi-strip" id="sumKpis">' + sumKpis(t) + "</div>" +
|
||
block("Сводная таблица", "13 показателей · 4 типа",
|
||
'<button class="kt-ai-btn" data-size="sm" data-act="open-log">' + icon("history") + " Журнал действий</button>",
|
||
'<div>' + toolbar + '<div class="table-wrap" id="sumTableWrap">' + sumTable(t) + "</div></div>");
|
||
}
|
||
|
||
/* ---------- экран 3: анализ и заключение ---------- */
|
||
var QUARTERS = ["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026"];
|
||
var TURN_TREND = [
|
||
{ label: "Q1 25", value: 10.2 },
|
||
{ label: "Q2 25", value: 10.8 },
|
||
{ label: "Q3 25", value: 11.0 },
|
||
{ label: "Q4 25", value: 11.7 },
|
||
{ label: "Q1 26", value: 12.1 },
|
||
{ label: "Q2 26", value: 14.6 }
|
||
];
|
||
var PERF_TREND = [
|
||
{ label: "Q1 25", value: 4.7 },
|
||
{ label: "Q2 25", value: 4.9 },
|
||
{ label: "Q3 25", value: 4.8 },
|
||
{ label: "Q4 25", value: 5.2 },
|
||
{ label: "Q1 26", value: 5.4 },
|
||
{ label: "Q2 26", value: 5.5 }
|
||
];
|
||
|
||
function barChart(items, unit, colorVar, title) {
|
||
var max = 0;
|
||
items.forEach(function (b) { if (b.value > max) max = b.value; });
|
||
var bars = items.map(function (b) {
|
||
var h = Math.round((b.value / max) * 158);
|
||
if (h < 3) h = 3;
|
||
return '<div class="bar" tabindex="0" data-kt-tip=\'' + JSON.stringify({ title: b.label, rows: [{ label: unit, value: String(b.value) }] }) + '\'>' +
|
||
'<div class="bar-fill" style="height:' + h + "px;background:var(" + colorVar + ')"></div><span>' + esc(b.label) + "</span></div>";
|
||
}).join("");
|
||
return '<div class="cc-head"><span class="cc-title">' + esc(title) + "</span></div>" +
|
||
'<div class="cc-bars">' + bars + "</div>";
|
||
}
|
||
|
||
function statusChip() {
|
||
if (S.reportStatus === "draft") return pill("muted", "Черновик");
|
||
if (S.reportStatus === "review") return pill("warn", "На проверке");
|
||
return pill("ok", "Утверждено");
|
||
}
|
||
|
||
function reportParagraphs(text) {
|
||
return String(text || "").split(/\n{1,}/).filter(function (p) { return p.trim(); })
|
||
.map(function (p) { return "<p>" + esc(p) + "</p>"; }).join("");
|
||
}
|
||
|
||
function viewAnalysis() {
|
||
var kpi =
|
||
'<div class="kt-ai-kpi"><span class="label">Текучесть (сводка)</span><span class="value">12,7%</span><span class="hint">из 3 источников, Q2 → +2,3 п.п.</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Закрытие вакансий</span><span class="value">34 дн.</span><span class="hint">максимум группы · АО «СтройИнвест»</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Назначения из резерва</span><span class="value">38%</span><span class="hint">ТОО «АгроПром» · максимум</span></div>' +
|
||
'<div class="kt-ai-kpi"><span class="label">Производительность</span><span class="value">5,5 млн ₸</span><span class="hint">на человека, сводка Q2</span></div>';
|
||
|
||
return pageHead("Анализ и заключение", "Результаты анализа и черновик отчёта, " + Q,
|
||
'<div class="report-meta">' + statusChip() +
|
||
(S.reportStatus === "draft"
|
||
? '<button class="kt-ai-btn" data-variant="primary" data-act="send-review">' + icon("send") + " Отправить на проверку</button>"
|
||
: "") +
|
||
'<button class="kt-ai-btn" data-act="download" data-size="sm">' + icon("download") + " Скачать отчёт</button>" +
|
||
"</div>") +
|
||
'<div class="kt-ai-kpi-strip">' + kpi + "</div>" +
|
||
'<div class="chart-cards">' +
|
||
barChart(TURN_TREND, "Текучесть, %", "--kt-ai-chart-blue", "Текучесть кадров по кварталам") +
|
||
barChart(PERF_TREND, "млн ₸/чел.", "--kt-ai-chart-pink", "Производительность труда по кварталам") +
|
||
"</div>" +
|
||
'<div class="report-wrap">' +
|
||
(S.reportStatus === "draft"
|
||
? '<div><div class="block-head" style="margin:0 0 var(--kt-ai-space-4)"><h2 class="block-title">Заключение</h2><span class="cap">сформировано ИИ · редактируется вручную</span></div>' +
|
||
'<textarea class="report-text" id="reportText">' + esc(S.reportText) + "</textarea></div>"
|
||
: '<div class="report-doc"><h3>Итоговое заключение · ' + esc(Q) + "</h3>" + reportParagraphs(S.reportText) + "</div>") +
|
||
'<div class="report-side">' +
|
||
block("Источники", "6 дочерних компаний", "",
|
||
COMPANIES.map(function (c) {
|
||
return '<div class="kt-ai-row" style="padding:var(--kt-ai-space-3) var(--kt-ai-space-2)">' +
|
||
'<span class="f-ic">' + icon("spreadsheet") + "</span>" +
|
||
'<div style="min-width:0;flex:1"><div class="f-name">' + esc(c.name) + "</div></div>" +
|
||
pill("ok", "получено") + "</div>";
|
||
}).join("")) +
|
||
"</div></div>";
|
||
}
|
||
|
||
/* ---------- экран 4: проверка экспертом ---------- */
|
||
function expertTable() {
|
||
var rows = ["turnover", "vacancy", "reserve", "performance"].map(function (type) {
|
||
var ms = METRICS.map(metric).filter(function (m) { return m.type === type && checkStatus(m) !== "error"; });
|
||
var vals = ms.map(function (m) { return m.q3; });
|
||
var avg = vals.length ? (vals.reduce(function (a, b) { return a + b; }, 0) / vals.length).toFixed(1) : "—";
|
||
return "<tr><td>" + esc(TYPE_LABELS[type]) + "</td>" +
|
||
ms.map(function (m) { return '<td class="num">' + esc(m.value) + "</td>"; }).join("") +
|
||
'<td class="num"><b>' + esc(avg) + "</b></td></tr>";
|
||
}).join("");
|
||
return '<table class="kt-ai-table" data-density="compact">' +
|
||
"<thead><tr><th>Тип показателя</th>" +
|
||
COMPANIES.map(function (c) { return '<th class="num">' + c.id + "</th>"; }).join("") +
|
||
'<th class="num">Сводка</th></tr></thead><tbody>' + rows + "</tbody></table>";
|
||
}
|
||
|
||
function historyList() {
|
||
return '<div class="log-list" style="max-height:none">' + S.history.map(function (h) {
|
||
return '<div class="hist-item"><span class="h-time">' + esc(h.time) + "</span>" +
|
||
'<div style="min-width:0;flex:1"><p>' + esc(h.text) + "</p>" +
|
||
'<span class="cell-sub">' + esc(h.by) + "</span></div></div>";
|
||
}).join("") + "</div>";
|
||
}
|
||
|
||
function viewExpert() {
|
||
var commentText = S.expertComment
|
||
? '<p style="margin:0 0 var(--kt-ai-space-4);font-size:var(--kt-ai-text-md);line-height:1.6">' + esc(S.expertComment) + "</p>"
|
||
: "";
|
||
var reviewState = S.reportStatus === "approved"
|
||
? pill("ok", "Утверждено")
|
||
: (S.reportStatus === "rejected" ? pill("risk", "Отклонено") : pill("warn", "На проверке"));
|
||
|
||
var side = block("Комментарий эксперта", "", "",
|
||
commentText +
|
||
(S.reportStatus === "rejected"
|
||
? '<div class="kt-ai-banner" data-tone="risk" style="margin-bottom:var(--kt-ai-space-4)">Заключение возвращено аналитику. После правок и повторной отправки проверьте вывод ещё раз.</div>'
|
||
: "") +
|
||
'<textarea class="kt-ai-textarea" id="expertComment" placeholder="Комментарий к заключению…" style="min-height:96px">' + esc(S.expertComment) + "</textarea>" +
|
||
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-4);margin-top:var(--kt-ai-space-5)">' +
|
||
(S.reportStatus === "approved"
|
||
? '<div class="kt-ai-banner" data-tone="ok">Заключение утверждено экспертом. Доступно для использования в решениях.</div>'
|
||
: '<button class="kt-ai-btn" data-variant="primary" data-act="approve">' + icon("checkCircle") + " Утвердить заключение</button>" +
|
||
'<button class="kt-ai-btn" data-variant="danger" data-act="reject" id="rejectBtn">' + icon("x") + " Отклонить и вернуть</button>") +
|
||
"</div>") +
|
||
block("История изменений", "", "", historyList());
|
||
|
||
return pageHead("Проверка экспертом", "Финальная валидация перед использованием, " + Q,
|
||
'<div class="report-meta">' + reviewState + '<button class="kt-ai-btn" data-act="to-analysis" data-size="sm" data-variant="ghost">К анализу ' + icon("chevronRight") + "</button></div>") +
|
||
block("Итоговое заключение", "только чтение", "",
|
||
'<div class="report-doc" style="border:none;background:transparent;padding:0" ><h3>Итоговое заключение · ' + esc(Q) + " · автор: Аналитик</h3>" + reportParagraphs(S.reportText) + "</div>") +
|
||
block("Сводная таблица цифр", "по типам показателей", "",
|
||
'<div class="table-wrap" style="max-height:none">' + expertTable() + "</div>") +
|
||
'<div class="grid-2-rev"><div class="report-side">' + side + "</div>" +
|
||
'<div></div></div>' +
|
||
'<section class="kt-ai-conclusion"><span class="title">Что показывает сводка</span>' +
|
||
'<span class="text">Цифры по 4 типам HR-показателей сопоставимы между компаниями. Текучесть остаётся в рабочих пределах 9–15%, назначение из резерва — сильная сторона группы, производительность варьируется по профилю бизнеса.</span>' +
|
||
'<span class="text">Значение «СетьРитейл» по производительности исключено из сводки до исправления формулы — эксперт подтверждает источник после ручной проверки выгрузки.</span></section>';
|
||
}
|
||
|
||
/* экран 5: о решении */
|
||
function viewAbout() {
|
||
var steps = [
|
||
"Аналитик собирает данные с дочерних компаний (ИИ)",
|
||
"Аналитик сводит данные в Excel (ИИ)",
|
||
"Аналитик проводит анализ показателей (ИИ)",
|
||
"Аналитик пишет текстовое заключение (ИИ)",
|
||
"Эксперт проверяет итоговое заключение"
|
||
];
|
||
var aiWorks = [
|
||
"Сбор и валидация данных из Excel — экран «Загрузка данных»",
|
||
"Сводка данных и проверка формул — экран «Сводка показателей»",
|
||
"Проведение анализа и генерация текстового заключения — экран «Анализ и заключение»"
|
||
];
|
||
var humans = [
|
||
"Аналитик: загружает исходные данные, проверяет сводку, редактирует заключение",
|
||
"Эффект: ускоряет процесс, повышает качество аналитики",
|
||
"Аналитик: время на один цикл — 2 недели при частоте раз в квартал. Оценка — около 20 часов в месяц"
|
||
];
|
||
var risks = [
|
||
"Ошибка ИИ при неправильном заполнении данных работниками",
|
||
"Ошибка ИИ при неверно выставленных формулах в Excel",
|
||
"Сроки и исключения по процессу определяет эксперт"
|
||
];
|
||
var measures = [
|
||
"Проверка результата человеком перед использованием",
|
||
"Журнал действий ИИ и возможность вернуть ручной режим"
|
||
];
|
||
|
||
var stepsItems = steps.map(function (s, i) {
|
||
return "<li>" +
|
||
'<span class="kt-ai-num-badge" data-size="sm">' + (i + 1) + "</span>" +
|
||
"<span>" + esc(s) + "</span></li>";
|
||
}).join("");
|
||
|
||
function ul(list) {
|
||
return "<ul>" + list.map(function (l) { return "<li>" + esc(l) + "</li>"; }).join("") + "</ul>";
|
||
}
|
||
|
||
return pageHead("О решении", "Автоматизировать сбор, сводку и анализ HR-показателей для подготовки итогового заключения") +
|
||
'<div class="pass-grid">' +
|
||
'<section class="kt-ai-card block"><div class="block-head"><h2 class="block-title">Паспорт</h2></div>' +
|
||
'<dl class="kv"><dt>Наименование</dt><dd>Сбор и анализ HR-показателей</dd>' +
|
||
"<dt>Исполнитель</dt><dd>владелец процесса – сборка на платформе Vibe AI</dd>" +
|
||
"<dt>Пользователь</dt><dd>Аналитик, Руководитель</dd>" +
|
||
"<dt>Дата внедрения</dt><dd>—</dd></dl></section>" +
|
||
"<section class=\"kt-ai-card block\"><div class=\"block-head\"><h2 class=\"block-title\">Предпосылки</h2></div>" +
|
||
'<ul><li>Необходимо собирать HR-аналитику в читабельном, доступном и точном формате' +
|
||
"</li><li>Отсутствует понимание, как и в какой программе собирать данные</li></ul>" +
|
||
"<div class=\"block-head\" style=\"margin-top:var(--kt-ai-space-5)\"><h2 class=\"block-title\">Входные данные и источники</h2></div>" +
|
||
'<ul><li>Данные в формате Excel, заполняются полностью вручную</li>' +
|
||
"<li>Примерно 30 HR-показателей (текучесть, срок закрытия вакансий, доля назначений из резерва, производительность труда)</li></ul>" +
|
||
"</section>" +
|
||
"<section class=\"kt-ai-card block\"><div class=\"block-head\"><h2 class=\"block-title\">Шаги процесса</h2></div>" +
|
||
'<ol class="steps-list">' + stepsItems + "</ol></section>" +
|
||
"<section class=\"kt-ai-card block\"><div class=\"block-head\"><h2 class=\"block-title\">Результат</h2></div>" +
|
||
'<ul><li>Таблица с цифрами, текстовый отчет с выводами и дашборд</li>' +
|
||
"<li>Итоговые заключения используются руководителями для принятия решений по управлению персоналом</li></ul>" +
|
||
'<div class="block-head" style="margin-top:var(--kt-ai-space-5)"><h2 class="block-title">Ожидаемый эффект</h2></div>' +
|
||
'<ul><li>Ускорение процесса</li><li>Повышение качества аналитики</li></ul>' +
|
||
"</section>" +
|
||
"<section class=\"kt-ai-card block\"><div class=\"block-head\"><h2 class=\"block-title\">Что делает ИИ</h2></div>" +
|
||
ul(aiWorks) +
|
||
'<div class="block-head" style="margin-top:var(--kt-ai-space-5)"><h2 class="block-title">Что проверяет и решает человек</h2></div>' +
|
||
ul(humans) +
|
||
"</section>" +
|
||
'<section class="kt-ai-card block"><div class="block-head"><h2 class="block-title">Контроль и риски</h2></div>' +
|
||
ul(risks) +
|
||
'<div class="block-head" style="margin-top:var(--kt-ai-space-5)"><h2 class="block-title">Меры по снижению</h2></div>' +
|
||
ul(measures) +
|
||
'<div class="block-head" style="margin-top:var(--kt-ai-space-5)"><h2 class="block-title">Формат реализации</h2></div>' +
|
||
'<ul><li>Веб-интерфейс на Vibe AI: сначала статичный макет всего процесса, после согласования — рабочее решение.</li>' +
|
||
"<li>Экраны макета: Загрузка данных; Сводка показателей; Анализ и заключение; Проверка экспертом.</li></ul>" +
|
||
"</section>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ---------- навигация и рендер ---------- */
|
||
var SCREENS = [
|
||
{ id: "upload", title: "Загрузка данных", sub: "исходные данные от дочерних компаний", icon: "upload", view: viewUpload },
|
||
{ id: "summary", title: "Сводка показателей", sub: "таблица, фильтры, проверки", icon: "table", view: viewSummary },
|
||
{ id: "analysis", title: "Анализ и заключение", sub: "результаты анализа и отчёт", icon: "sparkles", view: viewAnalysis },
|
||
{ id: "expert", title: "Проверка экспертом", sub: "валидация и утверждение", icon: "checkCircle", view: viewExpert },
|
||
{ id: "about", title: "О решении", sub: "паспорт и контекст процесса", icon: "info", view: viewAbout }
|
||
];
|
||
|
||
function currentTitle() {
|
||
for (var i = 0; i < SCREENS.length; i++) if (SCREENS[i].id === S.screen) return SCREENS[i].title;
|
||
return SCREENS[0].title;
|
||
}
|
||
|
||
function renderSidebar() {
|
||
var nav = $("sbNav");
|
||
if (!nav) return;
|
||
var html = [];
|
||
SCREENS.forEach(function (s, i) {
|
||
html.push('<a class="kt-ai-nav-item" href="#" data-screen="' + s.id + '" data-active="' + (s.id === S.screen) + '">' +
|
||
icon(s.icon) +
|
||
'<span class="lbl"><b>' + esc(s.title) + "</b><small>" + esc(s.sub) + "</small></span>" +
|
||
'<span class="count">' + (i + 1) + "</span></a>");
|
||
});
|
||
nav.innerHTML = html.join("");
|
||
}
|
||
|
||
function renderTopbar() {
|
||
var t = $("topTitle");
|
||
if (!t) return;
|
||
t.innerHTML = '<span class="t">' + esc(currentTitle()) + "</span>";
|
||
}
|
||
|
||
function render() {
|
||
renderSidebar();
|
||
renderTopbar();
|
||
var view = $("view");
|
||
if (!view) return;
|
||
for (var i = 0; i < SCREENS.length; i++) {
|
||
if (SCREENS[i].id === S.screen) {
|
||
view.innerHTML = SCREENS[i].view();
|
||
break;
|
||
}
|
||
}
|
||
syncSummaryControls();
|
||
syncSelects();
|
||
save();
|
||
window.scrollTo(0, 0);
|
||
}
|
||
|
||
function navigate(id) {
|
||
if (id === S.screen) return;
|
||
S.screen = id;
|
||
EDITING = null;
|
||
render();
|
||
}
|
||
|
||
function syncSummaryControls() {
|
||
var fCompany = $("fCompany");
|
||
if (fCompany) fCompany.value = CHK.co;
|
||
var fOnly = $("fOnly");
|
||
if (fOnly) fOnly.value = CHK.only;
|
||
var note = $("fNote");
|
||
if (S.screen === "summary" && note) {
|
||
var errN = METRICS.filter(function (m) { return checkStatus(m) === "error"; }).length;
|
||
note.textContent = errN ? ("Ошибок формул: " + errN) : "";
|
||
}
|
||
}
|
||
|
||
function syncSelects() {
|
||
var rc = $("srcCompany");
|
||
if (rc) rc.selectedIndex = 0;
|
||
}
|
||
|
||
/* --- события (делегирование) --- */
|
||
function onSegType(e) {
|
||
var b = e.target.closest(".seg");
|
||
if (!b) return;
|
||
CHK.type = b.getAttribute("data-v");
|
||
render();
|
||
}
|
||
|
||
function onFileListClick(e) {
|
||
var row = e.target.closest("[data-file]");
|
||
if (!row) return;
|
||
var idx = parseInt(row.getAttribute("data-file"), 10);
|
||
if (isNaN(idx)) return;
|
||
var f = S.files[idx];
|
||
if (!f) return;
|
||
if (f.status === "risk") {
|
||
toast("Файл содержит ошибку формулы: " + f.note, "risk");
|
||
} else if (f.status === "warn") {
|
||
toast("Загрузка с замечаниями: " + f.note, "warn");
|
||
} else {
|
||
toast("Файл загружен без ошибок", "ok");
|
||
}
|
||
addLog("Пользователь", "Просмотр файла", "ok", company(f.co) + ": файл открыт для просмотра");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function onTableClick(e) {
|
||
var row = e.target.closest("tr[data-row]");
|
||
if (!row) return;
|
||
var btn = e.target.closest("[data-act='error']");
|
||
if (btn) return;
|
||
var key = row.getAttribute("data-row");
|
||
var parts = key ? key.split("|") : [];
|
||
var id = parts[0], co = parts[1];
|
||
if (EDITING && EDITING.id === id && EDITING.co === co) return;
|
||
EDITING = { id: id, co: co };
|
||
render();
|
||
}
|
||
|
||
function onKeydown(e) {
|
||
if (e.key !== "Enter") return;
|
||
var inp = e.target;
|
||
if (!inp || !inp.getAttribute || !inp.getAttribute("data-edit")) return;
|
||
var key = inp.getAttribute("data-edit");
|
||
var parts = key.split("|");
|
||
var id = parts[0], co = parts[1];
|
||
var val = inp.value.trim();
|
||
var key2 = id + "_" + co;
|
||
S.overrides[key2] = { value: val, status: "ok" };
|
||
S.manual[key2] = true;
|
||
EDITING = null;
|
||
addLog("Пользователь", "Правка значения", "ok", "Значение исправлено вручную");
|
||
toast("Значение сохранено", "ok");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function onInput(e) {
|
||
if (e.target && e.target.id === "reportText") S.reportText = e.target.value;
|
||
}
|
||
|
||
function onDropzoneDragOver(e) {
|
||
e.preventDefault();
|
||
e.dataTransfer && e.dataTransfer.dropEffect && (e.dataTransfer.dropEffect = "copy");
|
||
}
|
||
|
||
function onFileInputChange(e) {
|
||
var input = e.target;
|
||
if (!input || !input.files || !input.files.length) return;
|
||
var file = input.files[0];
|
||
var srcSel = $("srcCompany");
|
||
var srcCo = srcSel ? srcSel.value : "A";
|
||
var ext = (file.name || "").toLowerCase();
|
||
var isExcel = ext.indexOf(".xlsx") !== -1 || ext.indexOf(".xls") !== -1;
|
||
if (!isExcel) {
|
||
toast("Нужен Excel-файл (.xlsx или .xls)", "risk");
|
||
return;
|
||
}
|
||
var fname = "HR_Q3-2026_" + srcCo + ".xlsx";
|
||
var existing = null;
|
||
for (var i = 0; i < S.files.length; i++) {
|
||
if (S.files[i].co === srcCo) { existing = S.files[i]; break; }
|
||
}
|
||
if (existing) {
|
||
existing.rows = 5 + Math.floor(Math.random() * 5);
|
||
existing.time = nowTime();
|
||
existing.status = "ok";
|
||
existing.note = "";
|
||
} else {
|
||
S.files.push({
|
||
name: fname, co: srcCo, rows: 5 + Math.floor(Math.random() * 5),
|
||
status: "ok", time: nowTime(), note: ""
|
||
});
|
||
}
|
||
var name = company(srcCo);
|
||
addLog("ИИ", "Загрузка", "ok", name + ": файл получен, показатели добавлены в сводку");
|
||
toast("Файл загружен: " + name, "ok");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function manualAction() {
|
||
addLog("Пользователь", "Ручной режим", "ok", "Сбор и анализ возвращены в ручной режим");
|
||
toast("Ручной режим включён", "warn");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function openLogAction() {
|
||
addLog("Пользователь", "Просмотр журнала", "ok", "Журнал действий открыт");
|
||
S.screen = "upload";
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function runAnalysisAction() {
|
||
addLog("ИИ", "Анализ", "ok", "Анализ заведомо выполнен: сформирован черновик заключения");
|
||
toast("Анализ запущен, черновик заключения сформирован", "ok");
|
||
S.screen = "analysis";
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function sendReviewAction() {
|
||
if (S.reportStatus !== "draft") return;
|
||
S.reportStatus = "review";
|
||
S.history.push({ time: nowTime(), text: "Отправлено на проверку эксперту", by: "Аналитик" });
|
||
addLog("Пользователь", "Отправка на проверку", "ok", "Заключение отправлено эксперту");
|
||
toast("Заключение отправлено на проверку", "ok");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function downloadAction() {
|
||
addLog("Пользователь", "Скачивание отчёта", "ok", "Отчёт сохранён в PDF");
|
||
toast("Отчёт сохранён в PDF", "ok");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function approveAction() {
|
||
if (S.reportStatus === "approved") return;
|
||
var cm = $("expertComment");
|
||
if (cm && cm.value && cm.value !== S.expertComment) {
|
||
S.expertComment = cm.value;
|
||
S.history.push({ time: nowTime(), text: "Эксперт оставил комментарий", by: "Эксперт" });
|
||
}
|
||
S.reportStatus = "approved";
|
||
S.history.push({ time: nowTime(), text: "Заключение утверждено", by: "Эксперт" });
|
||
addLog("Эксперт", "Утверждение", "ok", "Итоговое заключение утверждено");
|
||
toast("Заключение утверждено", "ok");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function rejectAction() {
|
||
if (S.reportStatus === "approved") return;
|
||
var cm = $("expertComment");
|
||
var c = cm && cm.value ? cm.value.trim() : "";
|
||
if (!c) {
|
||
toast("Введите комментарий к отклонению", "warn");
|
||
return;
|
||
}
|
||
S.expertComment = c;
|
||
S.reportStatus = "rejected";
|
||
S.history.push({ time: nowTime(), text: "Заключение отклонено с комментарием", by: "Эксперт" });
|
||
addLog("Эксперт", "Отклонение", "risk", "Заключение возвращено аналитику");
|
||
toast("Заключение отклонено, возвращено аналитику", "warn");
|
||
save();
|
||
render();
|
||
}
|
||
|
||
function toAnalysisAction() { S.screen = "analysis"; save(); render(); }
|
||
function toExpertAction() { S.screen = "expert"; save(); render(); }
|
||
function resetDemo() {
|
||
try { localStorage.removeItem(LS); } catch (e) {}
|
||
S = loadState();
|
||
toast("Данные сброшены", "ok");
|
||
render();
|
||
}
|
||
|
||
function onNavScreen(e) {
|
||
var a = e.target.closest("[data-screen]");
|
||
if (!a) return;
|
||
e.preventDefault();
|
||
navigate(a.getAttribute("data-screen"));
|
||
}
|
||
|
||
function onActClick(e) {
|
||
var b = e.target.closest("[data-act]");
|
||
if (!b) return;
|
||
var act = b.getAttribute("data-act");
|
||
var saveKey = b.getAttribute("data-save");
|
||
var input = b.closest("tr[data-row]");
|
||
if (act === "save-edit" && saveKey && input) {
|
||
var val = (input.querySelector("[data-edit]") || {}).value || "";
|
||
var parts = saveKey.split("|");
|
||
var key2 = parts[0] + "_" + parts[1];
|
||
S.overrides[key2] = { value: val.trim(), status: "ok" };
|
||
S.manual[key2] = true;
|
||
EDITING = null;
|
||
addLog("Пользователь", "Правка значения", "ok", "Значение исправлено вручную");
|
||
save();
|
||
render();
|
||
return;
|
||
}
|
||
if (act === "upload-open") {
|
||
var fi = $("fileInput");
|
||
if (fi) fi.click();
|
||
return;
|
||
}
|
||
if (act === "upload-file") {
|
||
toast("Выберите Excel-файл для загрузки", "info");
|
||
return;
|
||
}
|
||
if (act === "manual-mode") { manualAction(); return; }
|
||
if (act === "open-log") { openLogAction(); return; }
|
||
if (act === "run-analysis") { runAnalysisAction(); return; }
|
||
if (act === "send-review") { sendReviewAction(); return; }
|
||
if (act === "download") { downloadAction(); return; }
|
||
if (act === "approve") { approveAction(); return; }
|
||
if (act === "reject") { rejectAction(); return; }
|
||
if (act === "to-analysis") { toAnalysisAction(); return; }
|
||
if (act === "to-expert") { toExpertAction(); return; }
|
||
}
|
||
|
||
function setupEvents() {
|
||
document.addEventListener("click", function (e) {
|
||
if (e.target.closest(".kt-ai-segmented [data-v]")) { onSegType(e); return; }
|
||
if (e.target.closest("#fileList [data-file]")) { onFileListClick(e); return; }
|
||
if (e.target.closest("#sumTableWrap tr[data-row]")) { onTableClick(e); return; }
|
||
if (e.target.closest("[data-act]")) { onActClick(e); return; }
|
||
if (e.target.closest("[data-screen]")) { onNavScreen(e); return; }
|
||
});
|
||
|
||
document.addEventListener("keydown", onKeydown);
|
||
document.addEventListener("input", onInput);
|
||
|
||
var dz = $("dropzone");
|
||
function bindDropzone() {
|
||
var el = $("dropzone");
|
||
if (!el || el._dzBound) return;
|
||
el._dzBound = true;
|
||
el.addEventListener("dragover", onDropzoneDragOver);
|
||
el.addEventListener("drop", function (e) {
|
||
e.preventDefault();
|
||
toast("Выберите Excel-файл для загрузки", "info");
|
||
});
|
||
}
|
||
setInterval(bindDropzone, 200);
|
||
|
||
document.addEventListener("change", function (e) {
|
||
var t = e.target;
|
||
if (!t) return;
|
||
if (t.id === "fileInput") { onFileInputChange(e); return; }
|
||
if (t.id === "fCompany") { CHK.co = t.value; render(); return; }
|
||
if (t.id === "fOnly") { CHK.only = t.value; render(); return; }
|
||
if (t.id === "expertComment") { S.expertComment = t.value; save(); return; }
|
||
});
|
||
|
||
/* --- мобильное меню --- */
|
||
var burger = $("burger"), sidebar = $("sidebar"), scrim = $("scrim"), drawer = $("drawer");
|
||
function openDrawer(open) {
|
||
if (scrim) scrim.classList.toggle("on", open);
|
||
if (sidebar) sidebar.style.display = open ? "block" : "";
|
||
}
|
||
if (burger) burger.addEventListener("click", function () { openDrawer(true); });
|
||
if (scrim) scrim.addEventListener("click", function () { openDrawer(false); });
|
||
if (drawer) drawer.addEventListener("click", function (e) {
|
||
var a = e.target.closest("[data-screen]");
|
||
if (!a) return;
|
||
e.preventDefault();
|
||
openDrawer(false);
|
||
navigate(a.getAttribute("data-screen"));
|
||
});
|
||
|
||
/* --- тема --- */
|
||
var tg = $("themeToggle"), ti = $("themeIcon");
|
||
if (tg) tg.addEventListener("click", function () {
|
||
var dark = S.theme === "light";
|
||
S.theme = dark ? "dark" : "light";
|
||
document.documentElement.setAttribute("data-theme", S.theme);
|
||
if (ti) ti.setAttribute("href", dark ? "#sun" : "#moon");
|
||
save();
|
||
});
|
||
document.documentElement.setAttribute("data-theme", S.theme);
|
||
}
|
||
|
||
function init() {
|
||
var burger = $("burger");
|
||
if (burger) {
|
||
function toggleBurger() {
|
||
burger.hidden = window.innerWidth > 831;
|
||
}
|
||
window.addEventListener("resize", toggleBurger);
|
||
toggleBurger();
|
||
}
|
||
render();
|
||
setupEvents();
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
})();
|