import { Navigate, Route, Routes } from "react-router-dom"; import { lazy, Suspense, useEffect, useMemo, useState } from "react"; import { fetchMe, updateProfile } from "./api/auth"; import { fetchPlatformModules, fetchPlatformStatus } from "./api/platform"; import { AUTH_REQUIRED_EVENT, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client"; import type { ApiSettings, AuthInfo, AuthTenant, AuthTenantMembership, AuthUser, LoginResponse, PlatformModuleInfo, PlatformWebModule, UserUiPreferences } from "./types"; import AppShell from "./layout/AppShell"; import PublicLandingPage from "./features/auth/PublicLandingPage"; import LoginModal from "./features/auth/LoginModal"; import { PermissionBoundary } from "./components/AccessBoundary"; import { firstAccessibleRoute, loadRemoteWebModules, moduleInstalled, navItemsForModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules"; import { PlatformModulesProvider } from "./platform/ModuleContext"; import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents"; import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard"; import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext"; const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage")); const SettingsPage = lazy(() => import("./features/settings/SettingsPage")); const DEFAULT_UI_PREFERENCES: UserUiPreferences = { compact_tables: false, show_inline_help_hints: true, reduce_motion: false, sticky_section_sidebars: true, theme: "system" }; export default function App() { const [settings, setSettings] = useState(() => loadApiSettings()); const [auth, setAuth] = useState(null); const [checkingSession, setCheckingSession] = useState(true); const [platformModules, setPlatformModules] = useState(null); const [remoteWebModules, setRemoteWebModules] = useState([]); const [maintenanceMode, setMaintenanceMode] = useState<{enabled: boolean;message?: string | null;}>({ enabled: false, message: null }); const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null); const [reloginMessage, setReloginMessage] = useState(""); const localWebModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]); const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]); const navItems = useMemo(() => navItemsForModules(webModules), [webModules]); const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]); const moduleTranslations = useMemo(() => webModules.map((module) => module.translations).filter(Boolean), [webModules]); const dashboardModuleInstalled = useMemo(() => moduleInstalled("dashboard", webModules), [webModules]); function updateSettings(next: ApiSettings) { setSettings(next); saveApiSettings(next); } function updateAuth(next: AuthInfo | null, accessToken?: string) { const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings; setAuth(next ? normalizeAuthInfo(next) : null); if (accessToken !== undefined) { setSettings(nextSettings); saveApiSettings(nextSettings); } } function authFromLoginResponse(response: LoginResponse): AuthInfo { return normalizeAuthInfo(response); } function handlePublicLogin(response: LoginResponse) { updateAuth(authFromLoginResponse(response), ""); } function handleRelogin(response: LoginResponse) { updateAuth(authFromLoginResponse(response), ""); setReloginMessage(""); } useEffect(() => { function handleAuthRequired(event: Event) { if (!auth) return; const detail = (event as CustomEvent).detail; setReloginMessage(detail?.message || "i18n:govoplan-core.your_session_has_expired_sign_in_again_to_contin.4cc6a1b3"); } window.addEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); return () => window.removeEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); }, [auth]); useEffect(() => { let cancelled = false; async function refreshPlatformStatus() { try { const response = await fetchPlatformStatus(settings); if (cancelled) return; setMaintenanceMode(response.maintenance_mode); if (response.i18n) { setSystemLanguages({ available: response.i18n.available_languages.map((item) => ({ code: item.code, label: item.label, nativeLabel: item.native_label ?? undefined })), enabled: response.i18n.enabled_languages, defaultLanguage: response.i18n.default_language }); } } catch { if (!cancelled) setMaintenanceMode({ enabled: false, message: null }); } } void refreshPlatformStatus(); const interval = window.setInterval(() => {void refreshPlatformStatus();}, 30_000); window.addEventListener("focus", refreshPlatformStatus); return () => { cancelled = true; window.clearInterval(interval); window.removeEventListener("focus", refreshPlatformStatus); }; }, [settings.apiBaseUrl]); useEffect(() => { let cancelled = false; setCheckingSession(true); fetchMe(settings). then((me) => {if (!cancelled) setAuth(normalizeAuthInfo(me));}). catch(() => { if (!cancelled) { const cleared = { ...settings, accessToken: "" }; setSettings(cleared); saveApiSettings(cleared); setAuth(null); setPlatformModules(null); setRemoteWebModules([]); } }). finally(() => { if (!cancelled) setCheckingSession(false); }); return () => {cancelled = true;}; }, [settings.apiBaseUrl, settings.apiKey]); useEffect(() => { if (!auth) return; let cancelled = false; let inFlight = false; let lastRefreshAt = 0; function loadModules() { inFlight = true; lastRefreshAt = Date.now(); return fetchPlatformModules(settings). then((response) => {if (!cancelled) setPlatformModules(response.modules);}). catch(() => {if (!cancelled) setPlatformModules(null);}). finally(() => {inFlight = false;}); } void loadModules(); function handleModulesChanged() { void loadModules(); } function refreshVisibleModules() { if (document.visibilityState === "hidden" || inFlight) return; if (Date.now() - lastRefreshAt < 5_000) return; void loadModules(); } window.addEventListener(PLATFORM_MODULES_CHANGED_EVENT, handleModulesChanged); window.addEventListener("focus", refreshVisibleModules); document.addEventListener("visibilitychange", refreshVisibleModules); return () => { cancelled = true; window.removeEventListener(PLATFORM_MODULES_CHANGED_EVENT, handleModulesChanged); window.removeEventListener("focus", refreshVisibleModules); document.removeEventListener("visibilitychange", refreshVisibleModules); }; }, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]); useEffect(() => { const preferences = auth?.user.ui_preferences ?? DEFAULT_UI_PREFERENCES; const root = document.documentElement; root.classList.toggle("ui-compact-tables", preferences.compact_tables); root.classList.toggle("ui-hide-help-hints", !preferences.show_inline_help_hints); root.classList.toggle("ui-reduce-motion", preferences.reduce_motion); root.classList.toggle("ui-no-sticky-section-sidebars", !preferences.sticky_section_sidebars); if (preferences.theme === "system") { delete root.dataset.theme; } else { root.dataset.theme = preferences.theme; } }, [ auth?.user.ui_preferences?.compact_tables, auth?.user.ui_preferences?.show_inline_help_hints, auth?.user.ui_preferences?.reduce_motion, auth?.user.ui_preferences?.sticky_section_sidebars, auth?.user.ui_preferences?.theme ]); useEffect(() => { let cancelled = false; if (!auth || !platformModules?.length) { setRemoteWebModules([]); return () => {cancelled = true;}; } loadRemoteWebModules(platformModules, localWebModules). then((modules) => {if (!cancelled) setRemoteWebModules(modules);}). catch(() => {if (!cancelled) setRemoteWebModules([]);}); return () => {cancelled = true;}; }, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, platformModules, localWebModules]); useEffect(() => { if (!auth) return; let inFlight = false; let lastRefreshAt = 0; async function refreshVisibleSession() { if (document.visibilityState === "hidden" || inFlight) return; const now = Date.now(); if (now - lastRefreshAt < 5_000) return; inFlight = true; lastRefreshAt = now; try { setAuth(normalizeAuthInfo(await fetchMe(settings))); } catch { // A background refresh must not log the user out on a transient network error. } finally {inFlight = false;} } window.addEventListener("focus", refreshVisibleSession); document.addEventListener("visibilitychange", refreshVisibleSession); return () => { window.removeEventListener("focus", refreshVisibleSession); document.removeEventListener("visibilitychange", refreshVisibleSession); }; }, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]); if (checkingSession) { return (
i18n:govoplan-core.govoplan.a84c0a85

i18n:govoplan-core.checking_session.e7d81968

i18n:govoplan-core.please_wait_while_the_local_session_is_verified.1dbd8fd3

); } if (!auth) { return ( ); } const defaultRoute = firstAccessibleRoute(auth, webModules); const authAvailableLanguages = auth.available_languages?.map((item) => ({ code: item.code, label: item.label, nativeLabel: item.native_label ?? undefined })); function persistLanguagePreference(code: string) { void updateProfile(settings, { preferred_language: code }).then((next) => updateAuth(next)).catch(() => undefined); } return (

i18n:govoplan-core.loading_module.50161f3c

}> } /> {!dashboardModuleInstalled && } />} {moduleRoutes.map((route) => {route.render({ settings, auth, onAuthChange: updateAuth })} } /> )} } /> } />
{reloginMessage && setReloginMessage("")} onLogin={handleRelogin} /> }
); } type AuthPayload = Partial & { principal?: AuthInfo["principal"]; user?: Partial | null; tenant?: AuthTenant | null; active_tenant?: AuthTenant | null; tenants?: AuthTenantMembership[] | null; }; function normalizeAuthInfo(response: AuthPayload): AuthInfo { const principal = response.principal ?? null; const activeTenant = response.active_tenant ?? response.tenant ?? response.tenants?.[0] ?? null; const user = normalizeAuthUser(response.user, principal); if (!activeTenant) { throw new Error("Authentication response did not include an active tenant."); } if (!user) { throw new Error("Authentication response did not include a user."); } return { user, tenant: activeTenant, active_tenant: activeTenant, tenants: response.tenants ?? [activeTenant], scopes: response.scopes ?? principal?.scopes ?? [], roles: response.roles ?? [], groups: response.groups ?? [], principal, available_languages: response.available_languages ?? [], enabled_language_codes: response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [], default_language: response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "en" }; } function normalizeAuthUser(user: Partial | null | undefined, principal: AuthInfo["principal"]): AuthUser | null { if (user?.id && user.account_id) { return { id: user.id, account_id: user.account_id, email: user.email ?? principal?.email ?? "", display_name: user.display_name ?? principal?.display_name ?? principal?.email ?? null, tenant_display_name: user.tenant_display_name ?? null, is_tenant_admin: user.is_tenant_admin ?? false, password_reset_required: user.password_reset_required ?? false, preferred_language: user.preferred_language ?? null, enabled_language_codes: user.enabled_language_codes ?? [], ui_preferences: normalizeUiPreferences(user.ui_preferences) }; } if (!principal?.membership_id || !principal.account_id) { return null; } return { id: principal.membership_id, account_id: principal.account_id, email: principal.email ?? "", display_name: principal.display_name ?? principal.email ?? null, tenant_display_name: principal.display_name ?? null, is_tenant_admin: false, password_reset_required: false, preferred_language: null, enabled_language_codes: [], ui_preferences: DEFAULT_UI_PREFERENCES }; } function normalizeUiPreferences(value: Partial | null | undefined): UserUiPreferences { const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system"; return { compact_tables: Boolean(value?.compact_tables ?? DEFAULT_UI_PREFERENCES.compact_tables), show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints), reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion), sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars), theme }; } function mergeWebModules(localModules: PlatformWebModule[], remoteModules: PlatformWebModule[]): PlatformWebModule[] { if (remoteModules.length === 0) return localModules; const seen = new Set(localModules.map((module) => module.id)); return [ ...localModules, ...remoteModules.filter((module) => { if (seen.has(module.id)) return false; seen.add(module.id); return true; })]; }