Files
tpf/docs/superpowers/plans/2026-03-11-therapyfinder-v1.md
2026-03-11 11:11:16 +01:00

34 KiB

TherapyFinder V1 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: Build a local-first PWA that guides GKV-insured users through the psychotherapy access process — from first Sprechstunde to Kostenerstattungsantrag — with structured contact/rejection tracking and PDF export.

Architecture: Local-first PWA with all data stored in PGlite (IndexedDB). No backend for V1 — sensitive health data never leaves the device. The app is a multi-step process guide with a contact tracker that produces exportable rejection documentation. Therapist directory integration is deferred to V2 (data source still open).

Tech Stack: Bun, Vite, React 19, TanStack Router (file-based), shadcn/ui + Tailwind CSS v4, PGlite, Zustand, TanStack Form + Zod, vite-plugin-pwa, Vitest, Playwright, Biome


Scope

In scope (V1):

  • Project scaffolding and tooling
  • PGlite database with schema and migrations
  • Onboarding flow (capture insurance info, current process step)
  • Process stepper dashboard (5 phases, interactive checklist)
  • Contact tracker (log therapist contacts, outcomes, notes)
  • Rejection list PDF export (for Kostenerstattungsantrag)
  • Kostenerstattungs-Assistent (checklist + guidance)
  • PWA (offline-capable, installable)

Deferred (V2+):

  • Therapist directory / KV-Verzeichnis integration
  • Push notifications / reminders
  • Multi-language support (i18n)
  • Backend / cloud sync

File Structure

tpf/
├── .mise.toml
├── biome.json
├── package.json
├── tsconfig.json
├── vite.config.ts
├── index.html
├── public/
│   ├── manifest.webmanifest
│   └── icons/
│       ├── icon-192.png
│       └── icon-512.png
├── src/
│   ├── main.tsx
│   ├── app.tsx
│   ├── routes/
│   │   ├── __root.tsx
│   │   ├── index.tsx                          # redirects to /prozess or /onboarding
│   │   ├── onboarding/
│   │   │   └── index.tsx                      # Onboarding form
│   │   ├── prozess/
│   │   │   └── index.tsx                      # Process stepper dashboard
│   │   ├── kontakte/
│   │   │   ├── index.tsx                      # Contact list
│   │   │   └── neu.tsx                        # Add/edit contact
│   │   └── antrag/
│   │       └── index.tsx                      # Kostenerstattungs-Assistent
│   ├── features/
│   │   ├── onboarding/
│   │   │   ├── components/
│   │   │   │   └── onboarding-form.tsx
│   │   │   ├── schema.ts
│   │   │   └── index.ts
│   │   ├── prozess/
│   │   │   ├── components/
│   │   │   │   ├── process-stepper.tsx
│   │   │   │   └── phase-card.tsx
│   │   │   ├── schema.ts
│   │   │   ├── hooks.ts
│   │   │   └── index.ts
│   │   ├── kontakte/
│   │   │   ├── components/
│   │   │   │   ├── contact-form.tsx
│   │   │   │   ├── contact-list.tsx
│   │   │   │   └── contact-card.tsx
│   │   │   ├── schema.ts
│   │   │   ├── hooks.ts
│   │   │   └── index.ts
│   │   └── antrag/
│   │       ├── components/
│   │       │   ├── antrag-checklist.tsx
│   │       │   └── pdf-export-button.tsx
│   │       ├── schema.ts
│   │       ├── pdf.ts                         # PDF generation logic
│   │       └── index.ts
│   └── shared/
│       ├── components/
│       │   └── ui/                            # shadcn/ui components (generated)
│       ├── hooks/
│       │   └── use-db.ts                      # PGlite query hook
│       ├── db/
│       │   ├── client.ts                      # PGlite singleton
│       │   ├── migrations/
│       │   │   └── 001_init.sql
│       │   └── schema.ts                      # Zod schemas mirroring DB tables
│       └── lib/
│           ├── utils.ts                       # cn() and helpers
│           └── constants.ts                   # Process phases, enum labels
├── e2e/
│   ├── onboarding.spec.ts
│   └── kontakte.spec.ts
└── scripts/
    └── deploy.sh

Chunk 1: Project Scaffolding

Task 1: Initialize project with Vite + React + TypeScript

Files:

  • Create: .mise.toml

  • Create: package.json (via bun create)

  • Create: tsconfig.json

  • Create: vite.config.ts

  • Create: index.html

  • Create: src/main.tsx

  • Create: biome.json

  • Step 1: Create .mise.toml

[tools]
bun = "1.2.5"
  • Step 2: Install Bun via mise

Run: mise install

  • Step 3: Scaffold Vite project

Run: bun create vite . --template react-ts

Accept overwrite prompts for existing files. This generates package.json, tsconfig.json, vite.config.ts, index.html, src/main.tsx, src/App.tsx, etc.

  • Step 4: Install dependencies
bun add react@latest react-dom@latest
bun add -d @types/react @types/react-dom typescript vite @vitejs/plugin-react
  • Step 5: Configure Biome

Create biome.json:

{
	"$schema": "https://biomejs.dev/schemas/2.0.0-beta.1/schema.json",
	"organizeImports": {
		"enabled": true
	},
	"formatter": {
		"indentStyle": "tab"
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	}
}

Run: bun add -d @biomejs/biome

  • Step 6: Add scripts to package.json

Ensure these scripts exist:

{
	"scripts": {
		"dev": "vite",
		"build": "tsc && vite build",
		"preview": "vite preview",
		"test": "vitest run",
		"e2e": "playwright test",
		"lint": "biome check .",
		"format": "biome check --write ."
	}
}
  • Step 7: Verify dev server starts

Run: bun dev — should open on http://localhost:5173 with the default Vite template.

  • Step 8: Initialize git repo and commit
git init

Add .gitignore with: node_modules/, dist/, .env, .env.local, .mise.local.toml

git add -A && git commit -m "scaffold vite + react + typescript project"

Task 2: Add TanStack Router (file-based routing)

Files:

  • Modify: vite.config.ts

  • Modify: src/main.tsx

  • Create: src/app.tsx

  • Create: src/routes/__root.tsx

  • Create: src/routes/index.tsx

  • Step 1: Install TanStack Router

bun add @tanstack/react-router @tanstack/router-plugin
  • Step 2: Configure Vite plugin

Update vite.config.ts:

import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
	plugins: [TanStackRouterVite(), react()],
})
  • Step 3: Create root layout

Create src/routes/__root.tsx:

import { Outlet, createRootRoute } from "@tanstack/react-router"

export const Route = createRootRoute({
	component: () => (
		<div className="min-h-screen bg-background text-foreground">
			<main className="mx-auto max-w-2xl px-4 py-6">
				<Outlet />
			</main>
		</div>
	),
})
  • Step 4: Create index route

Create src/routes/index.tsx:

import { createFileRoute } from "@tanstack/react-router"

export const Route = createFileRoute("/")({
	component: () => <h1 className="text-2xl font-bold">TherapyFinder</h1>,
})
  • Step 5: Create app entry

Create src/app.tsx:

import { RouterProvider, createRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"

const router = createRouter({ routeTree })

declare module "@tanstack/react-router" {
	interface Register {
		router: typeof router
	}
}

export function App() {
	return <RouterProvider router={router} />
}
  • Step 6: Update src/main.tsx
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { App } from "./app"
import "./index.css"

createRoot(document.getElementById("root")!).render(
	<StrictMode>
		<App />
	</StrictMode>,
)
  • Step 7: Delete unused template files

Remove: src/App.tsx, src/App.css, src/assets/ (Vite template leftovers).

  • Step 8: Verify routing works

Run: bun dev — navigate to / and confirm "TherapyFinder" heading appears.

  • Step 9: Commit
git add -A && git commit -m "add tanstack router with file-based routing"

Task 3: Add Tailwind CSS v4 + shadcn/ui

Files:

  • Modify: package.json

  • Modify: src/index.css

  • Create: src/shared/lib/utils.ts

  • Create: src/shared/components/ui/ (generated by shadcn)

  • Step 1: Install Tailwind CSS v4

bun add tailwindcss @tailwindcss/vite

Update vite.config.ts to add the Tailwind plugin:

import tailwindcss from "@tailwindcss/vite"
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
	plugins: [TanStackRouterVite(), react(), tailwindcss()],
})
  • Step 2: Configure CSS entry

Replace src/index.css contents with:

@import "tailwindcss";
  • Step 3: Install and configure shadcn/ui

Run: bunx shadcn@latest init

Select: New York style, Zinc color, CSS variables: yes.

This creates components.json and sets up the src/shared/components/ui/ directory.

  • Step 4: Create cn() utility

Create src/shared/lib/utils.ts:

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(inputs))
}

Run: bun add clsx tailwind-merge

  • Step 5: Add initial shadcn components
bunx shadcn@latest add button card badge progress separator
  • Step 6: Verify styling works

Update src/routes/index.tsx to use a Button, run bun dev, confirm styled button renders.

  • Step 7: Commit
git add -A && git commit -m "add tailwind css v4, shadcn/ui with initial components"

Task 4: Add PGlite (local database)

Files:

  • Create: src/shared/db/client.ts

  • Create: src/shared/db/migrations/001_init.sql

  • Create: src/shared/db/schema.ts

  • Create: src/shared/hooks/use-db.ts

  • Step 1: Install PGlite

Run: bun add @electric-sql/pglite

  • Step 2: Create PGlite singleton

Create src/shared/db/client.ts:

import { PGlite } from "@electric-sql/pglite"

let _db: PGlite | null = null

export async function getDb(): Promise<PGlite> {
	if (!_db) {
		_db = new PGlite("idb://therapyfinder")
		await runMigrations(_db)
	}
	return _db
}

async function runMigrations(db: PGlite): Promise<void> {
	await db.exec(`
		CREATE TABLE IF NOT EXISTS _migrations (
			id SERIAL PRIMARY KEY,
			name TEXT NOT NULL UNIQUE,
			applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
		)
	`)

	const migrations = import.meta.glob("./migrations/*.sql", {
		query: "?raw",
		import: "default",
	})

	const sortedPaths = Object.keys(migrations).sort()

	for (const path of sortedPaths) {
		const name = path.split("/").pop()!
		const result = await db.query(
			"SELECT 1 FROM _migrations WHERE name = $1",
			[name],
		)
		if (result.rows.length > 0) continue

		const sql = (await migrations[path]()) as string
		await db.exec(sql)
		await db.query(
			"INSERT INTO _migrations (name) VALUES ($1)",
			[name],
		)
	}
}
  • Step 3: Create initial migration

Create src/shared/db/migrations/001_init.sql:

-- User profile (single row per device)
CREATE TABLE nutzer (
	id SERIAL PRIMARY KEY,
	name TEXT,
	plz TEXT,
	ort TEXT,
	krankenkasse TEXT,
	aktueller_schritt TEXT NOT NULL DEFAULT 'neu',
	dringlichkeitscode BOOLEAN NOT NULL DEFAULT FALSE,
	dringlichkeitscode_datum DATE,
	tss_beantragt BOOLEAN NOT NULL DEFAULT FALSE,
	tss_beantragt_datum DATE,
	erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
	aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Therapist contacts
CREATE TABLE therapeut (
	id SERIAL PRIMARY KEY,
	name TEXT NOT NULL,
	adresse TEXT,
	plz TEXT,
	stadt TEXT,
	telefon TEXT,
	email TEXT,
	website TEXT,
	therapieform TEXT,
	kassenzulassung TEXT DEFAULT 'gkv',
	erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Contact attempts
CREATE TABLE kontakt (
	id SERIAL PRIMARY KEY,
	therapeut_id INTEGER NOT NULL REFERENCES therapeut(id) ON DELETE CASCADE,
	datum DATE NOT NULL DEFAULT CURRENT_DATE,
	kanal TEXT NOT NULL DEFAULT 'telefon',
	ergebnis TEXT NOT NULL DEFAULT 'keine_antwort',
	notiz TEXT,
	antwort_datum DATE,
	erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Sprechstunden visits
CREATE TABLE sprechstunde (
	id SERIAL PRIMARY KEY,
	therapeut_id INTEGER NOT NULL REFERENCES therapeut(id) ON DELETE CASCADE,
	datum DATE NOT NULL,
	ergebnis TEXT,
	diagnose TEXT,
	dringlichkeitscode BOOLEAN NOT NULL DEFAULT FALSE,
	erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
  • Step 4: Create Zod schemas

Create src/shared/db/schema.ts:

import { z } from "zod"

export const prozessSchrittEnum = z.enum([
	"neu",
	"sprechstunde_absolviert",
	"diagnose_erhalten",
	"tss_beantragt",
	"eigensuche",
	"antrag_gestellt",
])
export type ProzessSchritt = z.infer<typeof prozessSchrittEnum>

export const kontaktKanalEnum = z.enum([
	"telefon",
	"email",
	"online_formular",
	"persoenlich",
])
export type KontaktKanal = z.infer<typeof kontaktKanalEnum>

export const kontaktErgebnisEnum = z.enum([
	"keine_antwort",
	"absage",
	"warteliste",
	"zusage",
])
export type KontaktErgebnis = z.infer<typeof kontaktErgebnisEnum>

export const therapieformEnum = z.enum([
	"verhaltenstherapie",
	"tiefenpsychologisch",
	"analytisch",
	"systemisch",
])
export type Therapieform = z.infer<typeof therapieformEnum>

export const nutzerSchema = z.object({
	id: z.number(),
	name: z.string().nullable(),
	plz: z.string().nullable(),
	ort: z.string().nullable(),
	krankenkasse: z.string().nullable(),
	aktueller_schritt: prozessSchrittEnum,
	dringlichkeitscode: z.boolean(),
	dringlichkeitscode_datum: z.string().nullable(),
	tss_beantragt: z.boolean(),
	tss_beantragt_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(),
	datum: z.string(),
	ergebnis: z.string().nullable(),
	diagnose: z.string().nullable(),
	dringlichkeitscode: z.boolean(),
})

Run: bun add zod

  • Step 5: Create database hook

Create src/shared/hooks/use-db.ts:

import { useCallback, useEffect, useState } from "react"
import { getDb } from "../db/client"

export function useDbQuery<T>(
	query: string,
	params: unknown[] = [],
	deps: unknown[] = [],
) {
	const [data, setData] = useState<T[]>([])
	const [loading, setLoading] = useState(true)
	const [error, setError] = useState<Error | null>(null)

	const refetch = useCallback(async () => {
		setLoading(true)
		try {
			const db = await getDb()
			const result = await db.query(query, params)
			setData(result.rows as T[])
			setError(null)
		} catch (e) {
			setError(e instanceof Error ? e : new Error(String(e)))
		} finally {
			setLoading(false)
		}
	}, [query, ...deps])

	useEffect(() => {
		refetch()
	}, [refetch])

	return { data, loading, error, refetch }
}

export async function dbExec(query: string, params: unknown[] = []) {
	const db = await getDb()
	return db.query(query, params)
}
  • Step 6: Verify database initializes

Update src/routes/index.tsx temporarily to test DB connection. Run bun dev — confirm DB initializes (check DevTools → Application → IndexedDB for therapyfinder).

  • Step 7: Commit
git add -A && git commit -m "add pglite with migration runner, zod schemas, db hook"

Task 5: Add Vitest + lint-staged + simple-git-hooks

Files:

  • Modify: package.json

  • Create: vitest.config.ts

  • Create: src/test-setup.ts

  • Create: src/shared/db/schema.test.ts

  • Step 1: Install test and code quality tooling

bun add -d vitest @testing-library/react @testing-library/jest-dom jsdom
bun add -d lint-staged simple-git-hooks
  • Step 2: Create Vitest config

Create vitest.config.ts:

import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import { resolve } from "node:path"

export default defineConfig({
	plugins: [react()],
	test: {
		environment: "jsdom",
		setupFiles: ["./src/test-setup.ts"],
		include: ["src/**/*.test.{ts,tsx}"],
	},
	resolve: {
		alias: {
			"@": resolve(__dirname, "src"),
		},
	},
})

Create src/test-setup.ts:

import "@testing-library/jest-dom/vitest"
  • Step 3: Configure lint-staged + simple-git-hooks in package.json

Add to package.json:

{
	"simple-git-hooks": {
		"pre-commit": "bunx lint-staged"
	},
	"lint-staged": {
		"*.{ts,tsx,json,css}": ["biome check --write"]
	}
}

Run: bunx simple-git-hooks

  • Step 4: Write a smoke test to verify setup

Create src/shared/db/schema.test.ts:

import { describe, expect, it } from "vitest"
import { prozessSchrittEnum } from "./schema"

describe("prozessSchrittEnum", () => {
	it("accepts valid steps", () => {
		expect(prozessSchrittEnum.parse("neu")).toBe("neu")
		expect(prozessSchrittEnum.parse("eigensuche")).toBe("eigensuche")
	})

	it("rejects invalid steps", () => {
		expect(() => prozessSchrittEnum.parse("invalid")).toThrow()
	})
})
  • Step 5: Run tests

Run: bun test Expected: 2 tests pass.

  • Step 6: Commit
git add -A && git commit -m "add vitest, testing library, lint-staged, simple-git-hooks"

Task 6: Add PWA support

Files:

  • Modify: vite.config.ts

  • Create: public/manifest.webmanifest

  • Create: public/icons/icon-192.png (placeholder)

  • Create: public/icons/icon-512.png (placeholder)

  • Step 1: Install vite-plugin-pwa

Run: bun add -d vite-plugin-pwa

  • Step 2: Configure PWA plugin

Update vite.config.ts to add VitePWA with registerType: "autoUpdate" and globPatterns: ["**/*.{js,css,html,wasm,data}"]. Use manifest: false (external manifest file).

  • Step 3: Create manifest

Create public/manifest.webmanifest:

{
	"name": "TherapyFinder",
	"short_name": "TherapyFinder",
	"start_url": "/",
	"display": "standalone",
	"background_color": "#ffffff",
	"theme_color": "#09090b",
	"icons": [
		{
			"src": "/icons/icon-192.png",
			"sizes": "192x192",
			"type": "image/png"
		},
		{
			"src": "/icons/icon-512.png",
			"sizes": "512x512",
			"type": "image/png"
		}
	]
}
  • Step 4: Add manifest link to index.html

Add to <head>:

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#09090b" />
  • Step 5: Create placeholder icons

Generate simple placeholder PNGs (solid color squares) for 192x192 and 512x512. Replace with proper icons later.

  • Step 6: Commit
git add -A && git commit -m "add pwa support with vite-plugin-pwa, manifest, placeholder icons"

Chunk 2: Onboarding and Process Stepper

Task 7: Define constants and label maps

Files:

  • Create: src/shared/lib/constants.ts

  • Create: src/shared/lib/constants.test.ts

  • Step 1: Create constants file

Create src/shared/lib/constants.ts with:

  • PROZESS_SCHRITTE: array of { key, label, beschreibung } for all 6 process steps (neu → antrag_gestellt)
  • KANAL_LABELS: Record<KontaktKanal, string> mapping enum values to German labels
  • ERGEBNIS_LABELS: Record<KontaktErgebnis, string> mapping enum values to German labels
  • THERAPIEFORM_LABELS: Record<Therapieform, string> mapping enum values to German labels

Labels:

  • neu → "Noch nicht begonnen"

  • sprechstunde_absolviert → "Sprechstunde absolviert"

  • diagnose_erhalten → "Diagnose erhalten"

  • tss_beantragt → "TSS kontaktiert"

  • eigensuche → "Eigensuche läuft"

  • antrag_gestellt → "Kostenerstattung beantragt"

  • telefon → "Telefon", email → "E-Mail", online_formular → "Online-Formular", persoenlich → "Persönlich"

  • keine_antwort → "Keine Antwort", absage → "Absage", warteliste → "Warteliste", zusage → "Zusage"

  • verhaltenstherapie → "Verhaltenstherapie (VT)", tiefenpsychologisch → "Tiefenpsychologisch fundierte PT (TP)", analytisch → "Analytische Psychotherapie (AP)", systemisch → "Systemische Therapie"

  • Step 2: Write test for constants

Create src/shared/lib/constants.test.ts: verify PROZESS_SCHRITTE has 6 entries, correct order, and all have non-empty label/beschreibung.

  • Step 3: Run tests, commit

Run: bun test

git add -A && git commit -m "add process step constants, channel/result label maps"

Task 8: Onboarding flow

Files:

  • Create: src/features/onboarding/schema.ts

  • Create: src/features/onboarding/schema.test.ts

  • Create: src/features/onboarding/components/onboarding-form.tsx

  • Create: src/features/onboarding/index.ts

  • Create: src/routes/onboarding/index.tsx

  • Modify: src/routes/index.tsx (redirect logic)

  • Step 1: Install TanStack Form

bun add @tanstack/react-form
  • Step 2: Add shadcn form components
bunx shadcn@latest add input label select
  • Step 3: Create onboarding schema

Create src/features/onboarding/schema.ts with a Zod schema for: name (min 1), plz (regex 5 digits), ort (min 1), krankenkasse (min 1), aktueller_schritt (prozessSchrittEnum).

  • Step 4: Write schema tests

Create src/features/onboarding/schema.test.ts: test valid data accepts, invalid PLZ rejects, empty name rejects.

  • Step 5: Run tests

Run: bun test — all pass.

  • Step 6: Create onboarding form component

Create src/features/onboarding/components/onboarding-form.tsx:

  • Uses useForm from TanStack Form with the onboarding schema

  • Fields: name, PLZ + Ort (grid), Krankenkasse, aktueller_schritt (select dropdown using PROZESS_SCHRITTE)

  • On submit: INSERT into nutzer table via dbExec, then navigate to /prozess

  • Welcoming header text explaining the app's purpose

  • Step 7: Create onboarding route

Create src/routes/onboarding/index.tsx rendering <OnboardingForm />.

  • Step 8: Create feature index

Create src/features/onboarding/index.ts re-exporting form and schema.

  • Step 9: Update root route with redirect logic

Update src/routes/index.tsx: in beforeLoad, query nutzer table. If no rows → redirect to /onboarding. If rows exist → redirect to /prozess.

  • Step 10: Verify onboarding flow

Run: bun dev → auto-redirect to /onboarding → fill form → submit → redirect to /prozess.

  • Step 11: Commit
git add -A && git commit -m "add onboarding flow with form, validation, db persistence"

Task 9: Process stepper dashboard

Files:

  • Create: src/features/prozess/hooks.ts

  • Create: src/features/prozess/components/phase-card.tsx

  • Create: src/features/prozess/components/process-stepper.tsx

  • Create: src/features/prozess/index.ts

  • Create: src/routes/prozess/index.tsx

  • Step 1: Create process data hooks

Create src/features/prozess/hooks.ts:

  • useNutzer(): query nutzer table, return first row

  • useKontaktStats(): aggregate query on kontakt table returning gesamt, absagen, warteliste, keine_antwort counts

  • updateSchritt(schritt): UPDATE nutzer aktueller_schritt

  • Step 2: Create phase card component

Create src/features/prozess/components/phase-card.tsx:

  • Props: label, beschreibung, status ("erledigt" | "aktuell" | "offen"), index

  • Renders a Card with a numbered circle (checkmark if erledigt), label, "Aktuell" badge if current, description only shown for current step

  • Visual styling: ring/border highlight for current, opacity-60 for completed

  • Step 3: Create process stepper component

Create src/features/prozess/components/process-stepper.tsx:

  • Props: aktuellerSchritt, kontaktGesamt, absagen

  • Renders heading "Dein Fortschritt" with step X of 6

  • Maps PROZESS_SCHRITTE to PhaseCard components with correct status

  • Shows contact stats card with links to /kontakte and /antrag when step >= "tss_beantragt"

  • Step 4: Create process route

Create src/routes/prozess/index.tsx: uses useNutzer() and useKontaktStats() hooks, renders <ProcessStepper>.

  • Step 5: Create feature index

Create src/features/prozess/index.ts re-exporting components and hooks.

  • Step 6: Verify process stepper

Run: bun dev → complete onboarding → verify stepper shows correct highlighted phase.

  • Step 7: Commit
git add -A && git commit -m "add process stepper dashboard with phase cards, contact stats"

Chunk 3: Contact Tracker

Task 10: Contact tracker — data layer and schemas

Files:

  • Create: src/features/kontakte/schema.ts

  • Create: src/features/kontakte/schema.test.ts

  • Create: src/features/kontakte/hooks.ts

  • Create: src/features/kontakte/index.ts

  • Step 1: Create contact feature schemas

Create src/features/kontakte/schema.ts:

  • therapeutFormSchema: name (required), optional adresse, plz (5-digit regex or empty), stadt, telefon, email (valid or empty), website, therapieform

  • kontaktFormSchema: therapeut_id (number), datum (required), kanal (kontaktKanalEnum), ergebnis (kontaktErgebnisEnum), notiz (optional)

  • Step 2: Write schema tests

Create src/features/kontakte/schema.test.ts: test valid therapist, required name, valid contact, required date.

  • Step 3: Run tests

Run: bun test — all pass.

  • Step 4: Create contact hooks

Create src/features/kontakte/hooks.ts:

  • useTherapeutenListe(): query therapeut table joined with latest kontakt subqueries (letzter_kontakt, letztes_ergebnis, kontakte_gesamt)

  • useKontakteForTherapeut(therapeutId): query kontakt table filtered by therapeut_id

  • createTherapeut(data): INSERT into therapeut, RETURNING id

  • createKontakt(data): INSERT into kontakt

  • Step 5: Create feature index

Create src/features/kontakte/index.ts re-exporting schemas and hooks.

  • Step 6: Commit
git add -A && git commit -m "add contact tracker data layer: schemas, hooks, db queries"

Task 11: Contact tracker — UI components

Files:

  • Create: src/features/kontakte/components/contact-card.tsx

  • Create: src/features/kontakte/components/contact-list.tsx

  • Create: src/features/kontakte/components/contact-form.tsx

  • Create: src/routes/kontakte/index.tsx

  • Create: src/routes/kontakte/neu.tsx

  • Modify: src/routes/__root.tsx (add bottom navigation)

  • Step 1: Add shadcn components

bunx shadcn@latest add textarea dialog
  • Step 2: Create contact card

Create src/features/kontakte/components/contact-card.tsx:

  • Shows therapist name, city, last contact date, result badge (color-coded: zusage=default, warteliste=secondary, absage=destructive, keine_antwort=outline), contact count

  • Entire card is a Link to /kontakte/neu?therapeutId=<id>

  • Step 3: Create contact list

Create src/features/kontakte/components/contact-list.tsx:

  • Heading "Kontakte" with "+ Neu" button linking to /kontakte/neu

  • Empty state with CTA to add first contact

  • Maps useTherapeutenListe() results to ContactCard components

  • Step 4: Create combined contact form

Create src/features/kontakte/components/contact-form.tsx:

  • Two sections separated by <Separator>: "Therapeut:in" (name, stadt, telefon, email) and "Kontaktversuch" (datum, kanal select, ergebnis select, notiz textarea)

  • On submit: createTherapeut() then createKontakt() with returned ID, navigate to /kontakte

  • Cancel button navigates back

  • Step 5: Create contact routes

Create src/routes/kontakte/index.tsx rendering <ContactList />. Create src/routes/kontakte/neu.tsx rendering <ContactForm />.

  • Step 6: Add bottom navigation to root layout

Update src/routes/__root.tsx: add a <nav> below <main> with three <Link> tabs: Fortschritt (/prozess), Kontakte (/kontakte), Antrag (/antrag). Use [&.active]:font-bold [&.active]:text-primary for active state.

  • Step 7: Verify contact flow

Run: bun dev → navigate to /kontakte → add a new contact → verify it appears in the list with correct badge.

  • Step 8: Commit
git add -A && git commit -m "add contact tracker: list, form, cards, bottom navigation"

Chunk 4: Kostenerstattung and PDF Export

Task 12: PDF export for rejection documentation

Files:

  • Create: src/features/antrag/pdf.ts

  • Create: src/features/antrag/components/pdf-export-button.tsx

  • Step 1: Install jspdf

Run: bun add jspdf

  • Step 2: Create PDF generation logic

Create src/features/antrag/pdf.ts:

  • generateAbsagenPdf(): async function that:

    • Queries nutzer (name, krankenkasse) and all kontakt rows joined with therapeut
    • Creates a jsPDF document with: header (title, user info, date), summary (total contacts, absagen, keine_antwort), table with columns (Datum, Therapeut:in, Ort, Kontaktweg, Ergebnis), footer note
    • Handles page breaks when y > 270
    • Calls doc.save("therapeutensuche-dokumentation.pdf")
  • Step 3: Create PDF export button

Create src/features/antrag/components/pdf-export-button.tsx:

  • Button that calls generateAbsagenPdf() with loading state

  • Step 4: Commit

git add -A && git commit -m "add pdf export for rejection documentation (jspdf)"

Task 13: Kostenerstattungs-Assistent

Files:

  • Create: src/features/antrag/components/antrag-checklist.tsx

  • Create: src/features/antrag/index.ts

  • Create: src/routes/antrag/index.tsx

  • Step 1: Create Antrag checklist component

Create src/features/antrag/components/antrag-checklist.tsx:

  • Uses useNutzer() and useKontaktStats() from prozess hooks

  • Renders 5 checklist items as Cards with green checkmark or grey number:

    1. "Psychotherapeutische Sprechstunde besucht" — fulfilled if step > neu
    2. "Diagnose / Dringlichkeitscode erhalten" — fulfilled if dringlichkeitscode is true
    3. "Terminservicestelle (TSS) kontaktiert" — fulfilled if tss_beantragt is true
    4. "Eigenständige Therapeutensuche dokumentiert" — fulfilled if absagen + keine_antwort >= 5
    5. "Absagenliste exportiert" — always unchecked (manual step)
  • Shows "Nächste Schritte" ordered list with guidance

  • Renders <PdfExportButton /> at the bottom

  • Step 2: Create feature index

Create src/features/antrag/index.ts re-exporting components and pdf function.

  • Step 3: Create Antrag route

Create src/routes/antrag/index.tsx rendering <AntragChecklist />.

  • Step 4: Verify full flow

Run: bun dev → complete onboarding → add 5+ contacts with "Absage" → navigate to /antrag → verify checklist → click PDF export → verify PDF downloads.

  • Step 5: Commit
git add -A && git commit -m "add kostenerstattungs-assistent with checklist, pdf export"

Chunk 5: Polish, Testing, and E2E

Task 14: Add dark mode toggle

Files:

  • Create: src/shared/hooks/use-theme.ts

  • Modify: src/routes/__root.tsx

  • Step 1: Install Zustand

Run: bun add zustand

  • Step 2: Create theme store

Create src/shared/hooks/use-theme.ts:

  • Zustand store with theme ("light" | "dark" | "system") and setTheme()

  • Persists to localStorage key "theme"

  • applyTheme() toggles dark class on document.documentElement

  • Applies theme on module load

  • Step 3: Add toggle to root layout

Update src/routes/__root.tsx: add a simple theme toggle button in the header area.

  • Step 4: Commit
git add -A && git commit -m "add dark mode toggle with zustand, local storage persistence"

Task 15: E2E tests with Playwright

Files:

  • Create: playwright.config.ts

  • Create: e2e/onboarding.spec.ts

  • Create: e2e/kontakte.spec.ts

  • Step 1: Install Playwright

bun add -d @playwright/test
bunx playwright install chromium
  • Step 2: Create Playwright config

Create playwright.config.ts: testDir "e2e", webServer command "bun dev" on port 5173, baseURL "http://localhost:5173".

  • Step 3: Write onboarding E2E test

Create e2e/onboarding.spec.ts:

  • Test: go to "/" → redirects to /onboarding → fill name, PLZ, Ort, Krankenkasse → click "Weiter" → redirects to /prozess → "Dein Fortschritt" visible

  • Step 4: Write contact tracker E2E test

Create e2e/kontakte.spec.ts:

  • beforeEach: complete onboarding

  • Test: navigate to Kontakte → click add → fill therapist name and city → save → see therapist in list

  • Step 5: Run E2E tests

Run: bun e2e Expected: both tests pass.

  • Step 6: Commit
git add -A && git commit -m "add playwright e2e tests for onboarding, contact tracker"

Task 16: Deploy script for Uberspace

Files:

  • Create: scripts/deploy.sh

  • Step 1: Create deploy script

Create scripts/deploy.sh: idempotent script that takes SSH host as argument, runs bun run build, then rsync the dist/ directory to /var/www/virtual/<user>/html/tpf/ on the remote host.

  • Step 2: Make executable

Run: chmod +x scripts/deploy.sh

  • Step 3: Commit
git add -A && git commit -m "add uberspace deploy script for static pwa hosting"

Summary

Chunk Tasks What it delivers
1: Scaffolding 1-6 Vite + React + TanStack Router + Tailwind + shadcn + PGlite + PWA + Vitest + Biome
2: Onboarding and Process 7-9 Onboarding form, process stepper dashboard with 6 phases
3: Contact Tracker 10-11 Therapist + contact attempt CRUD, list view, badge statuses
4: Kostenerstattung 12-13 PDF export of rejection documentation, Antrag checklist
5: Polish and Testing 14-16 Dark mode, E2E tests, deploy script

Total: 16 tasks, ~80 steps

After V1 ships, the main open items for V2 are:

  • Therapist directory integration (KV-Verzeichnis API or scraped data)
  • Push notifications / reminders
  • i18n (DE/EN/ES/FR)
  • DSGVO legal review