/* ===== Сверка документов при назначении руководителей ===== Рабочее решение: данные сохраняются на сервере (data.json), состояние карточки — её id. */ "use strict"; /* ---------- Справочники ---------- */ 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 STEPS = [ { label: "Загрузка пакета" }, { label: "Анализ ИИ" }, { label: "Заключение" }, { label: "Согласование" } ]; var SCREENS = { load: { title: "Загрузка пакета документов", sub: "Новая сверка · комплект кандидата" }, analysis: { title: "Анализ ИИ", sub: "Сверка с нормативами · поиск противоречий" }, concl: { title: "Заключение", sub: "Проект заключения эксперта" }, approve: { title: "Согласование", sub: "Утверждение заключения руководителем" }, about: { title: "О решении", sub: "Паспорт ИИ-решения" }, list: { title: "Сверки", sub: "Список проверок кандидатов" } }; var STAGE_CHIP = { new: "Ожидание загрузки", uploaded: "Загружен", analyzed: "Анализ завершён", draft: "Черновик", sent: "На согласовании", approved: "Согласовано", returned: "На доработке" }; var STAGE_TONE = { new: "gray", uploaded: "info", analyzed: "info", draft: "blue", sent: "warn", approved: "ok", returned: "risk" }; /* ---------- Состояние ---------- */ var S = { role: "expert", screen: "list", candId: null, filter: "" }; /* ---------- API ---------- */ function api(method, url, data) { return fetch(url, { method: method, headers: { "Content-Type": "application/json" }, body: data ? JSON.stringify(data) : undefined }).then(function (r) { if (!r.ok) throw new Error("API " + r.status); return r.json(); }); } function allCards() { return api("GET", "api/cards"); } function getCard(id) { return api("GET", "api/cards/" + id); } function saveCard(id, patch) { return api("PUT", "api/cards/" + id, patch); } function createCard(b) { return api("POST", "api/cards", b); } /* ---------- Утилиты ---------- */ var $ = function (id) { return document.getElementById(id); }; function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function cur() { return S.curCard; } /* Выводы по карточке: если ещё не прогоняли ИИ — пустой список; если прогнали (stage != uploaded) — список с сохранёнными статусами проверки */ function violationsOf(card) { if (!card) return []; if (card.stage !== "analyzed" && card.stage !== "draft" && card.stage !== "sent" && card.stage !== "approved" && card.stage !== "returned") { if (card.violations && card.violations.length) return card.violations; return []; } if (card.violations && card.violations.length) { var byId = {}; card.violations.forEach(function (v) { byId[String(v.id)] = v; }); return VIOL.map(function (t) { var saved = byId[String(t.id)]; return saved ? { doc: saved.doc, item: saved.item, desc: saved.desc, type: saved.type, norm: saved.norm, normId: saved.normId, id: t.id, check: saved.check } : { doc: t.doc, item: t.item, desc: t.desc, type: t.type, norm: t.norm, normId: t.normId, id: t.id, check: "pending" }; }); } return []; } function confirmCount(card) { return violationsOf(card).filter(function (v) { return v.check === "confirmed"; }).length; } function pendingCount(card) { return violationsOf(card).filter(function (v) { return v.check === "pending"; }).length; } 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 fmtDate(iso) { if (!iso) return "—"; var d = new Date(iso); var dd = ("0" + d.getDate()).slice(-2); var mm = ("0" + (d.getMonth() + 1)).slice(-2); return dd + "." + mm + "." + d.getFullYear(); } function fmtDT(iso) { if (!iso) return "—"; var d = new Date(iso); var dd = ("0" + d.getDate()).slice(-2); var mm = ("0" + (d.getMonth() + 1)).slice(-2); var hh = ("0" + d.getHours()).slice(-2); var mi = ("0" + d.getMinutes()).slice(-2); return dd + "." + mm + "." + d.getFullYear() + ", " + hh + ":" + mi; } 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); } /* ---------- Выводы ИИ (сверка с нормативами и поиск противоречий) ---------- */ 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: 0 }, { id: 6, type: "norm", doc: "Анкета кандидата", item: "Раздел 1 «Личные сведения»", desc: "Отсутствие сведений о судимости (заполнено «нет данных»)", norm: "ТК РК, ст. 68, п. 2", normId: 4 }, { id: 7, type: "norm", doc: "Должностная инструкция", item: "Приложение 1", desc: "Квалификационный разряд ниже требований к должности", norm: "Должностной регламент фонда, п. 5.1", normId: 6 }, { id: 8, type: "norm", doc: "Пояснительная записка", item: "Пункт 4", desc: "Указанный руководитель не подтверждён штатным расписанием", norm: "Должностной регламент фонда, п. 9.3", normId: 7 }, { id: 9, type: "contra", doc: "Анкета кандидата / ПЗ", item: "Раздел 2 / Пункт 3", desc: "Различаются периоды работы в одной организации", norm: "", normId: 0 }, { id: 10, type: "norm", doc: "Анкета кандидата", item: "Раздел 2 «Опыт работы»", desc: "Не подтверждён стаж работы по смежной специальности", norm: "Закон об АО, ст. 45", normId: 1 }, { id: 11, type: "contra", doc: "Пояснительная записка / ДИ", item: "Пункт 2 / Пункт 4.2", desc: "Совокупная нагрузка по должности указана расхождением", norm: "", normId: 0 }, { id: 12, type: "norm", doc: "Пояснительная записка", item: "Пункт 5", desc: "Отсутствует ссылка на приказ о назначении", norm: "Должностной регламент фонда, п. 3.7", normId: 8 } ]; function buildChecks(card) { var res = {}; var i; for (i = 0; i < VIOL.length; i++) { var id = String(VIOL[i].id); if (card && card.checks && card.checks[id]) res[id] = card.checks[id]; else res[id] = (i < 5) ? "confirmed" : (i === 5 ? "rejected" : "pending"); } return res; } /* ---------- Навигация ---------- */ function navHtml() { var items = [ { id: "list", label: "Сверки", ic: "list" }, { 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; } function curStage() { if (!cur()) return 1; if (S.screen === "load") return 1; if (S.screen === "analysis") return 2; if (S.screen === "concl") return 3; if (S.screen === "approve") return 4; var st = cur().stage; if (st === "sent" || st === "approved" || st === "returned") return 4; return 1; } /* ============================================================ ЭКРАН «СВЕРКИ» — список проверок ============================================================ */ function renderList() { var cards = S.cards || []; var f = S.filter; var filtered = cards.filter(function (c) { if (!f) return true; return (c.name + " " + (c.post || "")).toLowerCase().indexOf(f) !== -1; }); var rows = filtered.map(function (c) { var v = violationsOf(c); var confirmed = v.filter(function (x) { return x.check === "confirmed"; }).length; return '
' + '' + icon("user") + "" + '
' + esc(c.name) + "" + '' + esc(c.post || "") + " · Пакет: " + esc(c.submitted) + "
" + pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") + '' + confirmed + " найдено" + "
"; }).join(""); if (!rows) rows = '
Ничего не найденоСмените запрос.
'; return '
' + '
' + kpi(String(cards.length), "Всего сверок", "пакеты кандидатов") + kpi(String(cards.filter(function (c) { return c.stage === "sent"; }).length), "На согласовании", "у руководителя") + kpi(String(cards.filter(function (c) { return c.stage === "approved"; }).length), "Согласовано", "итого") + kpi(String(cards.filter(function (c) { return c.stage === "draft" || c.stage === "analyzed"; }).length), "В работе", "у эксперта") + "
" + '
' + '
' + '
' + '' + '
' + '" + '" + "
" + '
' + rows + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 1 — ЗАГРУЗКА ПАКЕТА ДОКУМЕНТОВ ============================================================ */ var FILE_TYPES = ["ДИ", "ПЗ", "Анкета"]; function filesOf(card) { if (card.files && card.files.length) return card.files; return FILE_TYPES.map(function (t) { return { type: t, name: "", status: "missing" }; }); } function filesComplete(card) { return filesOf(card).every(function (f) { return f.status === "done"; }); } function renderLoad() { var c = cur(); var files = c.files && c.files.length ? c.files : FILE_TYPES.map(function (t) { return { type: t, name: "", status: "missing" }; }); var missing = files.filter(function (f) { return f.status === "missing"; }).length; var complete = missing === 0; var btn = complete ? '" : 'Осталось загрузить: ' + missing + ""; var zoneHtml = files.map(function (f) { var has = f.status === "done"; var ic = has ? "file" : "upload"; var fname = f.name || (f.status === "pending" ? "Обработка файла…" : "Файл ещё не загружен"); var sub = has ? "Обработан" : (f.status === "pending" ? "Обработка…" : "Выбрать файл"); return '
' + '' + icon(ic) + "" + '' + esc(f.type) + "" + '' + esc(fname) + "" + '' + esc(sub) + "" + '' + '" + "
"; }).join(""); var rows = files.map(function (f) { if (f.status === "missing") return ""; return '
' + '' + icon("file") + "" + '
' + esc(f.name) + "" + 'Тип: ' + esc(f.type) + " · " + esc(f.date || "") + "
" + '
'; }).join(""); return '
' + '
' + kpi(String(files.length - missing) + " / " + files.length, "Документов загружено", "пакет кандидата") + kpi("150 мин", "Труда без автоматизации", "на одно назначение") + kpi("3 р.д", "Срок подготовки заключения", "по регламенту") + "
" + '
' + '
' + '

Новая сверка

' + '
' + "Заполните сведения о кандидате и загрузите комплект документов — система сверит их с нормативами и найдёт расхождения.
" + '
' + '
' + '
' + '
' + '
' + "
" + '
' + 'Загрузка документов' + '
' + '3 обязательных файла
' + '
' + zoneHtml + "
" + (missing ? "" : '
' + 'Загруженные файлы' + '
' + '
' + rows + "
") + '
' + btn + "
" + "
" + "
" + "
"; } /* ---------- Шапка-карточка кандидата ---------- */ function candHead() { var c = cur(); return '
' + '
' + '
' + '
' + esc(c.name) + "
" + '
' + esc(c.post) + " · Пакет: " + esc(c.submitted) + "
" + "
" + pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") + "
" + '
' + 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("") + "
" + "
"; } /* ============================================================ ЭКРАН 2 — АНАЛИЗ ИИ И ПРОВЕРКА ВЫВОДОВ ============================================================ */ function violCard(v) { var ch = v.check || "pending"; 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 applyViolFilters() { var c = cur(); var list = $("violList"); if (!c || !list) return; var typeEl = $("vType"), stateEl = $("vState"); var ty = typeEl ? typeEl.value : "all"; var st = stateEl ? stateEl.value : "all"; var html = ""; violationsOf(c).forEach(function (v) { if (ty !== "all" && v.type !== ty) return; if (st !== "all" && (v.check || "pending") !== st) return; html += violCard(v); }); if (!html) html = '
По фильтру ничего нетСмените тип нарушения или статус проверки.
'; list.innerHTML = html; } function renderAnalysis() { var c = cur(); var vs = violationsOf(c); var confirmed = vs.filter(function (v) { return v.check === "confirmed"; }).length; var rejected = vs.filter(function (v) { return v.check === "rejected"; }).length; var pending = vs.filter(function (v) { return v.check === "pending"; }).length; return '
' + candHead() + '
Сверка завершена: ' + vs.length + " выводов — сверка с нормативами и поиск противоречий" + '22.04 с
' + '
' + kpi(String(vs.length), "Найдено выводов", "сверка с нормативами") + kpi(String(confirmed), "Подтверждено", "экспертом") + kpi(String(rejected), "Отклонено", "ложные срабатывания") + kpi(String(pending), "Осталось проверить", "к ручному осмотру") + "
" + '
' + '
' + '
' + 'Выводы ИИ · ' + (vs.length - pending + "/" + vs.length) + " проверено" + '
' + '" + '" + "
" + '
' + '
' + '" + 'В заключение войдут ' + confirmed + " подтверждённых" + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 3 — СОСТАВЛЕНИЕ ЗАКЛЮЧЕНИЯ ============================================================ */ function confirmedViolHtml(card) { var c = 0; var html = ""; violationsOf(card).forEach(function (v) { if (v.check === "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(card) { var seen = {}; var out = []; violationsOf(card).forEach(function (v) { if (v.check === "confirmed" && v.type === "norm" && v.normId) { 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 defaultConclText(card) { return "ЗАКЛЮЧЕНИЕ о соответствии кандидата требованиям должности\n\n" + "Кандидат: " + card.name + "\n" + "Должность: " + card.post + "\n" + "Дата пакета: " + card.submitted + "\n\n" + "Проверены документы: должностная инструкция, пояснительная записка, анкета кандидата. " + "Сверка выполнена с квалификационными требованиями и внутренними нормативами фонда.\n\n" + "По результатам сверки зафиксированы несоответствия и расхождения, подробно изложенные ниже. " + "При назначении кандидата на должность рекомендуется устранить замечания в порядке, установленном правилами фонда.\n\n" + "Составил: эксперт кадровой службы"; } function renderConcl() { var c = cur(); var tone = c.stage === "sent" ? "warn" : "gray"; var shown = c.text && c.text.trim() ? c.text : defaultConclText(c); return '
' + candHead() + '
' + '
' + '
' + '
' + 'Текст заключения' + '
' + pill(c.stage === "sent" ? "На согласовании" : "Черновик", tone) + "
" + '' + '
' + '" + '" + "
" + '
Руководитель получит копию на согласование, кадровая служба — копию.
' + "
" + '
' + '
' + '
' + "Подтверждённые несоответствия (" + confirmCount(c) + ")" + '
' + 'из «Анализ ИИ»' + "
" + "
" + confirmedViolHtml(c) + "
" + "
" + '
' + '
Пункты нормативов
' + normativeRefsHtml(c) + "
" + "
" + "
" + "
" + "
"; } /* ============================================================ ЭКРАН 4 — СОГЛАСОВАНИЕ РУКОВОДИТЕЛЕМ ============================================================ */ function renderApprove() { var c = cur(); var chip = c.stage === "approved" ? pill("Согласовано", "ok") : c.stage === "returned" ? pill("На доработке", "risk") : pill("На согласовании", "warn"); var sentLine = c.sentAt ? "Отправлено: " + fmtDT(c.sentAt) : "Отправлено: —"; var approvedLine = c.stage === "approved" ? "Согласовано: " + fmtDT(c.approvedAt) : "Согласование: ожидается"; var text = c.text || defaultConclText(c); if (c.returned && c.stage === "returned") text += "\n\nВозвращено на доработку: " + c.returned; var actions = ""; if (c.stage === "approved") { actions = '
Заключение согласовано. Кадровая служба получит уведомление.
'; } else if (c.stage === "sent") { actions = '
' + '
' + '
' + '
' + '" + '" + "
"; } else { actions = '
Черновик возвращён эксперту на доработку. Дождитесь новой отправки.
'; } return '
' + candHead() + '
' + '
' + kpi(esc(c.name.split(" ")[0] + " " + (c.name.split(" ")[1] || "")), "Кандидат", "на согласовании") + kpi(c.sentAt ? "от " + fmtDT(c.sentAt).split(",")[1] : "—", "Отправлено", "экспертом") + kpi(c.sentAt ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") + "
" + '
' + '
' + '
' + '
Заключение эксперта
' + '
' + sentLine + " · " + approvedLine + "
" + "
" + chip + "
" + '
' + '
Подтверждённые несоответствия
' + confirmedViolHtml(c) + 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") + '
Поиск расхождений в документах
' + '
Человек решает: проверка выводов, написание текста, согласование.
' + "
" + "
" + "
" + "
" + ""; } /* ============================================================ РЕНДЕР ============================================================ */ function setShell() { var sc = SCREENS[S.screen] || SCREENS.list; var t = $("pageTitle"); if (t) t.textContent = sc.title; var sub = $("pageSub"); if (sub) { if (S.screen === "list" || S.screen === "about") { sub.style.display = "none"; } } var cr = $("crumbRole"); if (cr) cr.textContent = S.role === "leader" ? "Руководитель" : "Эксперт"; var av = $("sbAvatar"); if (av) av.textContent = S.role === "leader" ? "РУ" : "ЭК"; var nm = $("sbName"); if (nm) nm.textContent = S.role === "leader" ? "Руководитель" : "Эксперт"; var sn = $("snav"); if (sn) sn.innerHTML = navHtml(); } function render() { var v = $("view"); if (S.screen === "list") v.innerHTML = renderList(); else if (S.screen === "load" && cur()) v.innerHTML = renderLoad(); else if (S.screen === "analysis" && cur()) v.innerHTML = renderAnalysis(); else if (S.screen === "concl" && cur()) v.innerHTML = renderConcl(); else if (S.screen === "approve" && cur()) v.innerHTML = renderApprove(); else if (S.screen === "about") v.innerHTML = renderAbout(); else { S.screen = "list"; v.innerHTML = renderList(); } setShell(); if (S.screen === "analysis") applyViolFilters(); if (S.screen === "concl" && cur()) { var t = $("conclText"); if (t) t.value = cur().text && cur().text.trim() ? cur().text : defaultConclText(cur()); } if (S.screen === "approve" && cur()) { var at = $("approveText"); if (at) at.textContent = renderApproveText(); } } function renderApproveText() { var c = cur(); var text = c.text || defaultConclText(c); if (c.returned && c.stage === "returned") text += "\n\nВозвращено на доработку: " + c.returned; return text; } function go(screen) { S.screen = screen; render(); } function openCard(id) { getCard(id).then(function (c) { S.curCard = c; S.candId = c.id; go("load"); }); } function newCard() { createCard({ name: "", post: "" }).then(function (c) { S.curCard = c; S.candId = c.id; go("load"); }); } function refreshCard() { if (!S.candId) return; getCard(S.candId).then(function (c) { S.curCard = c; render(); }); } function saveChecks(card) { card.violations = VIOL.map(function (t) { var v = (card.violations || []).find(function (x) { return x.id === t.id; }); return { id: t.id, doc: t.doc, item: t.item, desc: t.desc, type: t.type, norm: t.norm, normId: t.normId, check: (card.checks && card.checks[String(t.id)]) || "pending" }; }); return saveCard(card.id, { checks: card.checks, violations: card.violations }); } function refreshCards() { allCards().then(function (cards) { S.cards = cards; S.curCard = cards.find(function (c) { return c.id === S.candId; }) || null; render(); }); } /* ---------- Обработчики ---------- */ document.addEventListener("click", function (e) { var cardRow = e.target.closest("[data-card]"); if (cardRow) { openCard(parseInt(cardRow.getAttribute("data-card"), 10)); return; } var navBtn = e.target.closest("[data-nav]"); if (navBtn) { var id = navBtn.getAttribute("data-nav"); if (id === "list" || id === "about") { go(id); return; } if (!cur()) { go("list"); return; } if (id === "approve" && S.role !== "leader") { toast("Раздел доступен в роли руководителя", "info"); return; } if (id === "load" || id === "analysis" || id === "concl") go("list"); if (cur().stage === "uploaded") go("load"); else if (cur().stage === "analyzed" || cur().stage === "draft") go("analysis"); else if (cur().stage === "sent") go("approve"); else go("approve"); return; } if (e.target.id === "btnNewCard") { newCard(); return; } if (e.target.id === "btnRefresh") { refreshCards(); return; } // Экран 1: загрузка файлов var fBtn = e.target.closest("[data-filetype]"); if (fBtn) { var t = fBtn.getAttribute("data-filetype"); var inp = $("file-" + t); if (inp) inp.click(); return; } if (e.target.id === "btnRunAnalysis") { if (!filesComplete(cur())) { toast("Загрузите все три файла", "warn"); return; } saveChecks(cur()).then(function () { saveCard(cur().id, { stage: "analyzed" }).then(function () { go("analysis"); toast("Сверка завершена, найдены выводы ИИ", "ok"); }); }); return; } // Экран 2: подтверждение / отклонение var actBtn = e.target.closest("[data-act]"); if (actBtn) { var vid = parseInt(actBtn.getAttribute("data-vid"), 10); var act = actBtn.getAttribute("data-act"); cur().checks = cur().checks || {}; if (act === "confirm") cur().checks[String(vid)] = "confirmed"; else if (act === "reject") cur().checks[String(vid)] = "rejected"; else cur().checks[String(vid)] = "pending"; saveChecks(cur()).then(function () { if (act === "confirm") toast("Несоответствие подтверждено", "ok"); if (act === "reject") toast("Отклонено как ложное срабатывание", "info"); if (act === "reopen") toast("Возвращено на проверку", "info"); applyViolFilters(); }); return; } if (e.target.id === "btnToConcl") { go("concl"); return; } // Экран 3: заключение if (e.target.id === "btnSaveConcl") { var ta = $("conclText"); if (ta) cur().text = ta.value; if (["uploaded", "analyzed"].indexOf(cur().stage) !== -1) cur().stage = "draft"; saveCard(cur().id, { text: cur().text, stage: cur().stage }); toast("Черновик сохранён", "ok"); render(); return; } if (e.target.id === "btnSendConcl") { var ta2 = $("conclText"); if (ta2) cur().text = ta2.value; cur().stage = "sent"; cur().sentAt = new Date().toISOString(); cur().returned = null; saveCard(cur().id, { text: cur().text, stage: "sent", sentAt: cur().sentAt, returned: null }); toast("Отправлено на согласование", "ok"); go("approve"); return; } // Экран 4: согласование if (e.target.id === "btnApprove") { cur().stage = "approved"; cur().approvedAt = new Date().toISOString(); var rc = $("rejectComment"); cur().comment = rc && rc.value ? rc.value : ""; saveCard(cur().id, { stage: "approved", approvedAt: cur().approvedAt, comment: cur().comment }); toast("Заключение согласовано", "ok"); go("list"); return; } if (e.target.id === "btnReturnConcl") { var rc2 = $("rejectComment"); cur().stage = "returned"; cur().returned = rc2 && rc2.value ? rc2.value : "Возвращено в экспертизу на доработку"; saveCard(cur().id, { stage: "returned", returned: cur().returned }); toast("Возвращено на доработку", "warn"); go("load"); return; } // Переключение роли if (e.target.closest("#sbUser")) { S.role = S.role === "expert" ? "leader" : "expert"; setShell(); toast("Переключено на роль: " + (S.role === "leader" ? "Руководитель" : "Эксперт"), "info"); } }); document.addEventListener("change", function (e) { if (e.target.id === "vType" || e.target.id === "vState") { applyViolFilters(); return; } if (e.target.id && e.target.id.indexOf("file-") === 0) { if (!e.target.files || !e.target.files.length) return; var t = e.target.id.slice(5); var f = cur().files.find(function (x) { return x.type === t; }); if (!f) return; f.name = e.target.files[0].name; f.date = fmtDate(new Date().toISOString()); f.status = "done"; saveCard(cur().id, { files: cur().files }).then(function () { toast("Файл загружен, статус: Обработан", "ok"); render(); }); } }); document.addEventListener("input", function (e) { if (e.target.id === "fSearch") { S.filter = e.target.value.toLowerCase(); renderListOnly(); } }); function renderListOnly() { var wrap = $("cardList"); if (!wrap) return; var filtered = (S.cards || []).filter(function (c) { if (!S.filter) return true; return (c.name + " " + (c.post || "")).toLowerCase().indexOf(S.filter) !== -1; }); var rows = filtered.map(function (c) { return '
' + '' + icon("user") + "" + '
' + esc(c.name) + "" + '' + esc(c.post || "") + " · Пакет: " + esc(c.submitted) + "
" + pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") + '' + confirmCount(c) + " найдено
"; }).join(""); wrap.innerHTML = rows || '
Ничего не найденоСмените запрос.
'; } document.addEventListener("keydown", function (e) { if (e.key === "Enter" && e.target.id === "fSearch") { return; } if (e.key === "Escape") { return; } }); /* Burger */ $("burger").addEventListener("click", function () { var sb = $("sidebar"); 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"; } }); /* ---------- Инициализация ---------- */ function seed() { return allCards().then(function (cards) { if (cards && cards.length) { S.cards = cards; } else { var base = [ { name: "Ахметов Ерлан Сергеевич", post: "Руководитель отдела", stage: "approved" }, { name: "Бекмембетова Динара Талгат", post: "Начальник управления", stage: "approved" }, { name: "Сапаров Ибрагим Нурлан", post: "Заместитель директора", stage: "sent" }, { name: "Мырзагалиев Айдос Керек", post: "Руководитель направления", stage: "draft" }, { name: "Оралова Гульнара Бакыт", post: "Начальник сектора", stage: "draft" }, { name: "Токтаров Марат Алиши", post: "Директор подразделения", stage: "uploaded" }, { name: "Утебаева Алия Дильдари", post: "Заместитель руководителя", stage: "uploaded" }, { name: "Жумабаев Нурлан Серик", post: "Руководитель службы", stage: "uploaded" } ]; var subDates = ["10.09.2026", "12.09.2026", "15.09.2026", "17.09.2026", "19.09.2026", "20.09.2026", "20.09.2026", "21.09.2026"]; var checks = {}; var i; for (i = 0; i < VIOL.length; i++) { checks[String(VIOL[i].id)] = (i < 5) ? "confirmed" : (i === 5 ? "rejected" : "pending"); } var viol = VIOL.map(function (t) { return { id: t.id, doc: t.doc, item: t.item, desc: t.desc, type: t.type, norm: t.norm, normId: t.normId, check: checks[String(t.id)] }; }); var baseStage = {}; base.forEach(function (b) { baseStage[b.name] = b.stage; }); var proms = base.map(function (b, idx) { var stage = baseStage[b.name]; var isAfterLoad = stage === "draft" || stage === "sent" || stage === "approved" || stage === "returned"; return createCard({ name: b.name, post: b.post, stage: stage, submitted: subDates[idx], files: FILE_TYPES.map(function (t) { return { type: t, name: t + "_" + b.name.split(" ")[0] + ".pdf", status: "done", date: "19.09.2026" }; }), checks: isAfterLoad ? checks : {}, violations: isAfterLoad ? viol : [], text: isAfterLoad ? "ЗАКЛЮЧЕНИЕ о соответствии кандидата требованиям должности\n\nКандидат: " + b.name + "\nДолжность: " + b.post + "\n\nПо результатам сверки зафиксированы несоответствия и расхождения." : "" }); }); return Promise.all(proms).then(function (created) { S.cards = created; }); } return S.cards; }); } allCards().then(function (cards) { if (!cards || !cards.length) { var seedProm = seed(); seedProm.then(function () { allCards().then(function (c2) { S.cards = c2; render(); }); }); } else { S.cards = cards; render(); } });