/* ===== Сверка документов при назначении руководителей ===== Статичный прототип: данные демонстрационные, состояние в 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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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 ''; } function kpi(value, label, hint) { return '
' + esc(value) + '' + esc(label) + "" + (hint ? '' + esc(hint) + "" : "") + "
"; } function pill(label, tone) { return '' + esc(label) + ""; } 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 += '"; }); 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 '
' + '
' + '
' + '
' + esc(c.name) + "
" + '
' + esc(c.post) + " · Пакет: " + esc(c.submitted) + "
" + "
" + pill(STAGE_CHIP[cand().stage] || "—", cand().stage === "sent" ? "warn" : cand().stage === "approved" ? "ok" : "info") + "
" + '
' + STEPS.map(function (s, i) { var st = (i + 1 < curStage()) ? "done" : (i + 1 === curStage()) ? "current" : ""; return '
" + (i + 1) + "" + '' + esc(s.label) + "
" + (i < STEPS.length - 1 ? '
' : ""); }).join("") + "
" + (extra || "") + "
"; } /* ============================================================ ЭКРАН 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 '
' + '' + icon(ic) + "" + '' + t + "" + '' + esc(fname) + "" + '' + esc(sub) + "" + '" + "
"; }).join(""); var rows = files.map(function (f, i) { var st = f.status === "done" ? "ok" : "warn"; var stLabel = f.status === "done" ? "Обработан" : "В обработке"; return '
' + '' + icon("file") + "" + '
' + esc(f.name) + "" + 'Тип: ' + esc(f.type) + "
" + '' + stLabel + "" + '
"; }).join(""); var btns = '" + ''; return '
' + candHead() + '
' + kpi(String(files.length) + " / 3", "Документов загружено", "пакет кандидата") + kpi("150 мин", "Труда без автоматизации", "на одно назначение") + kpi("3 р.д", "Срок подготовки заключения", "по регламенту") + "
" + '
' + '
' + '

Новая сверка

' + '
' + "Заполните сведения о кандидате и загрузите комплект документов — система сверит их с нормативами и найдёт расхождения.
" + '
' + '
' + '
' + '
' + '
' + "
" + '
' + 'Загрузка документов' + '
' + '3 обязательных файла
' + '
' + zoneHtml + "
" + '
' + 'Загруженные файлы' + '
' + 'Ожидание загрузки / обработки
' + '
' + rows + "
" + '
' + btns + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 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") ? ('Норматив: ' + esc(v.norm) + '') : 'Расхождение в данных документов'; var body = '
' + icon(t === "contra" ? "compare" : "scale") + "
" + '
' + '' + tLabel + "" + '
' + esc(v.desc) + "
" + '
Документ: ' + esc(v.doc) + " · Пункт: " + esc(v.item) + "
" + normLine + "
"; var actions = ch === "pending" ? '" + '' : ch === "confirmed" ? '' + pill("Подтверждено", "ok") : pill("Отклонено", "muted") + ''; return '
' + body + '
' + actions + "
"; } 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" ? '
Сверка завершена: найдено ' + VIOL.length + " несоответствий и " + VIOL.filter(function(v){return v.type==="contra"}).length + " расхождений" + '22.04 с
' : '
Анализ не завершён: один документ не распознан' + '
"; return '
' + candHead() + banner + '
' + kpi(String(VIOL.length), "Найдено несоответствий", "сверка с нормативами") + kpi(String(confirmed), "Подтверждено", "экспертом") + kpi(String(rejected), "Отклонено", "ложные срабатывания") + kpi(String(pending), "Осталось проверить", "к ручному осмотру") + "
" + '
' + '
' + '
' + 'Выводы ИИ · ' + (done + "/" + VIOL.length) + " проверено" + '
' + '" + '" + "
" + '
' + '
' + '" + 'В заключение войдут ' + confirmed + " подтверждённых" + (confirmed === 1 ? " пункт" : confirmed >= 2 && confirmed <= 4 ? " пункта" : " пунктов") + "" + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 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 += '
' + "
" + c + ". " + esc(v.desc) + "
" + '
' + esc(v.doc) + " · " + esc(v.item) + " · " + normTxt + "
"; } }); if (c === 0) html = '
Пока ни одного подтверждённогоВернитесь на «Анализ ИИ» и подтвердите несоответствия — они попадут в заключение.
'; 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("" + esc(ref.r) + "" + esc(ref.p) + ""); } } }); if (!out.length) return '
Ссылки появятся после подтверждения нормативных несоответствий.
'; return out.map(function (h) { return '
' + icon("link") + h + "
"; }).join(""); } function renderConcl() { var c = cand(); var tone = c.stage === "sent" ? "warn" : "gray"; return '
' + candHead() + '
' + '
' + '
' + '
' + 'Текст заключения' + '
' + pill(c.stage === "sent" ? "На согласовании" : "Черновик", tone) + "
" + '' + '
' + '" + '" + "
" + '
Руководитель получит копию на согласование, кадровая служба — копию.
' + "
" + '
' + '
' + '
' + 'Подтверждённые несоответствия (' + (function(){var n=0;VIOL.forEach(function(v){if(S.checks[v.id]==="confirmed")n++;});return n;})() + ")" + '
' + 'из «Анализ ИИ»' + "
" + '
' + confirmedViolHtml() + "
" + "
" + '
' + '
Пункты нормативов
' + normativeRefsHtml() + "
" + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 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" ? '
Заключение согласовано ' + esc(c.approvedAt || "сегодня") + ". Кадровая служба получит уведомление.
" : c.stage === "sent" ? '
' + '
' + '
' + '" + '" + "
" : '
Черновик возвращён эксперту на доработку. Дождитесь новой отправки.
'; var sentLine = c.sent ? "Отправлено: " + esc(c.sent) : "Отправлено: —"; var approvedLine = c.stage === "approved" ? "Согласовано руководителем: " + esc(c.approvedAt || "—") : "Согласование: ожидается"; return '
' + candHead() + '
' + '
' + kpi(esc(c.name.split(" ")[0] + " " + c.name.split(" ")[1]), "Кандидат", "на согласовании") + kpi(sentLine.split(":")[1] || "—", "Отправлено", "экспертом") + kpi(c.sent ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") + "
" + '
' + '
' + '
' + '
Заключение эксперта
' + '
' + sentLine + " · " + approvedLine + "
" + "
" + chip + "
" + '
' + '
Подтверждённые несоответствия
' + confirmedViolHtml() + actions + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 5 — О РЕШЕНИИ ============================================================ */ function renderAbout() { var proc = [ { t: "Эксперт получает пакет документов", who: "от кадровой службы" }, { t: "Эксперт читает пояснительную записку" }, { t: "ИИ сверяет документы с квалификационными требованиями", ai: true }, { t: "ИИ ищет расхождения в документах", ai: true }, { t: "Эксперт проверяет выводы ИИ" }, { t: "Эксперт готовит заключение с несоответствиями и ссылками" }, { t: "Руководитель согласовывает заключение" } ]; return '
' + '
' + '
Паспорт ИИ-решения
' + '
Ускоряет подготовку заключения о соответствии кандидата за счёт автоматизации сверки и поиска расхождений.
' + '
' + '
' + kpi("~50 ч/мес", "Экономия", "20 случаев × 150 мин") + kpi("3 р.д → 1 день", "Срок подготовки", "заключения") + kpi("20 случаев", "Объём в месяц", "оценка владельца процесса") + kpi("—", "Дата внедрения", "после согласования макета") + "
" + "
" + '
' + '
' + '
Поэтапный эффект
' + '
' + '
Сокращение чтения вручную
эксперт осматривает уже только выводы ИИ
' + '
Запасание расхождений при сверке
нормативы и требования — в одном месте
' + '
Стабильный срок заключения
руководитель получает в тот же день
' + "
" + "
" + '
' + '
Контроль и риски
' + '
' + '
Ошибочное срабатывание ИИ
в случаях расхождений данных
' + '
Митигация — ручная проверка
каждый вывод подтверждает или отклоняет эксперт
' + "
" + "
" + "
" + '
' + '
Данные на входе
' + '
' + '
' + icon("file") + '
Комплект документов кандидатадолжностная инструкция, пояснительная записка, анкета
' + '
' + icon("book") + '
Внутренние нормативыпапка на общем диске
' + "
" + "
" + '
' + '
Процесс
' + '
' + proc.map(function (p, i) { return '
' + (i + 1) + '' + "
" + esc(p.t) + '
' + (p.ai ? "ИИ" : (p.who ? esc(p.who) : "эксперт / руководитель")) + "
"; }).join("") + "
" + "
" + '
' + '
' + '
Роль пользователя
' + '
' + '
Эксперт
загружает документы, проверяет выводы ИИ, готовит и отправляет заключение
' + '
Руководитель
просматривает итог и согласовывает
' + '
Кадровая служба
передаст пакет документов и получает копию заключения
' + "
" + "
" + '
' + '
Что выполняет ИИ
' + '
' + '
' + icon("scale") + '
Сверка с квалификационными требованиями и нормативами
' + '
' + icon("compare") + '
Поиск расхождений в документах
' + '
Человек решает: проверка выводов, написание текста, согласование.
' + "
" + "
" + "
" + '
' + kpi("Владелец процесса", "Исполнитель", "—") + kpi("Ветеринация на Vibe AI", "Формат", "макет → рабочее решение") + kpi("ИИ-агент Alem", "Опция", "работа с документами") + "
" + "
" + ""; } /* ============================================================ РЕНДЕР ============================================================ */ 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; }