// Invoice extractor frontend (() => { const $ = (id) => document.getElementById(id); const sections = { upload: $("s-upload"), processing: $("s-processing"), result: $("s-result"), }; const show = (name) => { for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== name); }; let state = { jobId: null, data: null }; // ── Upload ── const fileInput = $("file-input"); const dropZone = $("drop-zone"); $("browse-btn").addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", (e) => e.target.files[0] && upload(e.target.files[0])); ["dragenter", "dragover"].forEach((ev) => dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); })); ["dragleave", "drop"].forEach((ev) => dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.remove("drag-over"); })); dropZone.addEventListener("drop", (e) => e.dataTransfer.files[0] && upload(e.dataTransfer.files[0])); async function upload(file) { show("processing"); try { const fd = new FormData(); fd.append("file", file); const r = await fetch("/api/upload", { method: "POST", body: fd }); if (!r.ok) throw new Error((await r.json()).detail || r.statusText); const json = await r.json(); state.jobId = json.job_id; state.data = json.data || {}; renderForm(); show("result"); } catch (err) { alert("Chyba: " + err.message); show("upload"); } } // ── Render editable form ── function field(key, label, value, opts = {}) { const isEmpty = value === null || value === undefined || value === ""; const display = isEmpty ? "" : value; const cls = "inv-input" + (isEmpty ? " is-empty" : ""); return `
`; } function nestedField(parent, key, label, value, opts = {}) { const isEmpty = value === null || value === undefined || value === ""; const display = isEmpty ? "" : value; const cls = "inv-input" + (isEmpty ? " is-empty" : ""); return `
`; } function renderForm() { const d = state.data; const sup = d.supplier || {}; const cust = d.customer || {}; const items = d.line_items || []; const vat = d.vat_breakdown || []; const html = `
Identifikace faktury
${field("invoice_number", "Číslo faktury", d.invoice_number)} ${field("variable_symbol", "Variabilní symbol", d.variable_symbol)} ${field("currency", "Měna", d.currency || "CZK")} ${field("issue_date", "Datum vystavení", d.issue_date, {type: "date"})} ${field("due_date", "Datum splatnosti", d.due_date, {type: "date"})} ${field("taxable_date", "DUZP", d.taxable_date, {type: "date"})} ${field("constant_symbol", "Konstantní symbol", d.constant_symbol)} ${field("specific_symbol", "Specifický symbol", d.specific_symbol)} ${field("payment_method", "Způsob platby", d.payment_method)}
Dodavatel
${nestedField("supplier", "name", "Název", sup.name, {wide: true})} ${nestedField("supplier", "ico", "IČO", sup.ico)} ${nestedField("supplier", "dic", "DIČ", sup.dic)} ${nestedField("supplier", "address", "Adresa", sup.address, {wide: true})}
Odběratel
${nestedField("customer", "name", "Název", cust.name, {wide: true})} ${nestedField("customer", "ico", "IČO", cust.ico)} ${nestedField("customer", "dic", "DIČ", cust.dic)} ${nestedField("customer", "address", "Adresa", cust.address, {wide: true})}
Platební údaje
${field("bank_account", "Číslo účtu", d.bank_account)} ${field("iban", "IBAN", d.iban)}
Položky faktury
${items.map((it, i) => renderItemRow(it, i)).join("")}
Popis Množství Jednotka Cena/jed. bez DPH DPH % Bez DPH S DPH
${vat.length ? `
Rekapitulace DPH
${vat.map((br, i) => renderVatRow(br, i)).join("")}
Sazba (%) Základ DPH Celkem
` : ""}
Celkem
Bez DPH
DPH
K úhradě
${d.notes ? `
Poznámka
` : ""} `; $("invoice-form").innerHTML = html; bindFieldHandlers(); } function renderItemRow(item, i) { return ` `; } function renderVatRow(br, i) { return ` `; } function bindFieldHandlers() { // Top-level + nested fields $("invoice-form").querySelectorAll("input[data-key]").forEach((el) => { el.addEventListener("input", () => { const key = el.dataset.key; const parent = el.dataset.parent; const val = parseFieldValue(key, el.value); if (parent) { if (!state.data[parent]) state.data[parent] = {}; state.data[parent][key] = val; } else { state.data[key] = val; } el.classList.toggle("is-empty", el.value === ""); }); }); // Item rows const itemsBody = $("items-body"); if (itemsBody) { itemsBody.addEventListener("input", (e) => { const inp = e.target.closest("input"); if (!inp) return; const tr = inp.closest("tr"); const i = +tr.dataset.row; const field = inp.dataset.field; if (!state.data.line_items) state.data.line_items = []; if (!state.data.line_items[i]) state.data.line_items[i] = {}; const numFields = ["quantity", "unit_price_excluding_vat", "vat_rate", "total_excluding_vat", "total_including_vat"]; state.data.line_items[i][field] = numFields.includes(field) ? parseNum(inp.value) : inp.value; }); itemsBody.addEventListener("click", (e) => { const btn = e.target.closest('[data-action="delete-item"]'); if (!btn) return; const tr = btn.closest("tr"); const i = +tr.dataset.row; state.data.line_items.splice(i, 1); renderForm(); }); } const addItem = $("add-item-btn"); if (addItem) { addItem.addEventListener("click", () => { if (!state.data.line_items) state.data.line_items = []; state.data.line_items.push({description: "", quantity: null, unit: "", unit_price_excluding_vat: null, vat_rate: null, total_excluding_vat: null, total_including_vat: null}); renderForm(); }); } // VAT rows const vatBody = $("vat-body"); if (vatBody) { vatBody.addEventListener("input", (e) => { const inp = e.target.closest("input"); if (!inp) return; const tr = inp.closest("tr"); const i = +tr.dataset.row; const field = inp.dataset.field; if (!state.data.vat_breakdown) state.data.vat_breakdown = []; if (!state.data.vat_breakdown[i]) state.data.vat_breakdown[i] = {}; state.data.vat_breakdown[i][field] = parseNum(inp.value); }); vatBody.addEventListener("click", (e) => { const btn = e.target.closest('[data-action="delete-vat"]'); if (!btn) return; const tr = btn.closest("tr"); const i = +tr.dataset.row; state.data.vat_breakdown.splice(i, 1); renderForm(); }); } } function parseFieldValue(key, str) { // Numeric top-level fields const num = ["total_excluding_vat", "total_vat", "total_including_vat"]; if (num.includes(key)) return parseNum(str); return str; } function parseNum(s) { if (s === "" || s == null) return null; const n = parseFloat(String(s).replace(",", ".").replace(/\s/g, "")); return Number.isFinite(n) ? n : null; } function fmtNum(v) { if (v === null || v === undefined || v === "") return ""; if (typeof v === "number") return String(v); return String(v); } // ── Export ── $("export-btn").addEventListener("click", async () => { if (!state.jobId) return; // Persist edits server-side before downloading await fetch(`/api/save/${state.jobId}`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({data: state.data}), }).catch(() => {}); window.location.href = `/api/export/${state.jobId}`; }); $("restart-btn").addEventListener("click", () => { state = { jobId: null, data: null }; fileInput.value = ""; $("invoice-form").innerHTML = ""; show("upload"); }); function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } function escapeHtmlAttr(s) { return escapeHtml(s); } })();