import { useRef, useState, useEffect } from "react"; import { Bell, Check, LogOut, Settings, UserCircle } from "lucide-react"; import type { ApiSettings, AuthInfo, AuthTenantMembership, AuthUpdate, LoginResponse, ViewsRuntimeUiCapability } from "../types"; import HelpMenu from "./HelpMenu"; import LanguageMenu from "./LanguageMenu"; import LoginModal from "../features/auth/LoginModal"; import DismissibleAlert from "../components/DismissibleAlert"; import { useGuardedNavigate, useUnsavedChanges } from "../components/UnsavedChangesGuard"; import { apiFetch, isApiError } from "../api/client"; import { logout, switchTenant } from "../api/auth"; import { hasAnyScope } from "../utils/permissions"; import { usePlatformLanguage } from "../i18n/LanguageContext"; import { usePlatformModules, usePlatformUiCapability } from "../platform/ModuleContext"; import { useEffectiveView } from "../platform/ViewContext"; type NotificationSummary = { unread: number; show_unread_badge?: boolean; }; type Props = { settings: ApiSettings; auth: AuthInfo | null; onSettingsChange: (settings: ApiSettings) => void; onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void; maintenanceMode?: {enabled: boolean;message?: string | null;}; backendReachable?: boolean; }; export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode, backendReachable = true }: Props) { const navigate = useGuardedNavigate(); const { requestNavigation } = useUnsavedChanges(); const [accountOpen, setAccountOpen] = useState(false); const [tenantOpen, setTenantOpen] = useState(false); const [loginOpen, setLoginOpen] = useState(false); const [switchingTenantId, setSwitchingTenantId] = useState(null); const [tenantError, setTenantError] = useState(""); const [unreadNotificationCount, setUnreadNotificationCount] = useState(0); const accountRef = useRef(null); const tenantRef = useRef(null); const { translateText } = usePlatformLanguage(); const modules = usePlatformModules(); const projection = useEffectiveView(); const viewsRuntime = usePlatformUiCapability("views.runtime"); const ViewSelector = viewsRuntime?.Selector ?? null; const activeTenant = auth?.active_tenant ?? auth?.tenant ?? null; const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []); const displayUserName = auth?.user?.display_name || auth?.user?.email || translateText("i18n:govoplan-core.sign_in.ada2e9e9"); const canSwitchTenant = tenants.length > 1; const canAdministerTenants = hasAnyScope(auth, [ "system:tenants:read", "system:tenants:create", "system:tenants:update", "system:tenants:suspend"] ); const showTenantControl = Boolean(activeTenant && (canSwitchTenant || canAdministerTenants)); const notificationsAvailable = modules.some((module) => module.id === "notifications" && module.routes?.some((route) => route.path === "/notifications")); const notificationBadgeLabel = unreadNotificationCount > 99 ? "99+" : String(unreadNotificationCount); useEffect(() => { function onPointerDown(event: MouseEvent) { const target = event.target as Node; if (accountRef.current && !accountRef.current.contains(target)) { setAccountOpen(false); } if (tenantRef.current && !tenantRef.current.contains(target)) { setTenantOpen(false); } } window.addEventListener("mousedown", onPointerDown); return () => window.removeEventListener("mousedown", onPointerDown); }, []); useEffect(() => { if (!auth || !notificationsAvailable) { setUnreadNotificationCount(0); return; } let cancelled = false; async function refreshNotificationSummary() { try { const summary = await apiFetch(settings, "/api/v1/notifications/summary", { cache: "no-store" }); if (!cancelled) { setUnreadNotificationCount(summary.show_unread_badge === false ? 0 : Math.max(0, Number(summary.unread) || 0)); } } catch (error) { if (!cancelled) { setUnreadNotificationCount(0); } if (!isApiError(error, 401, 403, 404)) { console.error("Failed to load notification summary", error); } } } void refreshNotificationSummary(); const intervalId = window.setInterval(refreshNotificationSummary, 60_000); window.addEventListener("focus", refreshNotificationSummary); window.addEventListener("govoplan:notifications-changed", refreshNotificationSummary); return () => { cancelled = true; window.clearInterval(intervalId); window.removeEventListener("focus", refreshNotificationSummary); window.removeEventListener("govoplan:notifications-changed", refreshNotificationSummary); }; }, [activeTenant?.id, auth?.user?.id, notificationsAvailable, settings.accessToken, settings.apiBaseUrl, settings.apiKey]); function handleLogin(response: LoginResponse) { onAuthChange(response, ""); } async function performLogout() { try { await logout(settings); } catch { // Logout is best effort; clear local session either way. } onAuthChange(null, ""); setAccountOpen(false); } async function performTenantSelect(tenant: AuthTenantMembership) { setSwitchingTenantId(tenant.id); setTenantError(""); try { const next = await switchTenant(settings, tenant.id); // Keep the current URL. The tenant-keyed route tree reloads in the new // context; permission and resource boundaries then retain the page or // fall back to the nearest accessible parent. onAuthChange(next); setTenantOpen(false); } catch (error) { setTenantError(error instanceof Error ? error.message : translateText("i18n:govoplan-core.tenant_switch_failed.c325b20b")); } finally { setSwitchingTenantId(null); } } function handleLogout() { requestNavigation(() => {void performLogout();}); } function handleTenantSelect(tenant: AuthTenantMembership) { if (!auth || tenant.id === activeTenant?.id || switchingTenantId) { setTenantOpen(false); return; } requestNavigation(() => {void performTenantSelect(tenant);}); } function handleAccountSettings() { setAccountOpen(false); navigate("/settings?section=profile"); } function openMaintenanceSettings() { navigate("/admin?section=system-settings"); } function openNotificationCenter() { navigate("/notifications"); } return (
{!backendReachable ?
System not reachable / offline!
: maintenanceMode?.enabled && } {auth && activeTenant && showTenantControl &&
{translateText("i18n:govoplan-core.tenant_label_prefix")} {canSwitchTenant ? <> {tenantOpen &&
{tenants.map((tenant) => { const active = tenant.id === activeTenant.id; return ( ); })} {tenantError && {tenantError}}
} : {activeTenant.name} }
}
{auth && ViewSelector && } {auth && notificationsAvailable && }
{accountOpen &&
{auth ? <> {auth.user &&
{auth.user.display_name || auth.user.email} {auth.user.email}
} : }
}
{loginOpen && setLoginOpen(false)} onLogin={handleLogin} />}
); }