Files
Dashboard/features/dashboard/components/layout/AppShell.tsx
T
dejanandClaude Opus 5 7183fad27a feat(dashboard): Platzhalterseite statt Hinweis-Toast
Der Menüpunkt 'Dashboard' löste bisher nur einen Toast aus. Ein
Navigationseintrag, der nirgendwohin führt, lässt den Leser im Unklaren, ob
etwas fehlt oder etwas kaputt ist.

Jetzt führt er auf /dashboard: ein Leerzustand nach dem Hausmuster
(ui/components/EmptyState), der benennt, warum hier nichts steht, was später
hinkommt (drei Rollen-Layouts, Widgets teils aus Analytics, teils nicht — offene
HR-Anträge, Aufgaben, Termine) und wo der Zuschnitt dokumentiert ist. Mit einem
Weg zurück nach Analytics.

Dabei:
- EmptyState aus der Cloud-Fassung nachgezogen; unsere Kopie hatte den
  action-Slot noch nicht.
- PageHeader bekommt 'standardAktionen': Auf einer leeren Seite gibt es nichts
  zu aktualisieren oder zu exportieren, also stehen dort auch keine
  Schaltflächen dafür.
- Die Variante 'software' in NavZiel entfällt — jeder Eintrag führt jetzt auf
  eine echte Seite, der Toast-Zweig in der AppShell war damit toter Code.
- verify-layout deckt /dashboard mit ab (jetzt 7 Seiten x 7 Breiten).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 20:56:35 +02:00

145 lines
6.1 KiB
TypeScript

"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FeatherBell, FeatherHelpCircle, FeatherSearch } from "@subframe/core";
import { SidebarWithSections } from "@/ui/components/SidebarWithSections";
import { NAV_GRUPPEN, NAV_SEKUNDAER, ANALYSE_SEITEN, type NavEintrag } from "./navigation";
import { FeatherIcon } from "./FeatherIcon";
import { DATENSATZ_INFO } from "../../data/seed";
import { formatDatum, formatZahl } from "../../utils/formatters";
import { HEUTE } from "../../utils/dateUtils";
/**
* Rahmen der Anwendung — Seitenleiste nach dem Vorbild von HiveGrid Cloud.
*
* Bewusst dieselbe Komponente wie dort (`~/ui/components/SidebarWithSections`)
* mit denselben Gruppen und derselben Reihenfolge. Wer den Prototyp neben der
* Software öffnet, soll dieselbe Navigation sehen; die Übergabe wird dadurch
* eine Frage des Einsetzens und nicht des Übersetzens.
*
* Jeder Eintrag führt auf eine echte Seite. „Dashboard" landet auf einer
* Platzhalterseite, weil es ein eigenes Feature ist und hier nicht gestaltet
* wird — aber ein Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob
* etwas fehlt oder etwas kaputt ist.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const [eingeklappt, setEingeklappt] = React.useState(false);
const istAktiv = (eintrag: NavEintrag) =>
eintrag.ziel.href === "/analytics"
? pathname.startsWith("/analytics")
: pathname === eintrag.ziel.href;
const renderEintrag = (eintrag: NavEintrag) => (
<Link
key={eintrag.id}
href={eintrag.ziel.href}
title={eingeklappt ? eintrag.label : undefined}
className="w-full no-underline"
>
<SidebarWithSections.NavItem
icon={<FeatherIcon name={eintrag.icon} />}
selected={istAktiv(eintrag)}
variant={eingeklappt ? "icon-only" : "icon-and-text"}
>
{eintrag.label}
</SidebarWithSections.NavItem>
</Link>
);
const aktiveAnalyse = ANALYSE_SEITEN.find((s) =>
s.href === "/analytics" ? pathname === "/analytics" : pathname.startsWith(s.href)
);
const bereich = pathname.startsWith("/komponenten")
? "Komponenten"
: pathname.startsWith("/dashboard")
? "Dashboard"
: "Analytics";
return (
<div className="flex min-h-screen w-full bg-neutral-50">
<div className="dv-no-print sticky top-0 h-screen flex-none">
<SidebarWithSections
variant={eingeklappt ? "closed" : "open"}
onToggleCollapse={() => setEingeklappt((wert) => !wert)}
toggleLabel={eingeklappt ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
>
<div className="flex h-full min-h-0 w-full flex-col">
<div className="flex min-h-0 w-full grow flex-col gap-1 overflow-x-hidden overflow-y-auto">
{NAV_GRUPPEN.map((gruppe) => (
<SidebarWithSections.NavSection
key={gruppe.id}
label={gruppe.titel}
showLabel={!eingeklappt && !!gruppe.titel}
>
{gruppe.eintraege.map(renderEintrag)}
</SidebarWithSections.NavSection>
))}
</div>
<div className="sticky bottom-0 z-10 mt-auto flex w-full flex-none flex-col gap-1 bg-surface-neutral-secondary pt-2">
<SidebarWithSections.NavDivider />
{NAV_SEKUNDAER.map(renderEintrag)}
{/* Einzeilig statt als Kasten: Die Seitenleiste soll die der
Software spiegeln, und dort steht kein Demo-Hinweis. Die
Information bleibt, nimmt aber keinen Navigationsplatz weg. */}
{!eingeklappt ? (
<p className="px-3 pb-2 pt-1 text-[11px] leading-tight text-text-neutral-tertiary">
Demo-Daten · {formatZahl(DATENSATZ_INFO.auftraege)} Aufträge ·{" "}
{formatDatum(HEUTE.toISOString())}
</p>
) : null}
</div>
</div>
</SidebarWithSections>
</div>
<div className="flex min-w-0 grow flex-col">
<header className="dv-no-print sticky top-0 z-30 flex h-14 flex-none items-center gap-4 border-b border-solid border-border-default bg-white/95 px-6 backdrop-blur">
<div className="flex min-w-0 items-center gap-2 text-[13px]">
<span className="text-text-neutral-tertiary">{bereich}</span>
{aktiveAnalyse ? (
<>
<span className="text-text-neutral-subtle">/</span>
<span className="truncate font-semibold text-text-neutral-primary">
{aktiveAnalyse.label}
</span>
</>
) : null}
</div>
<div className="ml-auto flex items-center gap-2">
<div className="hidden h-9 items-center gap-2 rounded-md border border-solid border-border-default px-3 text-[13px] text-text-neutral-subtle lg:flex">
<FeatherSearch className="text-[15px]" />
<span>Suchen</span>
<kbd className="ml-4 rounded border border-solid border-border-default bg-neutral-50 px-1.5 py-0.5 text-[11px] font-medium text-text-neutral-tertiary">
⌘K
</kbd>
</div>
<button
className="flex h-9 w-9 items-center justify-center rounded-md text-text-neutral-tertiary hover:bg-neutral-100"
aria-label="Benachrichtigungen"
>
<FeatherBell className="text-[17px]" />
</button>
<button
className="flex h-9 w-9 items-center justify-center rounded-md text-text-neutral-tertiary hover:bg-neutral-100"
aria-label="Hilfe"
>
<FeatherHelpCircle className="text-[17px]" />
</button>
<span className="ml-1 flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800 text-[12px] font-semibold text-white">
MK
</span>
</div>
</header>
<main className="min-w-0 grow">{children}</main>
</div>
</div>
);
}