/* ============================================================ ZENITH — App root: routing, roles, language, tweaks ============================================================ */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "lang": "en", "accent": "#1456C7", "zStyle": "gauge", "examStrict": true, "density": "regular" }/*EDITMODE-END*/; const NAV = { student: [ { id: "dashboard", en: "Dashboard", si: "උපකරණ පුවරුව", icon: "squares-four" }, { id: "exams", en: "My exams", si: "මගේ විභාග", icon: "exam" }, { id: "results", en: "Results & Z-score", si: "ප්‍රතිඵල සහ Z", icon: "chart-line-up" }, { id: "billing", en: "Billing", si: "ගෙවීම්", icon: "credit-card" }, ], teacher: [ { id: "queue", en: "Marking queue", si: "ලකුණු පෝලිම", icon: "stack" }, ], admin: [ { id: "overview", en: "Overview", si: "දළ විශ්ලේෂණය", icon: "gauge" }, { id: "streams", en: "Streams", si: "විෂය ධාරා", icon: "graduation-cap" }, { id: "teachers", en: "Teachers", si: "ගුරුවරු", icon: "chalkboard-teacher" }, { id: "backlog", en: "Marking list", si: "ලකුණු ලැයිස්තුව", icon: "list-checks" }, { id: "physical", en: "Physical scripts", si: "භෞතික පත්‍ර", icon: "printer" }, { id: "batch", en: "Batch & Release", si: "කාණ්ඩ හා නිකුතුව", icon: "upload-simple" }, { id: "payments", en: "Payments", si: "ගෙවීම්", icon: "currency-circle-dollar" }, ], }; const HOME = { student: "dashboard", teacher: "queue", admin: "overview" }; const TITLES = { dashboard: { en: "Dashboard", si: "උපකරණ පුවරුව" }, exams: { en: "My exams", si: "මගේ විභාග" }, results: { en: "Results & predicted Z-score", si: "ප්‍රතිඵල සහ පුරෝකථන Z" }, queue: { en: "Marking queue", si: "ලකුණු කිරීමේ පෝලිම" }, overview: { en: "Institute overview", si: "ආයතන දළ විශ්ලේෂණය" }, streams: { en: "Streams & subjects", si: "විෂය ධාරා හා විෂයයන්" }, teachers: { en: "Teachers & evaluators", si: "ගුරුවරු හා ඇගයීම්කරුවන්" }, backlog: { en: "Marking list", si: "ලකුණු කිරීමේ ලැයිස්තුව" }, physical: { en: "Physical script tracking", si: "භෞතික පත්‍ර ලුහුබැඳීම" }, batch: { en: "Batch upload & scheduled release", si: "කාණ්ඩ උඩුගත හා නිකුතුව" }, billing: { en: "Billing & payments", si: "ගෙවීම් හා ගිණුම්" }, payments: { en: "Payments & revenue", si: "ගෙවීම් හා ආදායම" }, }; const ROLE_SUB = { student: { en: "Combined Mathematics · A/L 2025", si: "සංයුක්ත ගණිතය · උ.පෙ 2025" }, teacher: { en: "Mr. R. Wijesinghe · Combined Maths", si: "ආර්. විජේසිංහ මයා · සංයුක්ත ගණිතය" }, admin: { en: "Open Ai Pvt Ltd · Colombo", si: "Open Ai Pvt Ltd · කොළඹ" }, }; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const lang = t.lang; const T = (en, si) => tx(lang, en, si); const [auth, setAuth] = useState(false); // false = logged out const [authMode, setAuthMode] = useState(() => (typeof location !== "undefined" && location.hash === "#admin") ? "admin" : "main"); const [role, setRoleRaw] = useState("student"); const [view, setView] = useState("dashboard"); const [exam, setExam] = useState(null); // {examId, paper, stage, result} const [script, setScript] = useState(null); // teacher: open script const [paidExams, setPaidExams] = useState(() => new Set((window.ZDATA.billing && window.ZDATA.billing.paidExams) || [])); const [authCfg, setAuthCfg] = useState(null); // {mode, googleClientId} — null until loaded const [needsOnboarding, setNeedsOnboarding] = useState(false); const [googleErr, setGoogleErr] = useState(null); const [googleBusy, setGoogleBusy] = useState(false); const [me, setMe] = useState(null); // signed-in user {name, picture} (google mode) const isGoogle = authCfg && authCfg.mode === "google"; // keep #admin path in sync useEffect(() => { const onHash = () => setAuthMode(location.hash === "#admin" ? "admin" : "main"); window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); // load the auth mode; in google mode, resume an existing session on refresh useEffect(() => { (async () => { let cfg = { mode: "dev", googleClientId: null }; try { cfg = await window.ZAPI.authConfig(); } catch (e) { /* backend down → dev fallback */ } if (cfg.mode === "google") { try { const s = await window.ZAPI.authSession(); if (s.authenticated) { setMe({ name: s.name, picture: s.picture }); if (s.needsOnboarding) { setRoleRaw("student"); setNeedsOnboarding(true); } else { await bootAuthed(s.role); } } } catch (e) { /* not signed in */ } } setAuthCfg(cfg); })(); }, []); const goAdmin = () => { location.hash = "#admin"; setAuthMode("admin"); }; const goMain = () => { if (location.hash) location.hash = ""; setAuthMode("main"); }; const goSignup = () => setAuthMode("signup"); // pick the dev identity for the backend, then (re)load that role's data // `who` overrides the default actor (e.g. { systemId: "SX-…" } after a real signup, // or { systemId: "", email } so the backend resolves a self-signed-up student by email) const reloadData = async (r, who) => { window.ZAPI.setActor(r, who); await window.ZDATA_load(r); setPaidExams(new Set((window.ZDATA.billing && window.ZDATA.billing.paidExams) || [])); }; const login = async (r, who) => { await reloadData(r, who); setRoleRaw(r); setView(HOME[r]); setScript(null); setExam(null); setAuth(true); }; // google mode: the session cookie identifies the actor, so just load + enter the app for that role const bootAuthed = async (r) => { await window.ZDATA_load(r); setPaidExams(new Set((window.ZDATA.billing && window.ZDATA.billing.paidExams) || [])); setRoleRaw(r); setView(HOME[r]); setScript(null); setExam(null); setNeedsOnboarding(false); setAuth(true); }; const onGoogleCredential = async (credential) => { setGoogleErr(null); setGoogleBusy(true); try { const res = await window.ZAPI.googleSignIn(credential); setMe({ name: res.name, picture: res.picture }); if (res.needsOnboarding) { setRoleRaw("student"); setNeedsOnboarding(true); } else { await bootAuthed(res.role); } } catch (e) { console.error("google sign-in failed", e); setGoogleErr(T("Sign-in failed — please try again.", "පිවිසීම අසාර්ථකයි — නැවත උත්සාහ කරන්න.")); } finally { setGoogleBusy(false); } }; const signOut = async () => { if (isGoogle) { try { await window.ZAPI.logout(); } catch (e) { /* ignore */ } } setAuth(false); setExam(null); setScript(null); setNeedsOnboarding(false); setMe(null); }; // apply accent + density to document useEffect(() => { document.documentElement.style.setProperty("--blue", t.accent); const dark = shade(t.accent, -0.28); document.documentElement.style.setProperty("--blue-dark", dark); }, [t.accent]); // ---- still resolving which auth mode the backend uses ---- if (authCfg === null) { return (
); } // ---- not logged in: show the appropriate login surface ---- if (!auth) { if (isGoogle) { if (needsOnboarding) return setTweak("lang", l)} onCreate={() => bootAuthed("student")} onBack={signOut} />; return setTweak("lang", l)} clientId={authCfg.googleClientId} onCredential={onGoogleCredential} error={googleErr} busy={googleBusy} />; } if (authMode === "admin") return setTweak("lang", l)} onLogin={() => login("admin")} onBack={goMain} />; if (authMode === "signup") return setTweak("lang", l)} onCreate={sysId => login("student", { systemId: sysId, email: "" })} onBack={goMain} />; return setTweak("lang", l)} onLogin={(r, id) => login(r, r === "student" && id ? { systemId: "", email: id } : undefined)} onAdmin={goAdmin} onSignup={goSignup} />; } const setRole = async r => { await reloadData(r); setRoleRaw(r); setView(HOME[r]); setScript(null); setExam(null); }; const nav = v => { setView(v); setScript(null); }; const startExam = async (examId, paper) => { const ex = window.ZDATA.exams.find(e => e.id === examId); if (ex && ex.fee && !paidExams.has(examId)) { setExam({ examId, paper, stage: "payment" }); return; } // pay gate let attemptId = null; try { const a = await window.ZAPI.startAttempt(examId, paper); attemptId = a.attempt_id; } catch (e) { console.error("startAttempt failed", e); } setExam({ examId, paper, stage: "instructions", attemptId }); }; const exitExam = () => setExam(null); const examObj = exam && window.ZDATA.exams.find(e => e.id === exam.examId); const isMcq = exam && exam.paper === "I"; const ctx = { lang, setLang: l => setTweak("lang", l), T, nav, role, setRole, startExam, exitExam, zStyle: t.zStyle, examStrict: t.examStrict, density: t.density }; return ( {/* Payment gate — shown before an unpaid exam; pays, then starts the attempt */} {exam && examObj && exam.stage === "payment" && { setPaidExams(s => new Set(s).add(exam.examId)); let attemptId = null; try { const a = await window.ZAPI.startAttempt(exam.examId, exam.paper); attemptId = a.attempt_id; } catch (e) { console.error("startAttempt failed", e); } setExam({ ...exam, stage: "instructions", attemptId }); }} onClose={exitExam} />} {/* Exam overlay takes over the whole screen */} {exam && examObj && exam.stage === "instructions" && setExam({ ...exam, stage: isMcq ? "running" : "config" })} onExit={exitExam} />} {/* Paper I — MCQ runner */} {exam && examObj && isMcq && exam.stage === "running" && setExam({ ...exam, stage: "submitted", result })} onExit={exitExam} />} {/* Paper II — Phase 1 config (pick 6 of 9 + sequence) */} {exam && examObj && !isMcq && exam.stage === "config" && setExam({ ...exam, stage: "running", chosen })} onExit={exitExam} />} {/* Paper II — Phase 2 sequential execution (photo upload, forward-only) */} {exam && examObj && !isMcq && exam.stage === "running" && setExam({ ...exam, stage: "submitted", result })} onExit={exitExam} />} {exam && examObj && exam.stage === "submitted" && } {/* Main app */}
{/* Sidebar */} {/* Main column */}

{T(TITLES[view].en, TITLES[view].si)}

{T(ROLE_SUB[role].en, ROLE_SUB[role].si)}
{/* dev-only role switcher (mobile) — hidden in google mode, matching the sidebar one */} {!isGoogle && (
{["student", "teacher", "admin"].map(r => ( ))}
)}
{/* Sign out — the sidebar (which holds the desktop sign-out) is hidden on mobile, so surface it here. `signout-m` is mobile-only (see app.css). */}
{role === "student" && view === "dashboard" && } {role === "student" && view === "exams" && } {role === "student" && view === "results" && } {role === "student" && view === "billing" && } {role === "teacher" && (script ? setScript(null)} /> : )} {role === "admin" && view === "overview" && } {role === "admin" && view === "streams" && } {role === "admin" && view === "teachers" && } {role === "admin" && view === "backlog" && } {role === "admin" && view === "physical" && } {role === "admin" && view === "batch" && } {role === "admin" && view === "payments" && }
{/* Mobile nav */}
{/* Tweaks */} setTweak("lang", v)} /> setTweak("accent", v)} /> setTweak("zStyle", v)} /> setTweak("examStrict", v)} />
); } /* darken/lighten a hex by amount (-1..1) */ function shade(hex, amt) { const n = parseInt(hex.slice(1), 16); let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; const f = amt < 0 ? 0 : 255, p = Math.abs(amt); r = Math.round((f - r) * p + r); g = Math.round((f - g) * p + g); b = Math.round((f - b) * p + b); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } // gate the first render until the backend bootstrap has populated window.ZDATA const __root = ReactDOM.createRoot(document.getElementById("root")); (window.ZBOOT && window.ZBOOT.promise ? window.ZBOOT.promise : Promise.resolve()) .then(() => __root.render());