959 lines
54 KiB
JavaScript
959 lines
54 KiB
JavaScript
/* ===== Сверка документов при назначении руководителей =====
|
||
Рабочее решение: данные сохраняются на сервере (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 '<svg class="kt-icon" viewBox="0 0 24 24"><use href="./design-system/icons/kt-ai-lucide-sprite.svg#' + name + '"></use></svg>';
|
||
}
|
||
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 '<div class="kt-ai-kpi"><span class="value">' + esc(value) + '</span><span class="label">' + esc(label) + "</span>" +
|
||
(hint ? '<span class="hint">' + esc(hint) + "</span>" : "") + "</div>";
|
||
}
|
||
function pill(label, tone) {
|
||
return '<span class="kt-ai-status-pill" data-status="' + tone + '">' + esc(label) + "</span>";
|
||
}
|
||
function toast(msg, tone) {
|
||
var stack = $("toasts");
|
||
if (!stack) return;
|
||
var el = document.createElement("div");
|
||
el.className = "kt-ai-toast";
|
||
if (tone) el.setAttribute("data-tone", tone);
|
||
el.textContent = msg;
|
||
stack.appendChild(el);
|
||
setTimeout(function () {
|
||
el.style.transition = "opacity .3s";
|
||
el.style.opacity = "0";
|
||
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
|
||
}, 2600);
|
||
}
|
||
|
||
/* ---------- Выводы ИИ (сверка с нормативами и поиск противоречий) ---------- */
|
||
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 += '<button class="kt-ai-nav-item" data-active="' + active + '" data-nav="' + it.id + '">' +
|
||
icon(it.ic) + '<span class="nav-label">' + esc(it.label) + "</span>" +
|
||
(st ? ' <span class="count">•</span>' : "") +
|
||
"</button>";
|
||
});
|
||
return html;
|
||
}
|
||
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 '<div class="kt-ai-doc" data-card="' + c.id + '" style="cursor:pointer">' +
|
||
'<span class="doc-ic">' + icon("user") + "</span>" +
|
||
'<div class="doc-meta"><span class="doc-name">' + esc(c.name) + "</span>" +
|
||
'<span class="doc-sub">' + esc(c.post || "") + " · Пакет: " + esc(c.submitted) + "</span></div>" +
|
||
pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") +
|
||
'<span class="kt-ai-t-xs kt-ai-t-faint" style="min-width:90px;text-align:right">' + confirmed + " найдено</span>" +
|
||
"</div>";
|
||
}).join("");
|
||
if (!rows) rows = '<div class="kt-ai-empty"><span class="title">Ничего не найдено</span>Смените запрос.</div>';
|
||
return '<div class="view">' +
|
||
'<div class="kt-ai-kpi-strip" data-cards="true">' +
|
||
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), "В работе", "у эксперта") +
|
||
"</div>" +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll" style="margin-bottom:var(--kt-ai-space-5)">' +
|
||
'<input class="kt-ai-input" id="fSearch" placeholder="Поиск по ФИО или должности" value="' + esc(f) + '" style="max-width:340px">' +
|
||
'<div class="kt-ai-spacer"></div>' +
|
||
'<button class="kt-ai-btn" id="btnRefresh">' + icon("refresh") + "</button>" +
|
||
'<button class="kt-ai-btn" data-variant="primary" id="btnNewCard">' + icon("upload") + " Новая сверка</button>" +
|
||
"</div>" +
|
||
'<div class="up-list" id="cardList">' + rows + "</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
ЭКРАН 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
|
||
? '<button class="kt-ai-btn" data-variant="primary" id="btnRunAnalysis">' + icon("play") + " Запустить анализ ИИ</button>"
|
||
: '<span class="kt-ai-chip" data-tone="gray">Осталось загрузить: ' + missing + "</span>";
|
||
|
||
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 '<div class="up-zone">' +
|
||
'<span class="doc-ic">' + icon(ic) + "</span>" +
|
||
'<span class="z-name">' + esc(f.type) + "</span>" +
|
||
'<span class="z-file">' + esc(fname) + "</span>" +
|
||
'<span class="z-file dim">' + esc(sub) + "</span>" +
|
||
'<input type="file" id="file-' + f.type + '" accept=".pdf,.doc,.docx" style="display:none">' +
|
||
'<button class="kt-ai-btn" data-size="sm" data-filetype="' + f.type + '">' + (has ? "Заменить файл" : "Загрузить файл") + "</button>" +
|
||
"</div>";
|
||
}).join("");
|
||
|
||
var rows = files.map(function (f) {
|
||
if (f.status === "missing") return "";
|
||
return '<div class="kt-ai-doc">' +
|
||
'<span class="doc-ic">' + icon("file") + "</span>" +
|
||
'<div class="doc-meta"><span class="doc-name">' + esc(f.name) + "</span>" +
|
||
'<span class="doc-sub">Тип: ' + esc(f.type) + " · " + esc(f.date || "") + "</span></div>" +
|
||
'<button class="kt-ai-btn" data-size="sm" data-filetype="' + f.type + '">Заменить</button></div>';
|
||
}).join("");
|
||
|
||
return '<div class="view">' +
|
||
'<div class="kt-ai-kpi-strip" data-cards="true">' +
|
||
kpi(String(files.length - missing) + " / " + files.length, "Документов загружено", "пакет кандидата") +
|
||
kpi("150 мин", "Труда без автоматизации", "на одно назначение") +
|
||
kpi("3 р.д", "Срок подготовки заключения", "по регламенту") +
|
||
"</div>" +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)">' +
|
||
'<h2 class="kt-ai-h2">Новая сверка</h2>' +
|
||
'<div class="kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1);margin-bottom:var(--kt-ai-space-7)">' +
|
||
"Заполните сведения о кандидате и загрузите комплект документов — система сверит их с нормативами и найдёт расхождения.</div>" +
|
||
'<div class="up-grid">' +
|
||
'<div class="kt-ai-field"><label for="fName">ФИО кандидата</label>' +
|
||
'<input class="kt-ai-input" id="fName" value="' + esc(c.name) + '"></div>' +
|
||
'<div class="kt-ai-field"><label for="fPost">Должность</label>' +
|
||
'<input class="kt-ai-input" id="fPost" value="' + esc(c.post) + '"></div>' +
|
||
"</div>" +
|
||
'<div style="margin-bottom:var(--kt-ai-space-5);display:flex;align-items:center;gap:var(--kt-ai-space-3)">' +
|
||
'<span class="kt-ai-t-sm kt-ai-t-medium" style="white-space:nowrap">Загрузка документов</span>' +
|
||
'<div class="kt-ai-spacer"></div>' +
|
||
'<span class="kt-ai-chip" data-tone="gray">3 обязательных файла</span></div>' +
|
||
'<div class="up-zones">' + zoneHtml + "</div>" +
|
||
(missing ? "" : '<div style="margin-top:var(--kt-ai-space-7);display:flex;align-items:center;gap:var(--kt-ai-space-3)">' +
|
||
'<span class="kt-ai-t-sm kt-ai-t-medium">Загруженные файлы</span>' +
|
||
'<div class="kt-ai-spacer"></div></div>' +
|
||
'<div class="up-list" style="margin-top:var(--kt-ai-space-4)">' + rows + "</div>") +
|
||
'<div class="kt-ai-h-stack" style="margin-top:var(--kt-ai-space-7)">' + btn + "</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ---------- Шапка-карточка кандидата ---------- */
|
||
function candHead() {
|
||
var c = cur();
|
||
return '<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll">' +
|
||
'<div style="flex:1;min-width:0">' +
|
||
'<div class="kt-ai-t-xl" style="font-weight:var(--kt-ai-weight-semibold);letter-spacing:-.01em">' + esc(c.name) + "</div>" +
|
||
'<div class="kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1)">' + esc(c.post) +
|
||
" · Пакет: " + esc(c.submitted) + "</div>" +
|
||
"</div>" +
|
||
pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") +
|
||
"</div>" +
|
||
'<div class="kt-ai-stepper">' + STEPS.map(function (s, i) {
|
||
var st = (i + 1 < curStage()) ? "done" : (i + 1 === curStage()) ? "current" : "";
|
||
return '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + '><span class="n">' + (i + 1) + "</span>" +
|
||
"<span>" + esc(s.label) + "</span></div>" + (i < STEPS.length - 1 ? '<div class="kt-ai-step-sep"></div>' : "");
|
||
}).join("") + "</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
ЭКРАН 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")
|
||
? '<span class="viol-meta">Норматив: ' + esc(v.norm) + "</span>"
|
||
: '<span class="viol-meta" style="color:var(--kt-ai-status-warn-fg)">Расхождение в данных документов</span>';
|
||
var body =
|
||
'<div class="viol-vic" data-t="' + t + '">' + icon(t === "contra" ? "compare" : "scale") + "</div>" +
|
||
'<div class="viol-body">' +
|
||
'<span class="kt-ai-chip" data-tone="' + (t === "contra" ? "orange" : "blue") + '">' + tLabel + "</span>" +
|
||
'<div class="viol-desc">' + esc(v.desc) + "</div>" +
|
||
'<div class="viol-meta">Документ: ' + esc(v.doc) + " · Пункт: " + esc(v.item) + "</div>" +
|
||
normLine +
|
||
"</div>";
|
||
var actions =
|
||
ch === "pending"
|
||
? '<button class="kt-ai-btn" data-size="sm" data-variant="primary" data-act="confirm" data-vid="' + v.id + '">' + icon("check") + " Подтвердить</button>" +
|
||
'<button class="kt-ai-btn" data-size="sm" data-act="reject" data-vid="' + v.id + '">Отклонить</button>'
|
||
: ch === "confirmed"
|
||
? '<button class="kt-ai-btn" data-size="sm" data-variant="ghost" data-act="reopen" data-vid="' + v.id + '">Вернуть на проверку</button>' + pill("Подтверждено", "ok")
|
||
: pill("Отклонено", "muted") + '<button class="kt-ai-btn" data-size="sm" data-variant="ghost" data-act="reopen" data-vid="' + v.id + '">Вернуть</button>';
|
||
return '<div class="viol-card" data-done="' + (ch === "pending" ? "false" : "true") + '">' + body +
|
||
'<div class="viol-actions">' + actions + "</div></div>";
|
||
}
|
||
|
||
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 = '<div class="kt-ai-empty"><span class="title">По фильтру ничего нет</span>Смените тип нарушения или статус проверки.</div>';
|
||
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 '<div class="view">' + candHead() +
|
||
'<div class="kt-ai-banner" data-tone="ok" style="display:flex;align-items:center;gap:var(--kt-ai-space-4)"><span style="flex:1;min-width:0">Сверка завершена: ' + vs.length + " выводов — сверка с нормативами и поиск противоречий</span>" +
|
||
'<span class="kt-ai-t-xs kt-ai-t-faint">22.04 с</span></div>' +
|
||
'<div class="kt-ai-kpi-strip" data-cards="true">' +
|
||
kpi(String(vs.length), "Найдено выводов", "сверка с нормативами") +
|
||
kpi(String(confirmed), "Подтверждено", "экспертом") +
|
||
kpi(String(rejected), "Отклонено", "ложные срабатывания") +
|
||
kpi(String(pending), "Осталось проверить", "к ручному осмотру") +
|
||
"</div>" +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-table-toolbar">' +
|
||
'<span class="kt-ai-t-sm kt-ai-t-medium">Выводы ИИ · ' + (vs.length - pending + "/" + vs.length) + " проверено</span>" +
|
||
'<div class="kt-ai-spacer"></div>' +
|
||
'<select class="kt-ai-select" id="vType" style="min-width:130px">' +
|
||
'<option value="all">Все типы</option>' +
|
||
'<option value="norm">Несоответствие нормативу</option>' +
|
||
'<option value="contra">Расхождение</option>' +
|
||
"</select>" +
|
||
'<select class="kt-ai-select" id="vState" style="min-width:130px">' +
|
||
'<option value="all">Все статусы</option>' +
|
||
'<option value="pending">Ожидает проверки</option>' +
|
||
'<option value="confirmed">Подтверждено</option>' +
|
||
'<option value="rejected">Отклонено</option>' +
|
||
"</select>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-v" data-gap="tight" id="violList"></div>' +
|
||
'<div class="kt-ai-h-stack" style="margin-top:var(--kt-ai-space-6)" data-nowrap="scroll">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" id="btnToConcl">' + icon("arrowRight") + " Перейти к составлению заключения</button>" +
|
||
'<span class="kt-ai-t-xs kt-ai-t-faint">В заключение войдут ' + confirmed + " подтверждённых</span>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
ЭКРАН 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 += '<div class="approve-viol"><span class="dot" style="background:' + toneDot + '"></span>' +
|
||
"<div><div><strong>" + c + ".</strong> " + esc(v.desc) + "</div>" +
|
||
'<div class="dim">' + esc(v.doc) + " · " + esc(v.item) + " · " + normTxt + "</div></div></div>";
|
||
}
|
||
});
|
||
if (c === 0) html = '<div class="kt-ai-empty"><span class="title">Пока ни одного подтверждённого</span>Вернитесь на «Анализ ИИ» и подтвердите несоответствия — они попадут в заключение.</div>';
|
||
return html;
|
||
}
|
||
|
||
function normativeRefsHtml(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("<span class=\"r\">" + esc(ref.r) + "</span><span class=\"dim\">" + esc(ref.p) + "</span>");
|
||
}
|
||
}
|
||
});
|
||
if (!out.length) return '<div class="kt-ai-t-muted kt-ai-t-xs">Ссылки появятся после подтверждения нормативных несоответствий.</div>';
|
||
return out.map(function (h) {
|
||
return '<div class="doc-ref">' + icon("link") + h + "</div>";
|
||
}).join("");
|
||
}
|
||
|
||
function 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 '<div class="view">' + candHead() +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="concl-layout">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll" style="margin-bottom:var(--kt-ai-space-5)">' +
|
||
'<span class="kt-ai-t-lg kt-ai-t-semibold">Текст заключения</span>' +
|
||
'<div class="kt-ai-spacer"></div>' +
|
||
pill(c.stage === "sent" ? "На согласовании" : "Черновик", tone) +
|
||
"</div>" +
|
||
'<textarea class="kt-ai-textarea" id="conclText" style="margin-bottom:var(--kt-ai-space-5);box-sizing:border-box;min-height:260px"></textarea>' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll">' +
|
||
'<button class="kt-ai-btn" id="btnSaveConcl">' + icon("check") + " Сохранить черновик</button>" +
|
||
'<button class="kt-ai-btn" data-variant="primary" id="btnSendConcl">' + icon("send") + " Отправить на согласование</button>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-t-xs kt-ai-t-faint" style="margin-top:var(--kt-ai-space-3)">Руководитель получит копию на согласование, кадровая служба — копию.</div>' +
|
||
"</div>" +
|
||
'<div class="concl-side">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-h-stack" data-gap="tight" style="margin-bottom:var(--kt-ai-space-4)">' +
|
||
"<span class=\"kt-ai-t-sm kt-ai-t-medium\">Подтверждённые несоответствия (" + confirmCount(c) + ")</span>" +
|
||
'<div class="kt-ai-spacer"></div>' +
|
||
'<span class="kt-ai-chip" data-tone="gray">из «Анализ ИИ»</span>' +
|
||
"</div>" +
|
||
"<div>" + confirmedViolHtml(c) + "</div>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-3)">Пункты нормативов</div>' +
|
||
normativeRefsHtml(c) +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
ЭКРАН 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 = '<div class="kt-ai-banner" data-tone="ok">Заключение согласовано. Кадровая служба получит уведомление.</div>';
|
||
} else if (c.stage === "sent") {
|
||
actions =
|
||
'<div class="kt-ai-v" data-gap="block" style="margin-top:var(--kt-ai-space-6)">' +
|
||
'<div class="kt-ai-field"><label for="rejectComment">Комментарий эксперту (необязательно)</label>' +
|
||
'<textarea class="kt-ai-textarea" id="rejectComment" style="min-height:70px" placeholder="Замечания к заключению, если возвращаете"></textarea></div>' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" id="btnApprove">' + icon("check") + " Согласовать</button>" +
|
||
'<button class="kt-ai-btn" id="btnReturnConcl">' + icon("arrowLeft") + " Вернуть на доработку</button>" +
|
||
"</div></div>";
|
||
} else {
|
||
actions = '<div class="kt-ai-banner" data-tone="warn">Черновик возвращён эксперту на доработку. Дождитесь новой отправки.</div>';
|
||
}
|
||
|
||
return '<div class="view">' + candHead() +
|
||
'<div class="kt-ai-v" data-gap="block">' +
|
||
'<div class="kt-ai-kpi-strip" data-cards="true">' +
|
||
kpi(esc(c.name.split(" ")[0] + " " + (c.name.split(" ")[1] || "")), "Кандидат", "на согласовании") +
|
||
kpi(c.sentAt ? "от " + fmtDT(c.sentAt).split(",")[1] : "—", "Отправлено", "экспертом") +
|
||
kpi(c.sentAt ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") +
|
||
"</div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)">' +
|
||
'<div class="kt-ai-h-stack" data-nowrap="scroll" style="margin-bottom:var(--kt-ai-space-5)">' +
|
||
'<div style="flex:1;min-width:0">' +
|
||
'<div class="kt-ai-t-lg kt-ai-t-semibold">Заключение эксперта</div>' +
|
||
'<div class="kt-ai-t-xs kt-ai-t-muted" style="margin-top:var(--kt-ai-space-1)">' + sentLine + " · " + approvedLine + "</div>" +
|
||
"</div>" +
|
||
chip +
|
||
"</div>" +
|
||
'<div class="approve-text" id="approveText"></div>' +
|
||
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-top:var(--kt-ai-space-7);margin-bottom:var(--kt-ai-space-3)">Подтверждённые несоответствия</div>' +
|
||
confirmedViolHtml(c) +
|
||
actions +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
ЭКРАН 5 — О РЕШЕНИИ
|
||
============================================================ */
|
||
function renderAbout() {
|
||
var proc = [
|
||
{ t: "Эксперт получает пакет документов", who: "от кадровой службы" },
|
||
{ t: "Эксперт читает пояснительную записку" },
|
||
{ t: "ИИ сверяет документы с квалификационными требованиями", ai: true },
|
||
{ t: "ИИ ищет расхождения в документах", ai: true },
|
||
{ t: "Эксперт проверяет выводы ИИ" },
|
||
{ t: "Эксперт готовит заключение с несоответствиями и ссылками" },
|
||
{ t: "Руководитель согласовывает заключение" }
|
||
];
|
||
return '<div class="view">' +
|
||
'<div class="kt-ai-v" data-gap="group">' +
|
||
'<div class="kt-ai-h2">Паспорт ИИ-решения</div>' +
|
||
'<div class="kt-ai-banner" data-tone="info">Ускоряет подготовку заключения о соответствии кандидата за счёт автоматизации сверки и поиска расхождений.</div>' +
|
||
'<div class="kt-ai-v" data-gap="tight">' +
|
||
'<div class="kt-ai-kpi-strip" data-cards="true">' +
|
||
kpi("~50 ч/мес", "Экономия", "20 случаев × 150 мин") +
|
||
kpi("3 р.д → 1 день", "Срок подготовки", "заключения") +
|
||
kpi("20 случаев", "Объём в месяц", "оценка владельца процесса") +
|
||
kpi("—", "Дата внедрения", "после согласования макета") +
|
||
"</div>" +
|
||
"</div>" +
|
||
'<div class="pass-grid">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-4)">Поэтапный эффект</div>' +
|
||
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3)">' +
|
||
'<div><span class="kt-ai-t-sm">Сокращение чтения вручную</span><div class="kt-ai-t-xs kt-ai-t-muted">эксперт осматривает уже только выводы ИИ</div></div>' +
|
||
'<div><span class="kt-ai-t-sm">Запасание расхождений при сверке</span><div class="kt-ai-t-xs kt-ai-t-muted">нормативы и требования — в одном месте</div></div>' +
|
||
'<div><span class="kt-ai-t-sm">Стабильный срок заключения</span><div class="kt-ai-t-xs kt-ai-t-muted">руководитель получает в тот же день</div></div>' +
|
||
"</div>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-t-sm kt-ai-t-medium" style="margin-bottom:var(--kt-ai-space-4)">Контроль и риски</div>' +
|
||
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3)">' +
|
||
'<div><span class="kt-ai-t-sm">Ошибочное срабатывание ИИ</span><div class="kt-ai-t-xs kt-ai-t-muted">в случаях расхождений данных</div></div>' +
|
||
'<div><span class="kt-ai-t-sm">Митигация — ручная проверка</span><div class="kt-ai-t-xs kt-ai-t-muted">каждый вывод подтверждает или отклоняет эксперт</div></div>' +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
|
||
'<div class="pass-sec">Данные на входе</div>' +
|
||
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4)">' +
|
||
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("file") + '</span><div class="doc-meta"><span class="doc-name">Комплект документов кандидата</span><span class="doc-sub">должностная инструкция, пояснительная записка, анкета</span></div></div>' +
|
||
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("book") + '</span><div class="doc-meta"><span class="doc-name">Внутренние нормативы</span><span class="doc-sub">папка на общем диске</span></div></div>' +
|
||
"</div>" +
|
||
"</div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
|
||
'<div class="pass-sec">Процесс</div>' +
|
||
'<div class="psteps" style="margin-top:var(--kt-ai-space-4)">' + proc.map(function (p, i) {
|
||
return '<div class="pstep' + (p.ai ? " ai" : "") + '"><span class="n">' + (i + 1) + "</span>" +
|
||
"<div><div>" + esc(p.t) + '</div><span class="who">' + (p.ai ? "ИИ" : (p.who ? esc(p.who) : "эксперт / руководитель")) + "</div></div></div>";
|
||
}).join("") + "</div>" +
|
||
"</div>" +
|
||
'<div class="pass-grid2">' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)">' +
|
||
'<div class="pass-sec">Что выполняет ИИ</div>' +
|
||
'<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-4);font-size:var(--kt-ai-text-sm)">' +
|
||
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("scale") + '</span><div class="doc-meta"><span class="doc-name">Сверка с квалификационными требованиями и нормативами</span></div></div>' +
|
||
'<div class="kt-ai-doc"><span class="doc-ic">' + icon("compare") + '</span><div class="doc-meta"><span class="doc-name">Поиск расхождений в документах</span></div></div>' +
|
||
'<div class="kt-ai-t-xs kt-ai-t-muted" style="margin-top:var(--kt-ai-space-2)">Человек решает: проверка выводов, написание текста, согласование.</div>' +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
|
||
/* ============================================================
|
||
РЕНДЕР
|
||
============================================================ */
|
||
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 '<div class="kt-ai-doc" data-card="' + c.id + '" style="cursor:pointer">' +
|
||
'<span class="doc-ic">' + icon("user") + "</span>" +
|
||
'<div class="doc-meta"><span class="doc-name">' + esc(c.name) + "</span>" +
|
||
'<span class="doc-sub">' + esc(c.post || "") + " · Пакет: " + esc(c.submitted) + "</span></div>" +
|
||
pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") +
|
||
'<span class="kt-ai-t-xs kt-ai-t-faint" style="min-width:90px;text-align:right">' + confirmCount(c) + " найдено</span></div>";
|
||
}).join("");
|
||
wrap.innerHTML = rows || '<div class="kt-ai-empty"><span class="title">Ничего не найдено</span>Смените запрос.</div>';
|
||
}
|
||
|
||
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();
|
||
}
|
||
});
|