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

1250 lines
34 KiB
Markdown

# 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`**
```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**
```bash
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`:
```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:
```json
{
"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**
```bash
git init
```
Add `.gitignore` with: `node_modules/`, `dist/`, `.env`, `.env.local`, `.mise.local.toml`
```bash
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**
```bash
bun add @tanstack/react-router @tanstack/router-plugin
```
- [ ] **Step 2: Configure Vite plugin**
Update `vite.config.ts`:
```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`:
```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`:
```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`:
```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`**
```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**
```bash
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**
```bash
bun add tailwindcss @tailwindcss/vite
```
Update `vite.config.ts` to add the Tailwind plugin:
```ts
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:
```css
@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`:
```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**
```bash
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**
```bash
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`:
```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`:
```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`:
```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`:
```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**
```bash
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**
```bash
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`:
```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`:
```ts
import "@testing-library/jest-dom/vitest"
```
- [ ] **Step 3: Configure lint-staged + simple-git-hooks in `package.json`**
Add to `package.json`:
```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`:
```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**
```bash
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`:
```json
{
"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>`:
```html
<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**
```bash
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`
```bash
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**
```bash
bun add @tanstack/react-form
```
- [ ] **Step 2: Add shadcn form components**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**
```bash
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