// 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();
})();