/*
20260319 ChatGPT
$Header$
*/
"use strict";
function currency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(value);
}
function ceilDivUsage(requiredUnits, packQty) {
return Math.ceil(requiredUnits / packQty);
}
function computePrintingCost(assumptions, boxCount) {
let materialCost = 0;
for (const material of assumptions.printed_materials) {
const totalGrams = material.grams_per_box * boxCount;
const totalKg = totalGrams / 1000.0;
materialCost += totalKg * material.cost_per_kg;
}
materialCost *= assumptions.waste_factor;
const totalKwh =
(assumptions.printer_average_watts / 1000.0) *
assumptions.print_hours_per_box *
boxCount;
const electricityCost = totalKwh * assumptions.electricity_cost_per_kwh;
return {
material_cost: materialCost,
electricity_cost: electricityCost,
total_printing_cost: materialCost + electricityCost
};
}
function computeHardwareScenario(bom, boxCount, includeOptional = true) {
const componentRows = [];
let totalHardwareCost = 0;
for (const component of bom.components) {
if (!includeOptional && component.optional) {
continue;
}
const requiredUnits = component.units_used_per_box * boxCount;
const packsRequired = ceilDivUsage(requiredUnits, component.pack_qty);
const purchasedUnits = packsRequired * component.pack_qty;
const leftoverUnits = purchasedUnits - requiredUnits;
const totalCost = packsRequired * component.pack_cost;
totalHardwareCost += totalCost;
componentRows.push({
id: component.id,
name: component.name,
required_units: requiredUnits,
pack_qty: component.pack_qty,
pack_cost: component.pack_cost,
packs_required: packsRequired,
purchased_units: purchasedUnits,
leftover_units: leftoverUnits,
total_cost: totalCost,
optional: component.optional,
unit_type: component.unit_type,
notes: component.notes
});
}
return {
box_count: boxCount,
total_hardware_cost: totalHardwareCost,
component_rows: componentRows
};
}
function renderAssumptions(assumptions) {
const div = document.getElementById("assumptions");
const materialLines = assumptions.printed_materials.map((m) => {
return `
${m.name}: ${m.grams_per_box} g/box at ${currency(m.cost_per_kg)}/kg`;
}).join("");
div.innerHTML = `
- Waste factor: ${assumptions.waste_factor}
- Electricity: ${currency(assumptions.electricity_cost_per_kwh)}/kWh
- Printer average load: ${assumptions.printer_average_watts} W
- Print hours per box: ${assumptions.print_hours_per_box}
- Target scenarios: ${assumptions.target_box_counts.join(", ")}
Printed materials
`;
}
function renderSummaryTable(bom, assumptions) {
const tbody = document.querySelector("#summary-table tbody");
tbody.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const printing = computePrintingCost(assumptions, boxCount);
const totalCost = hardware.total_hardware_cost + printing.total_printing_cost;
const costPerBox = totalCost / boxCount;
const tr = document.createElement("tr");
tr.innerHTML = `
${boxCount} |
${currency(hardware.total_hardware_cost)} |
${currency(printing.total_printing_cost)} |
${currency(totalCost)} |
${currency(costPerBox)} |
`;
tbody.appendChild(tr);
}
}
function renderComponentBreakdowns(bom, assumptions) {
const container = document.getElementById("component-breakdowns");
container.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const section = document.createElement("div");
section.className = "scenario-block";
let rowsHtml = "";
for (const row of hardware.component_rows) {
rowsHtml += `
| ${row.name}${row.optional ? " (optional)" : ""} |
${row.required_units} |
${row.pack_qty} |
${currency(row.pack_cost)} |
${row.packs_required} |
${currency(row.total_cost)} |
${row.notes || ""} |
`;
}
section.innerHTML = `
${boxCount} box${boxCount === 1 ? "" : "es"}
| Component |
Required Units |
Pack Qty |
Pack Cost |
Packs Required |
Total Cost |
Notes |
${rowsHtml}
`;
container.appendChild(section);
}
}
function renderLeftovers(bom, assumptions) {
const container = document.getElementById("leftovers");
container.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const section = document.createElement("div");
section.className = "scenario-block";
let rowsHtml = "";
for (const row of hardware.component_rows) {
rowsHtml += `
| ${row.name} |
${row.purchased_units} |
${row.required_units} |
${row.leftover_units} |
${row.unit_type} |
`;
}
section.innerHTML = `
${boxCount} box${boxCount === 1 ? "" : "es"}
| Component |
Purchased Units |
Used Units |
Leftover Units |
Unit Type |
${rowsHtml}
`;
container.appendChild(section);
}
}
async function loadJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load ${url}: ${response.status}`);
}
return await response.json();
}
async function main() {
try {
const bom = await loadJson("../data/bom.json");
const assumptions = await loadJson("../data/assumptions.json");
renderAssumptions(assumptions);
renderSummaryTable(bom, assumptions);
renderComponentBreakdowns(bom, assumptions);
renderLeftovers(bom, assumptions);
} catch (err) {
console.error(err);
document.body.innerHTML = `
Error
Unable to load project data.
${String(err)}
`;
}
}
main();