885 lines
46 KiB
JavaScript
885 lines
46 KiB
JavaScript
"use strict";
|
||
/* Экспертиза документов для советов — статичный прототип, состояние в localStorage. */
|
||
|
||
var LS_KEY = "edds-v1";
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
function esc(s) {
|
||
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||
});
|
||
}
|
||
function escJs(s) { return esc(String(s == null ? "" : s).replace(/\\/g, "\\\\").replace(/'/g, "\\'")); }
|
||
|
||
var ST = {
|
||
wait: { label: "Ожидает проверки", tone: "warn" },
|
||
ready: { label: "Готово к анализу", tone: "ok" },
|
||
process: { label: "Процесс отбора", tone: "info" },
|
||
qual: { label: "Квалификация", tone: "info" },
|
||
contra: { label: "Противоречие", tone: "risk" },
|
||
checked: { label: "На проверке", tone: "info" },
|
||
conf: { label: "Подтверждено", tone: "ok" },
|
||
falsep: { label: "Ложное срабатывание", tone: "muted" },
|
||
draft: { label: "Черновик", tone: "warn" },
|
||
sent: { label: "Готово к согласованию", tone: "info" },
|
||
agree: { label: "Согласовано", tone: "ok" },
|
||
decl: { label: "Отклонено", tone: "risk" },
|
||
pending: { label: "Ожидает решения", tone: "warn" }
|
||
};
|
||
|
||
function pill(key, label) {
|
||
var st = ST[key] || { label: label || key, tone: "muted" };
|
||
return '<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span>";
|
||
}
|
||
function num(s) { return '<span class="kt-ai-num">' + s + "</span>"; }
|
||
function icon(name) { return '<svg class="kt-icon" aria-hidden="true"><use href="#' + name + '"></use></svg>'; }
|
||
|
||
/* ---------- Демо-данные ---------- */
|
||
function seed() {
|
||
return {
|
||
view: "upload",
|
||
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 },
|
||
{ n: "Должностная инструкция SEO-1 (управление сетевыми сервисами)", s: "Внутренние документы", d: 14, u: 1 },
|
||
{ n: "Положение о совещаниях и комиссиях фонда", s: "Внутренние документы", d: 22, u: 1 }
|
||
],
|
||
files: [],
|
||
progress: null,
|
||
dropActive: false,
|
||
running: false,
|
||
contra: false,
|
||
falseDone: false,
|
||
log: [],
|
||
violations: [],
|
||
checklist: [],
|
||
verdict: "",
|
||
status: "draft",
|
||
rejected: null,
|
||
stamp: null
|
||
};
|
||
}
|
||
|
||
var D = null;
|
||
function load() {
|
||
try { var raw = localStorage.getItem(LS_KEY); D = raw ? JSON.parse(raw) : seed(); }
|
||
catch (e) { D = seed(); }
|
||
if (!D.files || !D.violations) D = seed();
|
||
}
|
||
function save() {
|
||
try { localStorage.setItem(LS_KEY, JSON.stringify(D)); } catch (e) {}
|
||
}
|
||
|
||
/* ---------- Прогресс процесса ---------- */
|
||
function allFilesReady() {
|
||
if (!D.files.length) return false;
|
||
for (var i = 0; i < D.files.length; i++) if (D.files[i].s !== "ready") return false;
|
||
return true;
|
||
}
|
||
function doneCount() {
|
||
var n = 0, i;
|
||
for (i = 0; i < D.violations.length; i++) if (D.violations[i].v === "conf") n++;
|
||
return n;
|
||
}
|
||
function stepNum() {
|
||
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;
|
||
}
|
||
var STEPS = ["Загрузка", "Анализ", "Заключение", "Согласование"];
|
||
|
||
/* ---------- UI: тосты, модалки ---------- */
|
||
var S = {};
|
||
function toast(msg, tone) {
|
||
var box = $("toasts");
|
||
if (!box) return;
|
||
var el = document.createElement("div");
|
||
el.className = "kt-ai-toast";
|
||
if (tone) el.dataset.tone = tone;
|
||
el.textContent = msg;
|
||
box.appendChild(el);
|
||
setTimeout(function () {
|
||
el.style.transition = "opacity .3s, transform .3s";
|
||
el.style.opacity = "0";
|
||
el.style.transform = "translateY(6px)";
|
||
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320);
|
||
}, 2600);
|
||
}
|
||
function openModal(html) { var m = $("modal"); m.innerHTML = html; m.style.display = "flex"; }
|
||
function closeModal() { var m = $("modal"); m.style.display = "none"; m.innerHTML = ""; }
|
||
S.closeModal = closeModal;
|
||
|
||
function openDrawer(html) { var d = $("drawer"); d.innerHTML = html; d.style.display = "block"; }
|
||
function closeDrawer() { var d = $("drawer"); d.style.display = "none"; d.innerHTML = ""; }
|
||
|
||
/* ---------- Навигация и рендер ---------- */
|
||
var NAV = [
|
||
{ id: "upload", label: "Загрузка и обзор", icon: "upload" },
|
||
{ id: "results", label: "Результаты анализа", icon: "sparkles" },
|
||
{ id: "verdict", label: "Формирование заключения", icon: "pencil" },
|
||
{ id: "approve", label: "Согласование", icon: "check-circle" },
|
||
{ id: "about", label: "О решении", icon: "info", group: "Решение" }
|
||
];
|
||
var TITLES = {
|
||
upload: "Загрузка и обзор материалов",
|
||
results: "Результаты анализа ИИ",
|
||
verdict: "Формирование заключения",
|
||
approve: "Согласование руководителем",
|
||
about: "О решении"
|
||
};
|
||
var SUBS = {
|
||
upload: "Пакет документов · первичное ознакомление",
|
||
results: "Сверка процесса · проверка квалификации",
|
||
verdict: "Верификация выводов · итоговый документ",
|
||
approve: "Итоговое заключение · решение руководителя",
|
||
about: "Паспорт ИИ-решения"
|
||
};
|
||
|
||
function renderNav() {
|
||
var nav = $("nav");
|
||
if (!nav) return;
|
||
var group = null, html = "";
|
||
for (var i = 0; i < NAV.length; i++) {
|
||
var n = NAV[i];
|
||
if (n.group && n.group !== group) {
|
||
html += '<div class="kt-ai-nav-group">' + esc(n.group) + "</div>";
|
||
group = n.group;
|
||
}
|
||
if (!n.group) group = null;
|
||
var active = D.view === n.id ? ' data-active="true"' : "";
|
||
html += '<div class="kt-ai-nav-item" role="link" tabindex="0" style="cursor:pointer"' + active +
|
||
'><svg class="kt-icon" aria-hidden="true"><use href="#' + n.icon + '"></use></svg>' +
|
||
'<span class="nav-label">' + esc(n.label) + "</span></div>";
|
||
(function (id) {
|
||
nav.lastChild.onclick = function () { S.go(id); };
|
||
})(n.id);
|
||
}
|
||
var sb = $("sbReq");
|
||
if (sb) sb.innerHTML = '<div class="sb-req-in"><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-2)">Текущая заявка</div><span class="kt-ai-meta">' + esc(D.req) + "</span></div>";
|
||
}
|
||
|
||
function render() {
|
||
var html;
|
||
if (D.view === "upload") html = vUpload();
|
||
else if (D.view === "results") html = vResults();
|
||
else if (D.view === "verdict") html = vVerdict();
|
||
else if (D.view === "approve") html = vApprove();
|
||
else html = vAbout();
|
||
var view = $("viewInner");
|
||
if (!view) view = $("view");
|
||
if (!view) return;
|
||
view.innerHTML = html;
|
||
$("pageTitle").textContent = TITLES[D.view] || "";
|
||
$("pageSub").textContent = SUBS[D.view] || "";
|
||
$("roleChip").textContent = (D.view === "approve") ? "Руководитель" : "Эксперт";
|
||
renderNav();
|
||
save();
|
||
}
|
||
function vStepper() {
|
||
var cur = stepNum();
|
||
var html = '<div class="kt-ai-stepper">';
|
||
for (var i = 0; i < STEPS.length; i++) {
|
||
var st = i + 1 < cur ? "done" : (i + 1 === cur ? "current" : "");
|
||
var n = i + 1 < cur ? "✓" : String(i + 1);
|
||
html += '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + '><span class="n">' + n + "</span>" + esc(STEPS[i]) + "</div>";
|
||
if (i < STEPS.length - 1) html += '<span class="kt-ai-step-sep"></span>';
|
||
}
|
||
return html + "</div>";
|
||
}
|
||
function kpiStrip(items) {
|
||
return '<div class="kt-ai-kpi-strip">' + items.map(function (k) {
|
||
return '<div class="kt-ai-kpi"><span class="label">' + esc(k.l) + '</span><span class="value">' + k.v + "</span>" + (k.h ? '<span class="hint">' + esc(k.h) + "</span>" : "") + "</div>";
|
||
}).join("") + "</div>";
|
||
}
|
||
|
||
/* ---------- Экран 1. Загрузка и обзор материалов ---------- */
|
||
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";
|
||
}
|
||
|
||
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 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 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>" +
|
||
'<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="S.resetDemo()" style="margin-left:var(--kt-ai-space-2)">Сбросить демо</button>' +
|
||
"</div>" + vStepper() + "</section>" +
|
||
(head ? '<div style="margin-bottom:var(--kt-ai-stack-tight)">' + head + "</div>" : "") +
|
||
'<section class="sec"><div class="sec-h">Материалы' +
|
||
'<span class="spacer"></span>' +
|
||
(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>" +
|
||
'<div class="tiles">' + D.sources.map(function (s) {
|
||
var pct = s.u ? 100 : 82;
|
||
return '<div class="kt-ai-card src-card"><div class="src-name">' + esc(s.n) + "</div>" +
|
||
'<div class="src-meta"><span>Класс<span>' + esc(s.s) + "</span></span>" +
|
||
'<span>Разделов<span class="kt-ai-num">' + num(s.d) + "</span></span>" +
|
||
'<span>Обновлён<span class="kt-ai-num">01.09.2026</span></span></div>' +
|
||
'<div class="src-progress"><i style="width:' + pct + '%"></i></div>' +
|
||
'<div>' + (s.u ? pill("ok", "Синхронизирован") : pill("checked", "Синхронизация")) + "</div></div>";
|
||
}).join("") + "</div></section>" +
|
||
'<section class="kt-ai-conclusion"><h3>Что на этом экране</h3>' +
|
||
"<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. Результаты анализа ИИ ---------- */
|
||
function falseCount() {
|
||
var n = 0;
|
||
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "falsep") n++;
|
||
return n;
|
||
}
|
||
function vResults() {
|
||
var confN = doneCount(), falseN = falseCount();
|
||
|
||
var procN = 0, qualN = 0, contraN = 0;
|
||
for (var i = 0; i < D.violations.length; i++) {
|
||
var y = D.violations[i].y;
|
||
if (y === "process") procN++;
|
||
else if (y === "qual") qualN++;
|
||
else if (y === "contra") contraN++;
|
||
}
|
||
var kpis = kpiStrip([
|
||
{ l: "Несогласованностей", v: num(confN + falseN) },
|
||
{ l: "Процесс отбора", v: num(procN) },
|
||
{ l: "Квалификация", v: num(qualN) },
|
||
{ l: "Противоречий", v: num(contraN) }
|
||
]);
|
||
|
||
var banner = "";
|
||
if (D.running) return runScreen();
|
||
if (D.contra) {
|
||
var contraRow = null;
|
||
for (var ci = 0; ci < D.violations.length; ci++) if (D.violations[ci].y === "contra") contraRow = ci;
|
||
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
|
||
'<strong>Найдено противоречие между документами</strong>' +
|
||
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Протокол — голосование 4:1, Правила назначений — единогласно. ' +
|
||
(contraRow != null ? "Проверьте вывод №" + num(contraRow + 1) + " вручную." : "Противоречие разобрано.") + "</span></div>";
|
||
}
|
||
|
||
var typeTone = { process: "blue", qual: "purple", contra: "red" };
|
||
var rows = D.violations.map(function (v, i) {
|
||
return "<tr>" +
|
||
'<td data-label="Ссылка на пункт"><span class="kt-ai-meta">' + esc(v.r) + "</span></td>" +
|
||
'<td data-label="Нарушение">' + esc(v.d) + "</td>" +
|
||
'<td data-label="Тип"><span class="kt-ai-chip" data-tone="' + (typeTone[v.y] || "gray") + '">' + esc(ST[v.y].label) + "</span></td>" +
|
||
'<td data-label="Статус">' + pill(v.v) + "</td>" +
|
||
'<td><div class="row-act"><button class="kt-ai-icon-button" title="Открыть пункт нормативного акта" onclick="S.openRef(' + i + ')">' + icon("link") + "</button>" +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="S.refute(' + i + ')">' + (v.v === "falsep" ? "Вернуть" : "Ложное") + "</button></div></td>" +
|
||
"</tr>";
|
||
}).join("");
|
||
|
||
var logRows = D.log.map(function (l) {
|
||
return '<div class="logline"><span class="lt">' + esc(l.t) + '</span><span class="lb">' + esc(l.b) + "</span></div>";
|
||
}).join("");
|
||
|
||
var nextOk = true;
|
||
for (var k = 0; k < D.violations.length; k++) if (D.violations[k].v === "checked") nextOk = false;
|
||
var next = "";
|
||
if (nextOk) {
|
||
next = '<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
|
||
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'upload\')">Назад</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'verdict\')">Перейти к подготовке заключения</button></div>';
|
||
}
|
||
|
||
return kpis + banner +
|
||
'<section class="sec"><div class="sec-h">Несогласованности</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>" + next + "</section>" +
|
||
'<section class="sec"><div class="sec-h">Журнал действия ИИ' +
|
||
'<span class="spacer"></span><span class="kt-ai-meta">10:42 — 10:46</span></div>' +
|
||
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + logRows + "</div></section>" +
|
||
'<section class="kt-ai-conclusion"><h3>Что показывает анализ</h3>' +
|
||
"<p>Система сверяет процесс отбора с внутренними документами и законодательством, затем проверяет квалификацию кандидата по должностной инструкции. " +
|
||
"Каждый вывод ссылается на пункт нормативного акта — эксперт проверяет ссылки и при необходимости отмечает ложные срабатывания.</p></section>";
|
||
}
|
||
|
||
function runScreen() {
|
||
var steps = [
|
||
"Извлечение текста из пакета документов",
|
||
"Сверка с законодательством и внутренними документами",
|
||
"Проверка квалификации кандидата",
|
||
"Формирование вывода"
|
||
];
|
||
var rows = steps.map(function (s, i) {
|
||
return '<div class="logline"><span class="lt">' + esc(stepsTime[i] || "10:4" + i) + '</span><span class="lb">' + esc(s) + "</span></div>";
|
||
}).join("");
|
||
return '<div class="kt-ai-banner" data-tone="info" 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></div>' +
|
||
'<section class="sec"><div class="sec-h">Ход работы' +
|
||
'<span class="spacer"></span><span class="kt-ai-status-pill" data-status="info">Идёт анализ</span></div>' +
|
||
'<div class="kt-ai-card log" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + rows + "</div>" +
|
||
"<p class='kt-ai-meta' style='margin-top:var(--kt-ai-space-3)'>Анализ занимает обычно 3–5 минут. Можно закрыть вкладку — прогресс сохранится.</p></section>";
|
||
}
|
||
var stepsTime = ["10:46", "10:46", "10:46", "10:46"];
|
||
|
||
/* ---------- Экран 3. Формирование заключения ---------- */
|
||
function vVerdict() {
|
||
var checks = D.checklist.map(function (idx) {
|
||
var v = D.violations[idx];
|
||
if (!v) return "";
|
||
return '<label class="chk-row" for="ck' + idx + '">' +
|
||
'<input type="checkbox" id="ck' + idx + '"' + (D.checklist.indexOf(idx) >= 0 ? " checked" : "") + ' onchange="S.ck(' + idx + ')">' +
|
||
'<span class="chk-main"><span class="chk-desc">' + esc(v.d) + "</span>" +
|
||
'<span class="chk-ref kt-ai-meta">' + icon("link") + esc(v.r) + "</span></span></label>";
|
||
}).join("");
|
||
|
||
var sent = D.status === "sent";
|
||
var status = sent ? pill("sent") : pill("draft");
|
||
|
||
var docText = $("docText") && $("docText").textContent ? $("docText").textContent : D.verdict;
|
||
if (docText !== D.verdict) D.verdict = docText;
|
||
|
||
return vStepper() +
|
||
'<section class="sec"><div class="sec-h">Итоговое заключение' +
|
||
'<span class="spacer"></span>' + status + "</div>" +
|
||
'<div class="doc-paper" id="docPaper">' +
|
||
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ' + esc("ЭД-2026-118") + " · SEO-1, управление сетевых сервисов</span></div>" +
|
||
'<div class="doc-text" id="docText" contenteditable="true" spellcheck="false">' + esc(docText) + "</div></div>" +
|
||
'<div class="row-btns" style="margin-top:var(--kt-ai-space-4)">' +
|
||
'<button class="kt-ai-btn" data-variant="ghost" onclick="S.go(\'results\')">Назад к анализу</button>' +
|
||
(sent
|
||
? '<button class="kt-ai-btn" data-variant="primary" onclick="S.go(\'approve\')">К согласованию</button>'
|
||
: '<button class="kt-ai-btn" data-variant="primary" onclick="S.sendForApproval()">Отправить на согласование руководителю</button>') +
|
||
"</div></section>" +
|
||
'<section class="sec"><div class="sec-h">Чек-лист подтвержденных нарушений' +
|
||
'<span class="spacer"></span><span class="kt-ai-meta">' + num(D.checklist.length + " из " + D.violations.length) + " включены в заключение</span></div>" +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
|
||
'<section class="sec"><div class="sec-h" style="margin-bottom:var(--kt-ai-space-4)"></div>' +
|
||
'<button class="kt-ai-btn" data-variant="danger" onclick="S.manualMode()">Вернуть в ручной режим</button>' +
|
||
'<span class="kt-ai-meta" style="margin-left:var(--kt-ai-space-4)">Без системы — вручную по действующим правилам</span></section>';
|
||
}
|
||
|
||
/* ---------- Экран 4. Согласование руководителем ---------- */
|
||
function vApprove() {
|
||
var st = D.status;
|
||
var banner = "";
|
||
var btns = "";
|
||
var kpis = kpiStrip([
|
||
{ l: "Заявка", v: num("ЭД-2026-118") },
|
||
{ l: "Подтверждено", v: num(D.checklist.length) },
|
||
{ l: "Исключено", v: num(D.violations.length - D.checklist.length) },
|
||
{ l: "Статус", v: ST[st] ? esc(ST[st].label) : "—" }
|
||
]);
|
||
|
||
if (st === "agree") {
|
||
banner = '<div class="kt-ai-banner" data-tone="ok" style="margin-bottom:var(--kt-stack-block)"><strong>Материал согласован</strong>' +
|
||
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Согласовано ' + num(D.stamp || "15.09.2026, 09:12") + " · руководитель</span></div>";
|
||
} else if (st === "decl") {
|
||
banner = '<div class="kt-ai-banner" data-tone="risk" style="margin-bottom:var(--kt-ai-stack-block)"><strong>Материал отклонён</strong>' +
|
||
'<span class="kt-ai-meta" style="display:block;margin-top:2px">' + num(D.stamp || "15.09.2026") + ' · ' + esc(D.rejected || "Без комментария") + "</span></div>";
|
||
} else {
|
||
banner = '<div class="kt-ai-banner" data-tone="warn" style="margin-bottom:var(--kt-ai-stack-block)">' +
|
||
'<strong>Ожидает решения руководителя</strong>' +
|
||
'<span class="kt-ai-meta" style="display:block;margin-top:2px">Проверены материалы на назначение руководителя SEO-1 · ' + num("14.09.2026") + "</span></div>";
|
||
btns = '<div class="row-btns" style="margin-top:var(--kt-ai-stack-block)">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.approve()">Согласовать документы</button>' +
|
||
'<button class="kt-ai-btn" data-variant="danger" onclick="S.declare()">Отклонить с комментарием</button></div>';
|
||
}
|
||
|
||
var checks = D.checklist.map(function (idx) {
|
||
var v = D.violations[idx];
|
||
if (!v) return "";
|
||
return '<div class="logline"><span class="lt">✓</span><span class="lb">' + esc(v.d) + ' <span class="kt-ai-meta">' + esc(v.r) + "</span></span></div>";
|
||
}).join("");
|
||
|
||
return kpis + banner +
|
||
'<section class="sec"><div class="sec-h">Итоговое заключение</div>' +
|
||
'<div class="doc-paper" style="pointer-events:none">' +
|
||
'<div class="doc-head"><strong>ЗАКЛЮЧЕНИЕ</strong><span class="doc-id">по заявке ЭД-2026-118 · SEO-1, управление сетевых сервисов</span></div>' +
|
||
'<div class="doc-text" style="border-color:transparent;background:transparent;padding:0">' + esc(D.verdict) + "</div></div></section>" +
|
||
'<section class="sec"><div class="sec-h">Подтвержденные нарушения</div>' +
|
||
'<div class="kt-ai-card" style="padding:var(--kt-ai-space-5) var(--kt-ai-space-6)">' + checks + "</div></section>" +
|
||
btns;
|
||
}
|
||
|
||
/* ---------- Экран 5. О решении ---------- */
|
||
function vAbout() {
|
||
return '<section class="sec"><div class="sec-h" style="font-size:var(--kt-ai-text-xl)">О решении</div></section>' +
|
||
'<div class="kt-ai-card" style="margin-bottom:var(--kt-ai-stack-block)"><div class="mini" style="padding:0"><div class="kv">' +
|
||
'<span class="k">Наименование</span><span class="v">Экспертиза документов для советов</span></div><div class="kv">' +
|
||
'<span class="k">Исполнитель</span><span class="v">Владелец процесса — сборка на платформе Vibe AI</span></div><div class="kv">' +
|
||
'<span class="k">Пользователь</span><span class="v">Эксперт, руководитель</span></div><div class="kv">' +
|
||
'<span class="k">Дата внедрения</span><span class="v">—</span></div></div></div>' +
|
||
'<div class="pass-grid" style="margin-bottom:var(--kt-ai-stack-block)">' +
|
||
passCard("Предпосылки", "<ul><li>Необходимость читать большое количество внутренних документов и законодательных актов</li><li>Необходимость сравнивать документы на соответствие нормативным требованиям</li></ul>") +
|
||
passCard("Входные данные и источники", "<ul><li>Трудовой кодекс Республики Казахстан</li><li>Закон «Об акционерных обществах»</li><li>Правила трудового распорядка</li><li>Правила поиска и подбора персонала</li><li>Корпоративный стандарт по управлению человеческими ресурсами фонда</li></ul>") +
|
||
passCard("Процессы для внедрения ИИ", "<ul><li>Сверка пояснительной записки с фактическим процессом отбора</li><li>Проверка соответствия кандидата квалификационным требованиям</li><li>Поиск несоответствий и противоречий в документах</li></ul>") +
|
||
passCard("Результат", "<ul><li>Согласование документов со списком нарушений и ссылками на пункты законов и внутренних документов</li><li>Заключение со списком нарушений и ссылками на пункты законов и внутренних документов</li></ul>") +
|
||
passCard("Ожидаемый эффект", "<ul><li>Сокращение времени рассмотрения материалов</li><li>Повышение качества проверки за счёт снижения риска человеческих ошибок</li></ul>") +
|
||
passCard("Экономический эффект", "<div class='kv'><span class='k'>Объём</span><span class='v'>20 экспертиз в месяц во всех экспертах</span></div><div class='kv'><span class='k'>Высвобождение</span><span class='v'>50 часов в месяц (оценка Төре)</span></div>") +
|
||
passCard("Контроль и риски", "<ul><li>Возможность ошибки ИИ при противоречиях в разных внутренних документах</li><li>Необходимость проверки выводов экспертом</li></ul>") +
|
||
passCard("Меры по снижению", "<ul><li>Проверка результата человеком перед использованием</li><li>Журнал действий ИИ и возможность вернуться в ручной режим</li></ul>") +
|
||
passCard("Формат реализации", "<ul><li>ИИ-агент на платформе Alem; при необходимости — простой веб-экран на Vibe AI</li><li>Экраны макета: Загрузка и обзор; Результаты анализа; Формирование заключения; Согласование руководителем</li></ul>") +
|
||
"</div>" +
|
||
'<section class="kt-ai-conclusion"><h3>Путь сотрудника</h3>' +
|
||
"<p>Эксперт загружает документы на экране «Загрузка и обзор материалов», переходит на «Результаты анализа ИИ» и проверяет выводы, " +
|
||
"формировает итоговое заключение на «Формировании заключения», руководитель просматривает и согласует документы на «Согласовании у руководителя».</p></section>";
|
||
}
|
||
function passCard(title, body) {
|
||
return '<div class="kt-ai-card mini"><h4>' + esc(title) + "</h4>" + body + "</div>";
|
||
}
|
||
|
||
function nowTime() {
|
||
var d = new Date();
|
||
function p(x) { return (x < 10 ? "0" : "") + x; }
|
||
return p(d.getHours()) + ":" + p(d.getMinutes());
|
||
}
|
||
|
||
/* ---------- Действия ---------- */
|
||
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));
|
||
}
|
||
_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();
|
||
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.resetDemo = function () {
|
||
if (confirm("Сбросить текущее состояние (загруженные файлы, результаты, черновик заключения)?")) {
|
||
D = seed();
|
||
save();
|
||
render();
|
||
toast("Демо сброшено — можно начать заново", "info");
|
||
}
|
||
};
|
||
|
||
S.delFile = function (i) {
|
||
D.files.splice(i, 1);
|
||
save();
|
||
render();
|
||
toast("Файл удалён из пакета", "info");
|
||
};
|
||
|
||
S.fileReady = function (i) {
|
||
D.files[i].s = "ready";
|
||
render();
|
||
toast("Файл отмечен как готовый к анализу", "ok");
|
||
};
|
||
|
||
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 (!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;
|
||
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;
|
||
D.violations = vio;
|
||
D.verdict = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + esc(D.candidate) + ") прошла проверку соответствия нормативной базе. Итоговое заключение будет сформировано после экспертной проверки выводов.";
|
||
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;
|
||
S.viewFile = function (i) {
|
||
var f = D.files[i];
|
||
openDrawer(
|
||
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-5)">' +
|
||
"<strong>" + esc(f.n) + "</strong>" + pill(f.s) +
|
||
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeDrawer()" style="margin-left:auto">' + icon("x") + "</button></div>" +
|
||
'<div class="doc-paper">' +
|
||
'<div class="doc-head"><strong>' + esc(f.t) + '</strong><span class="doc-id">' + esc(f.d) + "</span></div>" +
|
||
'<div class="doc-text" style="border:none;background:transparent;padding:0;font-size:var(--kt-ai-text-sm)">' +
|
||
"<p>Документ загружен во внутреннюю систему. Эксперт просматривает комплектность и при необходимости открывает исходник в корпоративном хранилище.</p>" +
|
||
"<p style='margin-top:var(--kt-ai-space-3)'>Статус: " + (ST[f.s] ? esc(ST[f.s].label) : esc(f.s)) + ".</p>" +
|
||
"</div></div>"
|
||
);
|
||
};
|
||
|
||
S.refute = function (i) {
|
||
var v = D.violations[i];
|
||
v.v = v.v === "falsep" ? "checked" : "falsep";
|
||
if (v.y === "contra") {
|
||
D.contra = v.v === "falsep";
|
||
D.falseDone = v.v === "falsep";
|
||
}
|
||
D.checklist.forEach(function (idx, pos) {
|
||
if (D.violations[idx].v === "falsep") { D.checklist.splice(pos, 1); pos--; }
|
||
});
|
||
render();
|
||
toast(v.v === "falsep" ? "Вывод отмечен как ложное срабатывание" : "Вывод возвращён в работу", v.v === "falsep" ? "warn" : "ok");
|
||
};
|
||
|
||
S.ck = function (idx) {
|
||
var pos = D.checklist.indexOf(idx);
|
||
var el = $("ck" + idx);
|
||
if (el && el.checked && pos < 0) D.checklist.push(idx);
|
||
if (el && !el.checked && pos >= 0) D.checklist.splice(pos, 1);
|
||
D.checklist.sort(function (a, b) { return a - b; });
|
||
save();
|
||
// обновить счётчик без перерисовки чек-бокса
|
||
render();
|
||
};
|
||
|
||
function rebuildVerdict() {
|
||
var parts = D.checklist.map(function (idx) {
|
||
var v = D.violations[idx];
|
||
return "\n" + (parts.indexOf(idx) + 1) + ". " + v.d + " (" + v.r + ").";
|
||
});
|
||
var base = "Заявка на назначение руководителя уровня SEO-1, управление сетевых сервисов (кандидат " + D.candidate + ") прошла проверку соответствия нормативной базе.";
|
||
if (D.checklist.length > 0) {
|
||
base += " По завершённой сверке зафиксированы следующие несоответствия:" + parts.join("") +
|
||
"\n\nРешение о назначении принимается с учётом перечисленных несоответствий. Материалы переданы на согласование руководителю.";
|
||
} else {
|
||
base += " Несоответствий не выявлено.";
|
||
}
|
||
return base;
|
||
}
|
||
|
||
S.sendForApproval = function () {
|
||
var n = 0;
|
||
for (var i = 0; i < D.violations.length; i++) if (D.violations[i].v === "checked") n++;
|
||
if (n > 0) { toast("Проверьте все выводы: " + n + " на проверке", "warn"); return; }
|
||
D.status = "sent";
|
||
D.rejected = null;
|
||
D.stamp = null;
|
||
D.verdict = rebuildVerdict();
|
||
render();
|
||
toast("Заключение отправлено на согласование руководителю", "ok");
|
||
};
|
||
|
||
S.approve = function () {
|
||
D.status = "agree";
|
||
D.stamp = nowTime();
|
||
render();
|
||
toast("Материал согласован", "ok");
|
||
};
|
||
|
||
S.declare = function () {
|
||
var m = prompt("Комментарий к отклонению:");
|
||
if (m === null) return;
|
||
D.status = "decl";
|
||
D.stamp = nowTime();
|
||
D.rejected = m || "Без комментария";
|
||
render();
|
||
toast("Материал отклонён", "warn");
|
||
};
|
||
|
||
S.manualMode = function () {
|
||
if (confirm("Вернуться в ручной режим? Текущий черновик заключения сохранится.")) {
|
||
toast("Ручной режим: работа ведётся без системы", "info");
|
||
closeModal();
|
||
}
|
||
};
|
||
|
||
S.openRef = function (i) {
|
||
var v = D.violations[i];
|
||
openModal(
|
||
'<div class="kt-ai-card" style="max-width:560px;padding:var(--kt-ai-space-6)">' +
|
||
'<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);margin-bottom:var(--kt-ai-space-4)">' +
|
||
icon("link") + "<strong>Ссылка на нормативный акт</strong>" +
|
||
'<button class="kt-ai-icon-button" title="Закрыть" onclick="S.closeModal()" style="margin-left:auto">' + icon("x") + "</button></div>" +
|
||
'<div class="mini" style="padding:0"><div class="kv"><span class="k">Пункт</span><span class="v">' + esc(v.r) + "</span></div>" +
|
||
"<div class='kv' style='display:block'><span class='k'>Вывод системы</span><span class='v' style='text-align:left;margin-top:var(--kt-ai-space-2)'>" + esc(v.d) + "</span></div></div>" +
|
||
'</div>'
|
||
);
|
||
};
|
||
|
||
S.burger = function () {
|
||
var sb = $("sidebar");
|
||
if (!sb) return;
|
||
if (sb.dataset.open === "true") sb.removeAttribute("data-open");
|
||
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);
|
||
};
|