/* solarengineering.online — dashboard (multi-project view) + new-project launcher */ (function () { const { Icon, Btn, Badge, ProjStatus, Card, ProjectNameEditor } = window; const h = React.createElement; const { useState } = React; function Dashboard({ projects, loading, onOpenProject, onResume, onNew, empty, onRefresh, }) { const items = projects || []; if (!loading && items.length === 0) return h(EmptyDashboard, { onNew }); const wasRefunded = (p) => /refunded/i.test(p.outcome || ""); const inflightStatuses = [ "paid", "validating", "in_progress", "sistering", "sistering_pending", ]; const delivered = items.filter((p) => p.status === "delivered").length; // missing_info is dual-purpose: pre-payment it means "customer must re-upload" // (needs action); post-payment it can also mean "found missing info too late, // fully refunded" (terminal, outcome contains "refunded") — only the former // still needs action. const inflight = items.filter( (p) => inflightStatuses.includes(p.status) || (p.status === "missing_info" && !wasRefunded(p)), ).length; // Billed less refunds: exclude anything never actually kept (rejected, // refunded, or a refunded missing_info). A sistering fee is only actually // billed once its status reaches 'delivered' with a confirmed payment // (outcome carries "SISTERING CONFIRMED") — while status is still // 'sistering'/'sistering_pending' it's just a quote awaiting the // customer's decision, not yet charged. const revenue = items.reduce((a, p) => { if (!p.fee) return a; if (p.status === "rejected" || p.status === "refunded") return a; if (p.status === "missing_info" && wasRefunded(p)) return a; let amt = p.fee; if ( p.status === "delivered" && /SISTERING CONFIRMED/i.test(p.outcome || "") ) amt += p.sisterFee || 0; return a + amt; }, 0); function rowClick(p) { if (p.status === "delivered" || p.status === "sistering") onOpenProject(p); else onResume(p); } return h( "div", { className: "dash fade-in" }, h( "div", { className: "dash-head" }, h("div", null, h("h1", null, "Projects")), h( "div", { style: { display: "flex", gap: 10 } }, h( Btn, { kind: "primary", icon: "plus", onClick: onNew }, "New project", ), ), ), h( "div", { className: "stat-grid" }, h(Stat, { v: delivered, l: "Delivered", sub: "all time" }), h(Stat, { v: inflight, l: "In flight", sub: "" }), h(Stat, { v: "$" + revenue.toFixed(0), l: "Billed", sub: "", }), h(Stat, { v: "< 10 min", l: "Turnaround", sub: "", }), ), loading ? h( "div", { style: { padding: 40, textAlign: "center", color: "var(--fg-muted)", }, }, "Loading…", ) : h( "div", { className: "proj-table" }, h( "div", { className: "proj-row head" }, h("span", null, "Project"), h("span", { className: "col-hide" }, "Address"), h("span", { className: "col-hide" }, "Outcome"), h("span", { className: "col-hide" }, "Fee"), h("span", null, "Status"), ), items.map((p, i) => h( "div", { key: p.id || i, className: "proj-row", onClick: () => rowClick(p), }, h( "div", null, h( "div", { className: "proj-name" }, h(ProjectNameEditor, { projectId: p.id, name: p.name, onSaved: () => onRefresh && onRefresh(), }), ), ), h("div", { className: "proj-addr col-hide" }, p.city), h( "div", { className: "col-hide", style: { fontSize: 12.5, color: "var(--fg-muted)" }, }, p.outcome || "—", ), h( "div", { className: "proj-fee col-hide" }, p.fee != null ? "$" + Number(p.fee).toFixed(2) : "—", ), h("div", null, h(ProjStatus, { s: p.status })), ), ), ), ); } // ─── Empty state ───────────────────────────────────────────────────────────── function EmptyDashboard({ onNew }) { const [drag, setDrag] = useState(false); const steps = [ { icon: "upload", n: "01", t: "Upload one drawing", d: "Just share a clear solar drawing PDF, and we'll take it from there—reading every sheet and tracking the source of each value with care.", }, { icon: "layers", n: "02", t: "Quoted & validated", d: "Classified and quoted from the project's actual scope, then checked value-by-value.", }, { icon: "file-check", n: "03", t: "PE-ready package", d: "Structural PE calculations and PE letters with immediate turnaround.", }, ]; return h( "div", { className: "dash empty-dash fade-in" }, h( "div", { className: "empty-hero" }, h( "div", { className: "eyebrow", style: { marginBottom: 14 } }, "Welcome to SolarEngineering · structural reports for PV solar and ESS projects", ), h("h1", null, "PE-stamped, within minutes."), h( "p", null, "Upload the drawing you already have. We classify it, price it, validate it, and return PE calculations and PE letters along with stamped plans.", ), ), h( "div", { className: "empty-start" + (drag ? " drag" : ""), onClick: onNew, onDragOver: (e) => { e.preventDefault(); setDrag(true); }, onDragLeave: () => setDrag(false), onDrop: (e) => { e.preventDefault(); setDrag(false); onNew(); }, }, h( "div", { className: "es-mark" }, h(Icon, { name: "upload", size: 30, stroke: 1.25 }), ), h( "div", { className: "es-body" }, h("div", { className: "es-title" }, "Start your first project"), h( "div", { className: "es-sub" }, "Drop a solar drawing PDF here, or click to begin. Multi-sheet welcome.", ), ), h( Btn, { kind: "primary", icon: "arrow-right", onClick: (e) => { e.stopPropagation(); onNew(); }, }, "Begin", ), ), h( "div", { className: "empty-steps" }, steps.map((s, i) => h( "div", { key: i, className: "estep" }, h( "div", { className: "estep-top" }, h( "div", { className: "estep-icon" }, h(Icon, { name: s.icon, size: 18 }), ), h("div", { className: "estep-n" }, s.n), ), h("div", { className: "estep-t" }, s.t), h("div", { className: "estep-d" }, s.d), ), ), ), h( "div", { className: "empty-strip" }, h(StripItem, { icon: "map-pin", k: "30 states", v: "eligibility by project address", }), h(StripItem, { icon: "dollar-sign", k: "Project-specific quote", v: "priced from the classified drawing scope", }), h(StripItem, { icon: "shield-check", k: "PASS", v: "every calculation value, unrounded", }), h(StripItem, { icon: "zap", k: "< 10 min", v: "immediate turnaround" }), ), h( "div", { className: "empty-foot" }, h(Icon, { name: "info", size: 14 }), h( "span", null, "Sawn rafters and manufactured trusses serve the same purpose but have vastly different span limitations.", ), ), ); } function StripItem({ icon, k, v }) { return h( "div", { className: "strip-item" }, h(Icon, { name: icon, size: 16 }), h( "div", null, h("div", { className: "si-k" }, k), h("div", { className: "si-v" }, v), ), ); } function Stat({ v, l, sub }) { return h( "div", { className: "stat" }, h("div", { className: "sv" }, v), h("div", { className: "sl" }, h("b", null, l), " · " + sub), ); } Object.assign(window, { Dashboard }); })();