/* solarengineering.online — production App */ (function () { const { Icon, Dashboard, DocViewer, ProjectNameEditor } = window; const h = React.createElement; const { useState, useEffect, useCallback, useRef } = React; const SCREENS = { upload: window.ScreenUpload, classify: window.ScreenClassify, pay: window.ScreenPay, validate: window.ScreenValidate, engineering: window.ScreenEngineering, result: window.ScreenResult, sistering: window.ScreenSistering, deliver: window.ScreenDeliver, }; // 'quote' step removed — merged into 'pay' (single combined screen) const VISIBLE_STEPS = ["upload", "pay", "deliver"]; const VISIBLE_LABELS = { upload: ["Upload", null], pay: ["Payment", null], deliver: ["Deliver", ""], }; function buildSeq(project) { const s = ["upload", "pay", "validate", "engineering", "result"]; if (project && project.status === "sistering") s.push("sistering"); s.push("deliver"); return s; } function visibleStepFor(stepKey) { if (stepKey === "pay" || stepKey === "quote") return "pay"; if (stepKey === "upload" || stepKey === "classify") return "upload"; return "deliver"; } function targetForVisibleStep(stepKey) { return { upload: "upload", pay: "pay", deliver: "deliver", }[stepKey]; } // ─── Auth hook ──────────────────────────────────────────────────────────────── function useAuth() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { if (window.API.auth.isLoggedIn()) { window.API.auth .me() .then((d) => setUser(d.user || d)) .catch(() => { window.API.auth.setToken(null); }) .finally(() => setLoading(false)); } else { setLoading(false); } }, []); const login = async (email, password) => { const d = await window.API.auth.login(email, password); setUser(d.user || d); return d; }; const logout = async () => { await window.API.auth.logout().catch(() => {}); setUser(null); }; return { user, loading, login, logout, setUser }; } // ─── Projects hook ──────────────────────────────────────────────────────────── function useProjects(user) { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const refresh = useCallback(() => { if (!user) return; setLoading(true); window.API.projects .list() .then((d) => setProjects(Array.isArray(d) ? d : d.projects || [])) .catch(() => {}) .finally(() => setLoading(false)); }, [user]); useEffect(() => { refresh(); }, [refresh]); return { projects, loading, refresh }; } // ─── Root App ───────────────────────────────────────────────────────────────── function App() { const { user, loading: authLoading, login, logout, setUser } = useAuth(); const [view, setView] = useState("onboard"); // onboard | dashboard | flow | profile // True when the URL has payment-return params that need async resolution — // keeps the loading spinner visible so onboarding never flashes before the // correct flow step is set. const [resolving, setResolving] = useState(() => { const p = new URLSearchParams(window.location.search); return !!(p.get("project") && p.get("payment")); }); const [project, setProject] = useState(null); const [idx, setIdx] = useState(0); const [dark, setDark] = useState( () => localStorage.getItem("se_dark") === "1", ); const [docState, setDocState] = useState(null); const [creatingProject, setCreatingProject] = useState(false); const { projects, loading: projLoading, refresh } = useProjects(user); useEffect(() => { document.documentElement.classList.toggle("dark", dark); localStorage.setItem("se_dark", dark ? "1" : "0"); }, [dark]); // Once auth resolves, route to the right view. // If returning from Stripe Checkout, handle payment verification first. useEffect(() => { if (authLoading) return; const params = new URLSearchParams(window.location.search); const projectId = params.get("project"); const payment = params.get("payment"); const sessionId = params.get("session_id"); if (user && projectId && payment) { window.history.replaceState({}, "", "/"); window.API.projects .get(projectId) .then((p) => { if (payment === "success" && sessionId) { return window.API.projects .paymentReturn(projectId, sessionId) .then((d) => { const updated = d && d.project ? d.project : p; setProject(updated); const s = buildSeq(updated); setIdx(s.indexOf("validate")); setView("flow"); setResolving(false); }) .catch(() => { // Verification failed — land on pay step so user can retry setProject(p); const s = buildSeq(p); setIdx(s.indexOf("pay")); setView("flow"); setResolving(false); }); } // payment=cancel — return to pay step setProject(p); const s = buildSeq(p); setIdx(s.indexOf("pay")); setView("flow"); setResolving(false); }) .catch(() => { setView("dashboard"); setResolving(false); }); return; } if (user) setView("dashboard"); else { setView("onboard"); setResolving(false); } }, [authLoading, user]); const seq = project ? buildSeq(project) : []; const stepKey = seq[idx]; const openDocs = useCallback( (p, files) => setDocState({ project: p, files }), [], ); // ─── Onboarding complete → dashboard ────────────────────────────────────────── const finishOnboarding = async () => { try { const me = await window.API.auth.me(); setUser(me.user || me); setView("dashboard"); } catch (err) { console.error("onboarding completion failed", err); } }; // ─── Resume a project from dashboard ───────────────────────────────────────── const resume = (p) => { const s = buildSeq(p); let j = 0; if (p.status === "uploaded" || p.status === "pre_screened") j = s.indexOf("upload"); else if (p.status === "classified" || p.status === "quoted") j = s.indexOf("pay"); else if (p.status === "payment_pending") j = s.indexOf("pay"); else if (p.status === "paid" || p.status === "validating") j = s.indexOf("validate"); else if (p.status === "in_progress") j = s.indexOf("engineering"); else if (p.status === "missing_info") j = s.indexOf("upload"); else if (p.status === "sistering" || p.status === "sistering_pending") j = s.indexOf("result"); else if (p.status === "rejected") j = s.indexOf("upload"); else if (p.status === "delivered") j = s.indexOf("deliver"); setProject(p); setIdx(Math.max(0, j)); setView("flow"); }; // ─── Start a brand-new project ──────────────────────────────────────────────── const startNew = async () => { if (creatingProject) return; setCreatingProject(true); try { const p = await window.API.projects.create("", "", "", ""); setProject(p); setIdx(0); setView("flow"); refresh(); } catch (err) { console.error("create project failed", err); } finally { setCreatingProject(false); } }; // go object passed to all screen components const go = { next: () => setIdx((i) => Math.min(i + 1, seq.length - 1)), prev: () => setIdx((i) => Math.max(i - 1, 0)), dashboard: () => { setView("dashboard"); setProject(null); setIdx(0); refresh(); }, toDeliver: () => setIdx(seq.indexOf("deliver")), toQuote: () => setIdx(seq.indexOf("pay")), toUpload: () => setIdx(seq.indexOf("upload")), updateProject: (p) => setProject(p), }; const Screen = stepKey ? SCREENS[stepKey] : null; if (authLoading || resolving) { return h( "div", { style: { display: "grid", placeItems: "center", height: "100vh" } }, h(Icon, { name: "sun", size: 32, style: { color: "var(--accent)", animation: "spin 1.5s linear infinite", }, }), ); } if (view === "onboard") { return h( React.Fragment, null, h(window.Onboarding, { onComplete: finishOnboarding, onLogin: login }), ); } const goProfile = () => setView("profile"); const backFromProfile = () => setView(project ? "flow" : "dashboard"); return h( "div", { className: "app" }, h(Topbar, { project: view === "flow" ? project : null, user, dark, onHome: go.dashboard, onToggleDark: () => setDark((d) => !d), onLogout: logout, onProfile: goProfile, onRenameProject: (n) => setProject((p) => (p ? { ...p, name: n } : p)), }), view === "profile" ? h( "div", { className: "app-body no-rail" }, h( "div", { className: "canvas" }, h(window.ProfileSettings, { user, onUpdate: (updated) => setUser(updated), onBack: backFromProfile, }), ), ) : view === "dashboard" ? h( "div", { className: "app-body no-rail" }, h( "div", { className: "canvas" }, h(Dashboard, { projects, loading: projLoading, empty: projects.length === 0, onNew: startNew, onResume: resume, onOpenProject: (p) => openDocs( p, p.status === "sistering" ? p.sisterFiles : p.files, ), onRefresh: refresh, }), ), ) : h( "div", { className: "app-body" }, h(Rail, { project, seq, idx, active: visibleStepFor(stepKey), onRenameProject: (n) => setProject((p) => (p ? { ...p, name: n } : p)), }), h( "div", { className: "canvas" }, h( "div", { className: "canvas-inner" + (stepKey === "deliver" || stepKey === "result" ? " wide" : ""), }, Screen && h(Screen, { key: stepKey + (project && project.id), project, tweaks: {}, go, openDocs, }), ), ), ), docState && h(DocViewer, { project: docState.project, files: docState.files, dcrStyle: "bars", onClose: () => setDocState(null), }), ); } // ─── Topbar ─────────────────────────────────────────────────────────────────── function Topbar({ project, user, dark, onHome, onToggleDark, onLogout, onProfile, onRenameProject, }) { const [menuOpen, setMenuOpen] = useState(false); const menuRef = useRef(null); const projectLocation = project ? project.city || project.address || "" : ""; const initials = user ? ( (user.first_name || "").slice(0, 1) + (user.last_name || "").slice(0, 1) ).toUpperCase() || (user.email || "U").slice(0, 2).toUpperCase() : "??"; const fullName = user ? [user.first_name, user.last_name].filter(Boolean).join(" ") : ""; useEffect(() => { const handler = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); return h( "div", { className: "topbar" }, h( "div", { className: "brand", onClick: onHome, style: { cursor: "pointer", lineHeight: 0 }, }, h("img", { src: "/static/solarengineering-logo.svg", alt: "solarengineering.online", style: { height: 30, width: "auto", display: "block" }, }), ), project && h( "div", { className: "topbar-ctx" }, h(Icon, { name: "map-pin", size: 13 }), h(ProjectNameEditor, { projectId: project.id, name: project.name, onSaved: onRenameProject, textStyle: { fontSize: "inherit", fontWeight: "inherit" }, }), projectLocation && h("span", { style: { color: "var(--border-strong)" } }, "·"), projectLocation && h("span", { className: "num" }, projectLocation), ), h("div", { className: "topbar-spacer" }), h( "button", { className: "topbar-btn", onClick: onToggleDark, title: "Toggle theme", }, h(Icon, { name: dark ? "sun" : "moon", size: 16 }), ), h( "button", { className: "topbar-btn", onClick: onHome }, h(Icon, { name: "folder", size: 15 }), "Projects", ), h( "div", { style: { position: "relative" }, ref: menuRef }, h( "div", { className: "avatar", title: user ? user.email : "", style: { cursor: "pointer" }, onClick: () => setMenuOpen((o) => !o), }, initials, ), menuOpen && h( "div", { className: "avatar-menu" }, h( "div", { className: "avatar-menu-header" }, h( "div", { style: { fontWeight: 600, fontSize: 14, color: "var(--fg)" }, }, fullName || user?.email, ), fullName && h( "div", { style: { fontSize: 12, color: "var(--fg-subtle)", marginTop: 2, }, }, user?.email, ), ), h("div", { className: "avatar-menu-sep" }), h( "div", { className: "avatar-menu-item", onClick: () => { onProfile(); setMenuOpen(false); }, }, h(Icon, { name: "user", size: 14 }), "Profile & Settings", ), h("div", { className: "avatar-menu-sep" }), h( "div", { className: "avatar-menu-item avatar-menu-item-danger", onClick: () => { onLogout(); setMenuOpen(false); }, }, h(Icon, { name: "log-out", size: 14 }), "Sign out", ), ), ), ); } // ─── Pipeline Rail ──────────────────────────────────────────────────────────── function Rail({ project, seq, idx, active, onRenameProject }) { if (!project) return null; const activeIndex = Math.max(0, VISIBLE_STEPS.indexOf(active)); return h( "div", { className: "rail" }, h( "div", { className: "rail-head" }, h( "div", { className: "rail-proj" }, h(ProjectNameEditor, { projectId: project.id, name: project.name, onSaved: onRenameProject, textStyle: { fontWeight: "inherit", fontSize: "inherit" }, }), ), ), h( "div", { className: "rail-group-label" }, h("div", { className: "eyebrow" }, "Pipeline"), ), h( "ul", { className: "steps" }, VISIBLE_STEPS.map((k, i) => { let state = i < activeIndex ? "done" : i === activeIndex ? "current" : "pending"; if (i === activeIndex) { if ( (project.truss || project.status === "rejected") && k === "upload" ) state = "blocked current"; if ( project.status === "missing_info" && (k === "upload" || k === "deliver") ) state = "blocked current"; if ( (project.status === "sistering" || project.status === "sistering_pending") && k === "deliver" ) state = "exception current"; } const [label, sub] = VISIBLE_LABELS[k]; const done = i < activeIndex; void seq; void idx; return h( "li", { key: k, className: "step " + state, style: { cursor: "default" }, }, h( "div", { className: "step-node" }, h( "div", { className: "step-dot" }, done ? h(Icon, { name: "check", size: 12 }) : i + 1, ), h("div", { className: "step-line" }), ), h( "div", { className: "step-label" }, label, sub && h("small", null, sub), ), ); }), ), h( "div", { className: "rail-foot" }, h( "div", { style: { fontSize: 11, color: "var(--fg-subtle)", lineHeight: 1.5 }, }, "© 2026 SolarEngineering. All rights reserved.", h("br"), h( "a", { href: "/privacy", target: "_blank", rel: "noopener noreferrer", style: { color: "inherit" }, }, "Privacy Policy", ), " | ", h( "a", { href: "/terms", target: "_blank", rel: "noopener noreferrer", style: { color: "inherit" }, }, "Terms & Conditions", ), ), ), ); } window.SolarEngApp = App; })();