249 lines
7.8 KiB
JavaScript
249 lines
7.8 KiB
JavaScript
|
|
/*
|
||
|
|
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 `<li>${m.name}: ${m.grams_per_box} g/box at ${currency(m.cost_per_kg)}/kg</li>`;
|
||
|
|
}).join("");
|
||
|
|
|
||
|
|
div.innerHTML = `
|
||
|
|
<ul>
|
||
|
|
<li>Waste factor: ${assumptions.waste_factor}</li>
|
||
|
|
<li>Electricity: ${currency(assumptions.electricity_cost_per_kwh)}/kWh</li>
|
||
|
|
<li>Printer average load: ${assumptions.printer_average_watts} W</li>
|
||
|
|
<li>Print hours per box: ${assumptions.print_hours_per_box}</li>
|
||
|
|
<li>Target scenarios: ${assumptions.target_box_counts.join(", ")}</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Printed materials</strong></p>
|
||
|
|
<ul>${materialLines}</ul>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
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 = `
|
||
|
|
<td>${boxCount}</td>
|
||
|
|
<td>${currency(hardware.total_hardware_cost)}</td>
|
||
|
|
<td>${currency(printing.total_printing_cost)}</td>
|
||
|
|
<td>${currency(totalCost)}</td>
|
||
|
|
<td>${currency(costPerBox)}</td>
|
||
|
|
`;
|
||
|
|
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 += `
|
||
|
|
<tr>
|
||
|
|
<td>${row.name}${row.optional ? " (optional)" : ""}</td>
|
||
|
|
<td>${row.required_units}</td>
|
||
|
|
<td>${row.pack_qty}</td>
|
||
|
|
<td>${currency(row.pack_cost)}</td>
|
||
|
|
<td>${row.packs_required}</td>
|
||
|
|
<td>${currency(row.total_cost)}</td>
|
||
|
|
<td>${row.notes || ""}</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
section.innerHTML = `
|
||
|
|
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
|
||
|
|
<table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Component</th>
|
||
|
|
<th>Required Units</th>
|
||
|
|
<th>Pack Qty</th>
|
||
|
|
<th>Pack Cost</th>
|
||
|
|
<th>Packs Required</th>
|
||
|
|
<th>Total Cost</th>
|
||
|
|
<th>Notes</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
${rowsHtml}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
`;
|
||
|
|
|
||
|
|
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 += `
|
||
|
|
<tr>
|
||
|
|
<td>${row.name}</td>
|
||
|
|
<td>${row.purchased_units}</td>
|
||
|
|
<td>${row.required_units}</td>
|
||
|
|
<td>${row.leftover_units}</td>
|
||
|
|
<td>${row.unit_type}</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
section.innerHTML = `
|
||
|
|
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
|
||
|
|
<table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Component</th>
|
||
|
|
<th>Purchased Units</th>
|
||
|
|
<th>Used Units</th>
|
||
|
|
<th>Leftover Units</th>
|
||
|
|
<th>Unit Type</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
${rowsHtml}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
`;
|
||
|
|
|
||
|
|
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 = `
|
||
|
|
<main style="padding: 2rem; font-family: Arial, Helvetica, sans-serif;">
|
||
|
|
<h1>Error</h1>
|
||
|
|
<p>Unable to load project data.</p>
|
||
|
|
<pre>${String(err)}</pre>
|
||
|
|
</main>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|