From e3c04723c3d72514cf213b1ef43737f909414f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 12 Mar 2026 12:28:30 +0100 Subject: [PATCH] fix process flow data integrity, add step-wise scenario seeding SprechstundeForm now syncs nutzer.dringlichkeitscode, TSS advance button sets nutzer.tss_beantragt. replace single mock-data button with step-wise scenario dropdown in dev settings. move mock data to dedicated scenarios module, fix stale test expecting 6 steps. Co-Authored-By: Claude Opus 4.6 --- .../components/settings-page.tsx | 71 ++++-- src/features/einstellungen/scenarios.ts | 214 ++++++++++++++++++ src/features/kontakte/hooks.ts | 127 ----------- .../prozess/components/process-stepper.tsx | 34 ++- src/shared/lib/constants.test.ts | 6 +- 5 files changed, 291 insertions(+), 161 deletions(-) create mode 100644 src/features/einstellungen/scenarios.ts diff --git a/src/features/einstellungen/components/settings-page.tsx b/src/features/einstellungen/components/settings-page.tsx index 829f0b1..1048af4 100644 --- a/src/features/einstellungen/components/settings-page.tsx +++ b/src/features/einstellungen/components/settings-page.tsx @@ -10,12 +10,14 @@ import { Trash2, } from "lucide-react"; import { useState } from "react"; -import { deleteAllData, seedMockData } from "@/features/kontakte/hooks"; +import { seedToStep } from "@/features/einstellungen/scenarios"; +import { deleteAllData } from "@/features/kontakte/hooks"; import { Button } from "@/shared/components/ui/button"; import { Card, CardContent } from "@/shared/components/ui/card"; 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 { useThemeStore } from "@/shared/hooks/use-theme"; type Theme = "light" | "dark" | "system"; @@ -33,7 +35,8 @@ export function SettingsPage() { const [devMode, setDevMode] = useState( () => localStorage.getItem("tpf-dev-mode") === "true", ); - const [mockStatus, setMockStatus] = useState<"idle" | "done">("idle"); + const [scenarioStep, setScenarioStep] = useState("neu"); + const [scenarioStatus, setScenarioStatus] = useState<"idle" | "done">("idle"); const { needRefresh: [needRefresh], @@ -164,30 +167,48 @@ export function SettingsPage() { {devMode && ( -
-
-

Testdaten

-

- 20 Therapeut:innen mit Kontaktversuchen einfügen -

-
-
- {mockStatus === "done" && ( - Fertig - )} - -
+
+

Szenario laden

+

+ App-Zustand für einen bestimmten Prozessschritt laden +

+
+ + +
+ {scenarioStatus === "done" && ( + Fertig + )} )} diff --git a/src/features/einstellungen/scenarios.ts b/src/features/einstellungen/scenarios.ts new file mode 100644 index 0000000..3604f1c --- /dev/null +++ b/src/features/einstellungen/scenarios.ts @@ -0,0 +1,214 @@ +import type { ProzessSchritt } from "@/shared/db/schema"; +import { dbExec } from "@/shared/hooks/use-db"; + +const MOCK_VORNAMEN = [ + "Anna", + "Marie", + "Sophie", + "Laura", + "Julia", + "Lena", + "Sarah", + "Lisa", + "Katharina", + "Eva", + "Thomas", + "Michael", + "Stefan", + "Andreas", + "Daniel", + "Markus", + "Christian", + "Martin", + "Jan", + "Felix", +]; +const MOCK_NACHNAMEN = [ + "Müller", + "Schmidt", + "Schneider", + "Fischer", + "Weber", + "Meyer", + "Wagner", + "Becker", + "Schulz", + "Hoffmann", + "Koch", + "Richter", + "Wolf", + "Schröder", + "Neumann", + "Schwarz", + "Braun", + "Zimmermann", + "Hartmann", + "Krüger", +]; +const MOCK_STAEDTE = [ + "Berlin", + "Hamburg", + "München", + "Köln", + "Frankfurt", + "Stuttgart", + "Düsseldorf", + "Leipzig", + "Dortmund", + "Essen", +]; +const MOCK_THERAPIEFORMEN = [ + "verhaltenstherapie", + "tiefenpsychologisch", + "analytisch", + "systemisch", +]; +const MOCK_ERGEBNISSE = [ + "keine_antwort", + "keine_antwort", + "keine_antwort", + "absage", + "absage", + "warteliste", +]; +const MOCK_KANALE = ["telefon", "email", "online_formular", "persoenlich"]; + +function daysAgoISO(days: number) { + const d = new Date(); + d.setDate(d.getDate() - days); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +async function clearData() { + await dbExec("DELETE FROM kontakt"); + await dbExec("DELETE FROM sprechstunde"); + await dbExec("DELETE FROM therapeut"); +} + +async function resetNutzer() { + await dbExec( + `UPDATE nutzer SET + aktueller_schritt = 'neu', + dringlichkeitscode = FALSE, + dringlichkeitscode_datum = NULL, + tss_beantragt = FALSE, + tss_beantragt_datum = NULL, + aktualisiert_am = NOW() + WHERE id = 1`, + ); +} + +async function seedSprechstunde() { + const result = await dbExec( + `INSERT INTO therapeut (name, stadt, therapieform, telefon, email) + VALUES ($1, $2, $3, $4, $5) RETURNING id`, + [ + "Anna Müller", + "Leipzig", + "verhaltenstherapie", + "0170 1000000", + "anna.mueller@example.de", + ], + ); + const therapeutId = (result.rows[0] as { id: number }).id; + + await dbExec( + `INSERT INTO sprechstunde (therapeut_id, datum, ergebnis, diagnose, dringlichkeitscode) + VALUES ($1, $2, 'erstgespraech', 'F32.1', TRUE)`, + [therapeutId, daysAgoISO(14)], + ); + + await dbExec( + `UPDATE nutzer SET + aktueller_schritt = 'diagnose_erhalten', + dringlichkeitscode = TRUE, + dringlichkeitscode_datum = $1::date, + aktualisiert_am = NOW() + WHERE id = 1`, + [daysAgoISO(14)], + ); +} + +async function seedTssBeantragt() { + await dbExec( + `UPDATE nutzer SET + aktueller_schritt = 'tss_beantragt', + tss_beantragt = TRUE, + tss_beantragt_datum = $1::date, + aktualisiert_am = NOW() + WHERE id = 1`, + [daysAgoISO(10)], + ); +} + +async function seedKontakte() { + for (let i = 0; i < 20; i++) { + const vorname = MOCK_VORNAMEN[i % MOCK_VORNAMEN.length]; + const nachname = MOCK_NACHNAMEN[i % MOCK_NACHNAMEN.length]; + const stadt = MOCK_STAEDTE[i % MOCK_STAEDTE.length]; + const therapieform = MOCK_THERAPIEFORMEN[i % MOCK_THERAPIEFORMEN.length]; + + const result = await dbExec( + `INSERT INTO therapeut (name, stadt, therapieform, telefon, email) + VALUES ($1, $2, $3, $4, $5) RETURNING id`, + [ + `${vorname} ${nachname}`, + stadt, + therapieform, + `0${170 + i} ${1000000 + i * 12345}`, + `${vorname.toLowerCase()}.${nachname.toLowerCase()}@example.de`, + ], + ); + const therapeutId = (result.rows[0] as { id: number }).id; + + const kontaktCount = 1 + (i % 3); + for (let j = 0; j < kontaktCount; j++) { + const daysBack = (kontaktCount - j) * 3 + i; + const ergebnis = MOCK_ERGEBNISSE[(i + j) % MOCK_ERGEBNISSE.length]; + const kanal = MOCK_KANALE[(i + j) % MOCK_KANALE.length]; + await dbExec( + `INSERT INTO kontakt (therapeut_id, datum, kanal, ergebnis) VALUES ($1, $2, $3, $4)`, + [therapeutId, daysAgoISO(daysBack), kanal, ergebnis], + ); + } + } + + await dbExec( + `UPDATE nutzer SET aktueller_schritt = 'eigensuche', aktualisiert_am = NOW() WHERE id = 1`, + ); +} + +// Steps in order — each builds on the previous +const STEP_ORDER: ProzessSchritt[] = [ + "neu", + "diagnose_erhalten", + "tss_beantragt", + "eigensuche", + "antrag_gestellt", +]; + +const STEP_SEEDERS: Record Promise) | null> = { + neu: null, + sprechstunde_absolviert: null, + diagnose_erhalten: seedSprechstunde, + tss_beantragt: seedTssBeantragt, + eigensuche: seedKontakte, + antrag_gestellt: async () => { + await dbExec( + `UPDATE nutzer SET aktueller_schritt = 'antrag_gestellt', aktualisiert_am = NOW() WHERE id = 1`, + ); + }, +}; + +export async function seedToStep(targetStep: ProzessSchritt) { + await clearData(); + await resetNutzer(); + + const targetIndex = STEP_ORDER.indexOf(targetStep); + if (targetIndex < 0) return; + + for (let i = 0; i <= targetIndex; i++) { + const seeder = STEP_SEEDERS[STEP_ORDER[i]]; + if (seeder) await seeder(); + } +} diff --git a/src/features/kontakte/hooks.ts b/src/features/kontakte/hooks.ts index 1727a12..a5fb3a3 100644 --- a/src/features/kontakte/hooks.ts +++ b/src/features/kontakte/hooks.ts @@ -134,130 +134,3 @@ export async function deleteAllData() { await dbExec("DELETE FROM therapeut"); await dbExec("DELETE FROM nutzer"); } - -const MOCK_VORNAMEN = [ - "Anna", - "Marie", - "Sophie", - "Laura", - "Julia", - "Lena", - "Sarah", - "Lisa", - "Katharina", - "Eva", - "Thomas", - "Michael", - "Stefan", - "Andreas", - "Daniel", - "Markus", - "Christian", - "Martin", - "Jan", - "Felix", -]; -const MOCK_NACHNAMEN = [ - "Müller", - "Schmidt", - "Schneider", - "Fischer", - "Weber", - "Meyer", - "Wagner", - "Becker", - "Schulz", - "Hoffmann", - "Koch", - "Richter", - "Wolf", - "Schröder", - "Neumann", - "Schwarz", - "Braun", - "Zimmermann", - "Hartmann", - "Krüger", -]; -const MOCK_STAEDTE = [ - "Berlin", - "Hamburg", - "München", - "Köln", - "Frankfurt", - "Stuttgart", - "Düsseldorf", - "Leipzig", - "Dortmund", - "Essen", -]; -const MOCK_THERAPIEFORMEN = [ - "verhaltenstherapie", - "tiefenpsychologisch", - "analytisch", - "systemisch", -]; -const MOCK_ERGEBNISSE = [ - "keine_antwort", - "keine_antwort", - "keine_antwort", - "absage", - "absage", - "warteliste", -]; - -function daysAgoISO(days: number) { - const d = new Date(); - d.setDate(d.getDate() - days); - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; -} - -export async function seedMockData() { - const kanale = ["telefon", "email", "online_formular", "persoenlich"]; - - for (let i = 0; i < 20; i++) { - const vorname = MOCK_VORNAMEN[i % MOCK_VORNAMEN.length]; - const nachname = MOCK_NACHNAMEN[i % MOCK_NACHNAMEN.length]; - const stadt = MOCK_STAEDTE[i % MOCK_STAEDTE.length]; - const therapieform = MOCK_THERAPIEFORMEN[i % MOCK_THERAPIEFORMEN.length]; - - const result = await dbExec( - `INSERT INTO therapeut (name, stadt, therapieform, telefon, email) - VALUES ($1, $2, $3, $4, $5) RETURNING id`, - [ - `${vorname} ${nachname}`, - stadt, - therapieform, - `0${170 + i} ${1000000 + i * 12345}`, - `${vorname.toLowerCase()}.${nachname.toLowerCase()}@example.de`, - ], - ); - const therapeutId = (result.rows[0] as { id: number }).id; - - const kontaktCount = 1 + (i % 3); - for (let j = 0; j < kontaktCount; j++) { - const daysBack = (kontaktCount - j) * 3 + i; - const ergebnis = MOCK_ERGEBNISSE[(i + j) % MOCK_ERGEBNISSE.length]; - const kanal = kanale[(i + j) % kanale.length]; - await dbExec( - `INSERT INTO kontakt (therapeut_id, datum, kanal, ergebnis) VALUES ($1, $2, $3, $4)`, - [therapeutId, daysAgoISO(daysBack), kanal, ergebnis], - ); - } - } - - // Create a sprechstunde for the first therapist (enables step 2) - const firstT = await dbExec("SELECT id FROM therapeut ORDER BY id LIMIT 1"); - if (firstT.rows.length > 0) { - const tId = (firstT.rows[0] as { id: number }).id; - await dbExec( - "INSERT INTO sprechstunde (therapeut_id, datum, ergebnis, diagnose, dringlichkeitscode) VALUES ($1, $2, 'erstgespraech', 'F32.1', true)", - [tId, daysAgoISO(14)], - ); - } - - // Advance user to eigensuche step so they can see the full kontakt flow - await dbExec( - "UPDATE nutzer SET aktueller_schritt = 'eigensuche', aktualisiert_am = NOW() WHERE id = 1", - ); -} diff --git a/src/features/prozess/components/process-stepper.tsx b/src/features/prozess/components/process-stepper.tsx index 88e551d..17e75d0 100644 --- a/src/features/prozess/components/process-stepper.tsx +++ b/src/features/prozess/components/process-stepper.tsx @@ -101,11 +101,27 @@ function StepAction({ return ; case "diagnose_erhalten": return ( - + <> + +
+ +
+ ); case "tss_beantragt": return ( @@ -204,7 +220,13 @@ function SprechstundeForm({ onDone }: { onDone: () => void }) { ], ); await dbExec( - "UPDATE nutzer SET aktueller_schritt = 'diagnose_erhalten', aktualisiert_am = NOW() WHERE id = 1", + `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(); diff --git a/src/shared/lib/constants.test.ts b/src/shared/lib/constants.test.ts index 2882a38..b7022c5 100644 --- a/src/shared/lib/constants.test.ts +++ b/src/shared/lib/constants.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it } from "vitest"; import { PROZESS_SCHRITTE } from "./constants"; describe("PROZESS_SCHRITTE", () => { - it("has 6 steps in correct order", () => { - expect(PROZESS_SCHRITTE).toHaveLength(6); + it("has 5 steps in correct order", () => { + expect(PROZESS_SCHRITTE).toHaveLength(5); expect(PROZESS_SCHRITTE[0].key).toBe("neu"); - expect(PROZESS_SCHRITTE[5].key).toBe("antrag_gestellt"); + expect(PROZESS_SCHRITTE[4].key).toBe("antrag_gestellt"); }); it("every step has label and description", () => {