From b884e4e8c46be0ece58e5f95a81e8f23ed37ee8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 11 Mar 2026 11:13:37 +0100 Subject: [PATCH] add process stepper dashboard with phase cards, contact stats Co-Authored-By: Claude Opus 4.6 --- .../prozess/components/phase-card.tsx | 53 ++++++++ .../prozess/components/process-stepper.tsx | 71 +++++++++++ src/features/prozess/hooks.ts | 40 ++++++ src/features/prozess/index.ts | 2 + src/routeTree.gen.ts | 114 ++++++++++-------- src/routes/index.tsx | 3 +- src/routes/prozess/index.tsx | 25 ++++ 7 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 src/features/prozess/components/phase-card.tsx create mode 100644 src/features/prozess/components/process-stepper.tsx create mode 100644 src/features/prozess/hooks.ts create mode 100644 src/features/prozess/index.ts create mode 100644 src/routes/prozess/index.tsx diff --git a/src/features/prozess/components/phase-card.tsx b/src/features/prozess/components/phase-card.tsx new file mode 100644 index 0000000..df63bf9 --- /dev/null +++ b/src/features/prozess/components/phase-card.tsx @@ -0,0 +1,53 @@ +import { Badge } from "@/shared/components/ui/badge"; +import { Card, CardContent } from "@/shared/components/ui/card"; +import { cn } from "@/shared/lib/utils"; + +type PhaseStatus = "erledigt" | "aktuell" | "offen"; + +interface PhaseCardProps { + label: string; + beschreibung: string; + status: PhaseStatus; + index: number; +} + +export function PhaseCard({ + label, + beschreibung, + status, + index, +}: PhaseCardProps) { + return ( + + +
+ {status === "erledigt" ? "✓" : index + 1} +
+
+
+ {label} + {status === "aktuell" && Aktuell} +
+ {status === "aktuell" && ( +

{beschreibung}

+ )} +
+
+
+ ); +} diff --git a/src/features/prozess/components/process-stepper.tsx b/src/features/prozess/components/process-stepper.tsx new file mode 100644 index 0000000..90bf489 --- /dev/null +++ b/src/features/prozess/components/process-stepper.tsx @@ -0,0 +1,71 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card"; +import type { ProzessSchritt } from "@/shared/db/schema"; +import { PROZESS_SCHRITTE } from "@/shared/lib/constants"; +import { PhaseCard } from "./phase-card"; + +interface ProcessStepperProps { + aktuellerSchritt: ProzessSchritt; + kontaktGesamt: number; + absagen: number; +} + +export function ProcessStepper({ + aktuellerSchritt, + kontaktGesamt, + absagen, +}: ProcessStepperProps) { + const currentIndex = PROZESS_SCHRITTE.findIndex( + (s) => s.key === aktuellerSchritt, + ); + + return ( +
+
+

Dein Fortschritt

+ + Schritt {currentIndex + 1} von {PROZESS_SCHRITTE.length} + +
+ +
+ {PROZESS_SCHRITTE.map((schritt, i) => { + const status = + i < currentIndex + ? "erledigt" + : i === currentIndex + ? "aktuell" + : "offen"; + + return ( + + ); + })} +
+ + {currentIndex >= 3 && ( + + + Kontaktübersicht + + +

+ {kontaktGesamt} Kontaktversuche insgesamt, davon {absagen}{" "} + Absagen. +

+
+
+ )} +
+ ); +} diff --git a/src/features/prozess/hooks.ts b/src/features/prozess/hooks.ts new file mode 100644 index 0000000..f286f50 --- /dev/null +++ b/src/features/prozess/hooks.ts @@ -0,0 +1,40 @@ +import type { ProzessSchritt } from "@/shared/db/schema"; +import { dbExec, useDbQuery } from "@/shared/hooks/use-db"; + +interface NutzerRow { + id: number; + name: string; + aktueller_schritt: ProzessSchritt; + dringlichkeitscode: boolean; + tss_beantragt: boolean; + krankenkasse: string; +} + +interface KontaktStats { + gesamt: number; + absagen: number; + warteliste: number; + keine_antwort: number; +} + +export function useNutzer() { + return useDbQuery("SELECT * FROM nutzer LIMIT 1"); +} + +export function useKontaktStats() { + return useDbQuery(` + SELECT + COUNT(*) as gesamt, + COUNT(*) FILTER (WHERE ergebnis = 'absage') as absagen, + COUNT(*) FILTER (WHERE ergebnis = 'warteliste') as warteliste, + COUNT(*) FILTER (WHERE ergebnis = 'keine_antwort') as keine_antwort + FROM kontakt + `); +} + +export async function updateSchritt(schritt: ProzessSchritt) { + await dbExec( + "UPDATE nutzer SET aktueller_schritt = $1, aktualisiert_am = NOW() WHERE id = 1", + [schritt], + ); +} diff --git a/src/features/prozess/index.ts b/src/features/prozess/index.ts new file mode 100644 index 0000000..13362ee --- /dev/null +++ b/src/features/prozess/index.ts @@ -0,0 +1,2 @@ +export { ProcessStepper } from "./components/process-stepper"; +export { updateSchritt, useKontaktStats, useNutzer } from "./hooks"; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 74becb9..eff7f13 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -8,70 +8,88 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root"; -import { Route as IndexRouteImport } from "./routes/index"; -import { Route as OnboardingIndexRouteImport } from "./routes/onboarding/index"; +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as OnboardingIndexRouteImport } from './routes/onboarding/index' +import { Route as ProzessIndexRouteImport } from './routes/prozess/index' const IndexRoute = IndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => rootRouteImport, -} as any); + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ProzessIndexRoute = ProzessIndexRouteImport.update({ + id: '/prozess/', + path: '/prozess/', + getParentRoute: () => rootRouteImport, +} as any) const OnboardingIndexRoute = OnboardingIndexRouteImport.update({ - id: "/onboarding/", - path: "/onboarding/", - getParentRoute: () => rootRouteImport, -} as any); + id: '/onboarding/', + path: '/onboarding/', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { - "/": typeof IndexRoute; - "/onboarding/": typeof OnboardingIndexRoute; + '/': typeof IndexRoute + '/onboarding/': typeof OnboardingIndexRoute + '/prozess/': typeof ProzessIndexRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute; - "/onboarding": typeof OnboardingIndexRoute; + '/': typeof IndexRoute + '/onboarding': typeof OnboardingIndexRoute + '/prozess': typeof ProzessIndexRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport; - "/": typeof IndexRoute; - "/onboarding/": typeof OnboardingIndexRoute; + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/onboarding/': typeof OnboardingIndexRoute + '/prozess/': typeof ProzessIndexRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/" | "/onboarding/"; - fileRoutesByTo: FileRoutesByTo; - to: "/" | "/onboarding"; - id: "__root__" | "/" | "/onboarding/"; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/onboarding/' | '/prozess/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/onboarding' | '/prozess' + id: '__root__' | '/' | '/onboarding/' | '/prozess/' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute; - OnboardingIndexRoute: typeof OnboardingIndexRoute; + IndexRoute: typeof IndexRoute + OnboardingIndexRoute: typeof OnboardingIndexRoute + ProzessIndexRoute: typeof ProzessIndexRoute } -declare module "@tanstack/react-router" { - interface FileRoutesByPath { - "/": { - id: "/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof IndexRouteImport; - parentRoute: typeof rootRouteImport; - }; - "/onboarding/": { - id: "/onboarding/"; - path: "/onboarding"; - fullPath: "/onboarding/"; - preLoaderRoute: typeof OnboardingIndexRouteImport; - parentRoute: typeof rootRouteImport; - }; - } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/prozess/': { + id: '/prozess/' + path: '/prozess' + fullPath: '/prozess/' + preLoaderRoute: typeof ProzessIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/onboarding/': { + id: '/onboarding/' + path: '/onboarding' + fullPath: '/onboarding/' + preLoaderRoute: typeof OnboardingIndexRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - OnboardingIndexRoute: OnboardingIndexRoute, -}; + IndexRoute: IndexRoute, + OnboardingIndexRoute: OnboardingIndexRoute, + ProzessIndexRoute: ProzessIndexRoute, +} export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes(); + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 5bd0f60..aa0a363 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -8,7 +8,6 @@ export const Route = createFileRoute("/")({ if (result.rows.length === 0) { throw redirect({ to: "/onboarding" }); } - // /prozess route will be added in a future task - throw redirect({ to: "/onboarding" as string }); + throw redirect({ to: "/prozess" }); }, }); diff --git a/src/routes/prozess/index.tsx b/src/routes/prozess/index.tsx new file mode 100644 index 0000000..bb1e369 --- /dev/null +++ b/src/routes/prozess/index.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ProcessStepper } from "@/features/prozess/components/process-stepper"; +import { useKontaktStats, useNutzer } from "@/features/prozess/hooks"; + +export const Route = createFileRoute("/prozess/")({ + component: ProzessPage, +}); + +function ProzessPage() { + const { data: nutzer, loading: nutzerLoading } = useNutzer(); + const { data: stats, loading: statsLoading } = useKontaktStats(); + + if (nutzerLoading || statsLoading) return

Laden…

; + if (!nutzer[0]) return

Bitte zuerst das Onboarding abschließen.

; + + const s = stats[0] ?? { gesamt: 0, absagen: 0 }; + + return ( + + ); +}