v2: полный процесс — drag&drop загрузка, живой прогресс, анализ по фактическим файлам, живой журнал
This commit is contained in:
parent
d2eeb631f1
commit
b19daf0005
@ -37,7 +37,9 @@
|
||||
<span class="spacer"></span>
|
||||
<span class="kt-ai-chip" data-tone="gray" id="roleChip">Эксперт</span>
|
||||
</header>
|
||||
<div id="view" class="kt-ai-work-column"></div>
|
||||
<div id="view" class="kt-ai-work-column">
|
||||
<div id="viewInner"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
455
script.js
455
script.js
@ -38,53 +38,28 @@ function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href=
|
||||
function seed() {
|
||||
return {
|
||||
view: "upload",
|
||||
req: "Кандидат: А. Т. Нурмолдаев · SEO-1, управление сетевых сервисов · заявка от 14.09.2026",
|
||||
req: "Заявка ЭД-2026-118 · Управление сетевых сервисов · 14.09.2026",
|
||||
candidate: "А. Т. Нурмолдаев",
|
||||
sources: [
|
||||
{ n: "Трудовой кодекс Республики Казахстан", s: "Законодательство", d: 207, u: 1 },
|
||||
{ n: "Закон Республики Казахстан «Об акционерных обществах»", s: "Законодательство", d: 148, u: 1 },
|
||||
{ n: "Правила трудового распорядка", s: "Внутренние документы", d: 12, u: 1 },
|
||||
{ n: "Правила поиска и подбора персонала", s: "Внутренние документы", d: 8, u: 1 },
|
||||
{ n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 }
|
||||
],
|
||||
files: [
|
||||
{ n: "Пояснительная записка на назначение — Нурмолдаев А.Т.docx", t: "Пояснительная записка", d: "14.09.2026, 10:24", s: "ready" },
|
||||
{ n: "План отбора 2026-09 — управление сетевых сервисов.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:24", s: "ready" },
|
||||
{ n: "Протокол собеседования №18-09.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:25", s: "ready" },
|
||||
{ n: "Резюме кандидата — Нурмолдаев А.Т.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:28", s: "wait" },
|
||||
{ n: "Анкета о соответствии требованиям.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:31", s: "ready" },
|
||||
{ n: "Проверка требований (внутренняя форма) — скан.pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:32", s: "ready" },
|
||||
{ n: "Справка о стаже и опыте работы.docx", t: "Подтверждающий документ", d: "14.09.2026, 10:33", s: "ready" },
|
||||
{ n: "Диплом о высшем образовании (копия).pdf", t: "Подтверждающий документ", d: "14.09.2026, 10:35", s: "ready" }
|
||||
{ n: "Корпоративный стандарт по управлению человеческими ресурсами фонда", s: "Внутренние документы", d: 96, u: 1 },
|
||||
{ n: "Должностная инструкция SEO-1 (управление сетевыми сервисами)", s: "Внутренние документы", d: 14, u: 1 },
|
||||
{ n: "Положение о совещаниях и комиссиях фонда", s: "Внутренние документы", d: 22, u: 1 }
|
||||
],
|
||||
files: [],
|
||||
progress: null,
|
||||
dropActive: false,
|
||||
running: false,
|
||||
log: [
|
||||
{ t: "10:42", b: "Пакет документов принят, 8 файлов" },
|
||||
{ t: "10:42", b: "Извлечение текста: пояснительная записка, 12 разд." },
|
||||
{ t: "10:43", b: "Сверка с Правилами поиска и подбора персонала: раздел «Организация отбора», п. 4.2" },
|
||||
{ t: "10:43", b: "Найдено отклонение: дата объявления о вакансии" },
|
||||
{ t: "10:44", b: "Сверка с Правилами трудового распорядка, гл. 5 «Назначение руководителей»" },
|
||||
{ t: "10:44", b: "Сверка с Законом «Об АО»: компетенции совета директоров, ст. 36" },
|
||||
{ t: "10:45", b: "Скрытое голосование: противоречие в протоколе и правилах" },
|
||||
{ t: "10:45", b: "Проверка квалификации: стаж, образование, аттестация" },
|
||||
{ t: "10:46", b: "Обнаружено 4 несоответствия, 1 противоречие" },
|
||||
{ t: "10:46", b: "Журнал завершён — выводы переданы на верификацию эксперту" }
|
||||
],
|
||||
found: 4,
|
||||
contra: true,
|
||||
violations: [
|
||||
{ d: "Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10", y: "process", r: "Правила поиска и подбора персонала, раздел 4.2", v: "conf" },
|
||||
{ d: "В пояснительной записке 3 кандидата, в протоколе собеседования фиксированы 2", y: "process", r: "План отбора 2026-09, п. 3", v: "conf" },
|
||||
{ d: "Срок согласования кандидатуры с материнской компанией — 2 дня при нормативе 5", y: "process", r: "Корпоративный стандарт по УХР, ред. 12", v: "conf" },
|
||||
{ d: "У кандидата 6 лет опыта в телекоммуникациях при требуемых 7 для уровня SEO-1", y: "qual", r: "Должностная инструкция SEO-1, п. 2 «Требования к квалификации»", v: "conf" },
|
||||
{ d: "Стаж в должности SEO-3: 3 года 4 месяца при нормативе не менее 4 лет", y: "qual", r: "Должностная инструкция SEO-2 и SEO-3, п. 2", v: "conf" },
|
||||
{ d: "Аттестация по управленческой программе в материалах не подтверждена", y: "qual", r: "Корпоративный стандарт по УХР, гл. 8", v: "conf" },
|
||||
{ d: "В протоколе — решение голосованием 4:1, в правилах назначений — единогласно", y: "contra", r: "Трудовой Кодекс РК, ст. 165, часть 3", v: "falsep" },
|
||||
{ d: "Внутренняя форма проверки заполнена не полностью — графа «Аттестация»", y: "process", r: "Правила трудового распорядка, приложение 2", v: "falsep" }
|
||||
],
|
||||
verdict: "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат А. Т. Нурмолдаев) прошла проверку соответствия нормативной базе. По завершённой сверке зафиксированы следующие несоответствия:\n\n1. Объявление о вакансии опубликовано за 5 календарных дней до открытия отбора, норматив — 10 (Правила поиска и подбора персонала, раздел 4.2).\n2. В пояснительной записке указан круг из 3 кандидатов, в протоколе собеседования зафиксировано 2 (План отбора 2026-09, п. 3).\n3. Срок согласования кандидатуры с материнской компанией составил 2 дня при нормативе 5 (Корпоративный стандарт по УХР, ред. 12).\n4. Стаж в отрасли 6 лет при требуемых 7 для уровня SEO-1 (Должностная инструкция SEO-1, п. 2).\n5. Стаж в должности SEO-3 — 3 года 4 месяца при нормативе не менее 4 лет (Должностная инструкция SEO-2 и SEO-3, п. 2).\n6. Аттестация по управленческой программе в материалах не подтверждена (Корпоративный стандарт по УХР, гл. 8).\n\nПротиворечие в способе принятия решения (протокол — голосование 4:1, правила — единогласно) рассмотрено экспертом: позиция кандидата, поданная в установленном порядке, исключает применение требования единогласия; вывод системы отклонён как не относящийся к рассматриваемому пакету.\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.",
|
||||
checklist: [1, 2, 3, 4, 5, 6],
|
||||
status: "sent",
|
||||
contra: false,
|
||||
falseDone: false,
|
||||
log: [],
|
||||
violations: [],
|
||||
checklist: [],
|
||||
verdict: "",
|
||||
status: "draft",
|
||||
rejected: null,
|
||||
stamp: null
|
||||
};
|
||||
@ -111,8 +86,8 @@ function doneCount() {
|
||||
return n;
|
||||
}
|
||||
function stepNum() {
|
||||
if (D.view === "upload") return D.running || allFilesReady() ? 2 : 1;
|
||||
if (D.view === "results") return D.contra && !D.falseDone ? 2 : 3;
|
||||
if (D.view === "upload") return allFilesReady() ? 2 : 1;
|
||||
if (D.view === "results") return 2;
|
||||
if (D.view === "verdict") return 3;
|
||||
if (D.view === "approve") return D.status === "agree" || D.status === "decl" ? 5 : 4;
|
||||
return 1;
|
||||
@ -196,7 +171,8 @@ function render() {
|
||||
else if (D.view === "verdict") html = vVerdict();
|
||||
else if (D.view === "approve") html = vApprove();
|
||||
else html = vAbout();
|
||||
var view = $("view");
|
||||
var view = $("viewInner");
|
||||
if (!view) view = $("view");
|
||||
if (!view) return;
|
||||
view.innerHTML = html;
|
||||
$("pageTitle").textContent = TITLES[D.view] || "";
|
||||
@ -223,46 +199,95 @@ function kpiStrip(items) {
|
||||
}
|
||||
|
||||
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */
|
||||
function vUpload() {
|
||||
var readyCount = 0;
|
||||
for (var i = 0; i < D.files.length; i++) if (D.files[i].s === "ready") readyCount++;
|
||||
var all = readyCount === D.files.length;
|
||||
function fmtSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
|
||||
var mb = bytes / 1024 / 1024;
|
||||
return (mb < 100 ? mb.toFixed(1) : Math.round(mb)) + " MB";
|
||||
}
|
||||
|
||||
var head = headReady;
|
||||
if (all && !D.running) {
|
||||
head = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-ai-stack-block);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
|
||||
'<span style="flex:1;min-width:0">Все материалы получены — пакет готов к запуску анализа</span>' +
|
||||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
|
||||
function classifyFile(name) {
|
||||
var n = name.toLowerCase();
|
||||
if (n.indexOf("пояснительная") >= 0 || n.indexOf("записк") >= 0 || n.indexOf("memo") >= 0) return "Пояснительная записка";
|
||||
var ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
|
||||
if (n.indexOf("договор") >= 0 || n.indexOf("contract") >= 0) return "Договор";
|
||||
if (n.indexOf("акт") >= 0 || n.indexOf("certificate") >= 0) return "Сертификат / Акт";
|
||||
if (n.indexOf("счёт") >= 0 || n.indexOf("счет") >= 0 || n.indexOf("invoice") >= 0) return "Счёт";
|
||||
if (["doc", "docx"].indexOf(ext) >= 0) return "Документ Word";
|
||||
if (["xls", "xlsx", "csv"].indexOf(ext) >= 0) return "Таблица";
|
||||
if (ext === "pdf") return "PDF";
|
||||
if (["jpg", "jpeg", "png"].indexOf(ext) >= 0) return "Изображение";
|
||||
return "Документ";
|
||||
}
|
||||
|
||||
function vUpload() {
|
||||
var progress = D.progress;
|
||||
var zoneBlock;
|
||||
if (progress) {
|
||||
zoneBlock = dropZoneHtml() +
|
||||
'<div class="progress">' +
|
||||
'<div class="pl">' +
|
||||
'<span class="pl-ico">' + (progress.step < 3 ? "…" : icon("check")) + "</span>" +
|
||||
'<div><strong>' + esc(STEPS_UPLOAD[progress.step] || "…") + "</strong>" +
|
||||
'<span class="kt-ai-meta">' + esc(progress.file) + "</span></div>" +
|
||||
'"</div>' +
|
||||
'<div class="kp">' +
|
||||
'<div class="bar"><i style="width:' + progress.pct + '%"></i></div>' +
|
||||
'<div class="lp"><span>' + esc(progress.file) + "</span>" +
|
||||
'<span class="kp-num">' + num(progress.pct) + " %" + "</span></div>" +
|
||||
"</div></div>";
|
||||
} else if (progress && progress.file) {
|
||||
zoneBlock = dropZoneHtml() +
|
||||
'<div class="progress done">' +
|
||||
'<div class="pl">' +
|
||||
'<span class="pl-ico">' + icon("check") + "</span>" +
|
||||
'<div><strong>Загрузка завершена</strong>' +
|
||||
'<span class="kt-ai-meta">' + esc(progress.file) + "</span></div>" +
|
||||
"</div>" +
|
||||
'<div class="kp"><div class="bar done"><i style="width:100%"></i></div>' +
|
||||
'<div class="lp"><span>' + esc(progress.file) + "</span>" +
|
||||
'<span class="kp-num">' + fmtSize(progress.size || 0) + "</span></div></div>" +
|
||||
"</div>";
|
||||
} else {
|
||||
zoneBlock = dropZoneHtml();
|
||||
}
|
||||
|
||||
var rows = D.files.map(function (f, i) {
|
||||
var statusCell = pill(f.s);
|
||||
if (f.s === "wait") statusCell += ' <span class="kt-ai-chip" data-tone="orange" style="margin-left:var(--kt-ai-space-2)">ждёт загрузки</span>';
|
||||
return "<tr" + (f.s === "wait" ? ' data-wait="true"' : "") + ">" +
|
||||
'<td data-label="Файл">' + esc(f.n) + "</td>" +
|
||||
'<td data-label="Тип"><span class="kt-ai-meta">' + esc(f.t) + "</span></td>" +
|
||||
'<td data-label="Загружен"><span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs)">' + esc(f.d) + "</span></td>" +
|
||||
'<td data-label="Статус">' + statusCell + "</td>" +
|
||||
'<td class="row-act"><button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="S.viewFile(' + i + ')">Смотреть</button></td>' +
|
||||
"</tr>";
|
||||
}).join("");
|
||||
var head = "";
|
||||
if (!progress && D.files.length) {
|
||||
var all = D.files.every(function (f) { return f.s === "ready"; });
|
||||
if (all && !D.running) {
|
||||
head = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-ai-stack-tight);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
|
||||
"<span style='flex:1;min-width:0'><strong>Все материалы получены</strong> — пакет готов к запуску анализа</span>" +
|
||||
'<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.analyze()">Запустить анализ ИИ</button></div>';
|
||||
} else if (!all && !D.running) {
|
||||
head = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-tight);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
|
||||
"<span style='flex:1;min-width:0'><strong>Пакет не собран</strong> — не все файлы получены от кандидата</span>" +
|
||||
'<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="S.uploadFiles()">Загрузка файлов</button></div>';
|
||||
}
|
||||
}
|
||||
|
||||
var headReady = hasWaiting
|
||||
? '<div class="kt-ai-banner" data-tone="info" style="margin-bottom:var(--kt-ai-stack-block)">' +
|
||||
"<strong>Пакет не собран</strong>" +
|
||||
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Не все файлы получены от кандидата — анализ станет доступен после загрузки остальных материалов.</span></div>'
|
||||
var table = D.files.length
|
||||
? '<div class="kt-ai-table-wrap" style="margin-top:var(--kt-ai-stack-tight)">' +
|
||||
'<table class="kt-ai-table"><thead><tr><th>Название</th><th>Размер</th><th>Дата</th><th>Статус</th><th></th></tr></thead>' +
|
||||
"<tbody>" + D.files.map(function (f, i) {
|
||||
return "<tr>" +
|
||||
'<td data-label="Название">' + esc(f.n) + "</td>" +
|
||||
'<td data-label="Размер"><span class="kt-ai-num">' + fmtSize(f.size || 0) + "</span></td>" +
|
||||
'<td data-label="Дата"><span class="kt-ai-num">' + esc(f.d) + "</span></td>" +
|
||||
'<td data-label="Статус">' + pill(f.s) + "</td>" +
|
||||
'<td><button class="kt-ai-icon-button" title="Удалить" onclick="S.delFile(' + i + ')">' + icon("trash") + "</button></td>" +
|
||||
"</tr>";
|
||||
}).join("") + "</tbody></table></div>"
|
||||
: "";
|
||||
|
||||
return '<section class="sec"><div class="sec-h">Процесс' +
|
||||
'<span class="spacer"></span>' +
|
||||
'<span class="kt-ai-meta">Заявка ' + num("ЭД-2026-118") + "</span></div>" + vStepper() + "</section>" +
|
||||
head +
|
||||
(head ? '<div style="margin-bottom:var(--kt-ai-stack-tight)">' + head + "</div>" : "") +
|
||||
'<section class="sec"><div class="sec-h">Материалы' +
|
||||
'<span class="spacer"></span>' +
|
||||
'<span class="kt-ai-meta">' + num(readyCount + " из " + D.files.length) + " готово</span></div>" +
|
||||
'<div class="kt-ai-table-wrap" data-framed="true">' +
|
||||
'<table class="kt-ai-table"><thead><tr><th>Файл</th><th>Тип</th><th>Загружен</th><th>Статус</th><th></th></tr></thead>' +
|
||||
"<tbody>" + rows + "</tbody></table></div></section>" +
|
||||
(D.files.length ? '<span class="kt-ai-meta">' + num(D.files.length) + " файл(а)</span>" : "") + "</div>" +
|
||||
zoneBlock + table + "</section>" +
|
||||
'<section class="sec"><div class="sec-h">Источники для сверки' +
|
||||
'<span class="spacer"></span>' +
|
||||
'<span class="kt-ai-meta">' + num(D.sources.length) + " источника</span></div>" +
|
||||
@ -276,8 +301,21 @@ function vUpload() {
|
||||
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
|
||||
}).join("") + "</div></section>" +
|
||||
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
|
||||
"<p>Строками идёт по заявке и пакету документов. Статусы файлов: «Ожидает проверки» — файл ещё получен не был, «Готово к анализу» — файл проверен и получен. " +
|
||||
"Когда собран весь пакет, вверху экрана появляется кнопка «Запустить анализ ИИ».</p></section>";
|
||||
"<p>Загрузите документы: перетащите файлы в зону загрузки или нажмите «выберите на устройстве». Система определит тип каждого файла, " +
|
||||
"отметит готовность и после полной загрузки пакета включит кнопку «Запустить анализ ИИ». Анализ будет идти по фактическим загруженным файлам — " +
|
||||
"сверка с внутренними правилами и законодательством по 20 источникам.</p></section>";
|
||||
}
|
||||
|
||||
function dropZoneHtml() {
|
||||
var active = D.dropActive ? ' data-active="true"' : "";
|
||||
return '<div class="drop" id="drop"' + active + '>' +
|
||||
'<div class="drop-main">' +
|
||||
'<span class="drop-ico">' + icon("upload") + "</span>" +
|
||||
"<strong>Перетащите файлы сюда</strong>" +
|
||||
"<span class='kt-ai-meta'>или <span class='drop-link' onclick='S.uploadFiles()'>выберите на устройстве</span></span>" +
|
||||
"</div>" +
|
||||
"<span class='drop-limit'>PDF, DOCX, XLSX, TXT — до 50 МБ</span>" +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
/* ---------- Экран 2. Результаты анализа ИИ ---------- */
|
||||
@ -485,42 +523,97 @@ function nowTime() {
|
||||
}
|
||||
|
||||
/* ---------- Действия ---------- */
|
||||
S.uploadFiles = function () {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept = ".pdf,.docx,.xlsx,.csv,.txt,.doc";
|
||||
input.onchange = function () {
|
||||
var files = input.files || [];
|
||||
if (files.length === 0) return;
|
||||
var added = 0;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i];
|
||||
var exists = false;
|
||||
for (var j = 0; j < D.files.length; j++) {
|
||||
if (D.files[j].s === "wait" && D.files[j].n.toLowerCase().indexOf(f.name.toLowerCase().slice(0, 20)) >= 0) {
|
||||
D.files[j].s = "ready";
|
||||
D.files[j].d = nowTime();
|
||||
exists = true;
|
||||
added++;
|
||||
break;
|
||||
var STEPS_UPLOAD = [
|
||||
"Проверка файла",
|
||||
"Сканирование на вирусы",
|
||||
"Извлечение текста"
|
||||
];
|
||||
|
||||
/* Скрытый input для выбора файлов на устройстве */
|
||||
if (typeof window !== "undefined") {
|
||||
var _fileInput = null;
|
||||
function getFileInput() {
|
||||
if (!_fileInput) {
|
||||
_fileInput = document.createElement("input");
|
||||
_fileInput.type = "file";
|
||||
_fileInput.multiple = true;
|
||||
_fileInput.accept = ".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt";
|
||||
_fileInput.style.display = "none";
|
||||
_fileInput.addEventListener("change", function () {
|
||||
if (_fileInput.files && _fileInput.files.length) {
|
||||
handleFiles(Array.prototype.slice.call(_fileInput.files));
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
var ext = (f.name.lastIndexOf(".") >= 0) ? f.name.slice(f.name.lastIndexOf(".")) : "";
|
||||
var typeLabel = "Подтверждающий документ";
|
||||
if (f.name.toLowerCase().indexOf("записк") >= 0) typeLabel = "Пояснительная записка";
|
||||
D.files.push({ n: f.name, t: typeLabel, d: nowTime() + " (загрузка)", s: "ready" });
|
||||
added++;
|
||||
}
|
||||
_fileInput.value = "";
|
||||
});
|
||||
document.body.appendChild(_fileInput);
|
||||
}
|
||||
return _fileInput;
|
||||
}
|
||||
window.__getFileInput = getFileInput;
|
||||
}
|
||||
|
||||
function handleFiles(arr) {
|
||||
if (!arr || !arr.length) return;
|
||||
var existing = {};
|
||||
for (var k = 0; k < D.files.length; k++) existing[D.files[k].n] = true;
|
||||
var toAdd = [];
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
var f = arr[i];
|
||||
if (existing[f.name]) continue;
|
||||
toAdd.push({ f: f, t: classifyFile(f.name), size: f.size, d: nowTime() + " · " + nowTime() });
|
||||
}
|
||||
if (!toAdd.length) { toast("Файлы уже загружены", "warn"); return; }
|
||||
var total = 0;
|
||||
for (var j = 0; j < toAdd.length; j++) total += toAdd[j].size;
|
||||
D.progress = { active: true, step: 0, pct: 0, file: toAdd[0].f.name, size: total, done: {} };
|
||||
D.files = [];
|
||||
save();
|
||||
render();
|
||||
simulateUpload(toAdd, 0, total);
|
||||
}
|
||||
|
||||
function simulateUpload(list, idx, total) {
|
||||
if (idx >= list.length) {
|
||||
if (typeof _uploadTimer !== "undefined" && _uploadTimer) clearInterval(_uploadTimer);
|
||||
D.progress = null;
|
||||
save();
|
||||
toast(D.files.length + " файл(ов) загружено в пакет", "ok");
|
||||
return;
|
||||
}
|
||||
var item = list[idx];
|
||||
var name = item.f.name;
|
||||
var size = item.size;
|
||||
var step = 0;
|
||||
var pct = 0;
|
||||
D.progress = { active: true, step: 0, pct: 0, file: name, size: total };
|
||||
save();
|
||||
render();
|
||||
var iv = setInterval(function () {
|
||||
pct += 8 + Math.floor(Math.random() * 10);
|
||||
if (pct >= 100) pct = 100;
|
||||
step = pct < 34 ? 0 : (pct < 67 ? 1 : 2);
|
||||
D.progress = { active: true, step: step, pct: pct, file: name, size: total };
|
||||
save();
|
||||
render();
|
||||
toast(added + " файл(ов) добавлено в пакет", "ok");
|
||||
var allNow = D.files.every(function (x) { return x.s === "ready"; });
|
||||
if (allNow) toast("Пакет собран — анализ стал доступен", "ok");
|
||||
};
|
||||
input.click();
|
||||
if (pct >= 100) {
|
||||
clearInterval(iv);
|
||||
setTimeout(function () {
|
||||
D.files.push({ n: name, t: item.t, size: size, d: item.d, s: "ready" });
|
||||
save();
|
||||
render();
|
||||
setTimeout(function () {
|
||||
simulateUpload(list, idx + 1, total);
|
||||
}, 700);
|
||||
}, 400);
|
||||
}
|
||||
}, 140);
|
||||
}
|
||||
|
||||
S.delFile = function (i) {
|
||||
D.files.splice(i, 1);
|
||||
save();
|
||||
render();
|
||||
toast("Файл удалён из пакета", "info");
|
||||
};
|
||||
|
||||
S.fileReady = function (i) {
|
||||
@ -531,49 +624,90 @@ S.fileReady = function (i) {
|
||||
|
||||
S.go = function (id) { D.view = id; render(); };
|
||||
|
||||
function genViolations() {
|
||||
var base = [];
|
||||
for (var i = 0; i < D.files.length; i++) {
|
||||
var f = D.files[i];
|
||||
base.push(
|
||||
{ d: f.n + ": текст извлечён и сверен с внутренними документами", y: "process", r: f.t + ", раздел 1", v: "checked" },
|
||||
{ d: f.n + ": найдено 2 несоответствия с нормативной базой", y: "qual", r: f.t + ", ст. 36", v: "checked" }
|
||||
);
|
||||
}
|
||||
base.push(
|
||||
{ d: "Протокол: решение голосованием 4:1, правила — единогласно", y: "contra", r: "ТК РК, ст. 165", v: "falsep" }
|
||||
);
|
||||
if (base.length === 1) {
|
||||
base.unshift({ d: "Пакет пуст — нет данных для полного анализа", y: "process", r: "—", v: "falsep" });
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
S.analyze = function () {
|
||||
if (!allFilesReady()) { toast("Дополните материалы — все файлы должны быть готовы", "warn"); return; }
|
||||
if (!D.files.length) { toast("Сначала загрузите файлы", "warn"); return; }
|
||||
if (D.files.some(function (f) { return f.s !== "ready"; })) { toast("Дождитесь завершения загрузки всех файлов", "warn"); return; }
|
||||
D.running = true;
|
||||
D.contra = false;
|
||||
D.falseDone = false;
|
||||
D.checklist = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
var vio = genViolations();
|
||||
D.checklist = [];
|
||||
for (var k = 0; k < vio.length; k++) {
|
||||
vio[k].v = "checked";
|
||||
D.checklist.push(k);
|
||||
}
|
||||
D.status = "draft";
|
||||
D.rejected = null;
|
||||
D.stamp = null;
|
||||
for (var i = 0; i < D.violations.length; i++) D.violations[i].v = D.violations[i].v === "conf" ? "checked" : "checked";
|
||||
D.violations[6].v = "falsep";
|
||||
D.violations[7].v = "falsep";
|
||||
D.violations = vio;
|
||||
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
|
||||
D.log = [
|
||||
{ t: nowTime(), b: "Пакет принят, 8 файлов" },
|
||||
{ t: nowTime(), b: "Запущен анализ: " + D.sources.length + " источника для сверки" }
|
||||
];
|
||||
render();
|
||||
setTimeout(function () { pushLogs(); }, 900);
|
||||
setTimeout(function () { pushLogs(); }, 2100);
|
||||
setTimeout(function () {
|
||||
D.running = false;
|
||||
D.violations[6].v = "falsep";
|
||||
D.contra = true;
|
||||
D.log.push({ t: nowTime(), b: "Обнаружено несоответствий: " + (D.violations.length - 2) + ", противоречий: 1" });
|
||||
D.log.push({ t: nowTime(), b: "Журнал завершён — выводы переданы эксперту" });
|
||||
S.go("results");
|
||||
}, 3600);
|
||||
};
|
||||
|
||||
function pushLogs() {
|
||||
var extra = [
|
||||
{ t: nowTime(), b: "Извлечение текста: пояснительная записка" },
|
||||
{ t: nowTime(), b: "Сверка с Правилами поиска и подбора персонала, п. 4.2" },
|
||||
{ t: nowTime(), b: "Найдено отклонение: дата объявления о вакансии" },
|
||||
{ t: nowTime(), b: "Сверка с Законом «Об АО», ст. 36" },
|
||||
{ t: nowTime(), b: "Проверка квалификации кандидата" }
|
||||
];
|
||||
if (D.log.length <= 2) { D.log.push(extra[0]); D.log.push(extra[1]); }
|
||||
else if (D.log.length <= 4) { D.log.push(extra[2]); D.log.push(extra[3]); }
|
||||
else { D.log.push(extra[4]); }
|
||||
D.log = [];
|
||||
save();
|
||||
render();
|
||||
startLogStream();
|
||||
setTimeout(function () {
|
||||
if (!D.running) return;
|
||||
for (var m = 0; m < D.violations.length; m++) if (D.violations[m].y === "contra") D.violations[m].v = "falsep";
|
||||
D.running = false;
|
||||
D.contra = true;
|
||||
var proc = 0, qual = 0, contra = 0;
|
||||
for (var n = 0; n < D.violations.length; n++) {
|
||||
var y = D.violations[n].y;
|
||||
if (y === "process") proc++;
|
||||
else if (y === "qual") qual++;
|
||||
else if (y === "contra") contra++;
|
||||
}
|
||||
D.log.push({ t: nowTime(), b: "Обнаружено несоответствий: " + (proc + qual) + ", противоречий: " + contra });
|
||||
D.log.push({ t: nowTime(), b: "Журнал завершён — выводы переданы эксперту" });
|
||||
D.view = "results";
|
||||
save();
|
||||
render();
|
||||
toast("Анализ завершён — переход в результаты", "ok");
|
||||
}, 6000);
|
||||
};
|
||||
|
||||
function genLogPlan() {
|
||||
var plan = [];
|
||||
var files = D.files.concat([]);
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
plan.push({ b: "Извлечение текста: " + files[i].n });
|
||||
}
|
||||
plan.push({ b: "Сверка с Правилами поиска и подбора персонала, п. 4.2" });
|
||||
plan.push({ b: "Сверка с Законом «Об АО», ст. 36" });
|
||||
plan.push({ b: "Проверка квалификации кандидата: стаж, образование, аттестация" });
|
||||
return plan.map(function (line) { return { t: nowTime(), b: line.b }; });
|
||||
}
|
||||
|
||||
function startLogStream() {
|
||||
var plan = genLogPlan();
|
||||
var delay = 300;
|
||||
for (var i = 0; i < plan.length; i++) (function (line) {
|
||||
setTimeout(function () {
|
||||
if (D.running) {
|
||||
D.log.push(line);
|
||||
save();
|
||||
render();
|
||||
}
|
||||
}, delay);
|
||||
})(plan[i]);
|
||||
}
|
||||
|
||||
S.closeDrawer = closeDrawer;
|
||||
@ -688,6 +822,51 @@ S.burger = function () {
|
||||
else sb.setAttribute("data-open", "true");
|
||||
};
|
||||
|
||||
/* ---------- Drag & drop ---------- */
|
||||
function initDnD() {
|
||||
document.addEventListener("dragover", function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf("Files") >= 0) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
D.dropActive = true;
|
||||
var d = $("drop");
|
||||
if (d) d.setAttribute("data-active", "true");
|
||||
}
|
||||
});
|
||||
document.addEventListener("dragleave", function (e) {
|
||||
if (e.relatedTarget === null) {
|
||||
D.dropActive = false;
|
||||
var d = $("drop");
|
||||
if (d) d.removeAttribute("data-active");
|
||||
}
|
||||
});
|
||||
document.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
D.dropActive = false;
|
||||
var d = $("drop");
|
||||
if (d) d.removeAttribute("data-active");
|
||||
var files = e.dataTransfer && e.dataTransfer.files;
|
||||
if (files && files.length) handleFiles(Array.prototype.slice.call(files));
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Инициализация ---------- */
|
||||
load();
|
||||
render();
|
||||
initDnD();
|
||||
|
||||
/* Обновление drop-зоны после dragover/drop без рендера всего экрана */
|
||||
function refreshDrop() {
|
||||
var d = $("drop");
|
||||
if (d) {
|
||||
if (D.dropActive) d.setAttribute("data-active", "true");
|
||||
else d.removeAttribute("data-active");
|
||||
}
|
||||
}
|
||||
var _dndTimer = null;
|
||||
var _origRender = render;
|
||||
render = function () {
|
||||
_origRender();
|
||||
if (_dndTimer) clearTimeout(_dndTimer);
|
||||
_dndTimer = setTimeout(refreshDrop, 0);
|
||||
};
|
||||
|
||||
74
style.css
74
style.css
@ -138,3 +138,77 @@ tr[data-wait="true"] td:first-child { color: var(--kt-ai-status-warn-fg); font-w
|
||||
.kt-ai-table-wrap .kt-ai-table tbody tr { display: table-row; }
|
||||
.kt-ai-table-wrap .kt-ai-table td { display: table-cell; }
|
||||
}
|
||||
|
||||
/* ====== Загрузка: зона + прогресс-бар ====== */
|
||||
.drop {
|
||||
border: 2px dashed var(--kt-ai-border);
|
||||
border-radius: var(--kt-ai-radius-2xl);
|
||||
background: var(--kt-ai-bg-elevated);
|
||||
padding: var(--kt-ai-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--kt-ai-space-5);
|
||||
cursor: pointer;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.drop:hover, .drop[data-active="true"] {
|
||||
border-color: var(--kt-ai-primary);
|
||||
background: var(--kt-ai-primary-subtle);
|
||||
}
|
||||
.drop-main { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; }
|
||||
.drop-main strong { font-size: var(--kt-ai-text-md); font-weight: var(--kt-ai-weight-semibold); }
|
||||
.drop-ico {
|
||||
width: 40px; height: 40px; flex: 0 0 40px;
|
||||
border-radius: var(--kt-ai-radius-lg);
|
||||
background: var(--kt-ai-bg-active);
|
||||
color: var(--kt-ai-fg-muted);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.drop[data-active="true"] .drop-ico { background: var(--kt-ai-primary); color: var(--kt-ai-fg-on-primary); }
|
||||
.drop-limit { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); }
|
||||
.drop-link { color: var(--kt-ai-link); text-decoration: underline; cursor: pointer; }
|
||||
|
||||
/* Прогресс-бар загрузки */
|
||||
.progress {
|
||||
margin-top: var(--kt-ai-space-4);
|
||||
padding: var(--kt-ai-space-6);
|
||||
border: 1px solid var(--kt-ai-divider);
|
||||
border-radius: var(--kt-ai-radius-xl);
|
||||
background: var(--kt-ai-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--kt-ai-space-6);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.progress .pl { display: flex; align-items: center; gap: var(--kt-ai-space-4); min-width: 280px; }
|
||||
.progress .pl-ico {
|
||||
width: 34px; height: 34px; flex: 0 0 34px;
|
||||
border-radius: var(--kt-ai-radius-md);
|
||||
background: var(--kt-ai-status-info-bg);
|
||||
color: var(--kt-ai-status-info-fg);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: var(--kt-ai-text-sm);
|
||||
}
|
||||
.progress.done .pl-ico { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
|
||||
.progress .pl strong { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); display: block; }
|
||||
.progress .pl .kt-ai-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); display: block; margin-top: 1px; }
|
||||
.progress .kp { flex: 1; min-width: 240px; }
|
||||
.progress .bar {
|
||||
height: 6px;
|
||||
border-radius: var(--kt-ai-radius-full);
|
||||
background: var(--kt-ai-bg-active);
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress .bar i {
|
||||
display: block; height: 100%;
|
||||
background: var(--kt-ai-primary);
|
||||
border-radius: var(--kt-ai-radius-full);
|
||||
transition: width .3s ease;
|
||||
}
|
||||
.progress.done .bar i { background: var(--kt-ai-status-ok-fg); }
|
||||
.progress .lp {
|
||||
display: flex; justify-content: space-between; gap: var(--kt-ai-space-3);
|
||||
margin-top: var(--kt-ai-space-2);
|
||||
font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted);
|
||||
}
|
||||
.progress .lp .kp-num { font-variant-numeric: tabular-nums; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user