ii-reshenie-dlya-protsessa/script.js

502 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

(() => {
"use strict";
const LS = "process-ai-v1";
// ---------- данные ----------
const DOC_DEF = [
{ id: 1, name: "Договор аренды №247-09.pdf", ext: "scn", from: "Отдел аренды", date: "21.09.2026", fields: [
["Контрагент", "ООО «ТрансЛогистик»", 98],
["Регистрационный номер", "210900041137", 97],
["Адрес объекта", "пр. Абая, 145", 91],
["Площадь, м²", "620", 88],
["Срок аренды", "12 месяцев", 95],
["Ответственное лицо", "К. Асетов", 84]
]},
{ id: 2, name: "Акт сверки 2026-08.xlsx", ext: "xlsx", from: "Финансовый отдел", date: "20.09.2026", fields: [
["Организация", "ТОО «СтройДом»", 99],
["Период", "август 2026", 100],
["Сальдо, тг", "1 240 500", 98],
["Расхождений", "нет", 96],
["Подпись", "Е. Тлеубаева", 76]
]},
{ id: 3, name: "Счёт-фактура 5518.pdf", ext: "scn", from: "Поставщик материалов", date: "20.09.2026", fields: [
["Номер", "5518", 99],
["Дата", "18.09.2026", 97],
["Сумма, тг", "384 900", 93],
["Срок оплаты", "28.09.2026", 90],
["Контрагент", "ТОО «МеталлБаза»", 81]
]},
{ id: 4, name: "Заявка на доступ 1192.pdf", ext: "scn", from: "Служба безопасности", date: "19.09.2026", fields: [
["Сотрудник", "А. Нурланов", 97],
["Должность", "инженер", 92],
["Объект", "ЦОД, 3 этаж", 89],
["Период", "до 31.12.2026", 94],
["Паспорт", "19 АБ 48210", 68]
]},
{ id: 5, name: "Реестр платежей 12.xlsx", ext: "xlsx", from: "Финансовый отдел", date: "19.09.2026", fields: [
["Период", "1–18.09.2026", 100],
["Платежей", "47", 99],
["Итого, тг", "5 812 300", 98],
["Просрочено", "0", 97],
["Проверено", "да", 95]
]},
{ id: 6, name: "Копия лицензии №000214.docx", ext: "scn", from: "Юридический отдел", date: "18.09.2026", fields: [
["Организация", "АО «СетьКом»", 98],
["Номер лицензии", "000214-005", 72],
["ОСН", "04112100", 80],
["Разрешение", "оптовая торговля", 93],
["Срок действия", "до 12.03.2027", 89]
]},
{ id: 7, name: "Накладная ТТН 7731.pdf", ext: "scn", from: "Склад", date: "17.09.2026", fields: [
["Номер ТТН", "7731", 99],
["Дата", "15.09.2026", 98],
["Наименование", "кабель, бухта 500 м", 91],
["Количество, шт", "24", 90],
["Получатель", "А. Нурланов", 86]
]},
{ id: 8, name: "Служебная записка 302.pdf", ext: "scn", from: "Внутренний документ", date: "17.09.2026", fields: [
["От", "отдел закупки", 96],
["Кому", "руководителю", 94],
["Дата", "16.09.2026", 97],
["Тема", "обновление ПО", 92],
["Основание", "протокол 11", 61]
]},
{ id: 9, name: "Договор субаренды 518.pdf", ext: "scn", from: "Отдел аренды", date: "16.09.2026", fields: [
["Субарендатор", "ТОО «ОфисЛайн»", 90],
["Рег. номер", "170930081211", 79],
["Объект", "ул. Толе би, 28, офис 4", 85],
["Площадь, м²", "85", 93],
["Плата за м²/мес", "4 200 тг", 88]
]},
{ id: 10, name: "Приходный ордер №902.pdf", ext: "scn", from: "Финансовый отдел", date: "15.09.2026", fields: [
["Номер", "902", 99],
["Дата", "14.09.2026", 98],
["Сумма, тг", "56 300", 86],
["Основание", "счёт 5490", 83],
["Касса", "№2", 71]
]}
];
const VERDICT = ["confirmed", "pending", "problem"];
const VERDICT_LABEL = { confirmed: "подтверждено", pending: "на проверке", problem: "проблемное" };
const VERDICT_CHIP = { confirmed: "ok", pending: "info", problem: "risk" };
const LOW = 85; // ниже — поле помечается
// ---------- состояние ----------
let db = load();
function load() {
try {
const raw = localStorage.getItem(LS);
if (raw) { const d = JSON.parse(raw); if (Array.isArray(d.docs) && d.docs.length) return d; }
} catch (e) {}
return seed();
}
function seed() {
const now = Date.now();
return {
view: "inbox",
filters: { inbox: "all", scan: "all", verify: "all" },
docs: DOC_DEF.map((d, i) => {
const allHigh = d.fields.every(f => f[2] >= LOW);
return Object.assign({}, d, {
inboxStatus: i < 2 ? "new" : "read",
scanStatus: i === 0 ? "scanning" : i <= 6 ? "done" : "failed",
fields: d.fields.map(f => ({ name: f[0], value: f[1], conf: f[2], verdict: VERDICT[i === 0 ? 1 : 2] })),
loadStatus: i <= 2 ? "loaded" : "idle",
scanMs: 1800 + i * 340,
ts: now - i * 3600e3 * 7
});
})
};
}
function save() { try { localStorage.setItem(LS, JSON.stringify(db)); } catch (e) {} }
// ---------- утилиты ----------
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
let toastTimer;
function toast(msg) {
const t = $("toast"); t.textContent = msg; t.classList.add("show");
clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("show"), 2200);
}
function kpi(label, value, hint) {
return `<div class="kpi"><div class="label">${label}</div><div class="value">${value}</div><div class="hint">${hint || ""}</div></div>`;
}
function chip(status, text) {
return `<span class="kt-ai-chip" data-tone="${VERDICT_CHIP[status] || "info"}">${text || VERDICT_LABEL[status]}</span>`;
}
function confBadge(v) {
const cls = v >= 90 ? "hi" : v >= 75 ? "mid" : "lo";
return `<span class="conf ${cls}">${v}%</span>`;
}
function docInList(d) {
const n = d.fields.length;
const problems = d.fields.filter(f => f.verdict === "problem").length;
let extra = "";
if (d.scanStatus === "scanning") extra = " · распознаётся…";
else if (d.scanStatus === "failed") extra = " · ошибка распознавания, повторить";
else if (d.scanStatus === "done") extra = ` · полей: ${n}` + (problems ? ` · проблемных: ${problems}` : "");
if (d.inboxStatus === "new") extra = (extra ? extra + " · " : "") + "(новое)";
if (d.loadStatus === "loaded") extra = " · загружено в систему";
return { n, extra };
}
function lowProblems(d) { return d.fields.filter(f => f.verdict === "problem"); }
function needsCheck(d) { return d.fields.some(f => f.verdict !== "confirmed"); }
// ---------- шаги ----------
function renderSteps() {
const defs = [
{ id: "inbox", name: "Входящие", cnt: db.docs.filter(d => d.inboxStatus === "new").length },
{ id: "scan", name: "Распознавание", cnt: db.docs.filter(d => d.scanStatus === "done" || d.scanStatus === "scanning" || d.scanStatus === "failed").length },
{ id: "verify", name: "Проверка", cnt: db.docs.filter(d => d.scanStatus === "done" && needsCheck(d)).length },
{ id: "loaded", name: "Загружено", cnt: db.docs.filter(d => d.loadStatus === "loaded").length }
];
$("steps").innerHTML = defs.map(s => `
<button class="step ${db.view === s.id ? "active" : ""}" data-view="${s.id}">
<span class="num">${s.id === "passport" ? "·" : ""}</span>
<span class="name">${s.name}</span>
${s.id !== "passport" ? `<span class="cnt">${s.cnt ? s.cnt + "" : ""}</span>` : ""}
</button>`).join("") +
`<button class="step ${db.view === "passport" ? "active" : ""}" data-view="passport">
<span class="num">·</span><span class="name">Паспорт</span>
</button>`;
[...$("steps").querySelectorAll("[data-view]")].forEach(el =>
el.onclick = () => { db.view = el.dataset.view; save(); render(); });
}
// ---------- экран 1: входящие ----------
function renderInbox() {
const all = db.docs;
const counts = { all: all.length, new: all.filter(d => d.inboxStatus === "new").length, read: all.filter(d => d.inboxStatus === "read").length };
const f = db.filters.inbox;
$("f-inbox").innerHTML = Object.keys(counts).map(k =>
`<button class="fchip ${f === k ? "on" : ""}" data-f="${k}">${k === "all" ? "Все" : k === "new" ? "Непрочитанные" : "Прочитанные"} <span class="n">${counts[k]}</span></button>`
).join("");
[...$("f-inbox").querySelectorAll("[data-f]")].forEach(el => el.onclick = () => { db.filters.inbox = el.dataset.f; save(); render(); });
const list = all.filter(d => f === "all" || (f === "new" ? d.inboxStatus === "new" : f === "read" ? d.inboxStatus === "read" : true));
const scanned = all.filter(d => d.scanStatus === "done").length;
const failed = all.filter(d => d.scanStatus === "failed").length;
const unstarted = all.filter(d => d.scanStatus === "idle").length;
$("kpis-1").innerHTML =
kpi("Всего документов", all.length, "за последние 5 дней") +
kpi("Новых", counts.new, "ещё не открыты") +
kpi("Ожидают распознавания", unstarted, "не запущены") +
kpi("Требуют внимания", failed, "ошибка распознавания");
$("c-inbox").textContent = list.length + " из " + all.length;
$("list-inbox").innerHTML = list.map(d => {
const { extra } = docInList(d);
return `<div class="doc" data-doc="${d.id}">
<span class="fic">${d.ext}</span>
<div style="min-width:0">
<div class="d1">${esc(d.name)}</div>
<div class="d2">${esc(d.from)} · ${extra}</div>
</div>
<span class="when">${d.date}</span>
<span class="acts">
<button class="kt-ai-btn" data-a="view" data-id="${d.id}">Открыть</button>
${d.inboxStatus === "new" ? `<button class="kt-ai-btn" data-variant="primary" data-a="scan" data-id="${d.id}">Распознать</button>` : ""}
</span>
</div>`;
}).join("") || emptyState();
bindDocs();
}
function emptyState() {
return `<div class="kt-ai-empty">
<div class="icon-spot"><svg style="width:20px;height:20px" aria-hidden="true"><use href="#i-inbox"/></svg></div>
<div class="title">Здесь пусто</div>
<div>Документы с такими параметрами не найдены</div>
</div>`;
}
// ---------- экран 2: распознавание ----------
function renderScan() {
const all = db.docs;
const counts = {
all: all.length,
scanning: all.filter(d => d.scanStatus === "scanning").length,
done: all.filter(d => d.scanStatus === "done").length,
failed: all.filter(d => d.scanStatus === "failed").length,
idle: all.filter(d => d.scanStatus === "idle").length
};
const f = db.filters.scan;
$("f-scan").innerHTML = [
["all", "Все"], ["scanning", "Распознаются"], ["done", "Готовы"], ["failed", "Ошибки"]
].map(([k, l]) => `<button class="fchip ${f === k ? "on" : ""}" data-f="${k}">${l} <span class="n">${counts[k]}</span></button>`).join("");
[...$("f-scan").querySelectorAll("[data-f]")].forEach(el => el.onclick = () => { db.filters.scan = el.dataset.f; save(); render(); });
const list = all.filter(d => f === "all" || d.scanStatus === f);
const sumMs = all.filter(d => d.scanStatus === "done").reduce((s, d) => s + d.scanMs, 0);
const avg = counts.done ? Math.round(sumMs / counts.done / 100) / 10 : 0;
const totalFields = all.filter(d => d.scanStatus === "done").reduce((s, d) => s + d.fields.length, 0);
$("kpis-2").innerHTML =
kpi("Готовы к проверке", counts.done, "из " + all.length) +
kpi("Среднее время", counts.done ? avg + " с" : "—", "распознавание скана") +
kpi("Извлечено полей", totalFields, "уверенность от 60%") +
kpi("Ошибки", counts.failed, "повторить распознавание");
$("c-scan").textContent = list.length + " из " + all.length;
$("list-scan").innerHTML = list.map(d => {
let status, act;
if (d.scanStatus === "scanning") {
status = `<span class="kt-ai-chip" data-tone="info">распознаётся…</span>`;
act = `<button class="kt-ai-btn" disabled>Подождите</button>`;
} else if (d.scanStatus === "failed") {
status = `<span class="kt-ai-chip" data-tone="risk">ошибка</span>`;
act = `<button class="kt-ai-btn" data-variant="primary" data-a="rescan" data-id="${d.id}">Повторить</button>`;
} else if (d.scanStatus === "idle") {
status = `<span class="kt-ai-chip" data-tone="info">ожидает</span>`;
act = `<button class="kt-ai-btn" data-variant="primary" data-a="scan" data-id="${d.id}">Распознать</button>`;
} else {
const probs = lowProblems(d).length;
status = probs ? `<span class="kt-ai-chip" data-tone="warn">полей: ${d.fields.length} · проблемных: ${probs}</span>`
: `<span class="kt-ai-chip" data-tone="ok">полей: ${d.fields.length}</span>`;
act = `<button class="kt-ai-btn" data-variant="primary" data-a="openverify" data-id="${d.id}">К проверке</button>
<button class="kt-ai-btn" data-a="load" data-id="${d.id}">В систему</button>`;
}
return `<div class="doc" data-doc="${d.id}">
<span class="fic">${d.ext}</span>
<div style="min-width:0">
<div class="d1">${esc(d.name)}</div>
<div class="d2">${esc(d.from)} · ${d.date}</div>
</div>
<span class="when">${status}</span>
<span class="acts">${act}</span>
</div>`;
}).join("") || emptyState();
bindDocs();
}
// ---------- экран 3: проверка ----------
function currentVerifyDoc() {
const done = db.docs.filter(d => d.scanStatus === "done");
return done.find(d => needsCheck(d)) || done[0] || null;
}
function renderVerify() {
$("f-verify").innerHTML = db.docs.filter(d => d.scanStatus === "done").map(d =>
`<button class="fchip ${db._vDoc === d.id ? "on" : ""}" data-did="${d.id}">${esc(d.name.split(".")[0])}</button>`
).join("");
[...$("f-verify").querySelectorAll("[data-did]")].forEach(el => el.onclick = () => {
const d = byId(+el.dataset.did); if (d) { d._v = true; save(); render(); }
});
// выбор документа: первый с неподтверждёнными полями
let vdoc = null;
for (const d of db.docs) if (d.scanStatus === "done" && d._v) { vdoc = d; break; }
vdoc = vdoc || currentVerifyDoc();
db._vDoc = vdoc ? vdoc.id : null; save();
if (!vdoc) {
$("kpis-3").innerHTML = kpi("Проверено", db.docs.filter(d => d.scanStatus === "done").length, "документов") + kpi("Поля", 0, "к проверке") + kpi("Проблемных", 0, "полей") + kpi("Готовность к загрузке", "—", "");
$("c-verify").textContent = "нет распознанных документов";
$("tb-verify").innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--kt-ai-fg-faint);padding:var(--kt-ai-space-8)">Нет распознанных документов</td></tr>`;
return;
}
const total = vdoc.fields.length;
const ok = vdoc.fields.filter(x => x.verdict === "confirmed").length;
const prob = vdoc.fields.filter(x => x.verdict === "problem").length;
const avg = Math.round(vdoc.fields.reduce((s, x) => s + x.conf, 0) / total);
$("kpis-3").innerHTML =
kpi("Документ", esc(vdoc.name.split(".")[0]), vdoc.date) +
kpi("Подтверждено", ok + " из " + total, "полей") +
kpi("Проблемных", prob, "уверенность &lt; " + LOW + "%") +
kpi("Средняя уверенность", avg + "%", "по всем полям");
$("c-verify").textContent = `всего: ${db.docs.filter(d => d.scanStatus === "done").length}`;
$("tb-verify").innerHTML = vdoc.fields.map((fld, i) => {
const val = fld.verdict === "problem" ? `<span class="val bad">${esc(fld.value)}</span>` : `<span class="val">${esc(fld.value)}</span>`;
let action = "";
if (fld.verdict === "confirmed") action = `<button class="kt-ai-btn" data-a="vopen" data-field="${i}">Показать</button>`;
else if (fld.verdict === "pending") action = `<button class="kt-ai-btn" data-variant="primary" data-a="vconfirm" data-field="${i}">Подтвердить</button><button class="kt-ai-btn" data-a="vproblem" data-field="${i}">Проблема</button>`;
else action = `<button class="kt-ai-btn" data-variant="danger" data-a="vfix" data-field="${i}">Исправить</button><button class="kt-ai-btn" data-a="vopen" data-field="${i}">Показать</button>`;
return `<tr data-d="${vdoc.id}">
<td><span class="fld">${esc(fld.name)}</span></td>
<td>${val}</td>
<td>${confBadge(fld.conf)}</td>
<td>${chip(fld.verdict)}</td>
<td class="num">${action}</td>
</tr>`;
}).join("");
[...$("tb-verify").querySelectorAll("[data-a]")].forEach(b => {
b.onclick = () => handleVerifyAction(vdoc, b.dataset.a, +b.dataset.field);
});
}
function handleVerifyAction(doc, a, fi) {
const fld = doc.fields[fi];
if (a === "vopen") { openDrawer(doc); return; }
if (a === "vconfirm") { fld.verdict = "confirmed"; toast(`Поле «${fld.name}» подтверждено`); }
if (a === "vproblem") { fld.verdict = "problem"; toast(`Поле «${fld.name}» помечено как проблемное`); }
if (a === "vfix") {
fld.verdict = "problem";
const val = prompt(`Исправить значение поля «${fld.name}»`, fld.value);
if (val !== null) { fld.value = val; fld.conf = 100; fld.verdict = "confirmed"; toast("Значение исправлено и подтверждено"); }
else return;
}
save(); render();
}
function byId(id) { return db.docs.find(d => d.id === id); }
// ---------- экран 4: загрузка ----------
function renderLoaded() {
const all = db.docs;
const loaded = all.filter(d => d.loadStatus === "loaded");
const pending = all.filter(d => d.scanStatus === "done" && !needsCheck(d) && d.loadStatus !== "loaded");
const fields = loaded.reduce((s, d) => s + d.fields.length, 0);
const today = new Date().toISOString().slice(0, 10);
$("kpis-4").innerHTML =
kpi("Загружено в систему", loaded.length, "документов") +
kpi("Полей передано", fields, "за текущий период") +
kpi("Готово к загрузке", pending.length, "все поля подтверждены") +
kpi("Осталось обработать", all.length - loaded.length, "документов");
// график: по дням (последние 7)
const days = [];
for (let i = 6; i >= 0; i--) {
const dt = new Date(); dt.setDate(dt.getDate() - i);
const key = dt.toISOString().slice(0, 10);
const inDay = all.filter(d => new Date(d.ts).toISOString().slice(0, 10) === key).length;
days.push({ label: dt.getDate().toString().padStart(2, "0") + "." + (dt.getMonth() + 1).toString().padStart(2, "0"), n: inDay, done: inDay, today: key === today });
}
const max = Math.max(1, ...days.map(d => d.n));
$("bars").innerHTML = days.map(d => `
<div class="bar ${d.n > 0 ? "done" : ""}" data-tip="${d.label}: ${d.n}">
<div class="col" style="height:${Math.round(d.n / max * 150)}px"></div>
<span class="lbl">${d.label}</span>
</div>`).join("");
[...$("bars").querySelectorAll(".bar")].forEach(b => {
b.onmouseenter = () => { $("chart-tip").textContent = b.dataset.tip; };
b.onmouseleave = () => { $("chart-tip").textContent = "наведите на столбец"; };
});
$("loaded-summary").innerHTML = `
<dt>Документов</dt><dd>${loaded.length}</dd>
<dt>Полей</dt><dd>${fields}</dd>
<dt>Сегодня</dt><dd>${days[6].n}</dd>
<dt>Просрочено</dt><dd>0</dd>`;
const ready = pending;
const list = [...loaded, ...ready];
$("list-loaded").innerHTML = list.map(d => {
const isL = d.loadStatus === "loaded";
const status = isL ? `<span class="kt-ai-chip" data-tone="ok">загружено</span>` : `<span class="kt-ai-chip" data-tone="info">готово к загрузке</span>`;
const act = isL ? `<button class="kt-ai-btn" data-a="view" data-id="${d.id}">Открыть</button>`
: `<button class="kt-ai-btn" data-variant="primary" data-a="load" data-id="${d.id}">Загрузить</button>`;
return `<div class="doc" data-doc="${d.id}">
<span class="fic">${d.ext}</span>
<div style="min-width:0">
<div class="d1">${esc(d.name)}</div>
<div class="d2">${esc(d.from)} · полей: ${d.fields.length}</div>
</div>
<span class="when">${d.date}</span>
<span class="acts">${act}</span>
</div>`;
}).join("") || emptyState();
bindDocs();
}
function doLoad(d) {
if (needsCheck(d)) { toast("Сначала подтвердите все поля в проверке"); db.view = "verify"; render(); return; }
d.loadStatus = "loaded";
toast(`«${d.name}» загружено в систему`);
save(); render();
}
// ---------- drawer ----------
function openDrawer(d) {
$("dw-title").textContent = d.name;
$("dw-sub").textContent = `${d.from} · ${d.date}`;
// мини-скан: 2 строки "текста" с выделением проблемных значений
let scan = "скан · " + d.name + "\n";
scan += d.fields.map(f => ` ${f.name}: ${f.value}` + (f.verdict === "problem" ? " [низкая уверенность]" : "")).join("\n");
$("dw-scan").innerHTML = esc(scan).split(" [низкая уверенность]").join(' <span class="hl">низкая уверенность</span>');
$("dw-fields").innerHTML = d.fields.map((f, i) =>
`<dt>${esc(f.name)}</dt><dd>${confBadge(f.conf)} <span style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-xs);margin-left:6px">${VERDICT_LABEL[f.verdict]}</span><div class="val" style="margin-top:2px">${esc(f.value)}</div></dd>`
).join("");
let foot = `<button class="kt-ai-btn" data-a="close">Закрыть</button>`;
if (d.scanStatus !== "done") {
foot = `<button class="kt-ai-btn" data-variant="primary" data-a="rescan">Повторить распознавание</button>` + foot;
} else if (needsCheck(d)) {
foot = `<button class="kt-ai-btn" data-variant="primary" data-a="gotoverify">К проверке</button>` + foot;
} else if (d.loadStatus !== "loaded") {
foot = `<button class="kt-ai-btn" data-variant="primary" data-a="load">Загрузить в систему</button>` + foot;
}
$("dw-foot").innerHTML = foot;
[...$("dw-foot").querySelectorAll("[data-a]")].forEach(b => b.onclick = () => {
const a = b.dataset.a;
if (a === "close") closeDrawer();
if (a === "gotoverify") { closeDrawer(); startVerify(d); }
if (a === "load") { doLoad(d); closeDrawer(); }
if (a === "rescan") { scanDoc(d, true); closeDrawer(); }
});
$("drawer").classList.add("open"); $("drawer").setAttribute("aria-hidden", "false");
$("overlay").classList.add("open");
}
function closeDrawer() {
$("drawer").classList.remove("open"); $("drawer").setAttribute("aria-hidden", "true");
$("overlay").classList.remove("open");
}
function startVerify(d) {
d._v = true; db.view = "verify"; save(); render();
}
// ---------- действия со списками ----------
function bindDocs() {
document.querySelectorAll("[data-a]").forEach(b => {
if (b.dataset.bound) return; b.dataset.bound = "1";
b.onclick = () => {
const a = b.dataset.a, id = +b.dataset.id;
const d = byId(id); if (!d) return;
if (a === "view") openDrawer(d);
if (a === "scan") scanDoc(d, true);
if (a === "rescan") { scanDoc(d, true); }
if (a === "openverify") startVerify(d);
if (a === "load") doLoad(d);
};
});
document.querySelectorAll(".doc[data-doc]").forEach(row => {
if (row.dataset.bound) return; row.dataset.bound = "1";
row.onclick = (e) => { if (e.target.closest("[data-a]")) return; openDrawer(byId(+row.dataset.doc)); };
});
}
function scanDoc(d, fromUi) {
d.scanStatus = "scanning";
save(); render();
setTimeout(() => {
let doc = byId(d.id); if (!doc) return;
doc.scanStatus = Math.random() < 0.15 ? "failed" : "done";
doc.scanMs = 1800 + Math.round(Math.random() * 2400);
doc.fields.forEach(f => { f.verdict = f.conf >= LOW ? "confirmed" : "problem"; });
save(); render();
if (fromUi) toast(doc.scanStatus === "done" ? `«${doc.name}» распознано` : `Ошибку распознавания «${doc.name}»`);
}, 1600);
}
// ---------- общий рендер ----------
function render() {
renderSteps();
document.querySelectorAll(".view").forEach(v => v.classList.remove("active"));
const v = $("view-" + db.view); if (v) v.classList.add("active");
if (db.view === "inbox") renderInbox();
if (db.view === "scan") renderScan();
if (db.view === "verify") renderVerify();
if (db.view === "loaded") renderLoaded();
}
// ---------- init ----------
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDrawer(); });
$("overlay").onclick = closeDrawer;
$("dw-close").onclick = closeDrawer;
$("btn-reset").onclick = () => {
if (!confirm("Сбросить все изменения и вернуть исходные данные?")) return;
db = seed(); save(); render(); toast("Данные возвращены к исходным");
};
// завершаем документы, оставшиеся "в распознавании" после перезахода
db.docs.filter(d => d.scanStatus === "scanning").forEach(d => {
d.scanStatus = Math.random() < 0.15 ? "failed" : "done";
d.fields.forEach(f => { f.verdict = f.conf >= LOW ? "confirmed" : "problem"; });
});
save();
render();
})();