Files
Dashboard/features/dashboard/components/layout/AppShell.tsx
T
dejanandClaude Opus 5 b8bf987d0e feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe
UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen
SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung,
nicht der Produktivbetrieb.

Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten),
26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein
deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026).

Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner
(Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und
IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste
Abweichungen stehen in docs/HERKUNFT.md.

Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM,
KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in
HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen).

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

160 lines
7.0 KiB
TypeScript

"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
FeatherBell,
FeatherHelpCircle,
FeatherPanelLeft,
FeatherSearch,
} from "@subframe/core";
import { ALLE_SEITEN, NAV_GRUPPEN } 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: feste Seitenleiste links, Inhalt rechts.
*
* Warum Seitenleiste statt Tab-Leiste: Die echte HiveGrid-Oberfläche
* navigiert links; so passt der Prototyp beim Einbau ohne Umbau der
* Informationsarchitektur. Die Leiste lässt sich auf Icon-Breite einklappen,
* damit breite Tabellen und 12-Spalten-Raster Platz bekommen.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const [eingeklappt, setEingeklappt] = React.useState(false);
const aktiveSeite =
ALLE_SEITEN.find((s) => s.href !== "/" && pathname.startsWith(s.href)) ??
ALLE_SEITEN.find((s) => s.href === "/");
return (
<div className="flex min-h-screen w-full bg-neutral-50">
<aside
className={`dv-no-print sticky top-0 flex h-screen flex-none flex-col border-r border-solid border-border-default bg-white transition-[width] duration-200 ${
eingeklappt ? "w-[68px]" : "w-[248px]"
}`}
>
<div className="flex h-14 flex-none items-center gap-2 border-b border-solid border-border-default px-4">
<span className="flex h-8 w-8 flex-none items-center justify-center rounded-lg bg-[#161616] text-[13px] font-bold text-white">
HG
</span>
{!eingeklappt ? (
<div className="flex min-w-0 flex-col">
<span className="truncate text-[13px] font-semibold leading-tight text-text-neutral-primary">
HiveGrid
</span>
<span className="truncate text-[11px] leading-tight text-text-neutral-tertiary">
Analytics
</span>
</div>
) : null}
</div>
<nav className="flex min-h-0 grow flex-col gap-5 overflow-y-auto px-3 py-4">
{NAV_GRUPPEN.map((gruppe) => (
<div key={gruppe.titel} className="flex flex-col gap-1">
{!eingeklappt ? (
<span className="px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-text-neutral-tertiary">
{gruppe.titel}
</span>
) : (
<span className="mx-2 mb-1 h-px bg-border-default" />
)}
{gruppe.eintraege.map((eintrag) => {
const aktiv = eintrag.href === pathname;
return (
<Link
key={eintrag.href}
href={eintrag.href}
title={eingeklappt ? eintrag.label : eintrag.beschreibung}
aria-current={aktiv ? "page" : undefined}
className={`flex items-center gap-2.5 rounded-md px-2.5 py-2 text-[13px] font-medium transition-colors ${
aktiv
? "bg-neutral-100 text-text-neutral-primary"
: "text-text-neutral-secondary hover:bg-neutral-50 hover:text-text-neutral-primary"
}`}
>
<FeatherIcon
name={eintrag.icon}
className={`text-[17px] ${aktiv ? "text-text-neutral-primary" : "text-text-neutral-tertiary"}`}
/>
{!eingeklappt ? <span className="truncate">{eintrag.label}</span> : null}
</Link>
);
})}
</div>
))}
</nav>
<div className="flex-none border-t border-solid border-border-default px-3 py-3">
{!eingeklappt ? (
<div className="mb-3 rounded-md bg-neutral-50 px-3 py-2.5">
<div className="text-[11px] font-semibold uppercase tracking-wide text-text-neutral-tertiary">
Demo-Datenstand
</div>
<div className="mt-1 text-[12px] leading-relaxed text-text-neutral-secondary">
{formatZahl(DATENSATZ_INFO.auftraege)} Aufträge
<br />
{formatZahl(DATENSATZ_INFO.rechnungen)} Rechnungen
<br />
Stichtag {formatDatum(HEUTE.toISOString())}
</div>
</div>
) : null}
<button
onClick={() => setEingeklappt((wert) => !wert)}
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-[13px] font-medium text-text-neutral-tertiary hover:bg-neutral-50 hover:text-text-neutral-primary"
aria-label={eingeklappt ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
>
<FeatherPanelLeft className="text-[17px]" />
{!eingeklappt ? <span>Einklappen</span> : null}
</button>
</div>
</aside>
<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">Statistiken</span>
<span className="text-text-neutral-subtle">/</span>
<span className="truncate font-semibold text-text-neutral-primary">
{aktiveSeite?.label ?? "Übersicht"}
</span>
</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-[10px] 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>
);
}