sbor-i-analiz-hr-pokazateley/script.js

946 lines
50 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Сбор и анализ 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);
})();