diff --git a/migrations/0019_kennzahlen_policy_rule.down.sql b/migrations/0019_kennzahlen_policy_rule.down.sql new file mode 100644 index 00000000..45030c44 --- /dev/null +++ b/migrations/0019_kennzahlen_policy_rule.down.sql @@ -0,0 +1 @@ +DELETE FROM policy_rules WHERE granted_by = 'migration:0019_kennzahlen_policy_rule'; diff --git a/migrations/0019_kennzahlen_policy_rule.up.sql b/migrations/0019_kennzahlen_policy_rule.up.sql new file mode 100644 index 00000000..52cb0e93 --- /dev/null +++ b/migrations/0019_kennzahlen_policy_rule.up.sql @@ -0,0 +1,18 @@ +-- ANA-02: Kennzahlen-Dashboard braucht ein eigenes Recht, analog zum +-- core.tenant.admin-Muster aus 0018 — nur tenant_admin, da aggregierte +-- Loeschkennzahlen ueber alle Module hinweg sensibler sind als einzelne +-- Modul-Leserechte. +WITH grants(role, permission) AS ( + VALUES + ('tenant_admin', 'kennzahlen.read') +), +ins AS ( + INSERT INTO policy_rules (role, permission, granted_by) + SELECT role, permission, 'migration:0019_kennzahlen_policy_rule' FROM grants + ON CONFLICT (role, permission) DO NOTHING + RETURNING role, permission +) +INSERT INTO policy_rule_changes (role, permission, action, actor, version) +SELECT i.role, i.permission, 'grant', 'migration:0019_kennzahlen_policy_rule', + COALESCE((SELECT MAX(c.version) FROM policy_rule_changes c WHERE c.role = i.role AND c.permission = i.permission), 0) + 1 +FROM ins i; diff --git a/web/ai-extraction-review/app/layout.tsx b/web/ai-extraction-review/app/layout.tsx index e0334991..d8d1c583 100644 --- a/web/ai-extraction-review/app/layout.tsx +++ b/web/ai-extraction-review/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Extraktions-Korrektur", @@ -36,7 +36,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/web/ai-summaries/app/layout.tsx b/web/ai-summaries/app/layout.tsx index 99fd2b53..edb7c03b 100644 --- a/web/ai-summaries/app/layout.tsx +++ b/web/ai-summaries/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Zusammenfassungen", @@ -36,7 +36,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/web/connect-status/app/layout.tsx b/web/connect-status/app/layout.tsx index 124ff103..c11ad297 100644 --- a/web/connect-status/app/layout.tsx +++ b/web/connect-status/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Verbindungsstatus", @@ -30,7 +30,7 @@ export default function RootLayout({ > - {children} + {children} diff --git a/web/datev-export/app/layout.tsx b/web/datev-export/app/layout.tsx index b917d86e..00fd2eac 100644 --- a/web/datev-export/app/layout.tsx +++ b/web/datev-export/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH DATEV-Export", @@ -28,7 +28,7 @@ export default function RootLayout({ > - {children} + {children} diff --git a/web/dms-upload/app/layout.tsx b/web/dms-upload/app/layout.tsx index 72aa3f9f..c7208201 100644 --- a/web/dms-upload/app/layout.tsx +++ b/web/dms-upload/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl"; import { AccountBar } from "../components/AccountBar"; export const metadata = { @@ -22,10 +22,10 @@ export default function RootLayout({ > - + {children} - + diff --git a/web/kennzahlen-dashboard/app/layout.tsx b/web/kennzahlen-dashboard/app/layout.tsx index c6a43035..924d06e5 100644 --- a/web/kennzahlen-dashboard/app/layout.tsx +++ b/web/kennzahlen-dashboard/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Kennzahlen", @@ -21,7 +21,7 @@ export default function RootLayout({ > - {children} + {children} diff --git a/web/mail-search/app/layout.tsx b/web/mail-search/app/layout.tsx index d4ec316c..d8001552 100644 --- a/web/mail-search/app/layout.tsx +++ b/web/mail-search/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Mail-Suche", @@ -43,7 +43,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/web/portal/config/modules.json b/web/portal/config/modules.json index 80a1d377..e81164a3 100644 --- a/web/portal/config/modules.json +++ b/web/portal/config/modules.json @@ -82,5 +82,12 @@ "description": "Rollen und Berechtigungen verwalten.", "href": "http://localhost:3112", "permission": "core.tenant.admin" + }, + { + "key": "kennzahlen_dashboard", + "label": "Kennzahlen", + "description": "Löschkennzahlen je Tenant und Modul einsehen.", + "href": "http://localhost:3113", + "permission": "kennzahlen.read" } ] diff --git a/web/rbac-admin/app/layout.tsx b/web/rbac-admin/app/layout.tsx index 7390a137..d8d8c4fd 100644 --- a/web/rbac-admin/app/layout.tsx +++ b/web/rbac-admin/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Rechteverwaltung", @@ -21,7 +21,7 @@ export default function RootLayout({ > - {children} + {children} diff --git a/web/routing-rules/app/layout.tsx b/web/routing-rules/app/layout.tsx index d4eaeae7..cefd7e93 100644 --- a/web/routing-rules/app/layout.tsx +++ b/web/routing-rules/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Routing-Regeln", @@ -36,7 +36,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/web/shl/components/ModuleNavShell.tsx b/web/shl/components/ModuleNavShell.tsx new file mode 100644 index 00000000..c42e2cf1 --- /dev/null +++ b/web/shl/components/ModuleNavShell.tsx @@ -0,0 +1,73 @@ +"use client"; + +// Globale Navigation ueber alle Modul-Frontends (SHL-01 Akzeptanzkriterium 1). +// Statische Registry statt Live-Abruf: die Modul-Frontends dieses Testhosts +// nutzen unterschiedliche, noch nicht vereinheitlichte Anmeldemodelle +// (Core-Sitzung vs. feste Demo-Kennung per Umgebungsvariable) -- ein +// gemeinsamer Live-Abruf ueber /api/visible-modules wuerde fuer die Haelfte +// der Module eine Core-Anmeldung voraussetzen, die sie bisher nicht haben. +// Dieselbe Liste wie web/portal/config/modules.json, hier dupliziert, weil +// jedes Modul sein eigenes, unabhaengig gebautes Next.js-Bundle ist. + +import type { ReactNode } from "react"; +import { Shell } from "./Shell"; + +export interface ModuleRegistryEntry { + key: string; + label: string; + href: string; +} + +export const MODULE_REGISTRY: ModuleRegistryEntry[] = [ + { key: "dms", label: "Dokumente", href: "http://localhost:3101" }, + { key: "dms_upload", label: "Dokumente hochladen", href: "http://localhost:3107" }, + { key: "mail", label: "Postfach", href: "http://localhost:3102" }, + { key: "workflow", label: "Aufgaben & Freigaben", href: "http://localhost:3103" }, + { key: "workflow_cases", label: "Akten & Vorgänge", href: "http://localhost:3108" }, + { key: "routing_rules", label: "Routing-Regeln", href: "http://localhost:3109" }, + { key: "ai", label: "KI-Funktionen", href: "http://localhost:3104" }, + { key: "ai_extraction", label: "Extraktions-Korrektur", href: "http://localhost:3110" }, + { key: "connect", label: "Externe Anbindungen", href: "http://localhost:3105" }, + { key: "connect_datev", label: "DATEV-Export", href: "http://localhost:3111" }, + { key: "tenant_admin", label: "Mandantenverwaltung", href: "http://localhost:3106" }, + { key: "rbac_admin", label: "Rollen & Rechte", href: "http://localhost:3112" }, + { key: "kennzahlen_dashboard", label: "Kennzahlen", href: "http://localhost:3113" }, + { key: "portal", label: "Übersicht", href: "http://localhost:3900" }, +]; + +export interface ModuleNavShellProps { + activeKey: string; + children: ReactNode; +} + +function resolveHref(href: string): string { + try { + const url = new URL(href); + if (url.hostname === "localhost" && typeof window !== "undefined" && window.location.hostname !== "localhost") { + url.hostname = window.location.hostname; + } + return url.toString(); + } catch { + return href; + } +} + +function tenantLabel(): string { + if (typeof process === "undefined") return ""; + return process.env.NEXT_PUBLIC_TENANT_SLUG ?? process.env.NEXT_PUBLIC_MAIL_TENANT_SLUG ?? "demo"; +} + +export function ModuleNavShell({ activeKey, children }: ModuleNavShellProps) { + const modules = MODULE_REGISTRY.map((m) => ({ + key: m.key, + label: m.label, + href: resolveHref(m.href), + active: m.key === activeKey, + })); + + return ( + + {children} + + ); +} diff --git a/web/shl/components/Shell.tsx b/web/shl/components/Shell.tsx index 81fc64f5..cdfbb6e0 100644 --- a/web/shl/components/Shell.tsx +++ b/web/shl/components/Shell.tsx @@ -40,14 +40,41 @@ const responsiveStyles = ` outline-offset: 2px; } .shl-shell *, .shl-shell *::before, .shl-shell *::after { box-sizing: inherit; } - .shl-shell-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 8px 16px; } - .shl-shell-header nav ul { display: flex; gap: 8px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; } - .shl-shell-header nav a, + .shl-shell-header { + display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; + padding: 10px 20px; + background: var(--shl-color-surface-raised, #fff); + border-bottom: 1px solid var(--shl-color-border, #d7dbe0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + position: sticky; top: 0; z-index: 10; + } + .shl-shell-header nav ul { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; } + .shl-shell-header nav a { + min-height: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 14px; + border-radius: 8px; font-weight: 500; text-decoration: none; + color: var(--shl-color-text-secondary, #4b5563); + transition: background-color 0.15s ease, color 0.15s ease; + } + .shl-shell-header nav a:hover { background: var(--shl-color-accent-subtle, #eef3ff); } + .shl-shell-header nav a[aria-current="page"] { + background: var(--shl-color-accent-subtle, #eef3ff); + color: var(--shl-color-accent, #1d4ed8); + font-weight: 600; + } .shl-shell-header button, .shl-nav-toggle { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 12px; + border-radius: 8px; } .shl-nav-toggle { display: none; background: none; border: 1px solid var(--shl-color-border, #d7dbe0); font-size: 1.25rem; } + .shl-shell-context { display: flex; align-items: center; gap: 12px; } + .shl-tenant-context { + font-size: 0.85rem; font-weight: 500; color: var(--shl-color-text-secondary, #4b5563); + padding: 4px 10px; border-radius: 999px; background: var(--shl-color-surface, #f5f6f8); + } + .shl-shell-context button { + background: none; border: 1px solid var(--shl-color-border, #d7dbe0); + } @media (max-width: 768px) { .shl-nav-toggle { display: inline-flex; } .shl-shell-header nav { display: none; width: 100%; order: 3; } diff --git a/web/shl/index.ts b/web/shl/index.ts index 5c1300ef..677e3b38 100644 --- a/web/shl/index.ts +++ b/web/shl/index.ts @@ -14,6 +14,9 @@ export type { Locale } from "./i18n/messages"; export { Shell } from "./components/Shell"; export type { ShellProps, ModuleLink } from "./components/Shell"; +export { ModuleNavShell, MODULE_REGISTRY } from "./components/ModuleNavShell"; +export type { ModuleNavShellProps, ModuleRegistryEntry } from "./components/ModuleNavShell"; + export { Dialog } from "./components/Dialog"; export type { DialogProps } from "./components/Dialog"; diff --git a/web/tenant-admin/app/layout.tsx b/web/tenant-admin/app/layout.tsx index 766b4359..0d018288 100644 --- a/web/tenant-admin/app/layout.tsx +++ b/web/tenant-admin/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Mandantenverwaltung", @@ -21,7 +21,7 @@ export default function RootLayout({ > - {children} + {children} diff --git a/web/workflow-approvals/app/layout.tsx b/web/workflow-approvals/app/layout.tsx index 8abb8ff4..7efb068f 100644 --- a/web/workflow-approvals/app/layout.tsx +++ b/web/workflow-approvals/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Genehmigungen", @@ -36,7 +36,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/web/workflow-cases/app/layout.tsx b/web/workflow-cases/app/layout.tsx index cf98c5df..3f64ffe8 100644 --- a/web/workflow-cases/app/layout.tsx +++ b/web/workflow-cases/app/layout.tsx @@ -1,5 +1,5 @@ import Script from "next/script"; -import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; +import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; export const metadata = { title: "NEXARCH Akten", @@ -36,7 +36,7 @@ export default function RootLayout({ - {children} + {children}