- Glocke im Backoffice-Header (ungelesen-Zähler, letzte 10, alle gelesen), mobil einbindbar über variant="mobile". - /notifications mit Filter gelesen/ungelesen/Art, Öffnen markiert gelesen (nur relative Links), Pagination. - /account Abschnitt Benachrichtigungen: E-Mail-Opt-out je Typ, Notdienst Pflicht. - /settings/email (tenant:manage): Absendername, Antwortadresse, Empfänger Notdienst und Abrechnung; Validierung gegen Header-Injection, max. 20 Adressen, Audit. - Actions unter actions/notifications mit moduleGuard + guard; Navigation ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
import {
|
|
LayoutDashboard,
|
|
ClipboardList,
|
|
FileInput,
|
|
Users,
|
|
Building2,
|
|
UsersRound,
|
|
FileText,
|
|
FolderOpen,
|
|
Settings,
|
|
Bell,
|
|
History,
|
|
Mail,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import type { ModuleKey } from "@/lib/modules";
|
|
import type { Permission } from "@/server/rbac";
|
|
|
|
/**
|
|
* Sidebar-Navigation des Backoffice (src/app/(app)/layout.tsx).
|
|
*
|
|
* Ein Eintrag ist sichtbar, wenn
|
|
* - sein Modul (falls gesetzt) für den Mandanten aktiv ist UND
|
|
* - die Session mindestens EINE der `permissions` hat (leer = immer).
|
|
* Das ist reiner Komfort — Seiten und Actions prüfen Modul + Rechte serverseitig selbst.
|
|
*
|
|
* Neue Backoffice-Seite ⇒ hier eintragen; `label` ist ein Schlüssel in messages nav.*.
|
|
* Mobile-Einträge (/m, /m/emergency) gehören NICHT hierher (eigene Mobile-Navigation).
|
|
*/
|
|
export interface NavItem {
|
|
href: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
module?: ModuleKey;
|
|
permissions?: readonly Permission[];
|
|
section: "main" | "admin";
|
|
}
|
|
|
|
export const NAV_ITEMS: readonly NavItem[] = [
|
|
{ href: "/dashboard", label: "dashboard", icon: LayoutDashboard, section: "main" },
|
|
{
|
|
href: "/work-orders",
|
|
label: "workOrders",
|
|
icon: ClipboardList,
|
|
module: "work_orders",
|
|
permissions: ["work_order:read_all", "work_order:read_team"],
|
|
section: "main",
|
|
},
|
|
{ href: "/imports", label: "imports", icon: FileInput, module: "imports", permissions: ["import:write"], section: "main" },
|
|
{ href: "/customers", label: "customers", icon: Users, module: "customers", permissions: ["customer:read"], section: "main" },
|
|
{ href: "/sites", label: "sites", icon: Building2, module: "sites", permissions: ["site:read"], section: "main" },
|
|
{ href: "/teams", label: "teams", icon: UsersRound, module: "teams", permissions: ["team:read"], section: "main" },
|
|
{ href: "/reports", label: "reports", icon: FileText, module: "reports", permissions: ["report:read"], section: "main" },
|
|
{ href: "/documents", label: "documents", icon: FolderOpen, module: "documents", permissions: ["document:read"], section: "main" },
|
|
{ href: "/notifications", label: "notifications", icon: Bell, module: "notifications", permissions: ["notification:read"], section: "main" },
|
|
{ href: "/settings", label: "settings", icon: Settings, permissions: ["tenant:manage"], section: "admin" },
|
|
{ href: "/settings/email", label: "email", icon: Mail, module: "notifications", permissions: ["tenant:manage"], section: "admin" },
|
|
{ href: "/settings/audit", label: "audit", icon: History, permissions: ["audit:read"], section: "admin" },
|
|
];
|
|
|
|
/** Filtert die Navigation nach aktiven Modulen und Rechten der Session. */
|
|
export function visibleNavItems(
|
|
items: readonly NavItem[],
|
|
opts: { disabledModules: ReadonlySet<string>; permissions: readonly string[] },
|
|
): NavItem[] {
|
|
return items.filter(
|
|
(item) =>
|
|
(!item.module || !opts.disabledModules.has(item.module)) &&
|
|
(!item.permissions?.length || item.permissions.some((p) => opts.permissions.includes(p))),
|
|
);
|
|
}
|