// VV price-check frontend (() => { const $ = (id) => document.getElementById(id); const sections = { upload: $("s-upload"), processing: $("s-processing"), result: $("s-result"), }; const show = (n) => { for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== n); }; let state = { jobId: null, result: 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/check", { 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.result = json.result; renderResult(); show("result"); } catch (err) { alert("Chyba: " + err.message); show("upload"); } } function fmtPrice(v) { if (v == null || isNaN(v)) return "—"; return v.toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " Kč"; } function renderResult() { const r = state.result; const vvCount = r.vv_sheet_count; const pricedCount = r.vv_sheets_with_prices || 0; const incCount = r.total_inconsistencies; const totalSheets = r.sheets.length; $("results-meta").textContent = `${totalSheets} listů v sešitu, ${vvCount} identifikováno jako VV, ${pricedCount} s vyplněnými cenami.`; const sheetsHtml = r.sheets.map((s) => { let cls = "not-vv"; let label = s.name; let icon = "·"; if (s.is_vv && s.priced_items > 0) { cls = "vv"; icon = "✓"; label = `${s.name} (${s.priced_items} s cenou)`; } else if (s.is_vv && s.items > 0) { cls = "vv-noprice"; icon = "⚠"; label = `${s.name} (${s.items} bez cen)`; } else if (s.is_vv) { cls = "not-vv"; label = `${s.name} (prázdné)`; } return `${icon} ${escapeHtml(label)}`; }).join(""); $("sheets-summary").innerHTML = `
${vvCount}
listů VV
${pricedCount}
s cenami
${incCount}
nesouladů
${sheetsHtml}
`; const wrap = $("incs-wrap"); // Special case: no VVs at all if (vvCount === 0) { wrap.innerHTML = `

V sešitu nebyl rozpoznán žádný výkaz výměr.

Aplikace hledá listy s hlavičkou „Popis / MJ / Výměra / Jedn. cena". Pokud váš sešit používá jiný formát, dejte vědět přes Návrh nového nástroje.

`; return; } // VVs found but none have prices — point user at correct tool if (pricedCount === 0) { wrap.innerHTML = `

Nalezeno ${vvCount} listů VV, ale žádný nemá vyplněné jednotkové ceny.

„Kontrola cen ve VV" porovnává jednotkové ceny napříč listy — bez vyplněných cen není co porovnávat.
Pokud chcete porovnat tento sešit s předchozí verzí (změny ve výměrách, přidané/odebrané položky), použijte Porovnání VV.

`; return; } // VVs with prices found but no inconsistencies if (incCount === 0) { wrap.innerHTML = `

Všechny položky se stejným názvem mají shodnou jednotkovou cenu ve všech VV listech.

`; return; } wrap.innerHTML = r.inconsistencies.map((inc) => { const minP = Math.min(...inc.rows.map(r => r.unit_price)); const maxP = Math.max(...inc.rows.map(r => r.unit_price)); const rows = inc.rows.map((rw) => { const cls = rw.unit_price === maxP && minP !== maxP ? "row-max" : rw.unit_price === minP && minP !== maxP ? "row-min" : ""; return ` ${escapeHtml(rw.sheet)} ${rw.row} ${escapeHtml(rw.mj)} ${fmtPrice(rw.unit_price)} `; }).join(""); const spread = maxP > 0 ? ((maxP - minP) / minP * 100) : 0; return `
${escapeHtml(inc.description)}
Vyskytuje se ${inc.occurrences}× ve ${new Set(inc.rows.map(r => r.sheet)).size} listech · rozdíl ${fmtPrice(minP)} – ${fmtPrice(maxP)} (rozptyl ${spread.toFixed(1)} %)
${rows}
List VVŘádekMJJed. cena
`; }).join(""); } $("download-btn").addEventListener("click", () => { if (!state.jobId) return; window.location.href = `/api/report/${state.jobId}`; }); $("restart-btn").addEventListener("click", () => { state = { jobId: null, result: null }; fileInput.value = ""; show("upload"); }); function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } })();