sverka-dokumentov-pri-naznac/script.js

809 lines
46 KiB
JavaScript
Raw 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.

/* ===== Сверка документов при назначении руководителей =====
Статичный прототип: данные демонстрационные, состояние в localStorage. */
"use strict";
var LS = "svd-nazn-v1";
/* ---------- Демо-данные (нейтральные примеры) ---------- */
var VIOL = [
{ id: 1, type: "norm", doc: "Анкета кандидата", item: "Раздел 2 «Опыт работы»", desc: "Общий стаж — 7 лет 4 месяца, требуется минимум 8 лет", norm: "Закон об АО, ст. 45", normId: 1 },
{ id: 2, type: "norm", doc: "Должностная инструкция", item: "Пункт 4.1", desc: "Требование «высшее образование» не соответствует профилю позиции", norm: "Положение о кадровом делопроизводстве, п. 12.4", normId: 2 },
{ id: 3, type: "norm", doc: "Анкета кандидата", item: "Раздел 3 «Образование»", desc: "Удостоверение об образовании не приложено", norm: "Положение о кадровом делопроизводстве, п. 7.2", normId: 3 },
{ id: 4, type: "norm", doc: "Пояснительная записка", item: "Пункт 3", desc: "Период работы по предыдущему месту не подтверждён справкой", norm: "ТК РК, ст. 68", normId: 4 },
{ id: 5, type: "contra", doc: "Пояснительная записка / Анкета", item: "Пункт 2 / Раздел 3", desc: "Указаны разные даты окончания предыдущей должности", norm: "—", normId: 5 },
{ id: 6, type: "norm", doc: "Анкета кандидата", item: "Раздел 1 «Личные сведения»", desc: "Отсутствие сведений о судимости (заполнено «нет данных»)", norm: "ТК РК, ст. 68, п. 2", normId: 6 },
{ id: 7, type: "norm", doc: "Должностная инструкция", item: "Приложение 1", desc: "Квалификационный разряд ниже требований к должности", norm: "Должностной регламент фонда, п. 5.1", normId: 7 },
{ id: 8, type: "norm", doc: "Пояснительная записка", item: "Пункт 4", desc: "Указанный руководитель не подтверждён штатным расписанием", norm: "Должностной регламент фонда, п. 9.3", normId: 8 },
{ id: 9, type: "contra", doc: "Анкета кандидата / ПЗ", item: "Раздел 2 / Пункт 3", desc: "Различаются периоды работы в одной организации", norm: "—", normId: 9 },
{ id: 10, type: "norm", doc: "Анкета кандидата", item: "Раздел 2 «Опыт работы»", desc: "Не подтверждён стаж работы по смежной специальности", norm: "Закон об АО, ст. 45", normId: 10 },
{ id: 11, type: "contra", doc: "Пояснительная записка / ДИ", item: "Пункт 2 / Пункт 4.2", desc: "Совокупная нагрузка по должности указана расхождением", norm: "—", normId: 11 },
{ id: 12, type: "norm", doc: "Пояснительная записка", item: "Пункт 5", desc: "Отсутствует ссылка на приказ о назначении", norm: "Должностной регламент фонда, п. 3.7", normId: 12 }
];
var NORMATIVES = [
{ id: 1, r: "Закон об АО", p: "ст. 45" },
{ id: 2, r: "Положение о кадровом делопроизводстве", p: "п. 12.4" },
{ id: 3, r: "Положение о кадровом делопроизводстве", p: "п. 7.2" },
{ id: 4, r: "ТК РК", p: "ст. 68" },
{ id: 5, r: "Закон об АО", p: "ст. 46" },
{ id: 6, r: "Должностной регламент фонда", p: "п. 5.1" },
{ id: 7, r: "Должностной регламент фонда", p: "п. 9.3" },
{ id: 8, r: "Должностной регламент фонда", p: "п. 3.7" }
];
var FILES_INIT = [
{ name: "Должностная инструкция.pdf", type: "ДИ", status: "done" },
{ name: "Пояснительная записка.docx", type: "ПЗ", status: "done" },
{ name: "Анкета кандидата.pdf", type: "Анкета", status: "pending"}
];
var CANDS = [
{ id: 1, name: "Ахметов Ерлан Сергеевич", post: "Руководитель отдела", stage: "approved", sent: null },
{ id: 2, name: "Бекмембетова Динара Талгат", post: "Начальник управления", stage: "approved", sent: null },
{ id: 3, name: "Сапаров Ибрагим Нурлан", post: "Заместитель директора", stage: "sent", sent: "21.09.2026, 13:05" },
{ id: 4, name: "Мырзагалиев Айдос Керек", post: "Руководитель направления", stage: "draft", sent: null },
{ id: 5, name: "Оралова Гульнара Бакыт", post: "Начальник сектора", stage: "ready", sent: null },
{ id: 6, name: "Токтаров Марат Алиши", post: "Директор подразделения", stage: "uploaded", sent: null },
{ id: 7, name: "Утебаева Алия Дильдари", post: "Заместитель руководителя", stage: "uploaded", sent: null },
{ id: 8, name: "Жумабаев Нурлан Серик", post: "Руководитель службы", stage: "uploaded", sent: null }
];
var STEPS = [
{ label: "Загрузка пакета" },
{ label: "Анализ ИИ" },
{ label: "Заключение" },
{ label: "Согласование" }
];
var SCREENS = {
load: { title: "Загрузка пакета документов", sub: "Пакет кандидата · экспертиза заключения" },
analysis: { title: "Анализ ИИ", sub: "Сверка с нормативами · поиск противоречий" },
concl: { title: "Заключение", sub: "Проект заключения эксперта" },
approve: { title: "Согласование", sub: "Утверждение заключения руководителем" },
about: { title: "О решении", sub: "Паспорт ИИ-решения" }
};
var STAGE_CHIP = {
uploaded: "Загружен",
ready: "Готов к анализу",
analyzed: "Анализ завершён",
draft: "Черновик",
sent: "На согласовании",
approved: "Согласовано"
};
/* ---------- Состояние ---------- */
var S = null;
function defaultState() {
var checks = {};
var i;
for (i = 0; i < VIOL.length; i++) {
checks[VIOL[i].id] =
(i < 5) ? "confirmed" :
(i === 5) ? "rejected" : "pending";
}
return {
screen: "load",
role: "expert",
candId: 5,
candidate: { name: "Оралова Гульнара Бакыт", post: "Начальник сектора", submitted: "19.09.2026" },
files: FILES_INIT.map(function (f) { return Object.assign({}, f); }),
analysisStatus: "done",
checks: checks,
text: "ЗАКЛЮЧЕНИЕ о соответствии кандидата требованиям должности\n\n" +
"Кандидат: Оралова Гульнара Бакыт\n" +
"Должность: Начальник сектора\n" +
"Дата пакета: 19.09.2026\n\n" +
"Проверены документы: должностная инструкция, пояснительная записка, анкета кандидата.\n" +
"Сверка выполнена с квалификационными требованиями и внутренние нормативы фонда.\n\n" +
"По результатам сверки зафиксированы несоответствия и расхождения, подробно изложенные ниже. При назначении кандидата на должность рекомендуется устранить замечания в порядке, установленном правилами фонда.\n\nСоставил: эксперт кадровой службы",
sentAt: null,
approveComment: null
};
}
function save() {
try { localStorage.setItem(LS, JSON.stringify(S)); } catch (e) { /* приватный режим */ }
}
function load() {
try {
var raw = localStorage.getItem(LS);
if (!raw) return null;
var st = JSON.parse(raw);
if (!st || !st.screen || !st.checks) return null;
return st;
} catch (e) { return null; }
}
/* ---------- Утилиты ---------- */
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function cand() { return CANDS.find(function (c) { return c.id === S.candId; }); }
function initials(s) {
var p = String(s).split(" ");
return (p[0] ? p[0].charAt(0) : "") + (p[1] ? p[1].charAt(0) : "");
}
function icon(name) {
return '<svg class="kt-icon" viewBox="0 0 24 24"><use href="./design-system/icons/kt-ai-lucide-sprite.svg#' + name + '"></use></svg>';
}
function kpi(value, label, hint) {
return '<div class="kt-ai-kpi"><span class="value">' + esc(value) + '</span><span class="label">' + esc(label) + "</span>" +
(hint ? '<span class="hint">' + esc(hint) + "</span>" : "") + "</div>";
}
function pill(label, tone) {
return '<span class="kt-ai-status-pill" data-status="' + tone + '">' + esc(label) + "</span>";
}
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.transition = "opacity .3s";
el.style.opacity = "0";
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
}, 2600);
}
function openDrawer(html) {
var d = $("drawer");
d.innerHTML = html;
d.style.display = "block";
}
function closeDrawer() {
var d = $("drawer");
d.style.display = "none";
d.innerHTML = "";
}
/* ---------- Навигация и оболочка ---------- */
function curStage() {
var st = cand().stage;
if (S.screen === "load") return 1;
if (S.screen === "analysis") return 2;
if (S.screen === "concl") return 3;
return 4;
}
function navHtml() {
var items = [
{ id: "load", label: "Загрузка пакета", ic: "upload" },
{ id: "analysis", label: "Анализ ИИ", ic: "sparkles" },
{ id: "concl", label: "Заключение", ic: "clipboard" },
{ id: "approve", label: "Согласование", ic: "check-circle" },
{ id: "about", label: "О решении", ic: "info" }
];
var cur = curStage();
var html = "";
items.forEach(function (it) {
var st = "";
if (it.id === "load") st = (cur === 1) ? "current" : "done";
else if (it.id === "analysis") st = (cur === 2) ? "current" : (cur > 2 ? "done" : "");
else if (it.id === "concl") st = (cur === 3) ? "current" : (cur > 3 ? "done" : "");
else if (it.id === "approve") st = (cur === 4) ? "current" : "";
var active = S.screen === it.id ? "true" : "false";
html += '<button class="kt-ai-nav-item" data-active="' + active + '" data-nav="' + it.id + '">' +
icon(it.ic) + '<span class="nav-label">' + esc(it.label) + "</span>" +
(st ? ' <span class="count">•</span>' : "") +
"</button>";
});
return html;
}
S = load() || defaultState();
$("pageTitle").textContent = SCREENS[S.screen].title;
$("pageSub").style.display = "none";
$("sbAvatar").textContent = initials(S.candidate.name);
$("sbName").textContent = S.candidate.name;
$("crumbRole").textContent = S.role === "leader" ? "Руководитель" : "Эксперт";
$("snav").innerHTML = navHtml();
/* ---------- Шапка-карточка кандидата (экраны 1–3) ---------- */
function candHead(sub, extra) {
var c = S.candidate;
var done = cand()._doneCount || 0;
return '<div class="kt-ai-v" data-gap="block">' +
'<div class="kt-ai-h-stack data-nowrap-scroll" data-nowrap="scroll">' +
'<div style="flex:1;min-width:0">' +
'<div class="kt-ai-t-xl" style="font-weight:var(--kt-ai-weight-semibold);letter-spacing:-.01em">' + esc(c.name) + "</div>" +
'<div class="kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1)">' + esc(c.post) +
" · Пакет: " + esc(c.submitted) + "</div>" +
"</div>" +
pill(STAGE_CHIP[cand().stage] || "—", cand().stage === "sent" ? "warn" : cand().stage === "approved" ? "ok" : "info") +
"</div>" +
'<div class="kt-ai-stepper">' + STEPS.map(function (s, i) {
var st = (i + 1 < curStage()) ? "done" : (i + 1 === curStage()) ? "current" : "";
return '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + "><span class=\"n\">" + (i + 1) + "</span>" +
'<span>' + esc(s.label) + "</span></div>" + (i < STEPS.length - 1 ? '<div class="kt-ai-step-sep"></div>' : "");
}).join("") + "</div>" +
(extra || "") +
"</div>";
}
/* ============================================================
ЭКРАН 1 — ЗАГРУЗКА ПАКЕТА ДОКУМЕНТОВ
============================================================ */
function renderLoad() {
var c = cand();
var files = S.files;
var zoneHtml = ["ДИ", "ПЗ", "Анкета"].map(function (t) {
var f = null;
for (var i = 0; i < files.length; i++) if (files[i].type === t) { f = files[i]; break; }
var has = !!f;
var ic = has ? "file" : "upload";
var fname = has ? f.name : "Файл ещё не загружен";
var sub = has ? (f.status === "done" ? "Обработан" : "Ожидание обработки") : "Перетащите или выберите файл";
return '<div class="up-zone" data-type="' + t + '">' +
'<span class="doc-ic">' + icon(ic) + "</span>" +
'<span class="z-name">' + t + "</span>" +
'<span class="z-file" style="' + (has ? "" : "color:var(--kt-ai-fg-faint)") + '">' + esc(fname) + "</span>" +
'<span class="z-file dim">' + esc(sub) + "</span>" +
'<button class="kt-ai-btn" data-size="sm" data-type="' + t + '">' + (has ? "Заменить файл" : "Загрузить файл") + "</button>" +
"</div>";
}).join("");
var rows = files.map(function (f, i) {
var st = f.status === "done" ? "ok" : "warn";
var stLabel = f.status === "done" ? "Обработан" : "В обработке";
return '<div class="kt-ai-doc">' +
'<span class="doc-ic">' + icon("file") + "</span>" +
'<div class="doc-meta"><span class="doc-name">' + esc(f.name) + "</span>" +
'<span class="doc-sub">Тип: ' + esc(f.type) + "</span></div>" +
'<span class="kt-ai-status-pill" data-status="' + st + '">' + stLabel + "</span>" +
'<button class="kt-ai-icon-button" data-type="' + f.type + '" aria-label="Сменить">' + icon("x") + "</button></div>";
}).join("");
var btns = '<button class="kt-ai-btn" data-variant="primary" id="btnRunAnalysis">' + icon("play") + " Запустить анализ ИИ</button>" +
'<button class="kt-ai-btn" id="btnCancelLoad" data-variant="ghost">Отменить</button>';
return '<div class="view">' + candHead() +
'<div class="kt-ai-kpi-strip" data-cards="true">' +
kpi(String(files.length) + " / 3", "Документов загружено", "пакет кандидата") +
kpi("150 мин", "Труда без автоматизации", "на одно назначение") +
kpi("3 р.д", "Срок подготовки заключения", "по регламенту") +
"</div>" +
'<div class="kt-ai-v" data-gap="block">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)">' +
'<h2 class="kt-ai-h2">Новая сверка</h2>' +
'<div class="kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1);margin-bottom:var(--kt-ai-space-7)">' +
"Заполните сведения о кандидате и загрузите комплект документов — система сверит их с нормативами и найдёт расхождения.</div>" +
'<div class="up-grid">' +
'<div class="kt-ai-field"><label for="fName">ФИО кандидата</label>' +
'<input class="kt-ai-input" id="fName" value="' + esc(S.candidate.name) + '"></div>' +
'<div class="kt-ai-field"><label for="fPost">Должность</label>' +
'<input class="kt-ai-input" id="fPost" value="' + esc(S.candidate.post) + '"></div>' +
"</div>" +
'<div style="margin-bottom:var(--kt-ai-space-5);display:flex;align-items:center;gap:var(--kt-ai-space-3)">' +
'<span class="kt-ai-t-sm kt-ai-t-medium" style="white-space:nowrap">Загрузка документов</span>' +
'<div class="kt-ai-spacer"></div>' +
'<span class="kt-ai-chip" data-tone="gray">3 обязательных файла</span></div>' +
'<div class="up-zones">' + zoneHtml + "</div>" +
'<div style="margin-top:var(--kt-ai-space-7);display:flex;align-items:center;gap:var(--kt-ai-space-3)">' +
'<span class="kt-ai-t-sm kt-ai-t-medium">Загруженные файлы</span>' +
'<div class="kt-ai-spacer"></div>' +
'<span class="kt-ai-t-xs kt-ai-t-faint">Ожидание загрузки / обработки</span></div>' +
'<div class="up-list" style="margin-top:var(--kt-ai-space-4)">' + rows + "</div>" +
'<div class="' + "kt-ai-h-stack" + '" style="margin-top:var(--kt-ai-space-7)">' + btns + "</div>" +
"</div>" +
"</div>" +
"</div>";
}
/* ============================================================
ЭКРАН 2 — АНАЛИЗ ИИ И ПРОВЕРКА ВЫВОДОВ
============================================================ */
function violCard(v) {
var ch = S.checks[v.id];
var t = v.type === "contra" ? "contra" : "norm";
var tLabel = v.type === "contra" ? "Расхождение" : "Несоответствие нормативу";
var normLine = (v.type === "norm")
? ('<span class="viol-meta">Норматив: ' + esc(v.norm) + '</span>')
: '<span class="viol-meta" style="color:var(--kt-ai-status-warn-fg)">Расхождение в данных документов</span>';
var body =
'<div class="viol-vic" data-t="' + t + '">' + icon(t === "contra" ? "compare" : "scale") + "</div>" +
'<div class="viol-body">' +
'<span class="kt-ai-chip" data-tone="' + (t === "contra" ? "orange" : "blue") + '">' + tLabel + "</span>" +
'<div class="viol-desc">' + esc(v.desc) + "</div>" +
'<div class="viol-meta">Документ: ' + esc(v.doc) + " · Пункт: " + esc(v.item) + "</div>" +
normLine +
"</div>";
var actions =
ch === "pending"
? '<button class="kt-ai-btn" data-size="sm" data-variant="primary" data-act="confirm" data-id="' + v.id + '">' + icon("check") + " Подтвердить</button>" +
'<button class="kt-ai-btn" data-size="sm" data-act="reject" data-id="' + v.id + '">Отклонить</button>'
: ch === "confirmed"
? '<button class="kt-ai-btn" data-size="sm" data-variant="ghost" data-act="reopen" data-id="' + v.id + '">Вернуть на проверку</button>' + pill("Подтверждено", "ok")
: pill("Отклонено", "muted") + '<button class="kt-ai-btn" data-size="sm" data-variant="ghost" data-act="reopen" data-id="' + v.id + '">Вернуть</button>';
return '<div class="viol-card" data-done="' + (ch === "pending" ? "false" : "true") + '">' + body +
'<div class="viol-actions">' + actions + "</div></div>";
}
function renderAnalysis() {
var done = 0, confirmed = 0, rejected = 0, pending = 0;
VIOL.forEach(function (v) {
var ch = S.checks[v.id];
if (ch === "pending") pending++;
else if (ch === "confirmed") confirmed++;
else rejected++;
if (ch !== "pending") done++;
});
var tone = S.analysisStatus === "done" ? "ok" : "risk";
var banner = S.analysisStatus === "done"
? '<div class="kt-ai-banner" data-tone="ok" style="display:flex;align-items:center;gap:var(--kt-ai-space-4)"><span style="flex:1;min-width:0">Сверка завершена: найдено ' + VIOL.length + " несоответствий и " + VIOL.filter(function(v){return v.type==="contra"}).length + " расхождений</span>" +
'<span class="kt-ai-t-xs kt-ai-t-faint">22.04 с</span></div>'
: '<div class="kt-ai-banner" data-tone="risk" style="display:flex;align-items:center;gap:var(--kt-ai-space-4)"><span style="flex:1;min-width:0">Анализ не завершён: один документ не распознан</span>' +
'<button class="kt-ai-btn" data-size="sm" id="btnRetryAnalysis">' + icon("refresh") + " Повторить</button></div>";
return '<div class="view">' + candHead() + banner +
'<div class="kt-ai-kpi-strip" data-cards="true">' +
kpi(String(VIOL.length), "Найдено несоответствий", "сверка с нормативами") +
kpi(String(confirmed), "Подтверждено", "экспертом") +
kpi(String(rejected), "Отклонено", "ложные срабатывания") +
kpi(String(pending), "Осталось проверить", "к ручному осмотру") +
"</div>" +
'<div class="kt-ai-v" data-gap="block">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
'<div class="kt-ai-table-toolbar">' +
'<span class="kt-ai-t-sm kt-ai-t-medium">Выводы ИИ · ' + (done + "/" + VIOL.length) + " проверено</span>" +
'<div class="kt-ai-spacer"></div>' +
'<select class="kt-ai-select" id="vType" style="min-width:150px">' +
'<option value="all">Все типы</option>' +
'<option value="norm">Несоответствие нормативу</option>' +
'<option value="contra">Расхождение</option>' +
"</select>" +
'<select class="kt-ai-select" id="vState" style="min-width:150px">' +
'<option value="all">Все статусы</option>' +
'<option value="pending">Ожидает проверки</option>' +
'<option value="confirmed">Подтверждено</option>' +
'<option value="rejected">Отклонено</option>' +
"</select>" +
"</div>" +
'<div class="kt-ai-v" data-gap="tight" id="violList"></div>' +
'<div class="kt-ai-h-stack" style="margin-top:var(--kt-ai-space-6)" data-nowrap="scroll">' +
'<button class="kt-ai-btn" data-variant="primary" id="btnToConcl">' + icon("arrowRight") + " Перейти к составлению заключения</button>" +
'<span class="kt-ai-t-xs kt-ai-t-faint">В заключение войдут ' + confirmed + " подтверждённых" + (confirmed === 1 ? " пункт" : confirmed >= 2 && confirmed <= 4 ? " пункта" : " пунктов") + "</span>" +
"</div>" +
"</div>" +
"</div>" +
"</div>";
}
/* ============================================================
ЭКРАН 3 — СОСТАВЛЕНИЕ ЗАКЛЮЧЕНИЯ
============================================================ */
function confirmedViolHtml(n) {
var c = 0;
var html = "";
VIOL.forEach(function (v) {
if (S.checks[v.id] === "confirmed") {
c++;
var toneDot = v.type === "contra" ? "var(--kt-ai-status-warn-fg)" : "var(--kt-ai-status-info-fg)";
var normTxt = v.type === "norm" ? esc(v.norm) : "Расхождение данных";
html += '<div class="approve-viol"><span class="dot" style="background:' + toneDot + '"></span>' +
"<div><div><strong>" + c + ".</strong> " + esc(v.desc) + "</div>" +
'<div class="dim">' + esc(v.doc) + " · " + esc(v.item) + " · " + normTxt + "</div></div></div>";
}
});
if (c === 0) html = '<div class="kt-ai-empty"><span class="title">Пока ни одного подтверждённого</span>Вернитесь на «Анализ ИИ» и подтвердите несоответствия — они попадут в заключение.</div>';
return html;
}
function normativeRefsHtml() {
var seen = {};
var out = [];
VIOL.forEach(function (v) {
if (S.checks[v.id] === "confirmed" && v.type === "norm") {
var ref = NORMATIVES.find(function (nr) { return nr.id === v.normId; });
if (ref && !seen[ref.id]) {
seen[ref.id] = true;
out.push("<span class=\"r\">" + esc(ref.r) + "</span><span class=\"dim\">" + esc(ref.p) + "</span>");
}
}
});
if (!out.length) return '<div class="kt-ai-t-muted kt-ai-t-xs">Ссылки появятся после подтверждения нормативных несоответствий.</div>';
return out.map(function (h) {
return '<div class="doc-ref">' + icon("link") + h + "</div>";
}).join("");
}
function renderConcl() {
var c = cand();
var tone = c.stage === "sent" ? "warn" : "gray";
return '<div class="view">' + candHead() +
'<div class="kt-ai-v" data-gap="block">' +
'<div class="concl-layout">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
'<div class="kt-ai-h-stack" data-nowrap="scroll" style="margin-bottom:var(--kt-ai-space-5)">' +
'<span class="kt-ai-t-lg kt-ai-t-semibold">Текст заключения</span>' +
'<div class="kt-ai-spacer"></div>' +
pill(c.stage === "sent" ? "На согласовании" : "Черновик", tone) +
"</div>" +
'<textarea class="kt-ai-textarea" id="conclText" style="margin-bottom:var(--kt-ai-space-5);box-sizing:border-box;min-height:260px"></textarea>' +
'<div class="kt-ai-h-stack" data-nowrap="scroll">' +
'<button class="kt-ai-btn" id="btnSaveConcl">' + icon("check") + " Сохранить черновик</button>" +
'<button class="kt-ai-btn" data-variant="primary" id="btnSendConcl">' + icon("send") + " Отправить на согласование</button>" +
"</div>" +
'<div class="kt-ai-t-xs kt-ai-t-faint" style="margin-top:var(--kt-ai-space-3)">Руководитель получит копию на согласование, кадровая служба — копию.</div>' +
"</div>" +
'<div class="concl-side">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
'<div class="kt-ai-h-stack" data-gap="tight" style="margin-bottom:var(--kt-ai-space-4)">' +
'<span class="kt-ai-t-sm kt-ai-t-medium">Подтверждённые несоответствия (' + (function(){var n=0;VIOL.forEach(function(v){if(S.checks[v.id]==="confirmed")n++;});return n;})() + ")</span>" +
'<div class="kt-ai-spacer"></div>' +
'<span class="kt-ai-chip" data-tone="gray">из «Анализ ИИ»</span>' +
"</div>" +
'<div>' + confirmedViolHtml() + "</div>" +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-3)">Пункты нормативов</div>' +
normativeRefsHtml() +
"</div>" +
"</div>" +
"</div>" +
"</div>" +
"</div>";
}
/* ============================================================
ЭКРАН 4 — СОГЛАСОВАНИЕ РУКОВОДИТЕЛЕМ
============================================================ */
function renderApprove() {
var c = cand();
var chip = c.stage === "approved"
? pill("Согласовано", "ok")
: c.stage === "sent"
? pill("На согласовании", "warn")
: pill("Вернуто на доработку", "risk");
var c2 = c;
var actions =
c.stage === "approved"
? '<div class="kt-ai-banner" data-tone="ok">Заключение согласовано ' + esc(c.approvedAt || "сегодня") + ". Кадровая служба получит уведомление.</div>"
: c.stage === "sent"
? '<div class="kt-ai-v" data-gap="block" style="margin-top:var(--kt-ai-space-6)">' +
'<div class="kt-ai-textarea" id="rejectComment" placeholder="Комментарий эксперту (необязательно)"></div>' +
'<div class="kt-ai-h-stack" data-nowrap="scroll">' +
'<button class="kt-ai-btn" data-variant="primary" id="btnApprove">' + icon("check") + " Согласовать</button>" +
'<button class="kt-ai-btn" id="btnReturnConcl">' + icon("arrowLeft") + " Вернуть на доработку</button>" +
"</div></div>"
: '<div class="kt-ai-banner" data-tone="warn">Черновик возвращён эксперту на доработку. Дождитесь новой отправки.</div>';
var sentLine = c.sent ? "Отправлено: " + esc(c.sent) : "Отправлено: —";
var approvedLine = c.stage === "approved" ? "Согласовано руководителем: " + esc(c.approvedAt || "—") : "Согласование: ожидается";
return '<div class="view">' + candHead() +
'<div class="kt-ai-v" data-gap="block">' +
'<div class="kt-ai-kpi-strip" data-cards="true">' +
kpi(esc(c.name.split(" ")[0] + " " + c.name.split(" ")[1]), "Кандидат", "на согласовании") +
kpi(sentLine.split(":")[1] || "—", "Отправлено", "экспертом") +
kpi(c.sent ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)">' +
'<div class="kt-ai-h-stack" data-nowrap="scroll" style="margin-bottom:var(--kt-ai-space-5)">' +
'<div style="flex:1;min-width:0">' +
'<div class="kt-ai-t-lg kt-ai-t-semibold">Заключение эксперта</div>' +
'<div class="kt-ai-t-xs kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1)">' + sentLine + " · " + approvedLine + "</div>" +
"</div>" +
chip +
"</div>" +
'<div class="approve-text" id="approveText"></div>' +
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-top:var(--kt-ai-space-7);margin-bottom:var(--kt-ai-space-3)">Подтверждённые несоответствия</div>' +
confirmedViolHtml() +
actions +
"</div>" +
"</div>" +
"</div>";
}
/* ============================================================
ЭКРАН 5 — О РЕШЕНИИ
============================================================ */
function renderAbout() {
var proc = [
{ t: "Эксперт получает пакет документов", who: "от кадровой службы" },
{ t: "Эксперт читает пояснительную записку" },
{ t: "ИИ сверяет документы с квалификационными требованиями", ai: true },
{ t: "ИИ ищет расхождения в документах", ai: true },
{ t: "Эксперт проверяет выводы ИИ" },
{ t: "Эксперт готовит заключение с несоответствиями и ссылками" },
{ t: "Руководитель согласовывает заключение" }
];
return '<div class="view">' +
'<div class="kt-ai-v" data-gap="group">' +
'<div class="kt-ai-h2">Паспорт ИИ-решения</div>' +
'<div class="kt-ai-banner" data-tone="info">Ускоряет подготовку заключения о соответствии кандидата за счёт автоматизации сверки и поиска расхождений.</div>' +
'<div class="kt-ai-v" data-gap="tight">' +
'<div class="kt-ai-kpi-strip" data-cards="true">' +
kpi("~50 ч/мес", "Экономия", "20 случаев × 150 мин") +
kpi("3 р.д → 1 день", "Срок подготовки", "заключения") +
kpi("20 случаев", "Объём в месяц", "оценка владельца процесса") +
kpi("—", "Дата внедрения", "после согласования макета") +
"</div>" +
"</div>" +
'<div class="pass-grid">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-4)">Поэтапный эффект</div>' +
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3)">' +
'<div><span class="kt-ai-t-sm">Сокращение чтения вручную</span><div class="kt-ai-t-xs kt-ai-t-muted">эксперт осматривает уже только выводы ИИ</div></div>' +
'<div><span class="kt-ai-t-sm">Запасание расхождений при сверке</span><div class="kt-ai-t-xs kt-ai-t-muted">нормативы и требования — в одном месте</div></div>' +
'<div><span class="kt-ai-t-sm">Стабильный срок заключения</span><div class="kt-ai-t-xs kt-ai-t-muted">руководитель получает в тот же день</div></div>' +
"</div>" +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-4)">Контроль и риски</div>' +
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3)">' +
'<div><span class="kt-ai-t-sm">Ошибочное срабатывание ИИ</span><div class="kt-ai-t-xs kt-ai-t-muted">в случаях расхождений данных</div></div>' +
'<div><span class="kt-ai-t-sm">Митигация — ручная проверка</span><div class="kt-ai-t-xs kt-ai-t-muted">каждый вывод подтверждает или отклоняет эксперт</div></div>' +
"</div>" +
"</div>" +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
'<div class="pass-sec">Данные на входе</div>' +
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4)">' +
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("file") + '</span><div class="doc-meta"><span class="doc-name">Комплект документов кандидата</span><span class="doc-sub">должностная инструкция, пояснительная записка, анкета</span></div></div>' +
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("book") + '</span><div class="doc-meta"><span class="doc-name">Внутренние нормативы</span><span class="doc-sub">папка на общем диске</span></div></div>' +
"</div>" +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
'<div class="pass-sec">Процесс</div>' +
'<div class="psteps" style="margin-top:var(--kt-ai-space-4)">' + proc.map(function (p, i) {
return '<div class="pstep' + (p.ai ? " ai" : "") + '"><span class="n">' + (i + 1) + '</span>' +
"<div><div>" + esc(p.t) + '</div><span class=\"who\">' + (p.ai ? "ИИ" : (p.who ? esc(p.who) : "эксперт / руководитель")) + "</div></div></div>";
}).join("") + "</div>" +
"</div>" +
'<div class="pass-grid2">' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
'<div class="pass-sec">Роль пользователя</div>' +
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4);font-size:var(--kt-ai-text-sm)">' +
'<div><span class="kt-ai-t-medium">Эксперт</span><div class="kt-ai-t-xs kt-ai-t-muted">загружает документы, проверяет выводы ИИ, готовит и отправляет заключение</div></div>' +
'<div><span class="kt-ai-t-medium">Руководитель</span><div class="kt-ai-t-xs kt-ai-t-muted">просматривает итог и согласовывает</div></div>' +
'<div><span class="kt-ai-t-medium">Кадровая служба</span><div class="kt-ai-t-xs kt-ai-t-muted">передаст пакет документов и получает копию заключения</div></div>' +
"</div>" +
"</div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
'<div class="pass-sec">Что выполняет ИИ</div>' +
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4);font-size:var(--kt-ai-text-sm)">' +
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("scale") + '</span><div class="doc-meta"><span class="doc-name">Сверка с квалификационными требованиями и нормативами</span></div></div>' +
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("compare") + '</span><div class="doc-meta"><span class="doc-name">Поиск расхождений в документах</span></div></div>' +
'<div class="kt-ai-t-xs kt-ai-t-muted" style="margin-top:var(--kt-ai-space-2)">Человек решает: проверка выводов, написание текста, согласование.</div>' +
"</div>" +
"</div>" +
"</div>" +
'<div class="kt-ai-kpi-strip">' +
kpi("Владелец процесса", "Исполнитель", "—") +
kpi("Ветеринация на Vibe AI", "Формат", "макет → рабочее решение") +
kpi("ИИ-агент Alem", "Опция", "работа с документами") +
"</div>" +
"</div>" +
"</div>";
}
/* ============================================================
РЕНДЕР
============================================================ */
function render() {
var v = $("view");
if (S.screen === "load") v.innerHTML = renderLoad();
else if (S.screen === "analysis") v.innerHTML = renderAnalysis();
else if (S.screen === "concl") v.innerHTML = renderConcl();
else if (S.screen === "approve") v.innerHTML = renderApprove();
else v.innerHTML = renderAbout();
var sc = SCREENS[S.screen];
$("pageTitle").textContent = sc.title;
$("snav").innerHTML = navHtml();
/* После вставки — заполнение значений в поле/подсветка текущего и навешивание действий через делегирование */
if (S.screen === "analysis") {
applyViolFilters();
}
if (S.screen === "concl") {
$("conclText").value = S.text;
}
if (S.screen === "approve") {
$("approveText").textContent = S.text;
}
}
function setScreen(name) {
S.screen = name;
save();
closeDrawer();
render();
}
/* ---------- Фильтры экрана 2 ---------- */
function applyViolFilters() {
var list = $("violList");
if (!list) return;
var typeEl = $("vType"), stateEl = $("vState");
var ty = typeEl ? typeEl.value : "all";
var st = stateEl ? stateEl.value : "all";
var html = "";
VIOL.forEach(function (v) {
if (ty !== "all" && v.type !== ty) return;
if (st !== "all" && S.checks[v.id] !== st) return;
html += violCard(v);
});
list.innerHTML = html;
}
/* ---------- Делегированные обработчики ---------- */
document.addEventListener("click", function (e) {
var navBtn = e.target.closest("[data-nav]");
if (navBtn) { setScreen(navBtn.getAttribute("data-nav")); return; }
var zoneBtn = e.target.closest(".up-grid button, .up-zones button.kt-ai-btn");
if (zoneBtn) {
var t = zoneBtn.getAttribute("data-type");
var f = S.files.find(function (x) { return x.type === t; });
if (f) {
f.status = "pending";
save();
toast("Файл загружен — ожидание обработки", "info");
render();
}
return;
}
var xBtn = e.target.closest(".up-list button.kt-ai-icon-button");
if (xBtn) {
var t2 = xBtn.getAttribute("data-type");
var f2 = S.files.find(function (x) { return x.type === t2; });
if (f2) { f2.status = "pending"; save(); render(); }
return;
}
if (e.target.id === "btnRunAnalysis") {
S.analysisStatus = "done";
save();
closeDrawer();
S.screen = "analysis";
render();
toast("Анализ завершён", "ok");
return;
}
if (e.target.id === "btnCancelLoad") {
S.files = FILES_INIT.map(function (f) { return Object.assign({}, f); });
save();
toast("Загрузка сброшена", "info");
render();
return;
}
var actBtn = e.target.closest("[data-act]");
if (actBtn) {
var id = parseInt(actBtn.getAttribute("data-id"), 10);
var act = actBtn.getAttribute("data-act");
if (act === "confirm") S.checks[id] = "confirmed";
else if (act === "reject") S.checks[id] = "rejected";
else if (act === "reopen") S.checks[id] = "pending";
save();
if (act === "confirm") toast("Несоответствие подтверждено", "ok");
if (act === "reject") toast("Отклонено как ложное срабатывание", "info");
if (act === "reopen") toast("Возвращено на проверку", "info");
applyViolFilters();
refreshKpis();
return;
}
if (e.target.id === "btnRetryAnalysis") {
S.analysisStatus = "done";
save();
render();
toast("Анализ завершён", "ok");
return;
}
if (e.target.id === "btnToConcl") {
S.screen = "concl"; save(); render();
return;
}
if (e.target.id === "btnSaveConcl") {
var ta = $("conclText");
if (ta) { S.text = ta.value; save(); }
var cc = cand();
if (cc.stage === "uploaded" || cc.stage === "ready" || cc.stage === "analyzed") cc.stage = "draft";
save();
toast("Черновик сохранён", "ok");
render();
return;
}
if (e.target.id === "btnSendConcl") {
var ta2 = $("conclText");
if (ta2) S.text = ta2.value;
var cc2 = cand();
cc2.stage = "sent";
cc2.sent = "21.09.2026, " + new Date().toTimeString().slice(0, 5);
S.text = S.text + "\n\nОтправлено на согласование: " + cc2.sent;
save();
toast("Отправлено на согласование", "ok");
S.screen = "approve";
render();
return;
}
if (e.target.id === "btnApprove") {
var cc3 = cand();
cc3.stage = "approved";
var rc = $("rejectComment");
cc3.approvedAt = rc && rc.value ? "С комментарием: " + rc.value : "без замечаний";
save();
toast("Заключение согласовано", "ok");
render();
return;
}
if (e.target.id === "btnReturnConcl") {
var cc4 = cand();
var rc2 = $("rejectComment");
cc4.stage = "draft";
cc4.returned = rc2 && rc2.value ? rc2.value : "Возвращено в экспертизу";
S.role = "expert";
S.screen = "concl";
save();
$("crumbRole").textContent = S.role === "leader" ? "Руководитель" : "Эксперт";
toast("Возвращено на доработку", "warn");
render();
return;
}
if (e.target.closest("#sbUser")) {
S.role = S.role === "expert" ? "leader" : "expert";
$("crumbRole").textContent = S.role === "leader" ? "Руководитель" : "Эксперт";
save();
toast("Переключено на роль: " + ($("crumbRole").textContent), "info");
}
});
/* Перерисовка KPI вверху экрана 2 после подтверждения/отклонения */
function refreshKpis() {
var strip = document.querySelector(".kt-ai-kpi-strip");
if (!strip) return;
var confirmed = 0, rejected = 0, pending = 0;
VIOL.forEach(function (v) {
var ch = S.checks[v.id];
if (ch === "confirmed") confirmed++;
else if (ch === "rejected") rejected++;
else pending++;
});
var vals = [
[String(pending), "Осталось проверить", "к ручному осмотру"],
[String(confirmed), "Подтверждено", "экспертом"],
[String(rejected), "Отклонено", "ложные срабатывания"]
];
var cards = strip.querySelectorAll(".kt-ai-kpi");
if (cards.length >= 4) {
cards[3].querySelector(".value").textContent = pending;
cards[1].querySelector(".value").textContent = confirmed;
cards[2].querySelector(".value").textContent = rejected;
}
}
/* Селекты на экране 2 */
document.addEventListener("change", function (e) {
if (e.target.id === "vType" || e.target.id === "vState") {
applyViolFilters();
}
});
/* Burger */
$("burger").addEventListener("click", function () {
var sb = $("sidebar");
var st = getComputedStyle(sb);
if (sb.dataset.open === "true") { sb.dataset.open = "false"; sb.style.position = "sticky"; }
else { sb.dataset.open = "true"; sb.style.position = "fixed"; sb.style.zIndex = 50; sb.style.top = "0"; sb.style.left = "0"; sb.style.right = "0"; sb.style.bottom = "0"; }
});
/* Первый рендер */
render();
if (S.screen === "concl") { var t = $("conclText"); if (t) t.value = S.text; }