diff --git a/docs/superpowers/plans/2026-03-12-process-model-overhaul.md b/docs/superpowers/plans/2026-03-12-process-model-overhaul.md new file mode 100644 index 0000000..f07dbc2 --- /dev/null +++ b/docs/superpowers/plans/2026-03-12-process-model-overhaul.md @@ -0,0 +1,1735 @@ +# Process Model Overhaul Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the manually-synced `aktueller_schritt` enum with a data-driven process model supporting multiple Erstgespräche with sessions and conditional TSS step. + +**Architecture:** New migration adds `sitzung` table and removes step-tracking columns from `nutzer`. A `useProcessStatus()` hook derives all step statuses from data queries. The process stepper renders steps with dynamic visibility and status. + +**Tech Stack:** React, TanStack Router, TanStack Form, PGlite (Postgres in IndexedDB), Zod, Vitest + +--- + +## File Structure + +| File | Responsibility | +|---|---| +| `src/shared/db/migrations/002_process_model.sql` | Schema migration: add sitzung, modify nutzer/sprechstunde | +| `src/shared/db/schema.ts` | Remove ProzessSchritt, add Sitzung type | +| `src/shared/lib/constants.ts` | Remove PROZESS_SCHRITTE, keep label records | +| `src/features/prozess/hooks.ts` | `useProcessStatus()` hook, `useErstgespraeche()`, data queries | +| `src/features/prozess/components/process-stepper.tsx` | Data-driven stepper with Erstgespräch list/form | +| `src/features/prozess/components/phase-card.tsx` | Accept `stepNumber` prop instead of `index` | +| `src/features/antrag/components/antrag-checklist.tsx` | Derive checks from data queries | +| `src/features/einstellungen/scenarios.ts` | Seed data rows instead of setting aktueller_schritt | +| `src/features/onboarding/components/onboarding-form.tsx` | Remove aktueller_schritt from INSERT | +| `src/features/onboarding/schema.ts` | Remove aktueller_schritt from validation schema | +| `src/features/onboarding/schema.test.ts` | Remove aktueller_schritt from test data | +| `src/features/prozess/index.ts` | Update barrel exports | +| `src/routes/prozess/index.tsx` | Pass process status instead of step string | + +--- + +## Chunk 1: Schema and types + +### Task 1: Write migration 002 + +**Files:** +- Create: `src/shared/db/migrations/002_process_model.sql` + +- [ ] **Step 1: Create migration file** + +```sql +-- Add tss_kontaktiert_datum before dropping old columns +ALTER TABLE nutzer ADD COLUMN tss_kontaktiert_datum DATE; + +-- Migrate existing TSS data +UPDATE nutzer SET tss_kontaktiert_datum = tss_beantragt_datum WHERE tss_beantragt = TRUE; + +-- Drop old tracking columns from nutzer +ALTER TABLE nutzer DROP COLUMN aktueller_schritt; +ALTER TABLE nutzer DROP COLUMN dringlichkeitscode; +ALTER TABLE nutzer DROP COLUMN dringlichkeitscode_datum; +ALTER TABLE nutzer DROP COLUMN tss_beantragt; +ALTER TABLE nutzer DROP COLUMN tss_beantragt_datum; + +-- Create sitzung table +CREATE TABLE sitzung ( + id SERIAL PRIMARY KEY, + sprechstunde_id INTEGER NOT NULL REFERENCES sprechstunde(id) ON DELETE CASCADE, + datum DATE NOT NULL, + erstellt_am TIMESTAMPTZ DEFAULT NOW() +); + +-- Migrate existing sprechstunde.datum to sitzung +INSERT INTO sitzung (sprechstunde_id, datum) +SELECT id, datum FROM sprechstunde WHERE datum IS NOT NULL; + +-- Drop migrated/vestigial columns from sprechstunde +ALTER TABLE sprechstunde DROP COLUMN datum; +ALTER TABLE sprechstunde DROP COLUMN ergebnis; + +-- One Erstgespräch per therapist +ALTER TABLE sprechstunde ADD CONSTRAINT sprechstunde_therapeut_unique UNIQUE (therapeut_id); +``` + +- [ ] **Step 2: Verify migration loads** + +Run: `npx vitest run --reporter verbose 2>&1 | head -30` + +The migration file just needs to exist and be valid SQL. The app's `runMigrations()` in `client.ts` auto-discovers `*.sql` files in the migrations folder. + +- [ ] **Step 3: Commit** + +```bash +git add src/shared/db/migrations/002_process_model.sql +git commit -m "add migration 002: data-driven process model, sitzung table" +``` + +### Task 2: Update schema types + +**Files:** +- Modify: `src/shared/db/schema.ts` + +- [ ] **Step 1: Remove ProzessSchritt, update nutzerSchema, update sprechstundeSchema, add sitzungSchema** + +Replace the entire file with: + +```typescript +import { z } from "zod" + +export const kontaktKanalEnum = z.enum([ + "telefon", + "email", + "online_formular", + "persoenlich", +]) +export type KontaktKanal = z.infer + +export const kontaktErgebnisEnum = z.enum([ + "keine_antwort", + "absage", + "warteliste", + "zusage", +]) +export type KontaktErgebnis = z.infer + +export const therapieformEnum = z.enum([ + "verhaltenstherapie", + "tiefenpsychologisch", + "analytisch", + "systemisch", +]) +export type Therapieform = z.infer + +export const nutzerSchema = z.object({ + id: z.number(), + name: z.string().nullable(), + plz: z.string().nullable(), + ort: z.string().nullable(), + krankenkasse: z.string().nullable(), + tss_kontaktiert_datum: z.string().nullable(), +}) + +export const therapeutSchema = z.object({ + id: z.number(), + name: z.string(), + adresse: z.string().nullable(), + plz: z.string().nullable(), + stadt: z.string().nullable(), + telefon: z.string().nullable(), + email: z.string().nullable(), + website: z.string().nullable(), + therapieform: z.string().nullable(), + kassenzulassung: z.string().nullable(), +}) + +export const kontaktSchema = z.object({ + id: z.number(), + therapeut_id: z.number(), + datum: z.string(), + kanal: kontaktKanalEnum, + ergebnis: kontaktErgebnisEnum, + notiz: z.string().nullable(), + antwort_datum: z.string().nullable(), +}) + +export const sprechstundeSchema = z.object({ + id: z.number(), + therapeut_id: z.number(), + diagnose: z.string().nullable(), + dringlichkeitscode: z.boolean(), +}) + +export const sitzungSchema = z.object({ + id: z.number(), + sprechstunde_id: z.number(), + datum: z.string(), +}) +``` + +- [ ] **Step 2: Verify no type errors** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +This will show errors in files that import `ProzessSchritt` — those are fixed in later tasks. + +- [ ] **Step 3: Commit** + +```bash +git add src/shared/db/schema.ts +git commit -m "update schema types: remove ProzessSchritt, add sitzung, update nutzer/sprechstunde" +``` + +### Task 3: Remove PROZESS_SCHRITTE and its test + +**Files:** +- Modify: `src/shared/lib/constants.ts` +- Delete: `src/shared/lib/constants.test.ts` + +- [ ] **Step 1: Remove PROZESS_SCHRITTE from constants.ts** + +Remove the `ProzessSchritt` import and the entire `PROZESS_SCHRITTE` export. Keep `KANAL_LABELS`, `ERGEBNIS_LABELS`, `THERAPIEFORM_LABELS`. + +New file content: + +```typescript +import type { + KontaktErgebnis, + KontaktKanal, + Therapieform, +} from "../db/schema"; + +export const KANAL_LABELS: Record = { + telefon: "Telefon", + email: "E-Mail", + online_formular: "Online-Formular", + persoenlich: "Persönlich", +}; + +export const ERGEBNIS_LABELS: Record = { + keine_antwort: "Keine Antwort", + absage: "Absage", + warteliste: "Warteliste", + zusage: "Zusage", +}; + +export const THERAPIEFORM_LABELS: Record = { + verhaltenstherapie: "Verhaltenstherapie (VT)", + tiefenpsychologisch: "Tiefenpsychologisch fundierte PT (TP)", + analytisch: "Analytische Psychotherapie (AP)", + systemisch: "Systemische Therapie", +}; +``` + +- [ ] **Step 2: Delete the test file** + +Delete `src/shared/lib/constants.test.ts` entirely. + +- [ ] **Step 3: Verify lint passes** + +Run: `npx biome check src/shared/lib/constants.ts` + +- [ ] **Step 4: Commit** + +```bash +git add src/shared/lib/constants.ts +git rm src/shared/lib/constants.test.ts +git commit -m "remove PROZESS_SCHRITTE array, delete stale test" +``` + +### Task 4: Update onboarding form + +**Files:** +- Modify: `src/features/onboarding/components/onboarding-form.tsx` + +- [ ] **Step 1: Remove aktueller_schritt from INSERT** + +In the `onSubmit` handler (around line 51-55), change: + +```typescript +// Old: +await dbExec( + `INSERT INTO nutzer (name, plz, ort, krankenkasse, aktueller_schritt) + VALUES ($1, $2, $3, $4, 'neu')`, + [value.name, value.plz, value.ort, value.krankenkasse], +); + +// New: +await dbExec( + `INSERT INTO nutzer (name, plz, ort, krankenkasse) + VALUES ($1, $2, $3, $4)`, + [value.name, value.plz, value.ort, value.krankenkasse], +); +``` + +- [ ] **Step 2: Verify no type errors** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +- [ ] **Step 3: Commit** + +```bash +git add src/features/onboarding/components/onboarding-form.tsx +git commit -m "remove aktueller_schritt from onboarding INSERT" +``` + +### Task 5: Update onboarding schema and test + +**Files:** +- Modify: `src/features/onboarding/schema.ts` +- Modify: `src/features/onboarding/schema.test.ts` + +- [ ] **Step 1: Remove aktueller_schritt from onboarding schema** + +Replace `src/features/onboarding/schema.ts` with: + +```typescript +import { z } from "zod"; + +export const onboardingSchema = z.object({ + name: z.string().min(1, "Bitte gib deinen Namen ein."), + plz: z.string().regex(/^\d{5}$/, "Bitte gib eine gültige PLZ ein."), + ort: z.string().min(1, "Bitte gib deinen Ort ein."), + krankenkasse: z.string().min(1, "Bitte gib deine Krankenkasse ein."), +}); + +export type OnboardingData = z.infer; +``` + +- [ ] **Step 2: Remove aktueller_schritt from test data** + +Replace `src/features/onboarding/schema.test.ts` with: + +```typescript +import { describe, expect, it } from "vitest"; +import { onboardingSchema } from "./schema"; + +describe("onboardingSchema", () => { + it("accepts valid data", () => { + const result = onboardingSchema.safeParse({ + name: "Max Mustermann", + plz: "10115", + ort: "Berlin", + krankenkasse: "TK", + }); + expect(result.success).toBe(true); + }); + + it("rejects invalid PLZ", () => { + const result = onboardingSchema.safeParse({ + name: "Max", + plz: "123", + ort: "Berlin", + krankenkasse: "TK", + }); + expect(result.success).toBe(false); + }); + + it("rejects empty name", () => { + const result = onboardingSchema.safeParse({ + name: "", + plz: "10115", + ort: "Berlin", + krankenkasse: "TK", + }); + expect(result.success).toBe(false); + }); +}); +``` + +- [ ] **Step 3: Run tests** + +Run: `npx vitest run src/features/onboarding/schema.test.ts --reporter verbose` + +Expected: All 3 tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add src/features/onboarding/schema.ts src/features/onboarding/schema.test.ts +git commit -m "remove aktueller_schritt from onboarding schema and tests" +``` + +--- + +## Chunk 2: Process status hook and stepper rewrite + +### Task 6: Write useProcessStatus hook + +**Files:** +- Rewrite: `src/features/prozess/hooks.ts` + +- [ ] **Step 1: Replace hooks.ts with data-driven queries** + +```typescript +import { dbExec, useDbQuery } from "@/shared/hooks/use-db"; + +interface NutzerRow { + id: number; + name: string; + krankenkasse: string; + tss_kontaktiert_datum: string | null; +} + +interface KontaktStats { + gesamt: number; + absagen: number; + warteliste: number; + keine_antwort: number; +} + +export interface ErstgespraechRow { + id: number; + therapeut_id: number; + therapeut_name: string; + therapeut_stadt: string | null; + diagnose: string | null; + dringlichkeitscode: boolean; + sitzung_count: number; +} + +interface SitzungRow { + id: number; + datum: string; +} + +export type StepStatus = "erledigt" | "aktuell" | "offen"; + +export interface ProcessStatus { + hasDiagnose: boolean; + hasDringlichkeit: boolean; + tssKontaktiert: boolean; + absagenUndKeineAntwort: number; + steps: { + key: string; + label: string; + beschreibung: string; + status: StepStatus; + visible: boolean; + }[]; +} + +export function useNutzer() { + return useDbQuery("SELECT id, name, krankenkasse, tss_kontaktiert_datum 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 function useErstgespraeche() { + return useDbQuery(` + SELECT + s.id, + s.therapeut_id, + t.name AS therapeut_name, + t.stadt AS therapeut_stadt, + s.diagnose, + s.dringlichkeitscode, + (SELECT COUNT(*)::int FROM sitzung WHERE sprechstunde_id = s.id) AS sitzung_count + FROM sprechstunde s + JOIN therapeut t ON t.id = s.therapeut_id + ORDER BY s.erstellt_am DESC + `); +} + +export function useSitzungen(sprechstundeId: number) { + return useDbQuery( + "SELECT id, datum FROM sitzung WHERE sprechstunde_id = $1 ORDER BY datum DESC", + [sprechstundeId], + [sprechstundeId], + ); +} + +export function useProcessStatus( + nutzer: NutzerRow | undefined, + stats: KontaktStats | undefined, + erstgespraeche: ErstgespraechRow[], +): ProcessStatus { + const hasDiagnose = erstgespraeche.some((e) => e.diagnose != null); + const hasDringlichkeit = erstgespraeche.some((e) => e.dringlichkeitscode); + const tssKontaktiert = nutzer?.tss_kontaktiert_datum != null; + const absagenUndKeineAntwort = stats + ? Number(stats.absagen) + Number(stats.keine_antwort) + : 0; + + const step1Erledigt = hasDiagnose; + const step2Erledigt = tssKontaktiert; + const step3Erledigt = absagenUndKeineAntwort >= 5; + const step4Available = + step1Erledigt && step3Erledigt && (!hasDringlichkeit || step2Erledigt); + + const steps = [ + { + key: "erstgespraech", + label: "Erstgespräch durchführen", + beschreibung: + "Das Erstgespräch heißt in der Fachsprache psychotherapeutische Sprechstunde (PTS). Dort wird eine erste Einschätzung vorgenommen und du erhältst ggf. eine Diagnose und einen Dringlichkeitscode. Tipp: Unter 116117 findest du freie PTS-Termine.", + status: (step1Erledigt ? "erledigt" : "aktuell") as StepStatus, + visible: true, + }, + { + key: "tss", + label: "Terminservicestelle kontaktieren", + beschreibung: + "Kontaktiere die Terminservicestelle (TSS) deiner Kassenärztlichen Vereinigung (KV), um nach einem Therapieplatz zu suchen. Die TSS ist ein Service der KV, erreichbar unter 116117 oder online.", + status: (step2Erledigt + ? "erledigt" + : step1Erledigt + ? "aktuell" + : "offen") as StepStatus, + visible: hasDringlichkeit, + }, + { + key: "eigensuche", + label: "Eigensuche durchführen", + beschreibung: + "Suche parallel selbst nach Therapieplätzen und dokumentiere deine Kontaktversuche.", + status: (step3Erledigt + ? "erledigt" + : step1Erledigt + ? "aktuell" + : "offen") as StepStatus, + visible: true, + }, + { + key: "antrag", + label: "Kostenerstattung beantragen", + beschreibung: + "Reiche den Kostenerstattungsantrag bei deiner Krankenkasse ein.", + status: (step4Available ? "aktuell" : "offen") as StepStatus, + visible: true, + }, + ]; + + return { + hasDiagnose, + hasDringlichkeit, + tssKontaktiert, + absagenUndKeineAntwort, + steps, + }; +} + +export async function setTssKontaktiert() { + const d = new Date(); + const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + await dbExec( + "UPDATE nutzer SET tss_kontaktiert_datum = $1, aktualisiert_am = NOW() WHERE id = 1", + [iso], + ); +} + +export async function createErstgespraech( + therapeutId: number, + datum: string, + diagnose: string | null, + dringlichkeitscode: boolean, +) { + const result = await dbExec( + `INSERT INTO sprechstunde (therapeut_id, diagnose, dringlichkeitscode) + VALUES ($1, $2, $3) RETURNING id`, + [therapeutId, diagnose || null, dringlichkeitscode], + ); + const sprechstundeId = (result.rows[0] as { id: number }).id; + await dbExec( + "INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)", + [sprechstundeId, datum], + ); +} + +export async function updateErstgespraech( + id: number, + diagnose: string | null, + dringlichkeitscode: boolean, +) { + await dbExec( + "UPDATE sprechstunde SET diagnose = $1, dringlichkeitscode = $2 WHERE id = $3", + [diagnose || null, dringlichkeitscode, id], + ); +} + +export async function addSitzung(sprechstundeId: number, datum: string) { + await dbExec( + "INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)", + [sprechstundeId, datum], + ); +} + +export async function deleteSitzung(id: number) { + await dbExec("DELETE FROM sitzung WHERE id = $1", [id]); +} + +export async function deleteErstgespraech(id: number) { + await dbExec("DELETE FROM sprechstunde WHERE id = $1", [id]); +} +``` + +- [ ] **Step 2: Verify no type errors in this file** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +Expect errors in other files that still import old types — that's fine for now. + +- [ ] **Step 3: Commit** + +```bash +git add src/features/prozess/hooks.ts +git commit -m "rewrite process hooks: data-driven status derivation, erstgespräch CRUD" +``` + +### Task 7: Update PhaseCard + +**Files:** +- Modify: `src/features/prozess/components/phase-card.tsx` + +- [ ] **Step 1: Replace `index` prop with `stepNumber`** + +Change the interface and usage: + +```typescript +interface PhaseCardProps { + label: string; + beschreibung: string; + status: PhaseStatus; + stepNumber: number; + children?: ReactNode; +} + +export function PhaseCard({ + label, + beschreibung, + status, + stepNumber, + children, +}: PhaseCardProps) { +``` + +And in the circle rendering (line 49): + +```typescript +{status === "erledigt" ? : stepNumber} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/features/prozess/components/phase-card.tsx +git commit -m "phase-card: replace index prop with explicit stepNumber" +``` + +### Task 8: Rewrite ProcessStepper + +**Files:** +- Rewrite: `src/features/prozess/components/process-stepper.tsx` + +- [ ] **Step 1: Replace entire file** + +```typescript +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 { ErstgespraechRow, ProcessStatus } from "../hooks"; +import { + addSitzung, + createErstgespraech, + setTssKontaktiert, + updateErstgespraech, + useErstgespraeche, + useSitzungen, +} from "../hooks"; +import { PhaseCard } from "./phase-card"; + +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"; + +function todayISO() { + const d = new Date(); + 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({ + processStatus, + tssKontaktiertDatum, + kontaktGesamt, + absagen, + onUpdate, +}: ProcessStepperProps) { + const visibleSteps = processStatus.steps.filter((s) => s.visible); + const currentIndex = visibleSteps.findIndex((s) => s.status === "aktuell"); + + return ( +
+
+

Dein Fortschritt

+ {currentIndex >= 0 && ( + + Schritt {currentIndex + 1} von {visibleSteps.length} + + )} +
+ +
+ {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 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)} + /> + + ) : ( + <> + +
+ +
+ + )} + + ); +} + +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 form = useForm({ + defaultValues: { + therapeut_id: "", + datum: todayISO(), + diagnose: "", + dringlichkeitscode: false, + }, + onSubmit: async ({ value }) => { + const therapeutId = Number(value.therapeut_id); + if (!therapeutId) return; + await createErstgespraech( + therapeutId, + value.datum, + value.diagnose || null, + value.dringlichkeitscode, + ); + onDone(); + }, + }); + + 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 ( + <> + +
+ +
+ + ); +} + +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 + +

+ + ); +} +``` + +- [ ] **Step 2: Verify no type errors** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +- [ ] **Step 3: Commit** + +```bash +git add src/features/prozess/components/process-stepper.tsx +git commit -m "rewrite process stepper: data-driven steps, erstgespräch list/form, conditional TSS" +``` + +### Task 9: Update route to pass ProcessStatus + +**Files:** +- Modify: `src/routes/prozess/index.tsx` + +- [ ] **Step 1: Replace entire file** + +```typescript +import { createFileRoute } from "@tanstack/react-router"; +import { ProcessStepper } from "@/features/prozess/components/process-stepper"; +import { + useErstgespraeche, + useKontaktStats, + useNutzer, + useProcessStatus, +} from "@/features/prozess/hooks"; + +export const Route = createFileRoute("/prozess/")({ + component: ProzessPage, +}); + +function ProzessPage() { + const { + data: nutzer, + loading: nutzerLoading, + refetch: refetchNutzer, + } = useNutzer(); + const { + data: stats, + loading: statsLoading, + refetch: refetchStats, + } = useKontaktStats(); + const { + data: erstgespraeche, + loading: egLoading, + refetch: refetchEg, + } = useErstgespraeche(); + + if (nutzerLoading || statsLoading || egLoading) return

Laden…

; + if (!nutzer[0]) return

Bitte zuerst das Onboarding abschließen.

; + + const s = stats[0] ?? { gesamt: 0, absagen: 0, warteliste: 0, keine_antwort: 0 }; + + const processStatus = useProcessStatus(nutzer[0], s, erstgespraeche); + + return ( + { + refetchNutzer(); + refetchStats(); + refetchEg(); + }} + /> + ); +} +``` + +- [ ] **Step 2: Verify no type errors** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +- [ ] **Step 3: Run dev server and verify /prozess loads** + +Run: `npx vite build 2>&1 | tail -10` + +- [ ] **Step 4: Commit** + +```bash +git add src/routes/prozess/index.tsx +git commit -m "update prozess route: pass process status from data queries" +``` + +--- + +## Chunk 3: Antrag checklist and scenarios + +### Task 10: Update antrag checklist + +**Files:** +- Modify: `src/features/antrag/components/antrag-checklist.tsx` + +- [ ] **Step 1: Replace entire file** + +```typescript +import { useErstgespraeche, useKontaktStats, useNutzer } from "@/features/prozess/hooks"; +import { Card, CardContent } from "@/shared/components/ui/card"; +import { Separator } from "@/shared/components/ui/separator"; +import { PdfExportButton } from "./pdf-export-button"; + +interface ChecklistItem { + label: string; + fulfilled: boolean; + visible: boolean; +} + +export function AntragChecklist() { + const { data: nutzerRows, loading: nutzerLoading } = useNutzer(); + const { data: statsRows, loading: statsLoading } = useKontaktStats(); + const { data: erstgespraeche, loading: egLoading } = useErstgespraeche(); + + if (nutzerLoading || statsLoading || egLoading) { + return

Laden…

; + } + + const nutzer = nutzerRows[0]; + const stats = statsRows[0]; + + if (!nutzer || !stats) { + return ( +

+ Keine Daten vorhanden. +

+ ); + } + + const hasDiagnose = erstgespraeche.some((e) => e.diagnose != null); + const hasDringlichkeit = erstgespraeche.some((e) => e.dringlichkeitscode); + + const items: ChecklistItem[] = [ + { + label: "Erstgespräch durchgeführt", + fulfilled: hasDiagnose, + visible: true, + }, + { + label: "Dringlichkeitscode erhalten", + fulfilled: hasDringlichkeit, + visible: true, + }, + { + label: "Terminservicestelle (TSS) kontaktiert", + fulfilled: nutzer.tss_kontaktiert_datum != null, + visible: hasDringlichkeit, + }, + { + label: "Therapeutensuche dokumentiert", + fulfilled: Number(stats.absagen) + Number(stats.keine_antwort) >= 5, + visible: true, + }, + { + label: "Absagenliste exportiert", + fulfilled: false, + visible: true, + }, + ]; + + const visibleItems = items.filter((i) => i.visible); + const fulfilledCount = visibleItems.filter((i) => i.fulfilled).length; + + return ( +
+
+

Kostenerstattungs-Assistent

+

+ {fulfilledCount} von {visibleItems.length} Voraussetzungen erfüllt +

+
+ +
+ {visibleItems.map((item, index) => ( + + + {item.fulfilled ? ( +
+ +
+ ) : ( +
+ {index + 1} +
+ )} + + {item.label} + +
+
+ ))} +
+ + + +
+

Nächste Schritte

+
    +
  1. + Besuche eine psychotherapeutische Sprechstunde und lass dir eine + Diagnose sowie einen Dringlichkeitscode geben. +
  2. +
  3. + Kontaktiere die Terminservicestelle (TSS) deiner Kassenärztlichen + Vereinigung unter 116 117. +
  4. +
  5. + Dokumentiere mindestens 5 erfolglose Kontaktversuche (Absagen oder + keine Antwort). +
  6. +
  7. + Exportiere die Absagenliste als PDF und lege sie deinem Antrag bei. +
  8. +
  9. + Reiche den Kostenerstattungsantrag bei deiner Krankenkasse ein. +
  10. +
+
+ + +
+ ); +} +``` + +- [ ] **Step 2: Verify no type errors** + +Run: `npx tsc --noEmit 2>&1 | head -30` + +- [ ] **Step 3: Commit** + +```bash +git add src/features/antrag/components/antrag-checklist.tsx +git commit -m "update antrag checklist: derive checks from data, conditional TSS visibility" +``` + +### Task 11: Rewrite scenarios + +**Files:** +- Modify: `src/features/einstellungen/scenarios.ts` + +- [ ] **Step 1: Replace entire file** + +```typescript +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 sitzung"); + await dbExec("DELETE FROM kontakt"); + await dbExec("DELETE FROM sprechstunde"); + await dbExec("DELETE FROM therapeut"); +} + +async function resetNutzer() { + await dbExec( + `UPDATE nutzer SET + tss_kontaktiert_datum = NULL, + aktualisiert_am = NOW() + WHERE id = 1`, + ); +} + +async function seedErstgespraech() { + 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; + + const spResult = await dbExec( + `INSERT INTO sprechstunde (therapeut_id, diagnose, dringlichkeitscode) + VALUES ($1, 'F32.1', TRUE) RETURNING id`, + [therapeutId], + ); + const sprechstundeId = (spResult.rows[0] as { id: number }).id; + + await dbExec( + `INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)`, + [sprechstundeId, daysAgoISO(14)], + ); + await dbExec( + `INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)`, + [sprechstundeId, daysAgoISO(7)], + ); +} + +async function seedTssKontaktiert() { + await dbExec( + `UPDATE nutzer SET tss_kontaktiert_datum = $1, 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], + ); + } + } +} + +export type Scenario = + | "erstgespraech" + | "diagnose_erhalten" + | "tss_kontaktiert" + | "eigensuche" + | "antrag_bereit"; + +const SCENARIO_ORDER: Scenario[] = [ + "erstgespraech", + "diagnose_erhalten", + "tss_kontaktiert", + "eigensuche", + "antrag_bereit", +]; + +const SCENARIO_SEEDERS: Record Promise) | null> = { + erstgespraech: null, + diagnose_erhalten: seedErstgespraech, + tss_kontaktiert: seedTssKontaktiert, + eigensuche: seedKontakte, + antrag_bereit: null, +}; + +export async function seedToScenario(target: Scenario) { + await clearData(); + await resetNutzer(); + + const targetIndex = SCENARIO_ORDER.indexOf(target); + if (targetIndex < 0) return; + + for (let i = 0; i <= targetIndex; i++) { + const seeder = SCENARIO_SEEDERS[SCENARIO_ORDER[i]]; + if (seeder) await seeder(); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/features/einstellungen/scenarios.ts +git commit -m "rewrite scenarios: seed data rows instead of setting aktueller_schritt" +``` + +### Task 12: Update settings page + +**Files:** +- Modify: `src/features/einstellungen/components/settings-page.tsx` + +- [ ] **Step 1: Update imports and dropdown options** + +Replace the `ProzessSchritt` import and `scenarioStep` state: + +Replace these two imports: +```typescript +import { seedToStep } from "@/features/einstellungen/scenarios"; +// ... +import type { ProzessSchritt } from "@/shared/db/schema"; +``` +with a single import: +```typescript +import { seedToScenario, type Scenario } from "@/features/einstellungen/scenarios"; +``` + +Change state: +```typescript +const [scenarioStep, setScenarioStep] = useState("neu"); +``` +to: +```typescript +const [scenarioStep, setScenarioStep] = useState("erstgespraech"); +``` + +Change dropdown options: +```html + + + + + +``` + +Change onClick: +```typescript +onClick={async () => { + await seedToScenario(scenarioStep); + setScenarioStatus("done"); +}} +``` + +Also change the `onChange` cast: +```typescript +onChange={(e) => { + setScenarioStep(e.target.value as Scenario); + setScenarioStatus("idle"); +}} +``` + +- [ ] **Step 2: Verify build** + +Run: `npx vite build 2>&1 | tail -10` + +- [ ] **Step 3: Commit** + +```bash +git add src/features/einstellungen/components/settings-page.tsx +git commit -m "update settings: use new scenario types" +``` + +### Task 13: Update deleteAllData and barrel export + +**Files:** +- Modify: `src/features/kontakte/hooks.ts` +- Modify: `src/features/prozess/index.ts` + +- [ ] **Step 1: Add sitzung to deleteAllData** + +The `deleteAllData()` function needs to also clear the new `sitzung` table. Add it before the existing deletes: + +Change: +```typescript +export async function deleteAllData() { + await dbExec("DELETE FROM kontakt"); + await dbExec("DELETE FROM sprechstunde"); + await dbExec("DELETE FROM therapeut"); + await dbExec("DELETE FROM nutzer"); +} +``` +to: +```typescript +export async function deleteAllData() { + await dbExec("DELETE FROM sitzung"); + await dbExec("DELETE FROM kontakt"); + await dbExec("DELETE FROM sprechstunde"); + await dbExec("DELETE FROM therapeut"); + await dbExec("DELETE FROM nutzer"); +} +``` + +- [ ] **Step 2: Update barrel export** + +Replace `src/features/prozess/index.ts` with: + +```typescript +export { ProcessStepper } from "./components/process-stepper"; +export { + useErstgespraeche, + useKontaktStats, + useNutzer, + useProcessStatus, +} from "./hooks"; +``` + +- [ ] **Step 3: Verify build** + +Run: `npx tsc --noEmit 2>&1 | head -20` + +- [ ] **Step 4: Run lint** + +Run: `npx biome check src/ 2>&1 | tail -10` + +- [ ] **Step 5: Commit** + +```bash +git add src/features/kontakte/hooks.ts src/features/prozess/index.ts +git commit -m "add sitzung to deleteAllData, update barrel export" +``` + +### Task 14: Final verification + +- [ ] **Step 1: Full build** + +Run: `npx vite build 2>&1 | tail -15` + +Expected: Build succeeds with no errors. + +- [ ] **Step 2: Run all tests** + +Run: `npx vitest run --reporter verbose 2>&1` + +Expected: All tests pass (the deleted constants.test.ts should no longer run). + +- [ ] **Step 3: Lint check** + +Run: `npx biome check src/ 2>&1 | tail -10` + +Expected: No errors. + +- [ ] **Step 4: Final commit if any fixes needed** + +Only if previous steps required fixes.