(() => { const queueList = document.getElementById("queueList"); const queueSearch = document.getElementById("queueSearch"); const queueCount = document.getElementById("queueCount"); const counter = document.getElementById("counter"); const form = document.getElementById("fieldsForm"); const preview = document.getElementById("preview"); const issuesEl = document.getElementById("issues"); const typeBadge = document.getElementById("typeBadge"); const fileBadge = document.getElementById("fileBadge"); const saveBtn = document.getElementById("saveBtn"); const prevBtn = document.getElementById("prevBtn"); const nextBtn = document.getElementById("nextBtn"); const toast = document.getElementById("toast"); let docs = []; let filtered = []; let currentId = null; let current = null; const params = new URLSearchParams(location.search); const focusId = params.get("id"); function showToast(message) { toast.textContent = message; toast.classList.add("show"); setTimeout(() => toast.classList.remove("show"), 1800); } function typeLabel(type) { return (type || "other").replaceAll("_", " "); } function fieldLabel(key) { return key.replaceAll("_", " "); } function needsReview(doc) { const conf = doc.confidence || {}; return Object.values(conf).some( (v) => v && v.needs_review ); } async function load() { const [readyRes, validatedRes] = await Promise.all([ fetch("/api/documents?limit=200&status=ready"), fetch("/api/documents?limit=200&status=validated"), ]); const ready = await readyRes.json(); const validated = await validatedRes.json(); const map = new Map(); [...ready, ...validated].forEach((d) => map.set(d.id, d)); docs = Array.from(map.values()).sort( (a, b) => new Date(b.created_at) - new Date(a.created_at) ); filtered = docs; queueCount.textContent = String(docs.length); renderQueue(); if (!docs.length) { preview.innerHTML = '
No documents ready for review yet. Upload files first.
'; form.innerHTML = 'Nothing to validate.
'; updateCounter(-1); return; } const start = docs.find((d) => d.id === focusId) || docs[0]; await show(start.id); } function renderQueue() { const q = (queueSearch.value || "").trim().toLowerCase(); filtered = docs.filter((d) => { if (!q) return true; return ( d.filename.toLowerCase().includes(q) || d.doc_type.toLowerCase().includes(q) ); }); queueList.innerHTML = ""; filtered.forEach((d) => { const li = document.createElement("li"); const btn = document.createElement("button"); btn.type = "button"; btn.className = "queue-item" + (d.id === currentId ? " active" : ""); const review = needsReview(d) ? 'review' : 'ok'; const dup = d.is_duplicate ? 'duplicate' : ""; btn.innerHTML = `Preview unavailable — ' + "the original file was removed. Extracted fields " + "on the right are still valid.
"; if (/\.(png|jpe?g|webp|gif|tif{1,2})$/.test(name)) { const img = new Image(); img.alt = "document"; img.onerror = () => { preview.innerHTML = missing; }; img.src = url; preview.innerHTML = ""; preview.appendChild(img); return; } if (name.endsWith(".pdf")) { preview.innerHTML = ``; return; } try { const res = await fetch(url); if (!res.ok) { preview.innerHTML = missing; return; } if (name.endsWith(".txt")) { const text = await res.text(); preview.innerHTML = `${escapeHtml(text)}`;
return;
}
preview.innerHTML =
`Open ${escapeHtml(doc.filename)}
`; } catch { preview.innerHTML = missing; } } function renderIssues(issues) { issuesEl.innerHTML = ""; issues.forEach((issue) => { const li = document.createElement("li"); li.textContent = issue.message || issue.code; if (issue.severity === "error") { li.classList.add("error"); } issuesEl.appendChild(li); }); } function renderFields(doc) { form.innerHTML = ""; const data = doc.extracted || {}; const conf = doc.confidence || {}; Object.keys(data).forEach((key) => { if ( key === "lines" || key === "items" || key === "parties" ) { const wrap = document.createElement("div"); wrap.className = "field"; wrap.innerHTML = `` + ``; form.appendChild(wrap); return; } const meta = conf[key] || {}; const wrap = document.createElement("div"); wrap.className = "field" + (meta.needs_review ? " needs-review" : ""); const score = meta.score != null ? `${(meta.score * 100).toFixed(0)}%` : ""; wrap.innerHTML = `` + ``; form.appendChild(wrap); }); } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">"); } function escapeAttr(s) { return String(s).replace(/"/g, """); } function collectPayload() { const extracted = {}; const fd = new FormData(form); for (const [key, value] of fd.entries()) { if ( key === "lines" || key === "items" || key === "parties" ) { try { extracted[key] = JSON.parse(value); } catch { extracted[key] = value; } continue; } if (value === "") { extracted[key] = null; continue; } if ( [ "amount_ht", "vat_amount", "amount_ttc", "amount", "quantity", ].includes(key) ) { const n = Number(value); extracted[key] = Number.isFinite(n) ? n : value; } else if (key === "auto_renewal") { extracted[key] = value === "true" || value === "1"; } else { extracted[key] = value; } } return extracted; } saveBtn.addEventListener("click", async () => { if (!current) return; const body = { extracted: collectPayload(), doc_type: current.doc_type, }; const res = await fetch(`/api/documents/${current.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { showToast("Save failed"); return; } current = await res.json(); const idx = docs.findIndex((d) => d.id === current.id); if (idx >= 0) docs[idx] = current; renderIssues(current.issues || []); renderFields(current); renderQueue(); showToast("Corrections saved"); }); prevBtn.addEventListener("click", () => { const i = docs.findIndex((d) => d.id === currentId); if (i > 0) show(docs[i - 1].id); }); nextBtn.addEventListener("click", () => { const i = docs.findIndex((d) => d.id === currentId); if (i >= 0 && i < docs.length - 1) { show(docs[i + 1].id); } }); queueSearch.addEventListener("input", renderQueue); load(); })();