/* ============================================================================
PRSM SOLAR — RACKING / STRUCTURAL TAB (with cost calculator)
========================================================================== */
const { useState: useStateR, useEffect: useEffectR, useMemo: useMemoR } = React;
const CALC_KEY = "prsm_racking_calc_v1";
const money = (n) => "$" + (Math.round(n * 100) / 100).toLocaleString("en-US", { minimumFractionDigits: n % 1 ? 2 : 0, maximumFractionDigits: 2 });
const money0 = (n) => "$" + Math.round(n).toLocaleString("en-US");
function loadCalc() {
try { return JSON.parse(localStorage.getItem(CALC_KEY)) || {}; } catch (e) { return {}; }
}
function RackingCalculator() {
const R = window.PRSM_RACK;
const [st, setSt] = useStateR(() => {
const saved = loadCalc();
const init = {};
R.bom.forEach((r) => { init[r.id] = { stock: saved[r.id]?.stock ?? 0, price: saved[r.id]?.price ?? r.price }; });
return init;
});
useEffectR(() => { try { localStorage.setItem(CALC_KEY, JSON.stringify(st)); } catch (e) {} }, [st]);
const setStock = (id, raw, need) => {
let v = raw === "" ? 0 : Math.max(0, Math.min(need, parseFloat(raw) || 0));
setSt((p) => ({ ...p, [id]: { ...p[id], stock: v } }));
};
const setPrice = (id, raw) => {
let v = raw === "" ? 0 : Math.max(0, parseFloat(raw) || 0);
setSt((p) => ({ ...p, [id]: { ...p[id], price: v } }));
};
const reset = () => { const init = {}; R.bom.forEach((r) => { init[r.id] = { stock: 0, price: r.price }; }); setSt(init); };
const zeroStock = () => setSt((p) => { const n = { ...p }; R.bom.forEach((r) => { n[r.id] = { ...n[r.id], stock: 0 }; }); return n; });
const calc = useMemoR(() => {
let toPurchaseTotal = 0, fullBuy = 0, lineCount = 0;
const rows = R.bom.map((r) => {
const s = st[r.id] || { stock: 0, price: r.price };
const stock = r.netting ? s.stock : 0;
const toPurchase = Math.max(0, r.need - stock);
const line = toPurchase * s.price;
const full = r.need * s.price;
toPurchaseTotal += line; fullBuy += full;
if (toPurchase > 0) lineCount++;
return { ...r, stock, price: s.price, toPurchase, line, full };
});
return { rows, toPurchaseTotal, fullBuy, saved: fullBuy - toPurchaseTotal, lineCount };
}, [st]);
const { band } = R.meta;
const gMin = band.lo * 0.55, gMax = band.hi * 1.2;
const pct = (v) => Math.max(0, Math.min(100, ((v - gMin) / (gMax - gMin)) * 100));
const total = calc.toPurchaseTotal;
const verdict = total < band.lo ? ["under", "under band"] : total > band.hi ? ["over", "over band"] : ["in", "in band"];
return (
<div className="sheet pad" style={{ marginBottom: 6 }}>
<div className="calc-head">
<SectionHead n="06" kicker="Interactive · saves automatically" title="Materials cost calculator"
sub="Phase 1 racking BOM with stockpile netting. Quantities are planning figures; per-unit prices are seed estimates." />
<div className="calc-actions" style={{ marginTop: 4 }}>
<button className="calc-btn" onClick={zeroStock}>Zero stockpile</button>
<button className="calc-btn" onClick={reset}>Reset all</button>
</div>
</div>
<div className="calc-how">
<span className="stamp computed" style={{ flex: "none" }}>How</span>
<p>Fill <b>STOCKPILE</b> with how many of each you already have on hand — lumber, hardware, concrete, consumables. Lumber must be exterior/PT (ideally MCA); hardware must be SS/HDG. <b>TO-PURCHASE</b> = NEED − STOCKPILE updates live. Override any <b>UNIT $</b> with your local VT quote. Totals and the band check below recompute instantly and persist on this device.</p>
</div>
<div className="calc-table">
<div className="calc-row head">
<div>Member</div><div>Spec</div><div>Need</div><div className="r">Stockpile</div><div className="r">To buy</div><div className="r">Unit $</div><div className="r">Line $</div>
</div>
{calc.rows.map((r) => (
<div className={"calc-row" + (r.toPurchase === 0 ? " zero" : "")} key={r.id}>
<div className="calc-m">{r.member}{r.note && <span className="tagnote">{r.note}</span>}</div>
<div className="calc-spec">{r.spec}</div>
<div className="calc-need">{r.need}<em>{r.unit}</em></div>
<div>
{r.netting ? (
<div className="calc-input">
<input className="calc-field" type="number" min="0" max={r.need} step="1"
value={r.stock === 0 ? "" : r.stock} placeholder="0"
onChange={(e) => setStock(r.id, e.target.value, r.need)} />
</div>
) : (<div className="calc-na">n/a</div>)}
</div>
<div className="calc-topurchase">{r.toPurchase}</div>
<div>
<div className="calc-input">
<span className="calc-price-wrap">
<input className="calc-field price" type="number" min="0" step="0.5"
value={r.price === 0 ? "" : r.price} placeholder="0"
onChange={(e) => setPrice(r.id, e.target.value)} />
</span>
</div>
</div>
<div className="calc-line">{money(r.line)}</div>
</div>
))}
<div className="calc-row sub">
<div className="lbl">To-purchase subtotal · {calc.lineCount} line{calc.lineCount === 1 ? "" : "s"} with a buy</div>
<div className="calc-topurchase">—</div>
<div className="calc-line">{money0(calc.toPurchaseTotal)}</div>
</div>
</div>
<div className="calc-result">
<div className="result-card">
<div className="result-big-k">Phase 1 racking · to purchase</div>
<div className="result-big-v">{money0(total)}</div>
<div className="result-sub">after netting <b>{money0(calc.saved)}</b> from your stockpile</div>
<div className="band">
<div className="band-top"><span>{money0(gMin)}</span><span>doc estimate band</span><span>{money0(gMax)}</span></div>
<div className="band-track">
<div className="band-range" style={{ left: pct(band.lo) + "%", right: (100 - pct(band.hi)) + "%" }} />
<div className="band-marker" style={{ left: pct(total) + "%" }} />
</div>
<div className="band-foot">
<span>doc reference: {money0(band.lo)}–{money0(band.hi)}</span>
<span className={"verdict " + verdict[0]}>{money0(total)} · {verdict[1]}</span>
</div>
</div>
</div>
<div className="result-card">
<div className="result-rows">
<div className="result-row"><span className="k">Full buy (no stockpile)</span><span className="v">{money0(calc.fullBuy)}</span></div>
<div className="result-row save"><span className="k">Saved from stockpile</span><span className="v">−{money0(calc.saved)}</span></div>
<div className="result-row"><span className="k">Net to purchase</span><span className="v">{money0(total)}</span></div>
<div className="result-row"><span className="k">+ VT use tax 6% (unless S-3E)</span><span className="v">{money0(total * 0.06)}</span></div>
<div className="result-row" style={{ borderTop: "1px solid var(--ink)", marginTop: 4, paddingTop: 10 }}>
<span className="k" style={{ fontWeight: 600, color: "var(--ink)" }}>Landed estimate</span>
<span className="v" style={{ fontSize: 15 }}>{money0(total * 1.06)}</span>
</div>
</div>
<p className="fine mono" style={{ marginTop: 12 }}>Excludes auger rental, fasteners freight & any ballast pivot. Get a local MCA-SPF quote (action A4) before committing.</p>
</div>
</div>
</div>
);
}
function Racking() {
const R = window.PRSM_RACK;
return (
<div className="tab-wrap">
<div className="tab-toolbar">
<SectionHead n="05" kicker="Mechanical / structural workstream" title={R.meta.title} sub={R.meta.basis} />
<span className="stamp warn">Est · not stamped</span>
</div>
<p className="basis-note">{R.meta.scope}. {R.meta.status}.</p>
<RackingOptions />
<div className="opt-divider">
<span className="mono">↓ committed build detail</span>
<span className="opt-divider-t">The structure, visualization, calculator & actions below detail <b>Option 1 — Fixed 55°</b>, the baseline build.</span>
</div>
<div className="rk-facts">
{R.structural.map((f) => (
<div className={"rk-fact " + f.tone} key={f.k}>
<div className="rk-fact-k">{f.k}</div>
<div className="rk-fact-v">{f.v}</div>
<div className="rk-fact-n">{f.n}</div>
</div>
))}
</div>
<RackingVisual />
<div className="rk-cols">
<div className="sheet pad">
<SectionHead n="—" title="Member verification" sub="Factored demand vs. Table A7 capacity (No.1/2 SPF)." />
<div className="rk-mem">
<div className="rk-mem-row head"><div>Member</div><div>Demand</div><div>Verdict</div></div>
{R.members.map((m) => (
<div className="rk-mem-row" key={m.m}>
<div className="rk-mem-m">{m.m}</div>
<div className="rk-mem-d">{m.demand}<br /><span style={{ opacity: 0.6 }}>{m.cap}</span></div>
<div className={"rk-mem-v " + m.flag}><i /><span>{m.verdict}</span></div>
</div>
))}
</div>
</div>
<div className="sheet pad">
<SectionHead n="—" title="Locked adaptations" sub="Greensboro changes from the published London design." />
<ul className="rk-adapt">
{R.adaptations.map((a, i) => (
<li key={i}><span className="num">{String(i + 1).padStart(2, "0")}</span>{a}</li>
))}
</ul>
</div>
</div>
<RackingCalculator />
<div className="rk-bottom">
<div className="sheet pad">
<SectionHead n="07" title="Electrical ↔ structural" sub="Where the two workstreams must agree." />
<ul className="intg-list">
{R.integration.map((it, i) => (
<li key={i}><div className="intg-t">{it.t}</div><div className="intg-d">{it.d}</div></li>
))}
</ul>
</div>
<div className="sheet pad">
<SectionHead n="08" title="Open actions" />
<ul className="act-list">
{R.actions.map((a) => (
<li key={a.id}>
<span className={"act-id" + (a.done ? " done" : "")}>{a.id}</span>
<span className="act-t">{a.t}{a.resolved && <span className="resolved">✓ {a.resolved}</span>}</span>
<span className="act-who">{a.who}</span>
</li>
))}
</ul>
</div>
</div>
<p className="fine mono">Structural figures are a defensible engineering estimate confirming member sizing — not a stamped calculation. A VT engineer should review before pour (action A7). Cut list (exact lengths + 55° angles) follows once Waaree mounting-hole spacing is confirmed (action A2).</p>
</div>
);
}
Object.assign(window, { Racking });