OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / design / racking.jsx

231 lines · jsx ref:
/* ============================================================================
   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&nbsp;$</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 });