/* Loadpath — flow screens, part 2: validate → engineering → result → sistering → deliver */ (function () { const { Icon, Btn, Badge, Status, Callout, DCRBar, Card, KV, Money, engineerTypeLabel, } = window; const h = React.createElement; const { useState, useEffect, useRef } = React; // ---------------- Validation gates ---------------- function ScreenValidate({ project, go }) { const [phase, setPhase] = useState("running"); // running | missing | ok | error const [current, setCurrent] = useState(project); const [err, setErr] = useState(""); useEffect(() => { let alive = true; window.API.projects .validate(project.id) .then((d) => { if (!alive) return; const p = d.project || project; setCurrent(p); if (go.updateProject) go.updateProject(p); setPhase(d.valid ? "ok" : "missing"); }) .catch((e) => { if (alive) { setErr(e.message || "Validation failed"); setPhase("error"); } }); return () => { alive = false; }; }, [project.id]); useEffect(() => { if (phase !== "ok") return; const t = setTimeout(() => go.next(), 700); return () => clearTimeout(t); }, [phase]); const steps = [ "Restating every extracted value with its source sheet", "Cross-checking values that should agree across sheets", "Physical-plausibility sanity checks (span vs. spacing)", "Patio / carport gate · battery placement note", ]; return h( "div", { className: "fade-in" }, h( "div", { className: "eyebrow screen-eyebrow" }, "Deliver · Preparing package", ), h("h1", { className: "screen-title" }, "Checking the drawing"), h( "p", { className: "screen-sub" }, "After payment, every value needed for the engineering package is confirmed present and readable. ", h("b", null, "No assumptions"), " are made for missing project-specific data.", ), phase === "running" && h( Card, null, h( "div", { className: "proc-log" }, steps.map((s, i) => h( "div", { key: i, className: "ln active" }, h("span", { className: "spin" }), s, ), ), ), ), phase === "error" && h(Callout, { kind: "danger", title: "Validation failed" }, err), phase === "missing" && h(MissingReport, { project: current, go }), phase === "ok" && h( "div", { className: "fade-in" }, h( Callout, { kind: "success", title: "Complete & consistent" }, "Every project-specific value the 13 sections require is present, legible, and agrees across sheets. No patio/carport issue. Proceeding to the deterministic calculation engine.", ), h( "div", { className: "grid-2", style: { marginTop: 16 } }, h( Card, { tight: true, eyebrow: "Patio / carport gate" }, h( KV, { k: "New patio on residential roof" }, h(Badge, { kind: "pass", icon: "check" }, "None"), ), h( "div", { style: { fontSize: 12, color: "var(--fg-subtle)", marginTop: 8, }, }, "Array is on the main building.", ), ), h( Card, { tight: true, eyebrow: "Standing note" }, h( "div", { style: { fontSize: 12.5, color: "var(--fg-muted)", lineHeight: 1.5, }, }, "Two batteries may never share one wall stud; distribute across studs. Carried into the calc and letter.", ), ), ), h( Card, { tight: true, className: "scanline", style: { marginTop: 16 } }, h( "div", { className: "proc-log" }, h( "div", { className: "ln active" }, h("span", { className: "spin" }), "Starting engineering calculations...", ), ), ), ), ); } function MissingReport({ project, go }) { return h( "div", { className: "fade-in" }, h( Callout, { kind: "warn", icon: "alert-triangle", title: "Missing-Information Report — specific items required", }, "The drawing passed the basic pre-screen but is missing or inconsistent on values the analysis needs. We do not guess any of them.", ), h( Card, { style: { marginTop: 16 }, eyebrow: "What\u2019s blocking the analysis", }, h( "table", { className: "dtable" }, h( "thead", null, h( "tr", null, h("th", null, "Item"), h("th", null, "Reason"), h("th", null, "Needed for"), ), ), h( "tbody", null, project.missing.map((m, i) => h( "tr", { key: i }, h("td", { className: "name" }, m.item), h("td", { style: { color: "var(--fg-muted)" } }, m.reason), h("td", null, h("span", { className: "srctag" }, m.section)), ), ), ), ), ), h( "div", { style: { marginTop: 16 } }, h( Callout, { kind: "info" }, h( "b", null, "No assumptions will be made for missing project-specific data.", ), " Please provide the items above and re-upload; the analysis will then be completed.", ), ), h( "div", { className: "btn-row end", style: { marginTop: 24 } }, h( Btn, { kind: "primary", icon: "upload", onClick: go.toUpload }, "Re-upload corrected drawing", ), ), ); } // ---------------- Engineering run ---------------- function ScreenEngineering({ project, go }) { const [err, setErr] = useState(""); const [warn, setWarn] = useState(""); const [rawErr, setRawErr] = useState(""); const [successRuns, setSuccessRuns] = useState(null); // array of EngineeringTypeResult on success useEffect(() => { let alive = true; let done = false; let timer = null; const terminal = (p) => [ "delivered", "sistering", "sistering_pending", "rejected", "missing_info", "refunded", ].includes(p && p.status); const captureRuns = (p) => { const runs = ( p.engineeringResults || p.engineering_results || [] ).filter((r) => r && (r.rawJSON || r.raw_json)); if (runs.length > 0) setSuccessRuns(runs); }; const finishIfReady = (p) => { if (!alive || !p) return false; if (go.updateProject) go.updateProject(p); if (!terminal(p)) return false; done = true; setWarn(""); captureRuns(p); setTimeout(() => { if (alive) go.next(); }, 350); return true; }; const schedulePoll = (delay) => { if (!alive || done) return; timer = setTimeout(poll, delay); }; const poll = () => { if (!alive || done) return; window.API.projects .get(project.id) .then((p) => { if (!alive || done) return; setWarn(""); if (!finishIfReady(p)) schedulePoll(5000); }) .catch((e) => { if (!alive || done) return; if (e && e.status) { setErr(e.message || "Could not check engineering status"); return; } setWarn( "Connection paused; the engineering job is still running on the server.", ); schedulePoll(5000); }); }; const onVisibility = () => { if (document.visibilityState !== "visible" || !alive || done) return; if (timer) clearTimeout(timer); poll(); }; document.addEventListener("visibilitychange", onVisibility); window.API.projects .engineer(project.id) .then((d) => { if (!alive) return; const p = d.project || project; setWarn(""); if (!finishIfReady(p)) schedulePoll(2500); }) .catch((e) => { if (!alive) return; const p = e && e.data && e.data.data && e.data.data.project; if (p && finishIfReady(p)) { return; } const raw = e && e.data && e.data.data && e.data.data.raw_json; setRawErr(raw || ""); if (e && e.status) { setErr(e.message || "Engineering failed"); return; } setWarn( "Connection paused; the engineering job is still running on the server.", ); schedulePoll(3000); }); return () => { alive = false; if (timer) clearTimeout(timer); document.removeEventListener("visibilitychange", onVisibility); }; }, [project.id]); return h( "div", { className: "fade-in" }, h( "div", { className: "eyebrow screen-eyebrow" }, "Deliver · Preparing package", ), h("h1", { className: "screen-title" }, "Running engineering"), h(SkippedEngineerWarning, { project }), err && h( "div", { style: { marginBottom: 14 } }, h(Callout, { kind: "danger", title: "Engineering failed" }, err), ), !err && warn && h( "div", { style: { marginBottom: 14 } }, h(Callout, { kind: "info", title: "Still running" }, warn), ), rawErr && h(RawClaudeJSON, { text: rawErr, title: "Raw Claude JSON (error)" }), !err && h( Card, { tight: true, className: "scanline", style: { marginTop: 16 } }, h( "div", { className: "proc-log" }, h( "div", { className: "ln active" }, h("span", { className: "spin" }), "Please hold on while we process your request - this may take up to 15 minutes to complete. Thank you for your patience.", ), ), ), successRuns && successRuns.length > 0 && h( "div", { style: { marginTop: 16 } }, h( "div", { className: "eyebrow", style: { marginBottom: 8 } }, "Raw Claude API response · engineering step", ), successRuns.map((run) => { const raw = run.rawJSON || run.raw_json || ""; const t = run.engineerType || run.engineer_type || ""; return ( raw && h(RawClaudeJSON, { key: t, text: raw, title: engineerTypeLabel(t) + " · raw JSON from Claude API", }) ); }), ), ); } // Build the sistering/failure explanation from what Claude actually returned — // never assume the cause is a framing bending failure (e.g. battery-only projects // have zero combinations and fail for entirely different reasons). function describeFailure(runs, combinations) { const failingChecks = []; (combinations || []).forEach((c) => { const checks = c.dcrChecks || c.dcr_checks || c.checks || []; checks.forEach((chk) => { if (chk.pass === false) failingChecks.push({ combo: c, chk }); }); }); if (failingChecks.length > 0) { const { combo, chk } = failingChecks[0]; const dcr = typeof chk.dcr === "number" ? chk.dcr.toFixed(3) : chk.dcr; const limit = typeof chk.limit === "number" ? chk.limit : 1.0; return ( "Bending exceeds capacity on the " + (combo.framing || "governing framing member") + " (DCR " + dcr + " > " + limit + "). This roof cannot be issued as-is. The standard remedy is rafter sistering." ); } const logs = []; (runs || []).forEach((r) => { (r.log || []).forEach((l) => logs.push(l)); }); if (logs.length > 0) return logs.join(" "); return "Claude flagged this project for sistering review. See the raw JSON below for details."; } // ---------------- Result (clear-PASS gate) ---------------- function ScreenResult({ project, tweaks, go }) { const failing = project.status === "sistering" || project.status === "sistering_pending"; const rejected = project.status === "rejected"; const missing = project.status === "missing_info"; const refunded = project.status === "refunded"; const runs = project.engineeringResults || project.engineering_results || []; const combinations = project.combinations || []; useEffect(() => { if (failing || rejected || missing || refunded) return; const t = setTimeout(() => go.toDeliver(), 700); return () => clearTimeout(t); }, [failing, rejected, missing, refunded, project.id]); return h( "div", { className: "fade-in" }, h( "div", { className: "eyebrow screen-eyebrow" }, "Deliver · Finalizing package", ), h( "h1", { className: "screen-title" }, rejected ? "Engineering rejected" : missing ? "More information needed" : refunded ? "Project refunded" : failing ? "One check exceeds 1.00" : "Final checks passed", ), h( "p", { className: "screen-sub" }, "A calculation may be issued only when ", h("b", null, "every"), " check has DCR ≤ 1.00 on the unrounded value. There is no marginal or flagged pass — a computed 1.004 fails.", ), h(SkippedEngineerWarning, { project }), !rejected && (runs.length > 0 ? runs.map((run) => h(EngineerResultGroup, { key: run.engineerType || run.engineer_type, project, run, tweaks, }), ) : combinations.map((c, i) => h(ComboResult, { key: i, project, combo: c, tweaks }), )), rejected ? h( "div", { style: { marginTop: 18 } }, h( Callout, { kind: "danger", icon: "alert-octagon", title: "Engineering rejected", }, project.outcome || "At least one engineering route returned REJECT.", ), h("div", null), ) : missing ? h( "div", { style: { marginTop: 18 } }, h( Callout, { kind: "warn", icon: "alert-triangle", title: "Missing information", }, "The drawing is missing or inconsistent on values the analysis needs. Please upload a corrected drawing.", ), h( "div", { className: "btn-row end", style: { marginTop: 20 } }, h( Btn, { kind: "primary", icon: "upload", onClick: go.toUpload }, "Re-upload corrected drawing", ), ), ) : refunded ? h( "div", { style: { marginTop: 18 } }, h( Callout, { kind: "info", icon: "rotate-ccw", title: "Refund issued" }, project.outcome || "This project was refunded and cannot continue until it is resubmitted.", ), h("div", null), ) : failing ? h( "div", { style: { marginTop: 18 } }, h( Callout, { kind: "danger", icon: "alert-octagon", title: "Does not pass as existing — remediation required", }, describeFailure( runs, combinations.length > 0 ? combinations : runs.flatMap((r) => r.combinations || []), ), ), h( "div", { className: "btn-row end", style: { marginTop: 20 } }, h( Btn, { kind: "primary", iconRight: "wrench", onClick: go.next, }, "Review sistering remedy", ), ), ) : h( "div", null, h( "div", { style: { marginTop: 18 } }, h( Callout, { kind: "success", icon: "shield-check", title: "All checks clear · seismic exempt", }, "Every check across ", project.combosCount, " combination", project.combosCount > 1 ? "s" : "", " is at or below 1.00. Assembling the deliverable package.", ), ), h( Card, { tight: true, className: "scanline", style: { marginTop: 16 }, }, h( "div", { className: "proc-log" }, h( "div", { className: "ln active" }, h("span", { className: "spin" }), "Opening deliverables...", ), ), ), ), ); } function EngineerResultGroup({ project, run, tweaks }) { const t = run.engineerType || run.engineer_type || ""; const units = run.calcUnits || run.calc_units || []; const combos = run.combinations || []; const raw = run.rawJSON || run.raw_json || ""; return h( "div", { style: { marginBottom: 18 } }, h( "div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, margin: "6px 0 10px", }, }, h( "div", null, h( "div", { className: "eyebrow", style: { marginBottom: 3 } }, engineerTypeLabel(t), ), h( "div", { style: { fontWeight: 650, fontSize: 15 } }, run.outcome || project.outcome, ), ), h( "span", { style: { display: "flex", gap: 8, alignItems: "center" } }, h( Badge, { kind: "neutral" }, units.length + " unit" + (units.length === 1 ? "" : "s"), ), h(Status, { s: (run.outcome || project.outcome || "").indexOf("PASS") > -1 ? "PASS" : "FAIL", }), ), ), units.length > 0 && h( "div", { className: "combo-list", style: { marginBottom: 12 } }, units.map((u, i) => h( "div", { key: i, className: "combo" }, h( "div", { className: "ci" }, h(Icon, { name: "calculator", size: 15 }), ), h( "div", null, h( "div", { className: "ctitle" }, "Unit " + (u.unitID || u.unit_id || i + 1), ), h( "div", { className: "csub" }, u.description || [u.structure, u.mount].filter(Boolean).join(" · ") || "Calculation unit", ), ), ), ), ), combos.map((c, i) => h(ComboResult, { key: i, project, combo: c, tweaks }), ), raw && h(RawClaudeJSON, { text: raw, title: "Raw Claude JSON · " + engineerTypeLabel(t), }), ); } function ComboResult({ project, combo, tweaks }) { const style = tweaks.dcrStyle || "bars"; const checks = combo.checks || combo.dcr_checks || []; const worst = checks.length ? Math.max.apply( null, checks.map((c) => c.dcr || 0), ) : 0; const pass = worst <= 1.0; const seismicLabel = combo.seismic && combo.seismic.ratio != null ? "Seismic " + (combo.seismic.ratio * 100).toFixed(1) + "% · EXEMPT" : "Seismic " + ((combo.seismic && (combo.seismic.category || combo.seismic.check)) || "checked"); return h( Card, { style: { marginBottom: 14 }, eyebrow: "Combination " + combo.key + " · " + combo.roof, title: combo.framing, right: h( "span", { style: { display: "flex", gap: 10, alignItems: "center" } }, h(Badge, { kind: "exempt", icon: "info" }, seismicLabel), pass ? h(Status, { s: "PASS", label: "CLEAR PASS" }) : h(Status, { s: "FAIL" }), ), }, style === "bars" && h(DCRBars, { combo }), style === "table" && h(DCRTable, { combo }), style === "gauge" && h(DCRGauges, { combo }), ); } function DCRBars({ combo }) { const checks = combo.checks || combo.dcr_checks || []; return h( "table", { className: "dtable" }, h( "thead", null, h( "tr", null, h("th", null, "Check"), h("th", null, "Combination"), h("th", null, "Demand-to-capacity"), h("th", { className: "r" }, "Status"), ), ), h( "tbody", null, checks.map((c, i) => h( "tr", { key: i }, h("td", { className: "name" }, c.name.split(" — ")[0]), h("td", null, h("span", { className: "srctag" }, c.combo)), h("td", { style: { width: 200 } }, h(DCRBar, { dcr: c.dcr })), h("td", { className: "r" }, h(Status, { s: c.status })), ), ), ), ); } function DCRTable({ combo }) { const checks = combo.checks || combo.dcr_checks || []; return h( "table", { className: "dtable" }, h( "thead", null, h( "tr", null, h("th", null, "Check"), h("th", { className: "r" }, "Demand"), h("th", { className: "r" }, "Capacity"), h("th", { className: "r" }, "DCR"), h("th", { className: "r" }, "Status"), ), ), h( "tbody", null, checks.map((c, i) => h( "tr", { key: i }, h("td", { className: "name" }, c.name.split(" — ")[0]), h( "td", { className: "val r" }, c.demand + " ", h("span", { className: "unit" }, c.unit), ), h( "td", { className: "val r" }, c.capacity + " ", h("span", { className: "unit" }, c.unit), ), h( "td", { className: "r", style: { fontFamily: "var(--font-mono)", fontWeight: 700, color: c.dcr > 1 ? "var(--signal-danger)" : "var(--fg)", }, }, c.dcr.toFixed(3), ), h("td", { className: "r" }, h(Status, { s: c.status })), ), ), ), ); } function DCRGauges({ combo }) { const checks = combo.checks || combo.dcr_checks || []; return h( "div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(140px,1fr))", gap: 14, }, }, checks.map((c, i) => h(Gauge, { key: i, check: c })), ); } function Gauge({ check }) { const dcr = check.dcr, over = dcr > 1; const frac = Math.min(dcr, 1.25) / 1.25; const R = 34, C = Math.PI * R; // semicircle const [p, setP] = useState(0); useEffect(() => { const t = setTimeout(() => setP(frac), 80); return () => clearTimeout(t); }, []); const color = over ? "var(--signal-danger)" : dcr > 0.85 ? "var(--signal-warning)" : "var(--signal-success)"; return h( "div", { style: { textAlign: "center", padding: "8px 4px" } }, h( "svg", { width: 96, height: 58, viewBox: "0 0 96 58" }, h("path", { d: "M10 52 A38 38 0 0 1 86 52", fill: "none", stroke: "var(--bg-sunken)", strokeWidth: 8, strokeLinecap: "round", }), h("path", { d: "M10 52 A38 38 0 0 1 86 52", fill: "none", stroke: color, strokeWidth: 8, strokeLinecap: "round", strokeDasharray: C, strokeDashoffset: C * (1 - p * 0.8), style: { transition: "stroke-dashoffset 0.7s var(--ease-out)" }, }), // gate tick at 1.0 (=0.8 of arc) h("line", { x1: 86, y1: 52, x2: 86, y2: 52, stroke: "var(--fg-faint)", }), ), h( "div", { style: { fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 16, marginTop: -8, color: over ? "var(--signal-danger)" : "var(--fg)", }, }, dcr.toFixed(3), ), h( "div", { style: { fontSize: 11, color: "var(--fg-subtle)", marginTop: 3, lineHeight: 1.3, }, }, check.name.split(" — ")[0], ), ); } // ---------------- Sistering sub-flow ---------------- function ScreenSistering({ project, tweaks, go }) { const style = tweaks.sisteringStyle || "modal"; const [decision, setDecision] = useState(null); // null | card | processing | confirmed | declined const [payInfo, setPayInfo] = useState(null); const [err, setErr] = useState(""); const stripeRef = useRef(null); const cardDivRef = useRef(null); const combo = (project.combinations && project.combinations[0]) || { checks: [], sistered: { checks: [], framing: "Sistering detail pending" }, framing: "Framing", roof: "Roof", }; const sistered = combo.sistered || combo.sistering || { checks: [], framing: "Sistering detail pending" }; async function startConfirm() { setErr(""); try { const data = await window.API.projects.sisteringConfirm(project.id); if (data.project && go.updateProject) go.updateProject(data.project); if (!window.Stripe) throw new Error("Stripe.js failed to load"); const stripeJs = window.Stripe(data.publishable_key); stripeRef.current = { stripe: stripeJs, elements: stripeJs.elements(), clientSecret: data.client_secret, intentId: data.payment_intent_id, cardElement: null, }; setPayInfo(data); setDecision("card"); } catch (e) { setErr(e.message || "Could not start sistering payment"); } } async function confirmSisteringPay() { if (!stripeRef.current || !stripeRef.current.cardElement) return; setDecision("processing"); setErr(""); const { stripe, cardElement, clientSecret } = stripeRef.current; const { error, paymentIntent } = await stripe.confirmCardPayment( clientSecret, { payment_method: { card: cardElement } }, ); if (error) { setErr(error.message); setDecision("card"); return; } try { const data = await window.API.projects.sisteringPay( project.id, paymentIntent.id, ); if (data.project && go.updateProject) go.updateProject(data.project); setDecision("confirmed"); } catch (e) { setErr(e.message || "Payment confirmed but server update failed"); setDecision("card"); } } async function decline() { setErr(""); try { const p = await window.API.projects.sisteringDecline(project.id); if (go.updateProject) go.updateProject(p); setDecision("declined"); } catch (e) { setErr(e.message || "Refund failed"); } } useEffect(() => { if ( decision !== "card" || !cardDivRef.current || !stripeRef.current || stripeRef.current.cardElement ) return; const card = stripeRef.current.elements.create("card"); card.mount(cardDivRef.current); stripeRef.current.cardElement = card; return () => { try { card.unmount(); } catch (e) {} }; }, [decision]); if (decision === "declined") return h(RejectedRefund, { project, go }); return h( "div", { className: "fade-in" }, h( "div", { className: "eyebrow screen-eyebrow" }, "Exception · Sistering sub-flow", ), h("h1", { className: "screen-title" }, "Rafter sistering required"), h( "p", { className: "screen-sub" }, "Roughly 2 in 100 rafter projects don\u2019t clear-pass. The remedy is sistering — a matching member alongside each existing rafter. It carries a paid add-on because it modifies an existing, third-party-built structure.", ), h( "div", { className: "grid-2", style: { marginBottom: 16 } }, h( Card, { tight: true, eyebrow: "As existing — fails" }, (combo.checks || []) .filter((c) => c.dcr > 1) .map((c, i) => h( "div", { key: i, style: { display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid var(--border)", }, }, h("span", { style: { fontSize: 13 } }, c.name.split(" — ")[0]), h( "span", { style: { fontFamily: "var(--font-mono)", fontWeight: 700, color: "var(--signal-danger)", }, }, c.dcr.toFixed(3), ), ), ), h( "div", { style: { marginTop: 10 } }, h(Badge, { kind: "fail", icon: "x" }, "Does not pass as existing"), ), ), h( Card, { tight: true, eyebrow: "After sistering — passes" }, (sistered.checks || []) .filter( (c) => ["Bending — D + S", "Deflection — D + S"].indexOf(c.name) > -1, ) .map((c, i) => h( "div", { key: i, style: { display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid var(--border)", }, }, h("span", { style: { fontSize: 13 } }, c.name.split(" — ")[0]), h( "span", { style: { fontFamily: "var(--font-mono)", fontWeight: 700, color: "var(--signal-success)", }, }, c.dcr.toFixed(3), ), ), ), h( "div", { style: { marginTop: 10 } }, h(Badge, { kind: "pass", icon: "check" }, sistered.framing), ), ), ), style !== "split" && h( Card, { style: { marginBottom: 16 } }, h( "div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 12, }, }, h( "div", null, h( "div", { style: { fontWeight: 650, fontSize: 15 } }, "Sistering add-on", ), h( "div", { style: { fontSize: 13, color: "var(--fg-muted)", marginTop: 3, maxWidth: "46ch", }, }, "Produces: stamped drawings · stamped sistering detail · combined calc (sistering + PV) · PE letter (passes once sistered).", ), ), h(Money, { amount: project.sisterFee, size: "big" }), ), ), err && h( "div", { style: { color: "var(--signal-danger)", fontSize: 13, marginBottom: 12, }, }, err, ), decision === "card" && h( Card, { style: { marginBottom: 16 }, eyebrow: "Sistering payment" }, h( "div", { className: "field" }, h("label", null, "Card"), h("div", { ref: cardDivRef, style: { padding: "11px 14px", border: "1.5px solid var(--border)", borderRadius: "var(--radius-sm)", background: "var(--bg-base)", minHeight: 44, }, }), ), h( "div", { className: "btn-row end", style: { marginTop: 14 } }, h( Btn, { kind: "primary", icon: "credit-card", onClick: confirmSisteringPay, }, "Pay " + (payInfo ? fmt(payInfo.amount / 100) : fmt(project.sisterFee)), ), ), ), decision === "processing" && h( Card, { style: { marginBottom: 16 } }, h( "div", { className: "proc-log" }, h( "div", { className: "ln active" }, h("span", { className: "spin" }), "Confirming sistering payment...", ), ), ), decision === "confirmed" ? h(SisterConfirmed, { project, go }) : decision == null && style === "modal" ? h(SisterModalTrigger, { project, onConfirm: startConfirm, onDecline: decline, }) : decision == null && h(SisterInline, { project, onConfirm: startConfirm, onDecline: decline, }), ); } function SisterInline({ project, onConfirm, onDecline }) { return h( "div", null, h( Callout, { kind: "warn", title: "Explicit confirmation required" }, "The project is on hold until you answer. Confirm you will perform the sistering and we produce the remediated package. Decline and the project is rejected and refunded in full.", ), h( "div", { className: "btn-row end", style: { marginTop: 20 } }, h(Btn, { kind: "ghost", onClick: onDecline }, "Decline & refund"), h( Btn, { kind: "primary", icon: "check", onClick: onConfirm }, "Confirm sistering (+" + fmt(project.sisterFee) + ")", ), ), ); } function SisterModalTrigger({ project, onConfirm, onDecline }) { const [open, setOpen] = useState(false); return h( "div", null, h( "div", { className: "btn-row end" }, h( Btn, { kind: "secondary", onClick: () => setOpen(true) }, "Respond to sistering", ), ), open && h( "div", { className: "overlay", onClick: (e) => { if (e.target.className === "overlay") setOpen(false); }, }, h( "div", { className: "modal" }, h( "div", { className: "modal-icon", style: { background: "color-mix(in srgb,var(--signal-warning) 16%,transparent)", color: "#a06f0c", }, }, h(Icon, { name: "wrench", size: 24 }), ), h("h2", null, "Confirm rafter sistering"), h( "p", null, "This project cannot be issued as-is. It requires rafter sistering (", h("b", null, fmt(project.sisterFee)), "). Confirm you will perform the sistering and the remediated package will be produced. If you decline, the project is rejected and your payment refunded in full.", ), h( "div", { className: "btn-row between" }, h( Btn, { kind: "ghost", onClick: () => { setOpen(false); onDecline(); }, }, "Decline & refund", ), h( Btn, { kind: "primary", icon: "check", onClick: () => { setOpen(false); onConfirm(); }, }, "Confirm (+" + fmt(project.sisterFee) + ")", ), ), ), ), ); } function SisterConfirmed({ project, go }) { return h( "div", { className: "fade-in" }, h( Callout, { kind: "success", icon: "check-circle", title: "Sistering confirmed — remediated set queued", }, "Producing stamped drawings, the rafter sistering detail, the combined calculation, and a PE letter certifying adequacy once the sistering is performed.", ), h( "div", { className: "btn-row end", style: { marginTop: 20 } }, h( Btn, { kind: "primary", iconRight: "arrow-right", onClick: go.toDeliver }, "Assemble remediated deliverables", ), ), ); } function RejectedRefund({ project, go }) { return h( "div", { className: "fade-in" }, h("div", { className: "eyebrow screen-eyebrow" }, "Exception · Declined"), h( "h1", { className: "screen-title" }, "Project rejected — refunded in full", ), h( "p", { className: "screen-sub" }, "You declined the sistering remedy, so the project cannot be issued. Your payment has been refunded in full automatically.", ), h( Callout, { kind: "info", icon: "rotate-ccw", title: "Refund issued · " + fmt(project.fee), }, "Money-state transition logged: paid → refunded. You can re-submit with a revised (lighter or differently-framed) design at any time.", ), h("div", null), ); } // ---------------- Deliver ---------------- function ScreenDeliver({ project, openDocs, go }) { const sistered = project.status === "sistering"; const files = (sistered ? project.sisterFiles : project.files) || []; const totalFee = (project.fee || 0) + (sistered ? project.sisterFee : 0); async function download(f) { const id = f.fileID || f.file_id || f.id; if (!id) return; try { const token = window.API.auth.getToken(); const res = await fetch("/api/v1/files/" + id + "/download", { headers: token ? { Authorization: "Bearer " + token } : {}, }); if (!res.ok) throw new Error("Download failed: " + res.status); const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = f.name || "document.pdf"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(blobUrl), 60000); } catch (e) { console.error("Download error", e); } } function downloadAll() { files.forEach((f, i) => setTimeout(() => download(f), i * 300)); } return h( "div", { className: "fade-in" }, h("div", { className: "eyebrow screen-eyebrow" }, "Deliver"), h( "div", { style: { display: "flex", alignItems: "center", gap: 12, marginBottom: 8, }, }, h( "div", { style: { width: 40, height: 40, borderRadius: "var(--radius-md)", background: "color-mix(in srgb,var(--signal-success) 14%,transparent)", color: "var(--signal-success)", display: "grid", placeItems: "center", }, }, h(Icon, { name: "check-circle", size: 24 }), ), h( "h1", { className: "screen-title", style: { margin: 0 } }, "Package ready", ), ), h( "p", { className: "screen-sub" }, files.length, " PE-ready PDFs", sistered ? " (remediated set)" : "", " generated with immediate turnaround. The PE seal is applied downstream by a ", project.state, "-licensed engineer before the files are permit-valid.", ), h(SkippedEngineerWarning, { project }), h( Card, { eyebrow: "PDF deliverables", title: project.outcome, right: h( "div", { style: { display: "flex", gap: 8 } }, h( Btn, { kind: "primary", size: "sm", icon: "download", onClick: downloadAll, }, "Download all", ), h( Btn, { kind: "secondary", size: "sm", icon: "eye", onClick: () => openDocs(project, files), }, "View online", ), ), }, files.map((f, i) => h( "div", { key: i, style: { display: "flex", alignItems: "center", gap: 12, padding: "11px 0", borderBottom: i < files.length - 1 ? "1px solid var(--border)" : "none", }, }, h( "div", { style: { width: 30, height: 30, borderRadius: 6, background: "var(--bg-sunken)", display: "grid", placeItems: "center", color: "var(--fg-muted)", }, }, h(Icon, { name: kindIcon(f.kind), size: 15 }), ), h( "div", { style: { flex: 1 } }, h("div", { style: { fontWeight: 600, fontSize: 13.5 } }, f.name), h( "div", { style: { fontSize: 12, color: "var(--fg-subtle)", fontFamily: "var(--font-mono)", }, }, f.meta, ), ), h("span", { className: "srctag" }, i + 1 + " / " + files.length), h( Btn, { kind: "ghost", size: "sm", icon: "download", onClick: () => download(f), }, "Download", ), ), ), ), h( "div", { className: "btn-row between", style: { marginTop: 24 } }, h( "div", { style: { display: "flex", gap: 8 } }, h( Btn, { kind: "secondary", icon: "eye", onClick: () => openDocs(project, files), }, "View documents", ), h( Btn, { kind: "primary", icon: "download", onClick: downloadAll }, "Download all", ), ), ), ); } function kindIcon(k) { return ( { drawing: "sun", drawings: "sun", stamped_drawing: "sun", calc: "calculator", pe_letter: "file-text", sistering_detail: "wrench", }[k] || "file" ); } function SkippedEngineerWarning({ project }) { const skipped = skippedEngineerTypes(project); if (!skipped.length) return null; return h( "div", { style: { marginBottom: 14 } }, h( Callout, { kind: "warn", icon: "alert-triangle", title: "Engineering route unavailable", }, skipped.map((t) => h( "div", { key: t }, "The " + engineerTypeLabel(t) + " route is currently unavailable and was excluded from this run.", ), ), ), ); } function RawClaudeJSON({ text, title, defaultOpen }) { if (!text) return null; const [open, setOpen] = React.useState(defaultOpen !== false); const [copied, setCopied] = React.useState(false); const pretty = formatRawJSON(text); const copy = () => { navigator.clipboard .writeText(pretty) .then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800); }) .catch(() => {}); }; const preStyle = { margin: 0, maxHeight: 420, overflow: "auto", border: "1px solid var(--border)", borderRadius: 6, background: "var(--bg-sunken)", color: "var(--fg)", padding: 12, fontSize: 11, lineHeight: 1.45, whiteSpace: "pre-wrap", wordBreak: "break-word", }; return h( "div", { style: { margin: "10px 0 14px", border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden", }, }, h( "div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 12px", background: "var(--bg-sunken)", borderBottom: open ? "1px solid var(--border)" : "none", cursor: "pointer", }, onClick: () => setOpen((o) => !o), }, h( "span", { style: { fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--fg-subtle)", }, }, "{ }", ), h( "span", { style: { flex: 1, fontSize: 12, fontWeight: 650, color: "var(--fg-muted)", }, }, title || "Raw Claude JSON", ), h( "button", { style: { fontSize: 11, color: "var(--fg-subtle)", background: "none", border: "1px solid var(--border)", borderRadius: 4, cursor: "pointer", padding: "2px 8px", fontFamily: "inherit", }, onClick: (e) => { e.stopPropagation(); copy(); }, }, copied ? "✓ Copied" : "Copy", ), h( "span", { style: { fontSize: 11, color: "var(--fg-subtle)" } }, open ? "▲" : "▼", ), ), open && h("pre", { style: preStyle }, pretty), ); } function formatRawJSON(text) { try { return JSON.stringify(JSON.parse(text), null, 2); } catch (e) { return String(text || ""); } } function engineerTypes(project) { const seen = {}; (project.calcUnits || project.calc_units || []).forEach((u) => { const t = String(u.engineerType || u.engineer_type || "") .trim() .toUpperCase(); if (t) seen[t] = true; }); return Object.keys(seen).sort(); } function skippedEngineerTypes(project) { const seen = {}; ( (project && (project.skippedEngineerTypes || project.skipped_engineer_types)) || [] ).forEach((t) => { t = String(t || "") .trim() .toUpperCase(); if (t) seen[t] = true; }); return Object.keys(seen).sort(); } function fmt(n) { return n == null ? "—" : "$" + n.toFixed(2); } Object.assign(window, { ScreenValidate, ScreenEngineering, ScreenResult, ScreenSistering, ScreenDeliver, }); })();