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 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 12:28:30 +01:00
parent ba3da207c0
commit e3c04723c3
5 changed files with 291 additions and 161 deletions

View File

@@ -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<ProzessSchritt>("neu");
const [scenarioStatus, setScenarioStatus] = useState<"idle" | "done">("idle");
const {
needRefresh: [needRefresh],
@@ -164,30 +167,48 @@ export function SettingsPage() {
{devMode && (
<Card>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Testdaten</p>
<p className="text-xs text-muted-foreground">
20 Therapeut:innen mit Kontaktversuchen einfügen
</p>
</div>
<div className="flex items-center gap-2">
{mockStatus === "done" && (
<span className="text-xs text-green-600">Fertig</span>
)}
<Button
size="sm"
variant="outline"
onClick={async () => {
await seedMockData();
setMockStatus("done");
}}
>
<Database className="mr-1 size-4" />
Einfügen
</Button>
</div>
<div>
<p className="text-sm font-medium">Szenario laden</p>
<p className="text-xs text-muted-foreground">
App-Zustand für einen bestimmten Prozessschritt laden
</p>
</div>
<div className="flex items-center gap-2">
<select
className="flex h-9 flex-1 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={scenarioStep}
onChange={(e) => {
setScenarioStep(e.target.value as ProzessSchritt);
setScenarioStatus("idle");
}}
>
<option value="neu">Schritt 1 Erstgespräch</option>
<option value="diagnose_erhalten">
Schritt 2 Diagnose erhalten
</option>
<option value="tss_beantragt">
Schritt 3 TSS kontaktiert
</option>
<option value="eigensuche">Schritt 4 Eigensuche</option>
<option value="antrag_gestellt">
Schritt 5 Antrag gestellt
</option>
</select>
<Button
size="sm"
variant="outline"
onClick={async () => {
await seedToStep(scenarioStep);
setScenarioStatus("done");
}}
>
<Database className="mr-1 size-4" />
Laden
</Button>
</div>
{scenarioStatus === "done" && (
<span className="text-xs text-green-600">Fertig</span>
)}
</CardContent>
</Card>
)}

View File

@@ -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<ProzessSchritt, (() => Promise<void>) | 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();
}
}

View File

@@ -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",
);
}

View File

@@ -101,11 +101,27 @@ function StepAction({
return <SprechstundeForm onDone={onUpdate} />;
case "diagnose_erhalten":
return (
<AdvanceButton
nextStep="tss_beantragt"
label="TSS kontaktiert"
onDone={onUpdate}
/>
<>
<Separator />
<div className="flex justify-end">
<Button
size="sm"
onClick={async () => {
await dbExec(
`UPDATE nutzer SET
aktueller_schritt = 'tss_beantragt',
tss_beantragt = TRUE,
tss_beantragt_datum = CURRENT_DATE,
aktualisiert_am = NOW()
WHERE id = 1`,
);
onUpdate();
}}
>
TSS kontaktiert
</Button>
</div>
</>
);
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();

View File

@@ -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", () => {