// Offer → Excel 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])); // Close any open combobox menu when clicking outside it (bound once). document.addEventListener("click", (e) => { document.querySelectorAll(".combo-menu").forEach((m) => { if (!m.closest(".combo-wrap").contains(e.target)) m.classList.add("hidden"); }); }); 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 || {}; await initOrder(); renderForm(); show("result"); } catch (err) { alert("Chyba: " + err.message); show("upload"); } } // ── Render editable form ── function nestedField(parent, key, label, value, opts = {}) { const isEmpty = value === null || value === undefined || value === ""; const cls = "inv-input" + (isEmpty ? " is-empty" : ""); return `
`; } function renderForm() { const d = state.data; const sup = d.supplier || {}; const items = d.line_items || []; const warn = (d.is_offer === false) ? `
⚠️ Tento dokument zřejmě není dodavatelská nabídka. Data mohou být neúplná — zkontrolujte je, nebo nahrajte jiný soubor.
` : ""; const html = ` ${warn}
Dodavatel
${nestedField("supplier", "name", "Jméno", sup.name, {wide: true})} ${nestedField("supplier", "ico", "IČO", sup.ico)} ${nestedField("supplier", "dic", "DIČ", sup.dic)} ${nestedField("supplier", "email", "Kontaktní email", sup.email)} ${nestedField("supplier", "address", "Adresa", sup.address, {wide: true})}
Položky (${items.length})
${items.map((it, i) => renderItemRow(it, i)).join("")}
Název zboží ID zboží dodavatele Množství Jednotka Jednotková cena Cena celkem
${orderCardHtml()}
`; $("offer-form").innerHTML = html; bindFieldHandlers(); bindOrderHandlers(); initDatePickers(); } function rerenderOrderCard() { const wrap = $("order-card-wrap"); if (!wrap) return; wrap.innerHTML = orderCardHtml(); bindOrderHandlers(); initDatePickers(); } function initDatePickers() { if (typeof flatpickr === "undefined") return; if (flatpickr.l10ns && flatpickr.l10ns.cs) flatpickr.localize(flatpickr.l10ns.cs); document.querySelectorAll(".fp-date").forEach((el) => { if (el._flatpickr) return; flatpickr(el, { dateFormat: "d.m.Y", // matches the PDF / store format allowInput: true, disableMobile: true, onChange: () => { state.order[el.dataset.order] = el.value; el.classList.toggle("is-empty", el.value === ""); }, }); }); } function renderItemRow(item, i) { return ` `; } function bindFieldHandlers() { $("offer-form").querySelectorAll("input[data-key]").forEach((el) => { el.addEventListener("input", () => { const key = el.dataset.key; const parent = el.dataset.parent; if (parent) { if (!state.data[parent]) state.data[parent] = {}; state.data[parent][key] = el.value; } else { state.data[key] = el.value; } el.classList.toggle("is-empty", el.value === ""); }); }); const itemsBody = $("items-body"); if (itemsBody) { itemsBody.addEventListener("input", (e) => { const inp = e.target.closest("input"); if (!inp) return; const i = +inp.closest("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", "total_price"]; 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 i = +btn.closest("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({name: "", supplier_id: "", quantity: null, unit: "", unit_price: null, total_price: null}); renderForm(); }); } } 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 ""; return String(v); } // ── Export ── $("export-btn").addEventListener("click", async () => { if (!state.jobId) return; 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 = ""; $("offer-form").innerHTML = ""; show("upload"); }); // ── Objednávka (order form) ────────────────────────────────── function sumTotals() { return (state.data.line_items || []).reduce( (a, it) => a + (Number(it.total_price) || 0), 0); } async function initOrder() { const d = state.data; const offerNo = d.offer_number || ""; state.order = { objednavka_number: "", kod_zakazky: "", nazev_zakazky: "", dodavatel: d.supplier || {}, text: "Objednáváme u vás zboží dle cenové nabídky " + offerNo, prilohy: offerNo, termin_dodani_text: "Co nejdříve", misto_dodani: "", kontaktni_osoba: "", splatnost: "dle VNP nebo VSP", zaruka: "", poznamka: "", vystavil: "", popis: "", zpusob_platby: "Bankovním převodem", zpusob_odberu: "-", zpusob_dopravy: "-", forma_objednavky: "mailem", termin_dodani: "", fakturujte: "s DPH", }; state.projects = []; state.orderHistory = {}; try { const r = await fetch("api/projects"); if (r.ok) { const j = await r.json(); state.projects = j.projects || []; if (j.next_order_number) state.order.objednavka_number = j.next_order_number; } } catch (_) {} } // Body fields. `hist` = remembered per project, shown with a history // quick-pick (most-used first). const ORDER_FIELDS = [ ["text", "Text", true, false], ["termin_dodani_text", "Termín dodání (text)", false, false], ["misto_dodani", "Místo dodání", true, true], ["kontaktni_osoba", "Kontaktní osoba v místě dodání", true, true], ["splatnost", "Splatnost", false, true], ["zaruka", "Záruka", false, true], ["poznamka", "Poznámka", false, false], ["prilohy", "Přílohy", false, false], ["vystavil", "Vystavil", false, true], ["popis", "Popis", false, true], ]; function fieldHtml(key, label, wide, hist) { return hist ? historyField(key, label, wide) : textField(key, label, wide); } // ── date helpers (PDF/store use DD.MM.YYYY, uses ISO) ── function toDateInput(s) { s = (s == null ? "" : String(s)).trim(); let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (m) return `${m[3]}-${m[2].padStart(2,"0")}-${m[1].padStart(2,"0")}`; if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; return ""; } function fromDateInput(s) { const m = (s || "").match(/^(\d{4})-(\d{2})-(\d{2})$/); return m ? `${+m[3]}.${+m[2]}.${m[1]}` : (s || ""); } // Combobox: type freely OR pick a previously-used value from a themed // dropdown attached to the field; each option can be deleted from history. function comboMenuHtml(key, filter) { const hist = (state.orderHistory && state.orderHistory[key]) || []; const f = (filter || "").toLowerCase(); const items = hist.filter((h) => !f || String(h.value).toLowerCase().includes(f)); if (!items.length) return '
Žádné dříve použité
'; return items.map((h) => `
${escapeHtml(h.value)} ${h.count}×
`).join(""); } function historyField(key, label, wide) { const v = state.order[key]; const cur = v == null ? "" : String(v); const hasHist = ((state.orderHistory && state.orderHistory[key]) || []).length > 0; return `
${hasHist ? '' : ""}
`; } // Dodací a platební podmínky — selectable presets (editable: type or pick). const TERMS_FIELDS = [ ["zpusob_platby", "Způsob platby", ["Bankovním převodem", "Hotově", "Kartou", "Zálohově", "Dobírka"]], ["forma_objednavky", "Forma objednávky", ["mailem", "telefonicky", "osobně", "písemně"]], ["zpusob_odberu", "Způsob odběru", ["-", "Osobní odběr", "Dodání na stavbu", "Dopravcem"]], ["termin_dodani", "Termín dodání", []], ["zpusob_dopravy", "Způsob dopravy", ["-", "Dodavatel", "Vlastní", "Dopravce"]], ["fakturujte", "Fakturujte", ["s DPH", "bez DPH"]], ]; function textField(key, label, wide) { const v = state.order[key]; const isEmpty = v === null || v === undefined || v === ""; return `
`; } function comboField(key, label, options) { const v = state.order[key]; const cur = v == null ? "" : String(v); // Termín dodání → flatpickr date picker (value stays DD.MM.YYYY). if (key === "termin_dodani") { return `
`; } // Real dropdown: history (most-used first) + presets + any custom value. const histVals = ((state.orderHistory && state.orderHistory[key]) || []) .map((h) => h.value); const vals = []; for (const x of [...histVals, ...(options || []), cur]) { if (x && !vals.includes(x)) vals.push(x); } const opts = [''] .concat(vals.map((o) => ``)) .join(""); return `
`; } function projectOptions() { const cur = state.order.kod_zakazky || ""; const opts = ['']; for (const p of state.projects || []) { const sel = p.kod === cur ? " selected" : ""; const label = p.nazev ? `${p.kod} — ${p.nazev}` : p.kod; opts.push(``); } return opts.join(""); } function orderCardHtml() { return `
Objednávka (vyplní se do PDF)

Zakázku vyberte z nabídky (nebo přidejte přes Spravovat zakázky). Pole jako Místo dodání, kontaktní osoba či Vystavil se pamatují podle zakázky. Číslo objednávky se předvyplní jako poslední + 1. Cena celkem z položek: ${fmtCzk(sumTotals())}.

${textField("objednavka_number", "Číslo objednávky", false)}
${textField("nazev_zakazky", "Název zakázky", true)} ${ORDER_FIELDS.map(([k, l, w, h]) => fieldHtml(k, l, w, h)).join("")}
Dodací a platební podmínky
${TERMS_FIELDS.map(([k, l, o]) => comboField(k, l, o)).join("")}
`; } function fmtCzk(n) { return (Number(n) || 0).toLocaleString("cs-CZ", {minimumFractionDigits: 2, maximumFractionDigits: 2}) + " Kč"; } function applyOrderToInputs() { document.querySelectorAll('[data-order]').forEach((inp) => { const k = inp.dataset.order; if (state.order[k] != null) { inp.value = state.order[k]; inp.classList.toggle("is-empty", inp.value === ""); } }); document.querySelectorAll('[data-order-date]').forEach((inp) => { const k = inp.dataset.orderDate; inp.value = toDateInput(state.order[k]); }); } async function loadProjectDefaults(code) { if (!code) { state.orderHistory = {}; return; } try { const r = await fetch(`api/order-defaults?code=${encodeURIComponent(code)}`); if (!r.ok) return; const { defaults } = await r.json(); if (defaults && Object.keys(defaults).length) { state.orderHistory = defaults._history || {}; delete defaults._history; Object.assign(state.order, defaults); // Re-render so history dropdowns reflect the selected project. rerenderOrderCard(); } } catch (_) {} } function renderProjPanel() { const rows = (state.projects || []).map((p) => `
${escapeHtml(p.kod)}
`).join(""); return `
${rows || '

Zatím žádné zakázky.

'}
`; } async function refreshProjects(list) { state.projects = list || state.projects; const sel = $("proj-select"); if (sel) sel.innerHTML = projectOptions(); const panel = $("proj-panel"); if (panel && !panel.classList.contains("hidden")) panel.innerHTML = renderProjPanel(); } function bindOrderHandlers() { document.querySelectorAll('[data-order]').forEach((el) => { const upd = () => { state.order[el.dataset.order] = el.value; el.classList.toggle("is-empty", el.value === ""); }; el.addEventListener("input", upd); el.addEventListener("change", upd); //