sverka-dokumentov-pri-naznac/script.js

959 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ===== Сверка документов при назначении руководителей =====
Рабочее решение: данные сохраняются на сервере (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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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();
}
});