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:
@@ -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>
|
||||
)}
|
||||
|
||||
214
src/features/einstellungen/scenarios.ts
Normal file
214
src/features/einstellungen/scenarios.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user