ekspertiza-dokumentov-dlya-s/script.js

642 lines
43 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.

"use strict";
/* Экспертиза документов для советов — статичный прототип, состояние в localStorage. */
var LS_KEY = "edds-v1";
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 escJs(s) { return esc(String(s == null ? "" : s).replace(/\\/g, "\\\\").replace(/'/g, "\\'")); }
var ST = {
wait: { label: "Ожидает проверки", tone: "warn" },
ready: { label: "Готово к анализу", tone: "ok" },
process: { label: "Процесс отбора", tone: "info" },
qual: { label: "Квалификация", tone: "info" },
contra: { label: "Противоречие", tone: "risk" },
checked: { label: "На проверке", tone: "info" },
conf: { label: "Подтверждено", tone: "ok" },
falsep: { label: "Ложное срабатывание", tone: "muted" },
draft: { label: "Черновик", tone: "warn" },
sent: { label: "Готово к согласованию", tone: "info" },
agree: { label: "Согласовано", tone: "ok" },
decl: { label: "Отклонено", tone: "risk" },
pending: { label: "Ожидает решения", tone: "warn" }
};
function pill(key, label) {
var st = ST[key] || { label: label || key, tone: "muted" };
return '<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span>";
}
function num(s) { return '<span class="kt-ai-num">' + s + "</span>"; }
function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href="#' + name + '"></use></svg>'; }
/* ---------- Демо-данные ---------- */
function seed() {
return {
view: "upload",
req: "Кандидат: А. Т. Нурмолдаев · SEO-1, управление сетевых сервисов · заявка от 14.09.2026",
candidate: "А. Т. Нурмолдаев",
sources: [
{ n: "Трудовой кодекс Республики Казахстан", s: "Законодательство", d: 207, u: 1 },
{ n: "Закон Республики Казахстан «Об акционерных обществах»", s: "Законодательство", d: 148, u: 1 },
{ n: "Правила трудового распорядка", s: "Внутренние документы", d: 12, u: 1 },
{ n: "Правила поиска и подбора персонала", s: "Внутренние документы", d: 8, u: 1 },
{ n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 }
],
files: [
{ n: "Пояснительная записка на назначение — Нурмолдаев А.Т.docx", t: "Пояснительная записка", d: "14.09.2026, 10:24", s: "ready" },
{ n: "План отбора 2026-09 — управление сетевых сервисов.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:24", s: "ready" },
{ n: "Протокол собеседования №18-09.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:25", s: "ready" },
{ n: "Резюме кандидата — Нурмолдаев А.Т.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:28", s: "wait" },
{ n: "Анкета о соответствии требованиям.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:31", s: "ready" },
{ n: "Проверка требований (внутренняя форма) — скан.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:32", s: "ready" },
{ n: "Справка о стаже и опыте работы.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:33", s: "ready" },
{ n: "Диплом о высшем образовании (копия).pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:35", s: "ready" }
],
running: false,
log: [
{ t: "10:42", b: "Пакет документов принят, 8 файлов" },
{ t: "10:42", b: "Извлечение текста: пояснительная записка, 12 разд." },
{ t: "10:43", b: "Сверка с Правилами поиска и подбора персонала: раздел «Организация отбора», п. 4.2" },
{ t: "10:43", b: "Найдено отклонение: дата объявления о вакансии" },
{ t: "10:44", b: "Сверка с Правилами трудового распорядка, гл. 5 «Назначение руководителей»" },
{ t: "10:44", b: "Сверка с Законом «Об АО»: компетенции совета директоров, ст. 36" },
{ t: "10:45", b: "Скрытое голосование: противоречие в протоколе и правилах" },
{ t: "10:45", b: "Проверка квалификации: стаж, образование, аттестация" },
{ t: "10:46", b: "Обнаружено 4 несоответствия, 1 противоречие" },
{ t: "10:46", b: "Журнал завершён — выводы переданы на верификацию эксперту" }
],
found: 4,
contra: true,
violations: [
{ d: "Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10", y: "process", r: "Правила поиска и подбора персонала, раздел 4.2", v: "conf" },
{ d: "В пояснительной записке 3 кандидата, в протоколе собеседования фиксированы 2", y: "process", r: "План отбора 2026-09, п. 3", v: "conf" },
{ d: "Срок согласования кандидатуры с материнской компанией — 2 дня при нормативе 5", y: "process", r: "Корпоративный стандарт по УХР, ред. 12", v: "conf" },
{ d: "У кандидата 6 лет опыта в телекоммуникациях при требуемых 7 для уровня SEO-1", y: "qual", r: "Должностная инструкция SEO-1, п. 2 «Требования к квалификации»", v: "conf" },
{ d: "Стаж в должности SEO-3: 3 года 4 месяца при нормативе не менее 4 лет", y: "qual", r: "Должностная инструкция SEO-2 и SEO-3, п. 2", v: "conf" },
{ d: "Аттестация по управленческой программе в материалах не подтверждена", y: "qual", r: "Корпоративный стандарт по УХР, гл. 8", v: "conf" },
{ d: "В протоколе — решение голосованием 4:1, в правилах назначений — единогласно", y: "contra", r: "Трудовой Кодекс РК, ст. 165, часть 3", v: "falsep" },
{ d: "Внутренняя форма проверки заполнена не полностью — графа «Аттестация»", y: "process", r: "Правила трудового распорядка, приложение 2", v: "falsep" }
],
verdict: "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат А. Т. Нурмолдаев) прошла проверку соответствия нормативной базе. По завершённой сверке зафиксированы следующие несоответствия:\n\n1. Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10 (Правила поиска и подбора персонала, раздел 4.2).\n2. В пояснительной записке указан круг из 3 кандидатов, в протоколе собеседования зафиксировано 2 (План отбора 2026-09, п. 3).\n3. Срок согласования кандидатуры с материнской компанией составил 2 дня при нормативе 5 (Корпоративный стандарт по УХР, ред. 12).\n4. Стаж в отрасли 6 лет при требуемых 7 для уровня SEO-1 (Должностная инструкция SEO-1, п. 2).\n5. Стаж в должности SEO-3 — 3 года 4 месяца при нормативе не менее 4 лет (Должностная инструкция SEO-2 и SEO-3, п. 2).\n6. Аттестация по управленческой программе в материалах не подтверждена (Корпоративный стандарт по УХР, гл. 8).\n\nПротиворечие в способе принятия решения (протокол — голосование 4:1, правила — единогласно) рассмотрено экспертом: позиция кандидата, поданная в установленном порядке, исключает применение требования единогласия; вывод системы отклонён как не относящийся к рассматриваемому пакету.\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.",
checklist: [1, 2, 3, 4, 5, 6],
status: "sent",
rejected: null,
stamp: null
};
}
var D = null;
function load() {
try { var raw = localStorage.getItem(LS_KEY); D = raw ? JSON.parse(raw) : seed(); }
catch (e) { D = seed(); }
if (!D.files || !D.violations) D = seed();
}
function save() {
try { localStorage.setItem(LS_KEY, JSON.stringify(D)); } catch (e) {}
}
/* ---------- Прогресс процесса ---------- */
function allFilesReady() {
for (var i = 0; i < D.files.length; i++) if (D.files[i].s !== "ready") return false;
return true;
}
function doneCount() {
var n = 0, i;
for (i = 0; i < D.violations.length; i++) if (D.violations[i].v === "conf") n++;
return n;
}
function stepNum() {
if (D.view === "upload") return D.running || allFilesReady() ? 2 : 1;
if (D.view === "results") return D.contra && !D.falseDone ? 2 : 3;
if (D.view === "verdict") return 3;
if (D.view === "approve") return D.status === "agree" || D.status === "decl" ? 5 : 4;
return 1;
}
var STEPS = ["Загрузка", "Анализ", "Заключение", "Согласование"];
/* ---------- UI: тосты, модалки ---------- */
var S = {};
function toast(msg, tone) {
var box = $("toasts");
if (!box) return;
var el = document.createElement("div");
el.className = "kt-ai-toast";
if (tone) el.dataset.tone = tone;
el.textContent = msg;
box.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(6px)";
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
}, 2600);
}
function openModal(html) { var m = $("modal"); m.innerHTML = html; m.style.display = "flex"; }
function closeModal() { var m = $("modal"); m.style.display = "none"; m.innerHTML = ""; }
S.closeModal = closeModal;
function openDrawer(html) { var d = $("drawer"); d.innerHTML = html; d.style.display = "block"; }
function closeDrawer() { var d = $("drawer"); d.style.display = "none"; d.innerHTML = ""; }
/* ---------- Навигация и рендер ---------- */
var NAV = [
{ id: "upload", label: "Загрузка и обзор", icon: "upload" },
{ id: "results", label: "Результаты анализа", icon: "sparkles" },
{ id: "verdict", label: "Формирование заключения", icon: "pencil" },
{ id: "approve", label: "Согласование", icon: "check-circle" },
{ id: "about", label: "О решении", icon: "info", group: "Решение" }
];
var TITLES = {
upload: "Загрузка и обзор материалов",
results: "Результаты анализа ИИ",
verdict: "Формирование заключения",
approve: "Согласование руководителем",
about: "О решении"
};
var SUBS = {
upload: "Пакет документов · первичное ознакомление",
results: "Сверка процесса · проверка квалификации",
verdict: "Верификация выводов · итоговый документ",
approve: "Итоговое заключение · решение руководителя",
about: "Паспорт ИИ-решения"
};
function renderNav() {
var nav = $("nav");
if (!nav) return;
var group = null, html = "";
for (var i = 0; i < NAV.length; i++) {
var n = NAV[i];
if (n.group && n.group !== group) {
html += '<div class="kt-ai-nav-group">' + esc(n.group) + "</div>";
group = n.group;
}
if (!n.group) group = null;
var active = D.view === n.id ? ' data-active="true"' : "";
html += '<div class="kt-ai-nav-item" role="link" tabindex="0" style="cursor:pointer"' + active +
'><svg class="kt-icon" aria-hidden="true"><use href="#' + n.icon + '"></use></svg>' +
'<span class="nav-label">' + esc(n.label) + "</span></div>";
(function (id) {
nav.lastChild.onclick = function () { S.go(id); };
})(n.id);
}
var sb = $("sbReq");
if (sb) sb.innerHTML = '<div class="sb-req-in"><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-2)">Текущая заявка</div><span class="kt-ai-meta">' + esc(D.req) + "</span></div>";
}
function render() {
var html;
if (D.view === "upload") html = vUpload();
else if (D.view === "results") html = vResults();
else if (D.view === "verdict") html = vVerdict();
else if (D.view === "approve") html = vApprove();
else html = vAbout();
var view = $("view");
if (!view) return;
view.innerHTML = html;
$("pageTitle").textContent = TITLES[D.view] || "";
$("pageSub").textContent = SUBS[D.view] || "";
$("roleChip").textContent = (D.view === "approve") ? "Руководитель" : "Эксперт";
renderNav();
save();
}
function vStepper() {
var cur = stepNum();
var html = '<div class="kt-ai-stepper">';
for (var i = 0; i < STEPS.length; i++) {
var st = i + 1 < cur ? "done" : (i + 1 === cur ? "current" : "");
var n = i + 1 < cur ? "✓" : String(i + 1);
html += '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + '><span class="n">' + n + "</span>" + esc(STEPS[i]) + "</div>";
if (i < STEPS.length - 1) html += '<span class="kt-ai-step-sep"></span>';
}
return html + "</div>";
}
function kpiStrip(items) {
return '<div class="kt-ai-kpi-strip">' + items.map(function (k) {
return '<div class="kt-ai-kpi"><span class="label">' + esc(k.l) + '</span><span class="value">' + k.v + "</span>" + (k.h ? '<span class="hint">' + esc(k.h) + "</span>" : "") + "</div>";
}).join("") + "</div>";
}
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */
function vUpload() {
var readyCount = 0;
for (var i = 0; i < D.files.length; i++) if (D.files[i].s === "ready") readyCount++;
var all = readyCount === D.files.length;
var head = "";
if (all && !D.running) {
head = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-ai-stack-block);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
'<span style="flex:1;min-width:0">Все материалы получены — пакет готов к запуску анализа</span>' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
}
var rows = D.files.map(function (f, i) {
return "<tr>" +
'<td data-label="Файл">' + esc(f.n) + '</td>' +
'<td data-label="Тип"><span class="kt-ai-meta">' + esc(f.t) + "</span></td>" +
'<td data-label="Загружен"><span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs)">' + esc(f.d) + "</span></td>" +
'<td data-label="Статус">' + pill(f.s) + "</td>" +
'<td><div class="row-act"><button class="kt-ai-icon-button" title="Просмотреть содержимое" onclick="S.viewFile(' + i + ")'>" + icon("eye") + "</button></div></td>" +
"</tr>";
}).join("");
return '<section class="sec"><div class="sec-h">Процесс' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">Заявка ' + num("ЭД-2026-118") + "</span></div>" + vStepper() + "</section>" +
head +
'<section class="sec"><div class="sec-h">Материалы' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(readyCount + " из " + D.files.length) + " готово</span></div>" +
'<div class="kt-ai-table-wrap" data-framed="true">' +
'<table class="kt-ai-table"><thead><tr><th>Файл</th><th>Тип</th><th>Загружен</th><th>Статус</th><th></th></tr></thead>' +
"<tbody>" + rows + "</tbody></table></div></section>" +
'<section class="sec"><div class="sec-h">Источники для сверки' +
'<span class="spacer"></span>' +
'<span class="kt-ai-meta">' + num(D.sources.length) + " источника</span></div>" +
'<div class="tiles">' + D.sources.map(function (s) {
var pct = s.u ? 100 : 82;
return '<div class="kt-ai-card src-card"><div class="src-name">' + esc(s.n) + "</div>" +
'<div class="src-meta"><span>Класс<span>' + esc(s.s) + "</span></span>" +
'<span>Разделов<span class="kt-ai-num">' + num(s.d) + "</span></span>" +
'<span>Обновлён<span class="kt-ai-num">01.09.2026</span></span></div>' +
'<div class="src-progress"><i style="width:' + pct + '%"></i></div>' +
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
}).join("") + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
"<p>Перед запуском анализа проверьте комплектность: для сверки нужны пояснительная записка и подтверждающие материалы. " +
"Анализ идёт по пяти источникам — законодательству и внутренним правилам. Статус «Готово к анализу» появляется после проверки каждого файла.</p></section>";
}
/* ---------- Экран 2. Результаты анализа ИИ ---------- */
function falseCount() {
var n = 0;
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "falsep") n++;
return n;
}
function vResults() {
var confN = doneCount(), falseN = falseCount();
var procN = 0, qualN = 0, contraN = 0;
for (var i = 0; i < D.violations.length; i++) {
var y = D.violations[i].y;
if (y === "process") procN++;
else if (y === "qual") qualN++;
else if (y === "contra") contraN++;
}
var kpis = kpiStrip([
{ l: "Несогласованностей", v: num(confN + falseN) },
{ l: "Процесс отбора", v: num(procN) },
{ l: "Квалификация", v: num(qualN) },
{ l: "Противоречий", v: num(contraN) }
]);
var banner = "";
if (D.running) return runScreen();
if (D.contra) {
var contraRow = null;
for (var ci = 0; ci < D.violations.length; ci++) if (D.violations[ci].y === "contra") contraRow = ci;
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
'<strong>Найдено противоречие между документами</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Протокол — голосование 4:1, Правила назначений — единогласно. ' +
(contraRow != null ? "Проверьте вывод №" + num(contraRow + 1) + " вручную." : "Противоречие разобрано.") + "</span></div>";
}
var typeTone = { process: "blue", qual: "purple", contra: "red" };
var rows = D.violations.map(function (v, i) {
return "<tr>" +
'<td data-label="Ссылка на пункт"><span class="kt-ai-meta">' + esc(v.r) + "</span></td>" +
'<td data-label="Нарушение">' + esc(v.d) + "</td>" +
'<td data-label="Тип"><span class="kt-ai-chip" data-tone="' + (typeTone[v.y] || "gray") + '">' + esc(ST[v.y].label) + "</span></td>" +
'<td data-label="Статус">' + pill(v.v) + "</td>" +
'<td><div class="row-act"><button class="kt-ai-icon-button" title="Открыть пункт нормативного акта" onclick="S.openRef(' + i + ')">' + icon("link") + "</button>" +
'<button class="kt-ai-btn" data-size="sm" onclick="S.refute(' + i + ')">' + (v.v === "falsep" ? "Вернуть" : "Ложное") + "</button></div></td>" +
"</tr>";
}).join("");
var logRows = D.log.map(function (l) {
return '<div class="logline"><span class="lt">' + esc(l.t) + '</span><span class="lb">' + esc(l.b) + "</span></div>";
}).join("");
var nextOk = true;
for (var k = 0; k < D.violations.length; k++) if (D.violations[k].v === "checked") nextOk = false;
var next = "";
if (nextOk) {
next = '<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'upload\')">Назад</button>' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'verdict\')">Перейти к подготовке заключения</button></div>';
}
return kpis + banner +
'<section class="sec"><div class="sec-h">Несогласованности</div>' +
'<div class="kt-ai-table-wrap" data-framed="true">' +
'<table class="kt-ai-table"><thead><tr><th>Ссылка на пункт</th><th>Нарушение</th><th>Тип</th><th>Статус</th><th>Действие</th></tr></thead>' +
"<tbody>" + rows + "</tbody></table></div>" + next + "</section>" +
'<section class="sec"><div class="sec-h">Журнал действия ИИ' +
'<span class="spacer"></span><span class="kt-ai-meta">10:42 — 10:46</span></div>' +
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + logRows + "</div></section>" +
'<section class="kt-ai-conclusion"><h3>Что показывает анализ</h3>' +
"<p>Система сверяет процесс отбора с внутренними документами и законодательством, затем проверяет квалификацию кандидата по должностной инструкции. " +
"Каждый вывод ссылается на пункт нормативного акта — эксперт проверяет ссылки и при необходимости отмечает ложные срабатывания.</p></section>";
}
function runScreen() {
var steps = [
"Извлечение текста из пакета документов",
"Сверка с законодательством и внутренними документами",
"Проверка квалификации кандидата",
"Формирование вывода"
];
var rows = steps.map(function (s, i) {
return '<div class="logline"><span class="lt">' + esc(stepsTime[i] || "10:4" + i) + '</span><span class="lb">' + esc(s) + "</span></div>";
}).join("");
return '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-block);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
'<span style="flex:1;min-width:0">Идёт анализ пакета — ожидайте</span></div>' +
'<section class="sec"><div class="sec-h">Ход работы' +
'<span class="spacer"></span><span class="kt-ai-status-pill" data-status="info">Идёт анализ</span></div>' +
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + rows + "</div>" +
"<p class='kt-ai-meta' style='margin-top:var(--kt-ai-space-3)'>Анализ занимает обычно 3–5 минут. Можно закрыть вкладку — прогресс сохранится.</p></section>";
}
var stepsTime = ["10:46", "10:46", "10:46", "10:46"];
/* ---------- Экран 3. Формирование заключения ---------- */
function vVerdict() {
var checks = D.checklist.map(function (idx) {
var v = D.violations[idx];
if (!v) return "";
return '<label class="chk-row" for="ck' + idx + '">' +
'<input type="checkbox" id="ck' + idx + '"' + (D.checklist.indexOf(idx) >= 0 ? " checked" : "") + ' onchange="S.ck(' + idx + ')">' +
'<span class="chk-main"><span class="chk-desc">' + esc(v.d) + "</span>" +
'<span class="chk-ref kt-ai-meta">' + icon("link") + esc(v.r) + "</span></span></label>";
}).join("");
var sent = D.status === "sent";
var status = sent ? pill("sent") : pill("draft");
var docText = $("docText") && $("docText").textContent ? $("docText").textContent : D.verdict;
if (docText !== D.verdict) D.verdict = docText;
return vStepper() +
'<section class="sec"><div class="sec-h">Итоговое заключение' +
'<span class="spacer"></span>' + status + "</div>" +
'<div class="doc-paper" id="docPaper">' +
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ' + esc("ЭД-2026-118") + " · SEO-1, управление сетевых сервисов</span></div>" +
'<div class="doc-text" id="docText" contenteditable="true" spellcheck="false">' + esc(docText) + "</div></div>" +
'<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'results\')">Назад к анализу</button>' +
(sent
? '<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'approve\')">К согласованию</button>'
: '<button class="kt-ai-btn" data-variant="primary" onclick="S.sendForApproval()">Отправить на согласование руководителю</button>') +
"</div></section>" +
'<section class="sec"><div class="sec-h">Чек-лист подтвержденных нарушений' +
'<span class="spacer"></span><span class="kt-ai-meta">' + num(D.checklist.length + " из " + D.violations.length) + " включены в заключение</span></div>" +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
'<section class="sec"><div class="sec-h" style="margin-bottom:var(--kt-ai-space-4)"></div>' +
'<button class="kt-ai-btn" data-variant="danger" onclick="S.manualMode()">Вернуть в ручной режим</button>' +
'<span class="kt-ai-meta" style="margin-left:var(--kt-ai-space-4)">Без системы — вручную по действующим правилам</span></section>';
}
/* ---------- Экран 4. Согласование руководителем ---------- */
function vApprove() {
var st = D.status;
var banner = "";
var btns = "";
var kpis = kpiStrip([
{ l: "Заявка", v: num("ЭД-2026-118") },
{ l: "Подтверждено", v: num(D.checklist.length) },
{ l: "Исключено", v: num(D.violations.length - D.checklist.length) },
{ l: "Статус", v: ST[st] ? esc(ST[st].label) : "—" }
]);
if (st === "agree") {
banner = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-stack-block)"><strong>Материал согласован</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Согласовано ' + num(D.stamp || "15.09.2026, 09:12") + " · руководитель</span></div>";
} else if (st === "decl") {
banner = '<div class="kt-ai-banner" data-tone="risk" style="margin-bottom:var(--kt-ai-stack-block)"><strong>Материал отклонён</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">' + num(D.stamp || "15.09.2026") + ' · ' + esc(D.rejected || "Без комментария") + "</span></div>";
} else {
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
'<strong>Ожидает решения руководителя</strong>' +
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Проверены материалы на назначение руководителя SEO-1 · ' + num("14.09.2026") + "</span></div>";
btns = '<div class="row-btns" style="margin-top:var(--kt-ai-stack-block)">' +
'<button class="kt-ai-btn" data-variant="primary" onclick="S.approve()">Согласовать документы</button>' +
'<button class="kt-ai-btn" data-variant="danger" onclick="S.declare()">Отклонить с комментарием</button></div>';
}
var checks = D.checklist.map(function (idx) {
var v = D.violations[idx];
if (!v) return "";
return '<div class="logline"><span class="lt">✓</span><span class="lb">' + esc(v.d) + ' <span class="kt-ai-meta">' + esc(v.r) + "</span></span></div>";
}).join("");
return kpis + banner +
'<section class="sec"><div class="sec-h">Итоговое заключение</div>' +
'<div class="doc-paper" style="pointer-events:none">' +
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ЭД-2026-118 · SEO-1, управление сетевых сервисов</span></div>' +
'<div class="doc-text" style="border-color:transparent;background:transparent;padding:0">' + esc(D.verdict) + "</div></div></section>" +
'<section class="sec"><div class="sec-h">Подтвержденные нарушения</div>' +
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
btns;
}
/* ---------- Экран 5. О решении ---------- */
function vAbout() {
return '<section class="sec"><div class="sec-h" style="font-size:var(--kt-ai-text-xl)">О решении</div></section>' +
'<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-stack-block)"><div class="mini" style="padding:0"><div class="kv">' +
'<span class="k">Наименование</span><span class="v">Экспертиза документов для советов</span></div><div class="kv">' +
'<span class="k">Исполнитель</span><span class="v">Владелец процесса — сборка на платформе Vibe AI</span></div><div class="kv">' +
'<span class="k">Пользователь</span><span class="v">Эксперт, руководитель</span></div><div class="kv">' +
'<span class="k">Дата внедрения</span><span class="v">—</span></div></div></div>' +
'<div class="pass-grid" style="margin-bottom:var(--kt-ai-stack-block)">' +
passCard("Предпосылки", "<ul><li>Необходимость читать большое количество внутренних документов и законодательных актов</li><li>Необходимость сравнивать документы на соответствие нормативным требованиям</li></ul>") +
passCard("Входные данные и источники", "<ul><li>Трудовой кодекс Республики Казахстан</li><li>Закон «Об акционерных обществах»</li><li>Правила трудового распорядка</li><li>Правила поиска и подбора персонала</li><li>Корпоративный стандарт по управлению человеческими ресурсами фонда</li></ul>") +
passCard("Процессы для внедрения ИИ", "<ul><li>Сверка пояснительной записки с фактическим процессом отбора</li><li>Проверка соответствия кандидата квалификационным требованиям</li><li>Поиск несоответствий и противоречий в документах</li></ul>") +
passCard("Результат", "<ul><li>Согласование документов со списком нарушений и ссылками на пункты законов и внутренних документов</li><li>Заключение со списком нарушений и ссылками на пункты законов и внутренних документов</li></ul>") +
passCard("Ожидаемый эффект", "<ul><li>Сокращение времени рассмотрения материалов</li><li>Повышение качества проверки за счёт снижения риска человеческих ошибок</li></ul>") +
passCard("Экономический эффект", "<div class='kv'><span class='k'>Объём</span><span class='v'>20 экспертиз в месяц во всех экспертах</span></div><div class='kv'><span class='k'>Высвобождение</span><span class='v'>50 часов в месяц (оценка Төре)</span></div>") +
passCard("Контроль и риски", "<ul><li>Возможность ошибки ИИ при противоречиях в разных внутренних документах</li><li>Необходимость проверки выводов экспертом</li></ul>") +
passCard("Меры по снижению", "<ul><li>Проверка результата человеком перед использованием</li><li>Журнал действий ИИ и возможность вернуться в ручной режим</li></ul>") +
passCard("Формат реализации", "<ul><li>ИИ-агент на платформе Alem; при необходимости — простой веб-экран на Vibe AI</li><li>Экраны макета: Загрузка и обзор; Результаты анализа; Формирование заключения; Согласование руководителем</li></ul>") +
"</div>" +
'<section class="kt-ai-conclusion"><h3>Путь сотрудника</h3>' +
"<p>Эксперт загружает документы на экране «Загрузка и обзор материалов», переходит на «Результаты анализа ИИ» и проверяет выводы, " +
"формировает итоговое заключение на «Формировании заключения», руководитель просматривает и согласует документы на «Согласовании у руководителя».</p></section>";
}
function passCard(title, body) {
return '<div class="kt-ai-card mini"><h4>' + esc(title) + "</h4>" + body + "</div>";
}
function nowTime() {
var d = new Date();
function p(x) { return (x < 10 ? "0" : "") + x; }
return p(d.getHours()) + ":" + p(d.getMinutes());
}
/* ---------- Действия ---------- */
S.go = function (id) { D.view = id; render(); };
S.analyze = function () {
if (!allFilesReady()) { toast("Дополните материалы — все файлы должны быть готовы", "warn"); return; }
D.running = true;
D.contra = false;
D.falseDone = false;
D.checklist = [0, 1, 2, 3, 4, 5, 6, 7];
D.status = "draft";
D.rejected = null;
D.stamp = null;
for (var i = 0; i < D.violations.length; i++) D.violations[i].v = D.violations[i].v === "conf" ? "checked" : "checked";
D.violations[6].v = "falsep";
D.violations[7].v = "falsep";
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
D.log = [
{ t: nowTime(), b: "Пакет принят, 8 файлов" },
{ t: nowTime(), b: "Запущен анализ: " + D.sources.length + " источника для сверки" }
];
render();
setTimeout(function () { pushLogs(); }, 900);
setTimeout(function () { pushLogs(); }, 2100);
setTimeout(function () {
D.running = false;
D.violations[6].v = "falsep";
D.contra = true;
D.log.push({ t: nowTime(), b: "Обнаружено несоответствий: " + (D.violations.length - 2) + ", противоречий: 1" });
D.log.push({ t: nowTime(), b: "Журнал завершён — выводы переданы эксперту" });
S.go("results");
}, 3600);
};
function pushLogs() {
var extra = [
{ t: nowTime(), b: "Извлечение текста: пояснительная записка" },
{ t: nowTime(), b: "Сверка с Правилами поиска и подбора персонала, п. 4.2" },
{ t: nowTime(), b: "Найдено отклонение: дата объявления о вакансии" },
{ t: nowTime(), b: "Сверка с Законом «Об АО», ст. 36" },
{ t: nowTime(), b: "Проверка квалификации кандидата" }
];
if (D.log.length <= 2) { D.log.push(extra[0]); D.log.push(extra[1]); }
else if (D.log.length <= 4) { D.log.push(extra[2]); D.log.push(extra[3]); }
else { D.log.push(extra[4]); }
save();
render();
}
S.closeDrawer = closeDrawer;
S.viewFile = function (i) {
var f = D.files[i];
openDrawer(
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-5)">' +
"<strong>" + esc(f.n) + "</strong>" + pill(f.s) +
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeDrawer()" style="margin-left:auto">' + icon("x") + "</button></div>" +
'<div class="doc-paper">' +
'<div class="doc-head"><strong>' + esc(f.t) + '</strong><span class="doc-id">' + esc(f.d) + "</span></div>" +
'<div class="doc-text" style="border:none;background:transparent;padding:0;font-size:var(--kt-ai-text-sm)">' +
"<p>Документ загружен во внутреннюю систему. Эксперт просматривает комплектность и при необходимости открывает исходник в корпоративном хранилище.</p>" +
"<p style='margin-top:var(--kt-ai-space-3)'>Статус: " + (ST[f.s] ? esc(ST[f.s].label) : esc(f.s)) + ".</p>" +
"</div></div>"
);
};
S.refute = function (i) {
var v = D.violations[i];
v.v = v.v === "falsep" ? "checked" : "falsep";
if (v.y === "contra") {
D.contra = v.v === "falsep";
D.falseDone = v.v === "falsep";
}
D.checklist.forEach(function (idx, pos) {
if (D.violations[idx].v === "falsep") { D.checklist.splice(pos, 1); pos--; }
});
render();
toast(v.v === "falsep" ? "Вывод отмечен как ложное срабатывание" : "Вывод возвращён в работу", v.v === "falsep" ? "warn" : "ok");
};
S.ck = function (idx) {
var pos = D.checklist.indexOf(idx);
var el = $("ck" + idx);
if (el && el.checked && pos < 0) D.checklist.push(idx);
if (el && !el.checked && pos >= 0) D.checklist.splice(pos, 1);
D.checklist.sort(function (a, b) { return a - b; });
save();
// обновить счётчик без перерисовки чек-бокса
render();
};
function rebuildVerdict() {
var parts = D.checklist.map(function (idx) {
var v = D.violations[idx];
return "\n" + (parts.indexOf(idx) + 1) + ". " + v.d + " (" + v.r + ").";
});
var base = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + D.candidate + ") прошла проверку соответствия нормативной базе.";
if (D.checklist.length > 0) {
base += " По завершённой сверке зафиксированы следующие несоответствия:" + parts.join("") +
"\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.";
} else {
base += " Несоответствий не выявлено.";
}
return base;
}
S.sendForApproval = function () {
var n = 0;
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "checked") n++;
if (n > 0) { toast("Проверьте все выводы: " + n + " на проверке", "warn"); return; }
D.status = "sent";
D.rejected = null;
D.stamp = null;
D.verdict = rebuildVerdict();
render();
toast("Заключение отправлено на согласование руководителю", "ok");
};
S.approve = function () {
D.status = "agree";
D.stamp = nowTime();
render();
toast("Материал согласован", "ok");
};
S.declare = function () {
var m = prompt("Комментарий к отклонению:");
if (m === null) return;
D.status = "decl";
D.stamp = nowTime();
D.rejected = m || "Без комментария";
render();
toast("Материал отклонён", "warn");
};
S.manualMode = function () {
if (confirm("Вернуться в ручной режим? Текущий черновик заключения сохранится.")) {
toast("Ручной режим: работа ведётся без системы", "info");
closeModal();
}
};
S.openRef = function (i) {
var v = D.violations[i];
openModal(
'<div class="kt-ai-card" style="max-width:560px;padding:var(--kt-ai-space-6)">' +
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-4)">' +
icon("link") + "<strong>Ссылка на нормативный акт</strong>" +
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeModal()" style="margin-left:auto">' + icon("x") + "</button></div>" +
'<div class="mini" style="padding:0"><div class="kv"><span class="k">Пункт</span><span class="v">' + esc(v.r) + "</span></div>" +
"<div class='kv' style='display:block'><span class='k'>Вывод системы</span><span class='v' style='text-align:left;margin-top:var(--kt-ai-space-2)'>" + esc(v.d) + "</span></div></div>" +
'</div>'
);
};
S.burger = function () {
var sb = $("sidebar");
if (!sb) return;
if (sb.dataset.open === "true") sb.removeAttribute("data-open");
else sb.setAttribute("data-open", "true");
};
/* ---------- Инициализация ---------- */
load();
render();