Files
AI_portal/vv-check/static/app.js
2026-07-07 13:16:26 +02:00

180 lines
6.8 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.

// 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 `<span class="sheet-pill ${cls}" title="${escapeHtml(s.name)}">${icon} ${escapeHtml(label)}</span>`;
}).join("");
$("sheets-summary").innerHTML = `
<div class="summary-stat">
<div class="summary-stat-value">${vvCount}</div>
<div class="summary-stat-label">listů VV</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value">${pricedCount}</div>
<div class="summary-stat-label">s cenami</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value ${incCount === 0 ? "ok" : "problem"}">${incCount}</div>
<div class="summary-stat-label">nesouladů</div>
</div>
<div class="detected-sheets">${sheetsHtml}</div>
`;
const wrap = $("incs-wrap");
// Special case: no VVs at all
if (vvCount === 0) {
wrap.innerHTML = `
<div class="empty-state">
<p><strong>V sešitu nebyl rozpoznán žádný výkaz výměr.</strong></p>
<p style="margin-top:8px;color:var(--text-tertiary);font-size:13px">
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
<a href="/apps/navrhy/">Návrh nového nástroje</a>.
</p>
</div>`;
return;
}
// VVs found but none have prices — point user at correct tool
if (pricedCount === 0) {
wrap.innerHTML = `
<div class="empty-state">
<p><strong>Nalezeno ${vvCount} listů VV, ale žádný nemá vyplněné jednotkové ceny.</strong></p>
<p style="margin-top:8px;color:var(--text-tertiary);font-size:13px">
„Kontrola cen ve VV" porovnává jednotkové ceny napříč listy — bez vyplněných cen není co porovnávat.<br>
Pokud chcete porovnat tento sešit s předchozí verzí (změny ve výměrách, přidané/odebrané položky),
použijte <a href="/apps/porovnani-vv/">Porovnání VV</a>.
</p>
</div>`;
return;
}
// VVs with prices found but no inconsistencies
if (incCount === 0) {
wrap.innerHTML = `
<div class="empty-state">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="m9 12 2 2 4-4"/>
</svg>
<p>Všechny položky se stejným názvem mají shodnou jednotkovou cenu ve všech VV listech.</p>
</div>`;
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 `<tr class="${cls}">
<td>${escapeHtml(rw.sheet)}</td>
<td>${rw.row}</td>
<td>${escapeHtml(rw.mj)}</td>
<td class="num">${fmtPrice(rw.unit_price)}</td>
</tr>`;
}).join("");
const spread = maxP > 0 ? ((maxP - minP) / minP * 100) : 0;
return `
<div class="inc-card">
<div class="inc-card-title">${escapeHtml(inc.description)}</div>
<div class="inc-card-meta">
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)} %)
</div>
<table class="inc-table">
<thead><tr><th>List VV</th><th>Řádek</th><th>MJ</th><th>Jed. cena</th></tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
}).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) =>
({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
}
})();