intermittent commit

This commit is contained in:
2026-07-14 13:22:10 +02:00
parent 8aa1943581
commit e6f7c45f0a
76 changed files with 6039 additions and 2188 deletions

View File

@@ -1,20 +1,27 @@
import { useRef, useState, useEffect } from "react";
import { Check, LogOut, Settings, UserCircle } from "lucide-react";
import type { ApiSettings, AuthInfo, AuthTenantMembership, LoginResponse } from "../types";
import { Bell, Check, LogOut, Settings, UserCircle } from "lucide-react";
import type { ApiSettings, AuthInfo, AuthTenantMembership, AuthUpdate, LoginResponse } 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 } from "../platform/ModuleContext";
type NotificationSummary = {
unread: number;
show_unread_badge?: boolean;
};
type Props = {
settings: ApiSettings;
auth: AuthInfo | null;
onSettingsChange: (settings: ApiSettings) => void;
onAuthChange: (auth: AuthInfo | null, accessToken?: string) => void;
onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void;
maintenanceMode?: {enabled: boolean;message?: string | null;};
};
@@ -26,9 +33,11 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
const [loginOpen, setLoginOpen] = useState(false);
const [switchingTenantId, setSwitchingTenantId] = useState<string | null>(null);
const [tenantError, setTenantError] = useState("");
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
const accountRef = useRef<HTMLDivElement>(null);
const tenantRef = useRef<HTMLDivElement>(null);
const { translateText } = usePlatformLanguage();
const modules = usePlatformModules();
const activeTenant = auth?.active_tenant ?? auth?.tenant ?? null;
const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []);
@@ -41,6 +50,8 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
"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) {
@@ -56,6 +67,41 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
return () => window.removeEventListener("mousedown", onPointerDown);
}, []);
useEffect(() => {
if (!auth || !notificationsAvailable) {
setUnreadNotificationCount(0);
return;
}
let cancelled = false;
async function refreshNotificationSummary() {
try {
const summary = await apiFetch<NotificationSummary>(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, "");
}
@@ -109,6 +155,10 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
navigate("/admin?section=system-settings");
}
function openNotificationCenter() {
navigate("/notifications");
}
return (
<header className="titlebar">
{maintenanceMode?.enabled &&
@@ -159,8 +209,19 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
<div className="titlebar-spacer" />
<HelpMenu />
<LanguageMenu />
<HelpMenu />
{auth && notificationsAvailable &&
<button className="titlebar-icon-link titlebar-notification-button" onClick={openNotificationCenter} title={translateText("i18n:govoplan-core.notifications.753a22b2")} aria-label={translateText("i18n:govoplan-core.notifications.753a22b2")}>
<Bell size={18} />
{unreadNotificationCount > 0 &&
<span className="titlebar-notification-badge" aria-label={`${unreadNotificationCount} unread`}>
{notificationBadgeLabel}
</span>
}
</button>
}
<div className="context-menu-wrap" ref={accountRef}>
<button className="account-pill" onClick={() => setAccountOpen(!accountOpen)}>