' +
- kpi(String(VIOL.length), "Найдено несоответствий", "сверка с нормативами") +
+ kpi(String(vs.length), "Найдено выводов", "сверка с нормативами") +
kpi(String(confirmed), "Подтверждено", "экспертом") +
kpi(String(rejected), "Отклонено", "ложные срабатывания") +
kpi(String(pending), "Осталось проверить", "к ручному осмотру") +
@@ -354,14 +413,14 @@ function renderAnalysis() {
'
' +
'
" +
@@ -381,11 +440,11 @@ function renderAnalysis() {
/* ============================================================
ЭКРАН 3 — СОСТАВЛЕНИЕ ЗАКЛЮЧЕНИЯ
============================================================ */
-function confirmedViolHtml(n) {
+function confirmedViolHtml(card) {
var c = 0;
var html = "";
- VIOL.forEach(function (v) {
- if (S.checks[v.id] === "confirmed") {
+ 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) : "Расхождение данных";
@@ -398,11 +457,11 @@ function confirmedViolHtml(n) {
return html;
}
-function normativeRefsHtml() {
+function normativeRefsHtml(card) {
var seen = {};
var out = [];
- VIOL.forEach(function (v) {
- if (S.checks[v.id] === "confirmed" && v.type === "norm") {
+ 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;
@@ -416,9 +475,22 @@ function normativeRefsHtml() {
}).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 = cand();
+ var c = cur();
var tone = c.stage === "sent" ? "warn" : "gray";
+ var shown = c.text && c.text.trim() ? c.text : defaultConclText(c);
return '
' + candHead() +
'
' +
'
' +
@@ -438,15 +510,15 @@ function renderConcl() {
'
' +
'
' +
'
' +
- '
Подтверждённые несоответствия (' + (function(){var n=0;VIOL.forEach(function(v){if(S.checks[v.id]==="confirmed")n++;});return n;})() + ") " +
+ "
Подтверждённые несоответствия (" + confirmCount(c) + ") " +
'
' +
'
из «Анализ ИИ» ' +
"
" +
- '
' + confirmedViolHtml() + "
" +
+ "
" + confirmedViolHtml(c) + "
" +
"
" +
'
' +
'
Пункты нормативов
' +
- normativeRefsHtml() +
+ normativeRefsHtml(c) +
"
" +
"
" +
"
" +
@@ -458,34 +530,40 @@ function renderConcl() {
ЭКРАН 4 — СОГЛАСОВАНИЕ РУКОВОДИТЕЛЕМ
============================================================ */
function renderApprove() {
- var c = cand();
+ var c = cur();
var chip = c.stage === "approved"
? pill("Согласовано", "ok")
- : c.stage === "sent"
- ? pill("На согласовании", "warn")
- : pill("Вернуто на доработку", "risk");
- var c2 = c;
- var actions =
- c.stage === "approved"
- ? '
Заключение согласовано ' + esc(c.approvedAt || "сегодня") + ". Кадровая служба получит уведомление.
"
- : c.stage === "sent"
- ? '
' +
- '' +
- '
' +
- '' + icon("check") + " Согласовать " +
- '' + icon("arrowLeft") + " Вернуть на доработку " +
- "
"
- : '
Черновик возвращён эксперту на доработку. Дождитесь новой отправки.
';
+ : c.stage === "returned"
+ ? pill("На доработке", "risk")
+ : pill("На согласовании", "warn");
+ var sentLine = c.sentAt ? "Отправлено: " + fmtDT(c.sentAt) : "Отправлено: —";
+ var approvedLine = c.stage === "approved" ? "Согласовано: " + fmtDT(c.approvedAt) : "Согласование: ожидается";
- var sentLine = c.sent ? "Отправлено: " + esc(c.sent) : "Отправлено: —";
- var approvedLine = c.stage === "approved" ? "Согласовано руководителем: " + esc(c.approvedAt || "—") : "Согласование: ожидается";
+ var text = c.text || defaultConclText(c);
+ if (c.returned && c.stage === "returned") text += "\n\nВозвращено на доработку: " + c.returned;
+
+ var actions = "";
+ if (c.stage === "approved") {
+ actions = '
Заключение согласовано. Кадровая служба получит уведомление.
';
+ } else if (c.stage === "sent") {
+ actions =
+ '
' +
+ '
Комментарий эксперту (необязательно) ' +
+ '
' +
+ '
' +
+ '' + icon("check") + " Согласовать " +
+ '' + icon("arrowLeft") + " Вернуть на доработку " +
+ "
";
+ } else {
+ actions = '
Черновик возвращён эксперту на доработку. Дождитесь новой отправки.
';
+ }
return '
' + candHead() +
'
' +
'
' +
- kpi(esc(c.name.split(" ")[0] + " " + c.name.split(" ")[1]), "Кандидат", "на согласовании") +
- kpi(sentLine.split(":")[1] || "—", "Отправлено", "экспертом") +
- kpi(c.sent ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") +
+ kpi(esc(c.name.split(" ")[0] + " " + (c.name.split(" ")[1] || "")), "Кандидат", "на согласовании") +
+ kpi(c.sentAt ? "от " + fmtDT(c.sentAt).split(",")[1] : "—", "Отправлено", "экспертом") +
+ kpi(c.sentAt ? "1 день" : "—", "В согласовании", "из 3 р.д по регламенту") +
"
" +
'
' +
'
' +
@@ -497,7 +575,7 @@ function renderApprove() {
"
" +
'
' +
'
Подтверждённые несоответствия
' +
- confirmedViolHtml() +
+ confirmedViolHtml(c) +
actions +
"
" +
"
" +
@@ -556,19 +634,11 @@ function renderAbout() {
'
' +
'
Процесс
' +
'
' + proc.map(function (p, i) {
- return '
' + (i + 1) + ' ' +
- "
" + esc(p.t) + '
' + (p.ai ? "ИИ" : (p.who ? esc(p.who) : "эксперт / руководитель")) + " ";
+ return '
' + (i + 1) + " " +
+ "
" + esc(p.t) + '
' + (p.ai ? "ИИ" : (p.who ? esc(p.who) : "эксперт / руководитель")) + " ";
}).join("") + "
" +
"
" +
'
' +
- '
' +
- '
Роль пользователя
' +
- '
' +
- '
Эксперт загружает документы, проверяет выводы ИИ, готовит и отправляет заключение
' +
- '
Руководитель просматривает итог и согласовывает
' +
- '
Кадровая служба передаст пакет документов и получает копию заключения
' +
- "
" +
- "
" +
'
' +
'
Что выполняет ИИ
' +
'
' +
@@ -578,11 +648,6 @@ function renderAbout() {
"
" +
"
" +
"
" +
- '
' +
- kpi("Владелец процесса", "Исполнитель", "—") +
- kpi("Ветеринация на Vibe AI", "Формат", "макет → рабочее решение") +
- kpi("ИИ-агент Alem", "Опция", "работа с документами") +
- "
" +
"
" +
"
";
}
@@ -590,219 +655,304 @@ function renderAbout() {
/* ============================================================
РЕНДЕР
============================================================ */
+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 === "load") v.innerHTML = renderLoad();
- else if (S.screen === "analysis") v.innerHTML = renderAnalysis();
- else if (S.screen === "concl") v.innerHTML = renderConcl();
- else if (S.screen === "approve") v.innerHTML = renderApprove();
- else v.innerHTML = renderAbout();
+ 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();
- var sc = SCREENS[S.screen];
- $("pageTitle").textContent = sc.title;
- $("snav").innerHTML = navHtml();
-
- /* После вставки — заполнение значений в поле/подсветка текущего и навешивание действий через делегирование */
- if (S.screen === "analysis") {
- applyViolFilters();
+ 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 === "concl") {
- $("conclText").value = S.text;
- }
- if (S.screen === "approve") {
- $("approveText").textContent = S.text;
+ 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 setScreen(name) {
- S.screen = name;
- save();
- closeDrawer();
+function go(screen) {
+ S.screen = screen;
render();
}
-
-/* ---------- Фильтры экрана 2 ---------- */
-function applyViolFilters() {
- var list = $("violList");
- if (!list) return;
- var typeEl = $("vType"), stateEl = $("vState");
- var ty = typeEl ? typeEl.value : "all";
- var st = stateEl ? stateEl.value : "all";
- var html = "";
- VIOL.forEach(function (v) {
- if (ty !== "all" && v.type !== ty) return;
- if (st !== "all" && S.checks[v.id] !== st) return;
- html += violCard(v);
+function openCard(id) {
+ getCard(id).then(function (c) {
+ S.curCard = c;
+ S.candId = c.id;
+ go("load");
});
- list.innerHTML = html;
+}
+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(); });
}
-/* ---------- Делегированные обработчики ---------- */
-document.addEventListener("click", function (e) {
- var navBtn = e.target.closest("[data-nav]");
- if (navBtn) { setScreen(navBtn.getAttribute("data-nav")); return; }
+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 });
+}
- var zoneBtn = e.target.closest(".up-grid button, .up-zones button.kt-ai-btn");
- if (zoneBtn) {
- var t = zoneBtn.getAttribute("data-type");
- var f = S.files.find(function (x) { return x.type === t; });
- if (f) {
- f.status = "pending";
- save();
- toast("Файл загружен — ожидание обработки", "info");
- render();
- }
+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; }
- var xBtn = e.target.closest(".up-list button.kt-ai-icon-button");
- if (xBtn) {
- var t2 = xBtn.getAttribute("data-type");
- var f2 = S.files.find(function (x) { return x.type === t2; });
- if (f2) { f2.status = "pending"; save(); render(); }
+ // Экран 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") {
- S.analysisStatus = "done";
- save();
- closeDrawer();
- S.screen = "analysis";
- render();
- toast("Анализ завершён", "ok");
- return;
- }
-
- if (e.target.id === "btnCancelLoad") {
- S.files = FILES_INIT.map(function (f) { return Object.assign({}, f); });
- save();
- toast("Загрузка сброшена", "info");
- render();
+ 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 id = parseInt(actBtn.getAttribute("data-id"), 10);
+ var vid = parseInt(actBtn.getAttribute("data-vid"), 10);
var act = actBtn.getAttribute("data-act");
- if (act === "confirm") S.checks[id] = "confirmed";
- else if (act === "reject") S.checks[id] = "rejected";
- else if (act === "reopen") S.checks[id] = "pending";
- save();
- if (act === "confirm") toast("Несоответствие подтверждено", "ok");
- if (act === "reject") toast("Отклонено как ложное срабатывание", "info");
- if (act === "reopen") toast("Возвращено на проверку", "info");
- applyViolFilters();
- refreshKpis();
+ 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 === "btnRetryAnalysis") {
- S.analysisStatus = "done";
- save();
- render();
- toast("Анализ завершён", "ok");
- return;
- }
+ if (e.target.id === "btnToConcl") { go("concl"); return; }
- if (e.target.id === "btnToConcl") {
- S.screen = "concl"; save(); render();
- return;
- }
+ // Экран 3: заключение
if (e.target.id === "btnSaveConcl") {
var ta = $("conclText");
- if (ta) { S.text = ta.value; save(); }
- var cc = cand();
- if (cc.stage === "uploaded" || cc.stage === "ready" || cc.stage === "analyzed") cc.stage = "draft";
- save();
+ 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) S.text = ta2.value;
- var cc2 = cand();
- cc2.stage = "sent";
- cc2.sent = "21.09.2026, " + new Date().toTimeString().slice(0, 5);
- S.text = S.text + "\n\nОтправлено на согласование: " + cc2.sent;
- save();
+ 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");
- S.screen = "approve";
- render();
+ go("approve");
return;
}
+
+ // Экран 4: согласование
if (e.target.id === "btnApprove") {
- var cc3 = cand();
- cc3.stage = "approved";
+ cur().stage = "approved";
+ cur().approvedAt = new Date().toISOString();
var rc = $("rejectComment");
- cc3.approvedAt = rc && rc.value ? "С комментарием: " + rc.value : "без замечаний";
- save();
+ cur().comment = rc && rc.value ? rc.value : "";
+ saveCard(cur().id, { stage: "approved", approvedAt: cur().approvedAt, comment: cur().comment });
toast("Заключение согласовано", "ok");
- render();
+ go("list");
return;
}
if (e.target.id === "btnReturnConcl") {
- var cc4 = cand();
var rc2 = $("rejectComment");
- cc4.stage = "draft";
- cc4.returned = rc2 && rc2.value ? rc2.value : "Возвращено в экспертизу";
- S.role = "expert";
- S.screen = "concl";
- save();
- $("crumbRole").textContent = S.role === "leader" ? "Руководитель" : "Эксперт";
+ cur().stage = "returned";
+ cur().returned = rc2 && rc2.value ? rc2.value : "Возвращено в экспертизу на доработку";
+ saveCard(cur().id, { stage: "returned", returned: cur().returned });
toast("Возвращено на доработку", "warn");
- render();
+ go("load");
return;
}
+ // Переключение роли
if (e.target.closest("#sbUser")) {
S.role = S.role === "expert" ? "leader" : "expert";
- $("crumbRole").textContent = S.role === "leader" ? "Руководитель" : "Эксперт";
- save();
- toast("Переключено на роль: " + ($("crumbRole").textContent), "info");
+ setShell();
+ toast("Переключено на роль: " + (S.role === "leader" ? "Руководитель" : "Эксперт"), "info");
}
});
-/* Перерисовка KPI вверху экрана 2 после подтверждения/отклонения */
-function refreshKpis() {
- var strip = document.querySelector(".kt-ai-kpi-strip");
- if (!strip) return;
- var confirmed = 0, rejected = 0, pending = 0;
- VIOL.forEach(function (v) {
- var ch = S.checks[v.id];
- if (ch === "confirmed") confirmed++;
- else if (ch === "rejected") rejected++;
- else pending++;
- });
- var vals = [
- [String(pending), "Осталось проверить", "к ручному осмотру"],
- [String(confirmed), "Подтверждено", "экспертом"],
- [String(rejected), "Отклонено", "ложные срабатывания"]
- ];
- var cards = strip.querySelectorAll(".kt-ai-kpi");
- if (cards.length >= 4) {
- cards[3].querySelector(".value").textContent = pending;
- cards[1].querySelector(".value").textContent = confirmed;
- cards[2].querySelector(".value").textContent = rejected;
+document.addEventListener("change", function (e) {
+ if (e.target.id === "vType" || e.target.id === "vState") { applyViolFilters(); return; }
+ if (e.target.id && e.target.id.indexOf("file-") === 0) {
+ if (!e.target.files || !e.target.files.length) return;
+ var t = e.target.id.slice(5);
+ var f = cur().files.find(function (x) { return x.type === t; });
+ if (!f) return;
+ f.name = e.target.files[0].name;
+ f.date = fmtDate(new Date().toISOString());
+ f.status = "done";
+ saveCard(cur().id, { files: cur().files }).then(function () {
+ toast("Файл загружен, статус: Обработан", "ok");
+ render();
+ });
}
+});
+
+document.addEventListener("input", function (e) {
+ if (e.target.id === "fSearch") { S.filter = e.target.value.toLowerCase(); renderListOnly(); }
+});
+
+function renderListOnly() {
+ var wrap = $("cardList");
+ if (!wrap) return;
+ var filtered = (S.cards || []).filter(function (c) {
+ if (!S.filter) return true;
+ return (c.name + " " + (c.post || "")).toLowerCase().indexOf(S.filter) !== -1;
+ });
+ var rows = filtered.map(function (c) {
+ return '
' +
+ '
' + icon("user") + " " +
+ '
' + esc(c.name) + " " +
+ '' + esc(c.post || "") + " · Пакет: " + esc(c.submitted) + "
" +
+ pill(STAGE_CHIP[c.stage] || "—", STAGE_TONE[c.stage] || "gray") +
+ '
' + confirmCount(c) + " найдено ";
+ }).join("");
+ wrap.innerHTML = rows || '
Ничего не найдено Смените запрос.
';
}
-/* Селекты на экране 2 */
-document.addEventListener("change", function (e) {
- if (e.target.id === "vType" || e.target.id === "vState") {
- applyViolFilters();
- }
+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");
- var st = getComputedStyle(sb);
if (sb.dataset.open === "true") { sb.dataset.open = "false"; sb.style.position = "sticky"; }
else { sb.dataset.open = "true"; sb.style.position = "fixed"; sb.style.zIndex = 50; sb.style.top = "0"; sb.style.left = "0"; sb.style.right = "0"; sb.style.bottom = "0"; }
});
-/* Первый рендер */
-render();
-if (S.screen === "concl") { var t = $("conclText"); if (t) t.value = S.text; }
+/* ---------- Инициализация ---------- */
+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();
+ }
+});
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..0db7f80
--- /dev/null
+++ b/server.js
@@ -0,0 +1,95 @@
+const http = require("http");
+const fs = require("fs");
+const path = require("path");
+
+const PORT = process.env.PORT || 3000;
+const DATA = path.join(__dirname, "data.json");
+
+function readData() {
+ try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { items: [], log: [] }; }
+}
+function writeData(d) { fs.writeFileSync(DATA, JSON.stringify(d, null, 2)); }
+function body(req) {
+ return new Promise((resolve) => {
+ let s = "";
+ req.on("data", (c) => (s += c));
+ req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
+ });
+}
+
+http.createServer(async (req, res) => {
+ const url = req.url.split("?")[0];
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
+
+ // API: все карточки кандидата
+ if (req.method === "GET" && url === "/api/cards") {
+ const d = readData();
+ res.end(JSON.stringify(d.items || []));
+ return;
+ }
+
+ // API: получить одну карточку
+ if (req.method === "GET" && url.startsWith("/api/cards/")) {
+ const id = url.split("/").pop();
+ const d = readData();
+ const card = (d.items || []).find((x) => String(x.id) === String(id));
+ if (!card) { res.writeHead(404); res.end(JSON.stringify({ error: "not found" })); return; }
+ res.end(JSON.stringify(card));
+ return;
+ }
+
+ // API: создать новую карточку
+ if (req.method === "POST" && url === "/api/cards") {
+ const b = await body(req);
+ const d = readData();
+ const card = {
+ id: Date.now(),
+ name: b.name || "",
+ post: b.post || "",
+ submitted: new Date().toISOString().slice(0, 10),
+ stage: "uploaded",
+ files: [],
+ analysisStatus: "idle",
+ violations: [],
+ checks: {},
+ text: "",
+ sentAt: null,
+ returned: null,
+ approvedAt: null,
+ createdAt: new Date().toISOString()
+ };
+ d.items.push(card);
+ d.log = d.log || [];
+ d.log.push({ when: new Date().toISOString(), what: "Создана карточка «" + card.name + "»" });
+ writeData(d);
+ res.end(JSON.stringify(card));
+ return;
+ }
+
+ // API: обновить карточку
+ if (req.method === "PUT" && url.startsWith("/api/cards/")) {
+ const id = url.split("/").pop();
+ const b = await body(req);
+ const d = readData();
+ const idx = (d.items || []).findIndex((x) => String(x.id) === String(id));
+ if (idx === -1) { res.writeHead(404); res.end(JSON.stringify({ error: "not found" })); return; }
+ d.items[idx] = Object.assign(d.items[idx], b, { id });
+ d.log = d.log || [];
+ d.log.push({ when: new Date().toISOString(), what: "Обновлена карточка «" + d.items[idx].name + "»" });
+ writeData(d);
+ res.end(JSON.stringify(d.items[idx]));
+ return;
+ }
+
+ // Статика
+ let file = url === "/" ? "/index.html" : url;
+ const full = path.join(__dirname, file);
+ if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
+ const ext = path.extname(full);
+ const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
+ res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
+ res.end(fs.readFileSync(full));
+ return;
+ }
+ res.writeHead(404); res.end("Not found");
+}).listen(PORT, () => console.log("Сервер на порту " + PORT));