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
160 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|