/* ============================================================ app.jsx — root: real auth + role-based render + mount No persona switcher: the signed-in user's role decides the app. A tokenised email link (?token=) opens the public approval page. ============================================================ */ const ROLE_LABEL = { brand_ops: "Brand Ops", finance: "Finance team", nakad: "Nakad admin", admin: "Lekha admin", }; function viewForRole(role) { if (role === "brand_ops") return BrandOpsApp; if (role === "finance") return FinanceApp; if (role === "nakad") return NakadApp; return AdminApp; } /* ---- Account menu — clean top-bar identity chip + dropdown ---------------- Replaces the raw "Reset demo / Log out" buttons. Navigation items dispatch a global `lekha-nav` event that the active persona app listens for (so the chip can jump to Team/Settings without owning their tab state). Profile opens a modal over the signed-in user's REAL fields — no invented data. */ function AccountMenu({ user, logout, store }) { const [open, setOpen] = React.useState(false); const [profileOpen, setProfileOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const away = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", away); return () => document.removeEventListener("mousedown", away); }, [open]); const go = (tab) => { setOpen(false); window.dispatchEvent(new CustomEvent("lekha-nav", { detail: tab })); }; const av = (user.name || "?").slice(0, 2).toUpperCase(); const hasTabs = user.role === "finance" || user.role === "brand_ops"; const item = (label, onClick, danger) => ( ); return (
{open && (
{user.name}
{user.email || ""}
{ROLE_LABEL[user.role] || user.role}
{item("Profile", () => { setOpen(false); setProfileOpen(true); })} {hasTabs && item("Team", () => go("team"))} {hasTabs && item("Settings", () => go("notif"))}
{item("Log out", () => { setOpen(false); logout(); }, true)}
)} {profileOpen && setProfileOpen(false)} />}
); } function ProfileModal({ user, onClose }) { const row = (k, v) => v ? (
{k} {v}
) : null; return (
e.stopPropagation()}>
Profile
{row("Name", user.name)} {row("Email", user.email)} {row("Role", ROLE_LABEL[user.role] || user.role)} {row("Company", user.company_name || user.business_name)}
); } function AuthedApp({ user, logout }) { const store = useStore(); store.user = user; // expose signed-in identity (role + brand) to personas store.logout = logout; // so the AppHeader account menu can sign out const View = viewForRole(user.role); return (
); } /* ---- Onboarding (spec Screen 2 — Tally detection: 2A confirm / 2B mismatch / 2C none) ---- Runs once right after signup. In the desktop app it probes the open Tally company (tally_list_companies) and asks the backend /detect to compare it with the account company; auto-polls every 5s so a company switch resolves without clicking. In the web app (no Tally probe) it goes straight to the confirm-details step. */ function Onboarding({ onDone, logout }) { const [state, setState] = React.useState("checking"); // checking|not_detected|mismatch|matched const [info, setInfo] = React.useState({}); const [gstin, setGstin] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); const invoke = () => (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) || null; const probe = React.useCallback(async () => { const inv = invoke(); let open_name = null, tally_online = false; if (inv) { try { const xml = await inv("tally_list_companies"); const m = xml && xml.match(/ g || r.expected_gstin || ""); }, [onDone]); React.useEffect(() => { if (state === "matched") return; // confirming → stop detecting probe(); const t = setInterval(probe, 5000); return () => clearInterval(t); }, [probe, state]); const save = async () => { setErr(""); setBusy(true); try { await api.post("/api/desktop/company-details", { gstin: gstin.trim() }); onDone(); } catch (e) { setErr((e && e.message) || "Couldn't save — check the GSTIN."); setBusy(false); } }; const shell = (children) => (
Lekha AI
{children}
{ e.preventDefault(); logout(); }} style={{ color: "var(--text-3)" }}>Log out
); if (state === "checking") return shell(
Detecting TallyPrime on this PC…
); if (state === "not_detected") return shell(
Open Tally to continue
Lekha can't detect TallyPrime on this computer. Open TallyPrime with {info.company_name} loaded — this screen continues automatically.
); if (state === "mismatch") return shell(
Wrong company open in Tally
Expected: {info.company_name} (your account)
Found: {info.open_name || "another company"} (open in Tally)
In Tally press F3 → select {info.company_name}. This screen continues automatically when it matches.
); // matched → Screen 2A confirm details return shell(
Confirm company details
We detected your company. Review and complete before your first posting.
COMPANY NAME
{info.company_name || ""} · locked
setGstin(e.target.value.toUpperCase())} placeholder="27AAAAA0000A1Z5" hint="State is derived from the GSTIN automatically." /> {err &&
{err}
}
); } function Root() { const { user, ready, login, googleLogin, signup, logout } = useAuth(); const [onboarding, setOnboarding] = React.useState(false); const doSignup = async (payload) => { const r = await signup(payload); setOnboarding(true); return r; }; if (!ready) return
Loading…
; if (!user) return ; if (onboarding && user.role === "finance") return setOnboarding(false)} logout={logout} />; return ; } // a tokenised email-link (?token=) opens the standalone approval landing // (public — no login); otherwise the authenticated app mounts. const _token = new URLSearchParams(window.location.search).get("token"); ReactDOM.createRoot(document.getElementById("root")) .render(_token ? : );