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(viabun 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 labelsERGEBNIS_LABELS:Record<KontaktErgebnis, string>mapping enum values to German labelsTHERAPIEFORM_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
useFormfrom TanStack Form with the onboarding schema -
Fields: name, PLZ + Ort (grid), Krankenkasse, aktueller_schritt (select dropdown using PROZESS_SCHRITTE)
-
On submit: INSERT into
nutzertable viadbExec, 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
/kontakteand/antragwhen 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()anduseKontaktStats()from prozess hooks -
Renders 5 checklist items as Cards with green checkmark or grey number:
- "Psychotherapeutische Sprechstunde besucht" — fulfilled if step > neu
- "Diagnose / Dringlichkeitscode erhalten" — fulfilled if dringlichkeitscode is true
- "Terminservicestelle (TSS) kontaktiert" — fulfilled if tss_beantragt is true
- "Eigenständige Therapeutensuche dokumentiert" — fulfilled if absagen + keine_antwort >= 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") andsetTheme() -
Persists to localStorage key "theme"
-
applyTheme()togglesdarkclass ondocument.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