/* 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 = `

    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"}

    ${rowsHtml}
    Component Required Units Pack Qty Pack Cost Packs Required Total Cost Notes
    `; 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"}

    ${rowsHtml}
    Component Purchased Units Used Units Leftover Units Unit Type
    `; 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();