// Spread — frontend glue. Sends an uploaded BOM to /api/analyze and renders // the returned line-by-line re-quote into the example table. (function () { "use strict"; var fileInput = document.getElementById("file-input"); var dropzone = document.getElementById("dropzone"); var wrapper = document.getElementById("bom-wrapper"); var body = document.getElementById("bom-body"); var title = document.getElementById("bom-title"); var meta = document.getElementById("bom-meta"); var errorBanner = document.getElementById("error-banner"); var els = { savings: document.getElementById("t-savings"), cut: document.getElementById("t-cut"), net: document.getElementById("t-net"), }; function money(n, digits) { return "$" + Number(n).toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits, }); } function qty(n) { return Number(n).toLocaleString("en-US"); } function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; } function showError(msg) { errorBanner.textContent = msg; errorBanner.classList.add("error"); } function clearError() { errorBanner.textContent = ""; errorBanner.classList.remove("error"); } function render(report) { var lines = report.lines || []; var rows = lines.map(function (l) { var pn = l.part_number || ""; var estTag = l.matched ? "" : ' · est.'; var saveClass = l.line_saving > 0 ? "save" : "save zero"; return ( "" + '' + esc(l.description) + '' + esc(pn) + estTag + "" + '' + qty(l.quantity) + "" + '' + money(l.unit_price, 4) + "" + '' + money(l.spread_price, 4) + "" + '' + money(l.line_saving, 2) + "" + "" ); }); body.innerHTML = rows.join("") || 'No line items found.'; var t = report.totals || {}; els.savings.textContent = money(t.savings || 0, 2); els.cut.textContent = money(t.our_cut || 0, 2); els.net.textContent = money(t.net_savings || 0, 2); var partWord = (t.lines === 1) ? "part" : "parts"; title.innerHTML = esc(report.bom_ref || "BOM") + '  · ' + (t.lines || 0) + " " + partWord + ""; meta.textContent = "analysis complete"; } function analyze(file) { clearError(); wrapper.classList.add("busy"); meta.textContent = "re-quoting…"; var fd = new FormData(); fd.append("bom", file, file.name); fetch("/api/analyze", { method: "POST", body: fd }) .then(function (res) { return res.json().then(function (data) { if (!res.ok) throw new Error(data.error || "Analysis failed (" + res.status + ")"); return data; }); }) .then(function (report) { render(report); }) .catch(function (err) { showError(err.message || "Something went wrong parsing that file."); meta.textContent = "upload failed"; }) .finally(function () { wrapper.classList.remove("busy"); }); } function loadExample() { clearError(); wrapper.classList.add("busy"); meta.textContent = "re-quoting…"; fetch("/example-bom.csv") .then(function (res) { if (!res.ok) throw new Error("could not load example"); return res.blob(); }) .then(function (blob) { analyze(new File([blob], "example-bom.csv", { type: "text/csv" })); }) .catch(function (err) { showError(err.message); wrapper.classList.remove("busy"); }); } // Wire the upload buttons. Array.prototype.forEach.call(document.querySelectorAll("[data-upload]"), function (btn) { btn.addEventListener("click", function () { fileInput.click(); }); }); Array.prototype.forEach.call(document.querySelectorAll("[data-example]"), function (btn) { btn.addEventListener("click", function (e) { e.preventDefault(); document.getElementById("example").scrollIntoView({ behavior: "smooth" }); loadExample(); }); }); fileInput.addEventListener("change", function () { if (fileInput.files && fileInput.files[0]) analyze(fileInput.files[0]); fileInput.value = ""; }); // Drag and drop. ["dragenter", "dragover"].forEach(function (ev) { dropzone.addEventListener(ev, function (e) { e.preventDefault(); dropzone.classList.add("drag"); }); }); ["dragleave", "drop"].forEach(function (ev) { dropzone.addEventListener(ev, function (e) { e.preventDefault(); if (ev === "dragleave" && dropzone.contains(e.relatedTarget)) return; dropzone.classList.remove("drag"); }); }); dropzone.addEventListener("drop", function (e) { if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) { analyze(e.dataTransfer.files[0]); } }); // Load the example on first paint so the section is never empty. loadExample(); })();