From c6db9cec652dac68035824e7a78623f01b4be2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 12 Mar 2026 19:42:57 +0100 Subject: [PATCH] =?UTF-8?q?rewrite=20process=20stepper:=20data-driven=20st?= =?UTF-8?q?eps,=20erstgespr=C3=A4ch=20list/form,=20conditional=20TSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../prozess/components/process-stepper.tsx | 735 +++++++++++------- 1 file changed, 454 insertions(+), 281 deletions(-) diff --git a/src/features/prozess/components/process-stepper.tsx b/src/features/prozess/components/process-stepper.tsx index 57ec748..d3a1485 100644 --- a/src/features/prozess/components/process-stepper.tsx +++ b/src/features/prozess/components/process-stepper.tsx @@ -1,24 +1,25 @@ import { useForm } from "@tanstack/react-form"; import { Link } from "@tanstack/react-router"; +import { ChevronDown, ChevronRight, Plus } from "lucide-react"; import { useState } from "react"; import { useTherapeutenListe } from "@/features/kontakte/hooks"; +import { Badge } from "@/shared/components/ui/badge"; import { Button } from "@/shared/components/ui/button"; import { DateInput } from "@/shared/components/ui/date-input"; import { Label } from "@/shared/components/ui/label"; import { Separator } from "@/shared/components/ui/separator"; import { Switch } from "@/shared/components/ui/switch"; -import type { ProzessSchritt } from "@/shared/db/schema"; -import { dbExec } from "@/shared/hooks/use-db"; -import { PROZESS_SCHRITTE } from "@/shared/lib/constants"; +import type { ErstgespraechRow, ProcessStatus } from "../hooks"; +import { + addSitzung, + createErstgespraech, + setTssKontaktiert, + updateErstgespraech, + useErstgespraeche, + useSitzungen, +} from "../hooks"; import { PhaseCard } from "./phase-card"; -interface ProcessStepperProps { - aktuellerSchritt: ProzessSchritt; - kontaktGesamt: number; - absagen: number; - onUpdate: () => void; -} - const inputClasses = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"; @@ -27,177 +28,236 @@ function todayISO() { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } +interface ProcessStepperProps { + processStatus: ProcessStatus; + tssKontaktiertDatum: string | null; + kontaktGesamt: number; + absagen: number; + onUpdate: () => void; +} + export function ProcessStepper({ - aktuellerSchritt, + processStatus, + tssKontaktiertDatum, kontaktGesamt, absagen, onUpdate, }: ProcessStepperProps) { - // Map legacy step to current steps - const effectiveSchritt = - aktuellerSchritt === "sprechstunde_absolviert" - ? "diagnose_erhalten" - : aktuellerSchritt; - - const currentIndex = PROZESS_SCHRITTE.findIndex( - (s) => s.key === effectiveSchritt, - ); + const visibleSteps = processStatus.steps.filter((s) => s.visible); + const currentIndex = visibleSteps.findIndex((s) => s.status === "aktuell"); return (

Dein Fortschritt

- - Schritt {currentIndex + 1} von {PROZESS_SCHRITTE.length} - + {currentIndex >= 0 && ( + + Schritt {currentIndex + 1} von {visibleSteps.length} + + )}
- {PROZESS_SCHRITTE.map((schritt, i) => { - const status = - i < currentIndex - ? "erledigt" - : i === currentIndex - ? "aktuell" - : "offen"; - - return ( - - {status === "aktuell" && ( - - )} - - ); - })} + {visibleSteps.map((step, i) => ( + + {step.key === "erstgespraech" && ( + + )} + {step.key === "tss" && step.status === "aktuell" && ( + + )} + {step.key === "tss" && + step.status === "erledigt" && + tssKontaktiertDatum && } + {step.key === "eigensuche" && ( + + )} + {step.key === "antrag" && step.status === "aktuell" && ( + + )} + + ))}
); } -function StepAction({ - schritt, - kontaktGesamt, - absagen, - onUpdate, -}: { - schritt: ProzessSchritt; - kontaktGesamt: number; - absagen: number; - onUpdate: () => void; -}) { - switch (schritt) { - case "neu": - return ; - case "diagnose_erhalten": - return ( +function ErstgespraechAction({ onUpdate }: { onUpdate: () => void }) { + const { data: erstgespraeche, refetch } = useErstgespraeche(); + const [showForm, setShowForm] = useState(false); + + const handleUpdate = () => { + refetch(); + onUpdate(); + }; + + return ( + <> + {erstgespraeche.length > 0 && ( + <> + +
+ {erstgespraeche.map((eg) => ( + + ))} +
+ + )} + {showForm ? ( + <> + + { + setShowForm(false); + handleUpdate(); + }} + onCancel={() => setShowForm(false)} + /> + + ) : ( <>
-
- ); - case "tss_beantragt": - return ( - - ); - case "eigensuche": - return ( - <> - -
- {kontaktGesamt} Kontaktversuche, davon {absagen} Absagen.{" "} - - Kontakte verwalten - -
- - - ); - case "antrag_gestellt": - return ( - <> - -

- Dein Kostenerstattungsantrag wurde eingereicht.{" "} - - Zum Antrag - -

- - ); - default: - return null; - } -} - -function AdvanceButton({ - nextStep, - label, - onDone, -}: { - nextStep: ProzessSchritt; - label: string; - onDone: () => void; -}) { - return ( - <> - -
- -
+ )} ); } -function SprechstundeForm({ onDone }: { onDone: () => void }) { +function ErstgespraechCard({ + erstgespraech, + onUpdate, +}: { + erstgespraech: ErstgespraechRow; + onUpdate: () => void; +}) { + const [expanded, setExpanded] = useState(false); + const [editing, setEditing] = useState(false); + + return ( +
+ + + {expanded && ( +
+ + {editing ? ( + { + setEditing(false); + onUpdate(); + }} + onCancel={() => setEditing(false)} + /> + ) : ( +
+ +
+ )} +
+ )} +
+ ); +} + +function SitzungList({ + sprechstundeId, + onUpdate, +}: { + sprechstundeId: number; + onUpdate: () => void; +}) { + const { data: sitzungen, refetch } = useSitzungen(sprechstundeId); + const [addingDate, setAddingDate] = useState(""); + + return ( +
+

Sitzungen

+
    + {sitzungen.map((s) => ( +
  • + {new Date(s.datum).toLocaleDateString("de-DE")} +
  • + ))} +
+
+ setAddingDate(iso)} /> + +
+
+ ); +} + +function ErstgespraechForm({ + onDone, + onCancel, +}: { + onDone: () => void; + onCancel: () => void; +}) { const { data: therapeuten, loading } = useTherapeutenListe(); - const [saved, setSaved] = useState(false); const form = useForm({ defaultValues: { @@ -209,145 +269,258 @@ function SprechstundeForm({ onDone }: { onDone: () => void }) { onSubmit: async ({ value }) => { const therapeutId = Number(value.therapeut_id); if (!therapeutId) return; - - await dbExec( - `INSERT INTO sprechstunde (therapeut_id, datum, ergebnis, diagnose, dringlichkeitscode) VALUES ($1, $2, 'erstgespraech', $3, $4)`, - [ - therapeutId, - value.datum, - value.diagnose || null, - value.dringlichkeitscode, - ], + await createErstgespraech( + therapeutId, + value.datum, + value.diagnose || null, + value.dringlichkeitscode, ); - await dbExec( - `UPDATE nutzer SET - aktueller_schritt = 'diagnose_erhalten', - dringlichkeitscode = $1, - dringlichkeitscode_datum = CASE WHEN $1 = TRUE THEN $2::date ELSE NULL END, - aktualisiert_am = NOW() - WHERE id = 1`, - [value.dringlichkeitscode, value.datum], - ); - setSaved(true); onDone(); }, }); - if (saved) { - return ( -

- Erstgespräch erfasst. + return ( +

{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-3" + > +

Erstgespräch dokumentieren

+

+ Freie Termine findest du unter{" "} + 116117{" "} + (Telefon oder online).

- ); - } + + {(field) => ( +
+ + {loading ? ( +

Laden…

+ ) : therapeuten.length === 0 ? ( +

+ Lege zuerst unter{" "} + + Kontakte + {" "} + einen Eintrag an. +

+ ) : ( + + )} +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(iso)} + /> +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> +
+ )} +
+ + + {(field) => ( +
+
+ field.handleChange(checked)} + /> + +
+
+ )} +
+ +
+ + +
+
+ ); +} + +function EditErstgespraechForm({ + erstgespraech, + onDone, + onCancel, +}: { + erstgespraech: ErstgespraechRow; + onDone: () => void; + onCancel: () => void; +}) { + const form = useForm({ + defaultValues: { + diagnose: erstgespraech.diagnose ?? "", + dringlichkeitscode: erstgespraech.dringlichkeitscode, + }, + onSubmit: async ({ value }) => { + await updateErstgespraech( + erstgespraech.id, + value.diagnose || null, + value.dringlichkeitscode, + ); + onDone(); + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-3" + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> +
+ )} +
+ + + {(field) => ( +
+
+ field.handleChange(checked)} + /> + +
+
+ )} +
+ +
+ + +
+
+ ); +} + +function TssAction({ onUpdate }: { onUpdate: () => void }) { return ( <> -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-3" - > -

Erstgespräch dokumentieren

-

- Freie Termine findest du unter{" "} - 116117{" "} - (Telefon oder online). -

- - - {(field) => ( -
- - {loading ? ( -

Laden…

- ) : therapeuten.length === 0 ? ( -

- Lege zuerst unter{" "} - - Kontakte - {" "} - einen Eintrag an. -

- ) : ( - - )} -
- )} -
- - - {(field) => ( -
- - field.handleChange(iso)} - /> -
- )} -
- - - {(field) => ( -
- - field.handleChange(e.target.value)} - /> -
- )} -
- - - {(field) => ( -
-
- field.handleChange(checked)} - /> - -
- {!field.state.value && ( -

- Ohne Dringlichkeitscode kann die TSS dich ggf. nicht - vermitteln. Frage in der Sprechstunde gezielt danach. -

- )} -
- )} -
- -
- -
-
+
+ +
+ + ); +} + +function TssDone({ datum }: { datum: string }) { + return ( + <> + +

+ TSS kontaktiert am {new Date(datum).toLocaleDateString("de-DE")} +

+ + ); +} + +function EigensucheInfo({ + kontaktGesamt, + absagen, +}: { + kontaktGesamt: number; + absagen: number; +}) { + return ( + <> + +
+ {kontaktGesamt} Kontaktversuche, davon {absagen} Absagen.{" "} + + Kontakte verwalten + +
+ + ); +} + +function AntragLink() { + return ( + <> + +

+ Dein Kostenerstattungsantrag kann eingereicht werden.{" "} + + Zum Antrag + +

); }