642 lines
43 KiB
JavaScript
642 lines
43 KiB
JavaScript
"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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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();
|