feat(ui): Huelle mit Seitenleiste und Platzhalterseiten (#002)
Die Huelle bekommt Benutzer und optional den aktiven Pfad als Props und kennt weder Sitzung noch Datenbank; ohne Pfad ermittelt die Navigation ihn per usePathname(), weil ein Layout den Pfad nicht kennt. Die Benutzerermittlung steckt in huellenBenutzerErmitteln(), damit #004 nur diese Funktion ersetzen muss. Unter 1024 px klappt die Leiste ein; geschlossen ist sie invisible und damit auch nicht per Tab erreichbar. Alle Navigationsziele haben eine Platzhalterseite, damit nichts auf eine 404 fuehrt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Einstellungen" };
|
||||||
|
|
||||||
|
export default function Einstellungen() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Einstellungen"
|
||||||
|
untertitel="Deployment-Key, eigene Vorlagen, Benachrichtigungen"
|
||||||
|
phase={3}
|
||||||
|
issue="#019"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Hostgruppen" };
|
||||||
|
|
||||||
|
export default function Hostgruppen() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Hostgruppen"
|
||||||
|
untertitel="Hosts bündeln, um Schlüssel gemeinsam zuzuweisen"
|
||||||
|
phase={1}
|
||||||
|
issue="#008"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Hosts" };
|
||||||
|
|
||||||
|
export default function Hosts() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Hosts"
|
||||||
|
untertitel="Verwaltete Server, Host-Keys und Erreichbarkeit"
|
||||||
|
phase={1}
|
||||||
|
issue="#008"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { HuellenBenutzer } from "@/components/huelle/typen";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den Benutzer, den die Seitenleiste anzeigt.
|
||||||
|
*
|
||||||
|
* TODO (#004): durch die echte Sitzung ersetzen (Benutzer + Rolle aus der Datenbank,
|
||||||
|
* Herkunft „authentik“ oder „Notfall-Admin“) und ohne Sitzung zur Anmeldung umleiten.
|
||||||
|
* Bis dahin ein fester Beispielbenutzer, damit der Prototyp ohne Anmeldung läuft.
|
||||||
|
* async schon jetzt, damit sich die Aufrufer beim Umstieg nicht ändern müssen.
|
||||||
|
*/
|
||||||
|
export async function huellenBenutzerErmitteln(): Promise<HuellenBenutzer> {
|
||||||
|
return { name: "Florian Wilde", rolle: "ADMIN", herkunft: "authentik" };
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Huelle } from "@/components/huelle/Huelle";
|
||||||
|
import { huellenBenutzerErmitteln } from "./huellen-benutzer";
|
||||||
|
|
||||||
|
/** Alle angemeldeten Seiten teilen sich die Hülle mit Seitenleiste (#002). */
|
||||||
|
export default async function AppLayout({ children }: { children: ReactNode }) {
|
||||||
|
const benutzer = await huellenBenutzerErmitteln();
|
||||||
|
return <Huelle benutzer={benutzer}>{children}</Huelle>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Personen" };
|
||||||
|
|
||||||
|
export default function Personen() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite titel="Personen" untertitel="Wem welche Schlüssel gehören" phase={2} issue="#013" />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Audit-Log" };
|
||||||
|
|
||||||
|
export default function Protokoll() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Audit-Log"
|
||||||
|
untertitel="Lückenloses, verkettetes Protokoll aller Aktionen"
|
||||||
|
phase={5}
|
||||||
|
issue="#027"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Schlüssel" };
|
||||||
|
|
||||||
|
export default function Schluessel() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Schlüssel"
|
||||||
|
untertitel="SSH-Schlüssel erzeugen, importieren und ausrollen"
|
||||||
|
phase={2}
|
||||||
|
issue="#012"
|
||||||
|
/>
|
||||||
|
<p className="text-muted text-[14px]">
|
||||||
|
Prototyp ansehen:{" "}
|
||||||
|
<Link href="/schluessel/beispiel" className="text-accent font-medium hover:underline">
|
||||||
|
Schlüsseldetail mit Beispieldaten
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Vorlagen" };
|
||||||
|
|
||||||
|
export default function Vorlagen() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Vorlagen"
|
||||||
|
untertitel="Wiederkehrende Zugänge wie Deployment oder Backup"
|
||||||
|
phase={3}
|
||||||
|
issue="#019"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: "Zuweisungen" };
|
||||||
|
|
||||||
|
export default function Zuweisungen() {
|
||||||
|
return (
|
||||||
|
<Platzhalterseite
|
||||||
|
titel="Zuweisungen"
|
||||||
|
untertitel="Welcher Schlüssel auf welchem Host-Konto gilt"
|
||||||
|
phase={2}
|
||||||
|
issue="#016"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Bildmarke, Kurzname, Marke, Wortmarke } from "@/components/marke";
|
||||||
|
import { VERSION } from "@/lib/version";
|
||||||
|
import { Navigation } from "./Navigation";
|
||||||
|
import { SeitenleistenRahmen } from "./SeitenleistenRahmen";
|
||||||
|
import { ROLLEN_TEXT, type HuellenBenutzer } from "./typen";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grundlayout (#002): feste Seitenleiste links (248 px), Hauptbereich rechts.
|
||||||
|
* Benutzer und aktiver Pfad kommen als Props – die Hülle kennt weder Sitzung noch
|
||||||
|
* Datenbank, damit sie in Tests und Prototypen ohne Anmeldung rendert.
|
||||||
|
*/
|
||||||
|
export function Huelle({
|
||||||
|
benutzer,
|
||||||
|
aktiverPfad,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
benutzer: HuellenBenutzer;
|
||||||
|
/** Ohne Angabe ermittelt die Navigation den Pfad im Browser. */
|
||||||
|
aktiverPfad?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-bg min-h-dvh lg:flex">
|
||||||
|
<a
|
||||||
|
href="#inhalt"
|
||||||
|
className="bg-accent text-bg rounded-box sr-only z-[60] px-4 py-3 font-semibold focus:not-sr-only focus:fixed focus:top-3 focus:left-3"
|
||||||
|
>
|
||||||
|
Zum Inhalt springen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<SeitenleistenRahmen
|
||||||
|
kopf={
|
||||||
|
<Link href="/" className="rounded-box flex min-h-11 items-center gap-2 px-1">
|
||||||
|
<Bildmarke groesse={28} />
|
||||||
|
<Wortmarke groesse={19} />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex h-full flex-col overflow-y-auto px-4 pt-6 pb-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-box mb-8 flex items-center px-2 py-1"
|
||||||
|
aria-label="SSHKeyVault – Übersicht"
|
||||||
|
>
|
||||||
|
<Marke bildGroesse={34} textGroesse={22} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Navigation aktiverPfad={aktiverPfad} />
|
||||||
|
|
||||||
|
<div className="mt-auto pt-6">
|
||||||
|
<BenutzerKasten benutzer={benutzer} />
|
||||||
|
<p className="text-muted mt-3 px-2 font-mono text-[11.5px]">
|
||||||
|
<Kurzname /> v{VERSION}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SeitenleistenRahmen>
|
||||||
|
|
||||||
|
<main id="inhalt" tabIndex={-1} className="min-w-0 flex-1 outline-none">
|
||||||
|
<div className="mx-auto max-w-[1440px] px-4 py-6 sm:px-8 sm:py-8 xl:px-10">{children}</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BenutzerKasten({ benutzer }: { benutzer: HuellenBenutzer }) {
|
||||||
|
const kuerzel = benutzer.name
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((t) => t[0]?.toUpperCase())
|
||||||
|
.join("");
|
||||||
|
return (
|
||||||
|
<div className="border-border bg-surface rounded-box flex items-center gap-3 border p-3">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="bg-raised text-accent font-display flex size-9 shrink-0 items-center justify-center rounded-full text-[13px] font-semibold"
|
||||||
|
>
|
||||||
|
{kuerzel}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 text-[13px] leading-snug">
|
||||||
|
<p className="text-text truncate text-[14px] font-medium">{benutzer.name}</p>
|
||||||
|
<p className="text-muted truncate">
|
||||||
|
{ROLLEN_TEXT[benutzer.rolle]} · über {benutzer.herkunft}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { HAUPTNAVIGATION, istAktiv, NEBENNAVIGATION, type NavPunkt } from "./nav-punkte";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation der Seitenleiste. Client-Komponente, weil ein Layout den aktuellen Pfad
|
||||||
|
* nicht kennt: Ohne `aktiverPfad` nimmt sie ihn aus usePathname(); Seiten oder Tests
|
||||||
|
* können ihn ausdrücklich vorgeben.
|
||||||
|
*/
|
||||||
|
export function Navigation({ aktiverPfad }: { aktiverPfad?: string }) {
|
||||||
|
const routerPfad = usePathname();
|
||||||
|
const pfad = aktiverPfad ?? routerPfad ?? "/";
|
||||||
|
return (
|
||||||
|
<nav aria-label="Hauptnavigation" className="flex flex-col gap-1">
|
||||||
|
<NavListe punkte={HAUPTNAVIGATION} pfad={pfad} />
|
||||||
|
<hr className="border-border mx-2 my-3" />
|
||||||
|
<NavListe punkte={NEBENNAVIGATION} pfad={pfad} />
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavListe({ punkte, pfad }: { punkte: NavPunkt[]; pfad: string }) {
|
||||||
|
return (
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{punkte.map(({ href, titel, icon: Icon }) => {
|
||||||
|
const aktiv = istAktiv(href, pfad);
|
||||||
|
return (
|
||||||
|
<li key={href}>
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
aria-current={aktiv ? "page" : undefined}
|
||||||
|
className={clsx(
|
||||||
|
"rounded-box flex min-h-11 items-center gap-3 px-3 text-[14.5px] transition-colors",
|
||||||
|
aktiv
|
||||||
|
? "bg-raised text-accent font-semibold"
|
||||||
|
: "text-muted hover:bg-raised/60 hover:text-text font-medium",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon aria-hidden className="size-[18px] shrink-0" strokeWidth={aktiv ? 2.2 : 1.8} />
|
||||||
|
{titel}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Construction } from "lucide-react";
|
||||||
|
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||||||
|
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Für Navigationsziele, die erst in späteren Phasen gebaut werden (#002) – damit die
|
||||||
|
* Navigation nie auf eine 404 führt.
|
||||||
|
*/
|
||||||
|
export function Platzhalterseite({
|
||||||
|
titel,
|
||||||
|
untertitel,
|
||||||
|
phase,
|
||||||
|
issue,
|
||||||
|
}: {
|
||||||
|
titel: string;
|
||||||
|
untertitel?: string;
|
||||||
|
phase: number;
|
||||||
|
/** Issue-Nummer, z. B. "#008". */
|
||||||
|
issue?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-7">
|
||||||
|
<Seitenkopf titel={titel} untertitel={untertitel} />
|
||||||
|
<Leerzustand
|
||||||
|
icon={<Construction className="size-5" />}
|
||||||
|
titel={`Folgt in Phase ${phase}`}
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
Diese Ansicht ist noch nicht gebaut
|
||||||
|
{issue && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
– geplant in <span className="font-mono">{issue}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState, type MouseEvent, type ReactNode } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schaltet die Seitenleiste unter 1024 px ein und aus (#002). Der Inhalt der Leiste kommt
|
||||||
|
* als Server-gerenderte children; nur das Auf/Zu lebt im Browser.
|
||||||
|
*
|
||||||
|
* Geschlossen ist die Leiste mobil `invisible` und damit auch aus der Tab-Reihenfolge –
|
||||||
|
* nur verschieben würde unsichtbare Links fokussierbar lassen.
|
||||||
|
*/
|
||||||
|
export function SeitenleistenRahmen({ kopf, children }: { kopf: ReactNode; children: ReactNode }) {
|
||||||
|
const [offen, setOffen] = useState(false);
|
||||||
|
const knopf = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!offen) return;
|
||||||
|
function taste(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setOffen(false);
|
||||||
|
knopf.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("keydown", taste);
|
||||||
|
return () => window.removeEventListener("keydown", taste);
|
||||||
|
}, [offen]);
|
||||||
|
|
||||||
|
// Ein Klick auf einen Link in der Leiste navigiert – danach soll das Menü zu sein.
|
||||||
|
function klickInLeiste(e: MouseEvent<HTMLElement>) {
|
||||||
|
if ((e.target as HTMLElement).closest("a")) setOffen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Kopfleiste nur auf schmalen Bildschirmen */}
|
||||||
|
<div className="border-border bg-sidebar sticky top-0 z-30 flex h-14 items-center gap-2 border-b px-2 lg:hidden">
|
||||||
|
<button
|
||||||
|
ref={knopf}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOffen((o) => !o)}
|
||||||
|
aria-expanded={offen}
|
||||||
|
aria-controls="seitenleiste"
|
||||||
|
aria-label={offen ? "Menü schließen" : "Menü öffnen"}
|
||||||
|
className="text-text hover:bg-raised rounded-box inline-flex size-11 items-center justify-center"
|
||||||
|
>
|
||||||
|
{offen ? <X className="size-5" aria-hidden /> : <Menu className="size-5" aria-hidden />}
|
||||||
|
</button>
|
||||||
|
{kopf}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{offen && (
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="fixed inset-0 z-40 bg-black/60 lg:hidden"
|
||||||
|
onClick={() => setOffen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ab 1024 px eine eigene Spalte über die volle Seitenhöhe; die Leiste darin klebt
|
||||||
|
oben (sticky), damit Hintergrund und Rahmen auch bei langen Seiten durchlaufen. */}
|
||||||
|
<div className="lg:border-border lg:bg-sidebar lg:w-[var(--seitenleiste-breite)] lg:shrink-0 lg:border-r">
|
||||||
|
<aside
|
||||||
|
id="seitenleiste"
|
||||||
|
onClick={klickInLeiste}
|
||||||
|
className={clsx(
|
||||||
|
"border-border bg-sidebar fixed inset-y-0 left-0 z-50 flex w-[280px] max-w-[85vw] flex-col border-r",
|
||||||
|
"transition-[transform,visibility] duration-200 lg:visible lg:translate-x-0 lg:transition-none",
|
||||||
|
"lg:sticky lg:top-0 lg:bottom-auto lg:h-dvh lg:w-full lg:max-w-none lg:border-r-0",
|
||||||
|
offen ? "visible translate-x-0" : "invisible -translate-x-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Der Abdunkler verdeckt den Menü-Button oben, daher ein eigener Schließen-Knopf. */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOffen(false);
|
||||||
|
knopf.current?.focus();
|
||||||
|
}}
|
||||||
|
aria-label="Menü schließen"
|
||||||
|
className="text-muted hover:bg-raised hover:text-text rounded-box absolute top-4 right-2 inline-flex size-11 items-center justify-center lg:hidden"
|
||||||
|
>
|
||||||
|
<X className="size-5" aria-hidden />
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { Huelle } from "./Huelle";
|
||||||
|
export { Platzhalterseite } from "./Platzhalterseite";
|
||||||
|
export { ROLLEN_TEXT, type HuellenBenutzer, type Rolle } from "./typen";
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Boxes,
|
||||||
|
KeyRound,
|
||||||
|
LayoutDashboard,
|
||||||
|
LayoutTemplate,
|
||||||
|
Network,
|
||||||
|
ScrollText,
|
||||||
|
Server,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export type NavPunkt = { href: string; titel: string; icon: LucideIcon };
|
||||||
|
|
||||||
|
/** Hauptnavigation (#002). Reihenfolge folgt dem Arbeitsablauf: Hosts → Schlüssel → Zuweisen. */
|
||||||
|
export const HAUPTNAVIGATION: NavPunkt[] = [
|
||||||
|
{ href: "/", titel: "Übersicht", icon: LayoutDashboard },
|
||||||
|
{ href: "/hosts", titel: "Hosts", icon: Server },
|
||||||
|
{ href: "/hostgruppen", titel: "Hostgruppen", icon: Boxes },
|
||||||
|
{ href: "/schluessel", titel: "Schlüssel", icon: KeyRound },
|
||||||
|
{ href: "/personen", titel: "Personen", icon: Users },
|
||||||
|
{ href: "/zuweisungen", titel: "Zuweisungen", icon: Network },
|
||||||
|
{ href: "/vorlagen", titel: "Vorlagen", icon: LayoutTemplate },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Unter dem Trenner: Nachvollziehen und Verwalten statt Tagesgeschäft. */
|
||||||
|
export const NEBENNAVIGATION: NavPunkt[] = [
|
||||||
|
{ href: "/protokoll", titel: "Audit-Log", icon: ScrollText },
|
||||||
|
{ href: "/einstellungen", titel: "Einstellungen", icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** „/“ nur exakt, sonst zählt auch jede Unterseite (/schluessel/abc → Schlüssel). */
|
||||||
|
export function istAktiv(href: string, pfad: string): boolean {
|
||||||
|
if (href === "/") return pfad === "/";
|
||||||
|
return pfad === href || pfad.startsWith(`${href}/`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export type Rolle = "ADMIN" | "OPERATOR" | "LESER";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was die Hülle vom angemeldeten Benutzer wissen muss – bewusst nur Anzeigedaten,
|
||||||
|
* damit die Hülle unabhängig von Sitzung und Datenbank bleibt (#002, #004).
|
||||||
|
*/
|
||||||
|
export type HuellenBenutzer = { name: string; rolle: Rolle; herkunft: string };
|
||||||
|
|
||||||
|
export const ROLLEN_TEXT: Record<Rolle, string> = {
|
||||||
|
ADMIN: "Admin",
|
||||||
|
OPERATOR: "Operator",
|
||||||
|
LESER: "Leser",
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user