diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..117726b --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { DashboardPlatzhalterSeite } from "@/features/dashboard/pages/DashboardPlatzhalterSeite"; + +export default function Page() { + return ; +} diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md index 380bc3d..42856f5 100644 --- a/docs/NAVIGATION.md +++ b/docs/NAVIGATION.md @@ -9,7 +9,7 @@ was dazukommt, nicht was schon da ist. ``` HIVEGRID -├── Dashboard eigenes Feature, nicht Teil dieses Prototyps +├── Dashboard Platzhalterseite — eigenes Feature, hier nicht gestaltet └── Analytics ◄── der Prototyp lebt hier /analytics ├───────────────────── └── Komponenten Bausteingalerie des Prototyps /komponenten @@ -60,8 +60,11 @@ Leads mit seinen acht. Drei Gründe geben den Ausschlag: Ein weiterer Vorteil: Die Leiste wächst. Käme eine sechste Analyse-Seite dazu, ist das eine Zeile mehr — ein Reiterband würde eng. -„Dashboard" führt hier nirgendwohin und meldet das beim Klick — es ist ein -eigenes Feature, siehe [SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md). +„Dashboard" führt auf eine **Platzhalterseite** unter `/dashboard`: ein +Leerzustand, der erklärt, warum hier nichts steht und was später hinkommt. Ein +Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob etwas fehlt oder +etwas kaputt ist. Siehe +[SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md). --- diff --git a/features/dashboard/components/layout/AppShell.tsx b/features/dashboard/components/layout/AppShell.tsx index cc980a7..bfddbd0 100644 --- a/features/dashboard/components/layout/AppShell.tsx +++ b/features/dashboard/components/layout/AppShell.tsx @@ -7,7 +7,6 @@ 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 { useToast } from "../../context/ToastContext"; import { DATENSATZ_INFO } from "../../data/seed"; import { formatDatum, formatZahl } from "../../utils/formatters"; import { HEUTE } from "../../utils/dateUtils"; @@ -20,24 +19,27 @@ import { HEUTE } from "../../utils/dateUtils"; * Software öffnet, soll dieselbe Navigation sehen; die Übergabe wird dadurch * eine Frage des Einsetzens und nicht des Übersetzens. * - * **Nur „Analytics" führt hier irgendwohin.** Die übrigen Einträge zeigen den - * Ort in der fertigen Anwendung und melden beim Klick, dass sie zur Software - * gehören — eine tote Schaltfläche ohne Rückmeldung wäre schlechter als gar - * keine. + * 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 { zeigeToast } = useToast(); const [eingeklappt, setEingeklappt] = React.useState(false); const istAktiv = (eintrag: NavEintrag) => - eintrag.ziel.art === "route" && - (eintrag.ziel.href === "/analytics" + eintrag.ziel.href === "/analytics" ? pathname.startsWith("/analytics") - : pathname.startsWith(eintrag.ziel.href)); + : pathname === eintrag.ziel.href; - const renderEintrag = (eintrag: NavEintrag) => { - const item = ( + const renderEintrag = (eintrag: NavEintrag) => ( + } selected={istAktiv(eintrag)} @@ -45,40 +47,17 @@ export function AppShell({ children }: { children: React.ReactNode }) { > {eintrag.label} - ); - - if (eintrag.ziel.art === "route") { - return ( - - {item} - - ); - } - - return ( - - ); - }; + + ); const aktiveAnalyse = ANALYSE_SEITEN.find((s) => s.href === "/analytics" ? pathname === "/analytics" : pathname.startsWith(s.href) ); - const bereich = pathname.startsWith("/komponenten") ? "Komponenten" : "Analytics"; + const bereich = pathname.startsWith("/komponenten") + ? "Komponenten" + : pathname.startsWith("/dashboard") + ? "Dashboard" + : "Analytics"; return (
diff --git a/features/dashboard/components/layout/PageHeader.tsx b/features/dashboard/components/layout/PageHeader.tsx index 74eba73..21c6e0d 100644 --- a/features/dashboard/components/layout/PageHeader.tsx +++ b/features/dashboard/components/layout/PageHeader.tsx @@ -13,10 +13,17 @@ export function PageHeader({ titel, beschreibung, aktionen, + /** + * Aktualisieren, Export und Teilen. Auf Seiten ohne Inhalt gibt es nichts zu + * aktualisieren oder zu exportieren — dort schaltet man sie ab, statt + * Schaltflächen anzubieten, die ins Leere greifen. + */ + standardAktionen = true, }: { titel: string; beschreibung: string; aktionen?: React.ReactNode; + standardAktionen?: boolean; }) { const { zeigeToast } = useToast(); @@ -31,6 +38,8 @@ export function PageHeader({
{aktionen} + {standardAktionen ? ( + <> + + ) : null}
); diff --git a/features/dashboard/components/layout/navigation.ts b/features/dashboard/components/layout/navigation.ts index d45ce89..fe6bb45 100644 --- a/features/dashboard/components/layout/navigation.ts +++ b/features/dashboard/components/layout/navigation.ts @@ -12,12 +12,12 @@ import type { SeitenId } from "../../data/filtering"; * (`ui/components/SidebarWithSections`, aus der Cloud-Fassung portiert), damit * Aussehen und Verhalten übereinstimmen. * - * „Dashboard" führt hier nirgendwohin: Es ist ein eigenes Feature und nicht - * Teil dieses Prototyps (siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`). Der - * Eintrag meldet das beim Klick, statt still zu bleiben. + * „Dashboard" führt auf eine Platzhalterseite: Es ist ein eigenes Feature und + * in diesem Prototyp nicht gestaltet (siehe + * `docs/SCOPE-ANALYTICS-DASHBOARD.md`). */ -export type NavZiel = { art: "route"; href: string } | { art: "software" }; +export type NavZiel = { art: "route"; href: string }; export interface NavEintrag { id: string; @@ -43,12 +43,13 @@ export const NAV_GRUPPEN: NavGruppe[] = [ titel: "", eintraege: [ { - // Gehört zu uns, ist aber ein eigenes Feature und nicht Teil dieses - // Prototyps — siehe docs/SCOPE-ANALYTICS-DASHBOARD.md. + // Eigenes Feature, in diesem Prototyp nicht gestaltet. Der Punkt führt + // trotzdem auf eine Seite: Ein Menüeintrag, der nur einen Hinweis + // auslöst, lässt offen, ob etwas fehlt oder etwas kaputt ist. id: "dashboard", label: "Dashboard", icon: "LayoutDashboard", - ziel: { art: "software" }, + ziel: { art: "route", href: "/dashboard" }, }, { // Der neue Menüpunkt. Die fünf Analyse-Seiten sind ausschließlich hier diff --git a/features/dashboard/pages/DashboardPlatzhalterSeite.tsx b/features/dashboard/pages/DashboardPlatzhalterSeite.tsx new file mode 100644 index 0000000..9e56f98 --- /dev/null +++ b/features/dashboard/pages/DashboardPlatzhalterSeite.tsx @@ -0,0 +1,69 @@ +"use client"; + +import Link from "next/link"; +import { FeatherArrowRight } from "@subframe/core"; +import { Button } from "@/ui/components/Button"; +import { EmptyState } from "@/ui/components/EmptyState"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer } from "../components/layout/PageContainer"; +import { ANALYTICS_START } from "../components/layout/navigation"; + +/** + * Platzhalter für das Dashboard. + * + * Das Dashboard ist ein eigenes Feature und in diesem Prototyp nicht + * gestaltet — siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`. Der Menüpunkt führt + * trotzdem auf eine echte Seite statt nur einen Hinweis auszulösen: Ein + * Navigationspunkt, der nirgendwohin führt, lässt den Leser im Unklaren, ob + * etwas fehlt oder etwas kaputt ist. Diese Seite sagt es ihm. + * + * Der Inhalt beschreibt, was hier später steht — nicht als Gestaltung, sondern + * als Verabredung. So bleibt die Seite ehrlich leer und trotzdem nützlich. + */ +export function DashboardPlatzhalterSeite() { + return ( + <> + + + + + + Analytics und Dashboard sind zwei Features. Dieser Prototyp + gestaltet Analytics — den Analysebereich mit + Zeitraum- und Dimensionsfiltern. Das Dashboard beantwortet eine + andere Frage: {"„Wie ist meine Lage?“ statt „Warum ist das so?“."} + + + Geplant sind drei feste Rollen-Layouts (Geschäftsführung, Büro, + Monteur) aus Widgets. Ein Teil davon kommt aus Analytics — + Kennzahlen-Kacheln, Trend-Diagramme, Listen. Der andere Teil + nicht: offene HR-Anträge, eigene Aufgaben, Termine des Tages. + + + Zuschnitt und offene Punkte stehen in{" "} + + docs/SCOPE-ANALYTICS-DASHBOARD.md + + + + } + action={ + + + + } + /> + + + ); +} diff --git a/scripts/verify-layout.mjs b/scripts/verify-layout.mjs index 58ce6a3..d3aede3 100644 --- a/scripts/verify-layout.mjs +++ b/scripts/verify-layout.mjs @@ -19,6 +19,7 @@ import { chromium } from "playwright-core"; const BASIS = process.env.DASHBOARD_URL ?? "http://localhost:4300"; const SEITEN = [ + "/dashboard", "/analytics", "/analytics/finanzen", "/analytics/auftraege", diff --git a/ui/components/EmptyState/EmptyState.tsx b/ui/components/EmptyState/EmptyState.tsx index 4ba5566..2cc99a4 100644 --- a/ui/components/EmptyState/EmptyState.tsx +++ b/ui/components/EmptyState/EmptyState.tsx @@ -7,103 +7,68 @@ import React from "react"; import * as SubframeUtils from "../../utils"; -import { Button } from "../Button"; -export interface EmptyStateRootProps - extends Omit, "title"> { +export interface EmptyStateRootProps extends Omit, "title"> { variant?: "default" | "no-results" | "error"; outlined?: boolean; fillContainer?: boolean; title?: React.ReactNode; body?: React.ReactNode; + // Optional call-to-action rendered below the text. When omitted (the default), + // no button is shown. Replaces the previous hard-coded "Create project" button. + action?: React.ReactNode; className?: string; } -const EmptyStateRoot = React.forwardRef( - function EmptyStateRoot( - { - variant = "default", - outlined = false, - fillContainer = false, - title, - body, - className, - ...otherProps - }: EmptyStateRootProps, - ref - ) { - return ( -
-
-
- - - -
- {title ? ( - - {title} - - ) : null} - {body ? ( - - {body} - - ) : null} -
- +const EmptyStateRoot = React.forwardRef(function EmptyStateRoot( + { variant = "default", outlined = false, fillContainer = false, title, body, action, className, ...otherProps }: EmptyStateRootProps, + ref +) { + return ( +
+
+
+ + + +
+ {title ? {title} : null} + {body ? ( + {body} + ) : null}
+ {action ?
{action}
: null}
- ); - } -); +
+ ); +}); export const EmptyState = EmptyStateRoot; diff --git a/ui/components/EmptyState/index.tsx b/ui/components/EmptyState/index.tsx index 5d5aa8a..20113fe 100644 --- a/ui/components/EmptyState/index.tsx +++ b/ui/components/EmptyState/index.tsx @@ -1,10 +1,18 @@ "use client"; -import { EmptyState as EmptyStateComponent } from "./EmptyState"; +import React from "react"; +import { EmptyState as EmptyStateComponent, type EmptyStateRootProps } from "./EmptyState"; /** * Add wrapper components and business logic here. * If you modify this file, disable Subframe sync for it to prevent overwrites. * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + * + * History: this wrapper used to add `[&_button]:hidden` to suppress the synced + * component's hardcoded, non-wired english CTA ("Create project"). The synced + * component has since gained a proper `action` slot and no hardcoded button, + * so the class only hid the real action buttons screens pass in (create/retry + * CTAs). Do not re-add a blanket button-hiding class here. */ - -export const EmptyState = EmptyStateComponent; +export const EmptyState = React.forwardRef(function EmptyState(props, ref) { + return ; +});