# DAK Zweitmeinungs-Portal — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build a full-stack portal for managing DAK second-opinion medical cases — from CSV import through ICD coding to weekly Excel reports.

**Architecture:** FastAPI backend (Python, SQLAlchemy 2.0, MariaDB) serving a React SPA (Vite, TypeScript, shadcn/ui). JWT auth with RBAC (admin / dak_mitarbeiter). Deployed on Hetzner 1 via systemd + Plesk-Nginx.

**Tech Stack:** Python 3.13/FastAPI/SQLAlchemy/Alembic/Pandas/openpyxl (Backend), React 18/Vite/TypeScript/Tailwind CSS/shadcn-ui/Recharts (Frontend), MariaDB 10.11.14

**Spec Reference:** `/home/frontend/dak_c2s/Dak_projekt_spezifikation_final.md`

**DB Connection (Dev):** Remote to Hetzner 1 MariaDB `dak_c2s` / `dak_c2s_admin`

---

## Phase 1: Project Setup + Database + Auth

### Task 1: Project Scaffolding

**Files:**
- Create: `backend/app/__init__.py`
- Create: `backend/app/config.py`
- Create: `backend/app/database.py`
- Create: `backend/app/main.py`
- Create: `backend/requirements.txt`
- Create: `backend/.env.example`
- Create: `backend/.env`
- Create: `.gitignore`

**Step 1: Initialize git repo and create directory structure**

```bash
cd /home/frontend/dak_c2s
git init
git checkout -b develop
```

Create full directory tree:
```
backend/
  app/
    __init__.py
    config.py
    database.py
    main.py
    models/__init__.py
    schemas/__init__.py
    api/__init__.py
    services/__init__.py
    core/__init__.py
    utils/__init__.py
  alembic/
  scripts/
  tests/
    __init__.py
    conftest.py
frontend/
data/
docs/
```

**Step 2: Write requirements.txt**

```
# Backend dependencies
fastapi==0.115.6
uvicorn[standard]==0.34.0
gunicorn==23.0.0
sqlalchemy==2.0.36
alembic==1.14.1
pymysql==1.1.1
cryptography==44.0.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
pyotp==2.9.0
qrcode==8.0
python-multipart==0.0.20
pandas==2.2.3
openpyxl==3.1.5
pydantic==2.10.4
pydantic-settings==2.7.1
python-dotenv==1.0.1
email-validator==2.2.0
httpx==0.28.1

# Dev/Test
pytest==8.3.4
pytest-asyncio==0.25.0
pytest-cov==6.0.0
```

**Step 3: Write config.py (Pydantic Settings)**

```python
# backend/app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    # Database
    DB_HOST: str = "localhost"
    DB_PORT: int = 3306
    DB_NAME: str = "dak_c2s"
    DB_USER: str = "dak_c2s_admin"
    DB_PASSWORD: str = ""

    # JWT
    JWT_SECRET_KEY: str = "change-me-in-production"
    JWT_ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7

    # SMTP
    SMTP_HOST: str = "smtp.complexcaresolutions.de"
    SMTP_PORT: int = 465
    SMTP_USER: str = "noreply@complexcaresolutions.de"
    SMTP_PASSWORD: str = ""
    SMTP_FROM: str = "noreply@complexcaresolutions.de"

    # App
    APP_NAME: str = "DAK Zweitmeinungs-Portal"
    CORS_ORIGINS: str = "http://localhost:5173,https://dak.complexcaresolutions.de"
    MAX_UPLOAD_SIZE: int = 20 * 1024 * 1024  # 20MB

    @property
    def database_url(self) -> str:
        return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset=utf8mb4"

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

@lru_cache
def get_settings() -> Settings:
    return Settings()
```

**Step 4: Write database.py**

```python
# backend/app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from app.config import get_settings

settings = get_settings()
engine = create_engine(settings.database_url, pool_pre_ping=True, pool_recycle=3600)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

class Base(DeclarativeBase):
    pass

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
```

**Step 5: Write main.py (minimal FastAPI app)**

```python
# backend/app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import get_settings

settings = get_settings()

app = FastAPI(title=settings.APP_NAME, docs_url="/docs")

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS.split(","),
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/health")
def health_check():
    return {"status": "ok"}
```

**Step 6: Write .env.example and .env, create .gitignore**

`.env.example` — all keys without values.
`.env` — actual values with Hetzner 1 DB credentials.
`.gitignore` — Python, Node, `.env`, `data/`, `__pycache__/`, `venv/`, `node_modules/`, `dist/`.

**Step 7: Create Python venv, install deps, test server starts**

```bash
cd /home/frontend/dak_c2s/backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# Verify: GET http://localhost:8000/api/health → {"status": "ok"}
```

**Step 8: Commit**

```bash
git add -A
git commit -m "feat: project scaffolding with FastAPI, config, database connection"
```

---

### Task 2: SQLAlchemy Models

**Files:**
- Create: `backend/app/models/user.py`
- Create: `backend/app/models/case.py`
- Create: `backend/app/models/report.py`
- Create: `backend/app/models/audit.py`
- Modify: `backend/app/models/__init__.py`

**Step 1: Write User models**

`backend/app/models/user.py` — 4 models matching the SQL schema:
- `User` (id, username, email, password_hash, role, mfa_secret, mfa_enabled, is_active, must_change_password, last_login, failed_login_attempts, locked_until, created_at, updated_at)
- `RefreshToken` (id, user_id → FK users, token_hash, expires_at, revoked, created_at)
- `InvitationLink` (id, token, email, role, created_by → FK users, expires_at, used_at, used_by → FK users, is_active, created_at)
- `AllowedDomain` (id, domain, role, is_active, created_at)

Use `mapped_column()` syntax (SQLAlchemy 2.0 declarative). All column types, defaults, indexes, and constraints must match the SQL schema in Spec Section 4 exactly.

**Step 2: Write Case models**

`backend/app/models/case.py` — 2 models:
- `Case` — 45+ columns matching spec exactly. Include all CHECK constraints as Python-level validation (SQLAlchemy `CheckConstraint`). Indexes: `idx_jahr_kw`, `idx_kvnr`, `idx_fallgruppe`, `idx_datum`, `idx_nachname_vorname`, `idx_pending_icd`, `idx_pending_coding`.
- `CaseICDCode` (id, case_id → FK cases ON DELETE CASCADE, icd_code, icd_hauptgruppe, created_at)

**Step 3: Write Report models**

`backend/app/models/report.py` — 2 models:
- `WeeklyReport` (id, jahr, kw, report_date, report_file_path, report_data [JSON], generated_by, generated_at)
- `YearlySummary` — all 40+ aggregation columns matching spec exactly (erstberatungen through ta_uebertherapie, per-fallgruppe breakdown)

**Step 4: Write Audit models**

`backend/app/models/audit.py` — 3 models:
- `ImportLog` (id, filename, import_type, cases_imported/skipped/updated, errors, details [JSON], imported_by, imported_at) — CHECK constraint on import_type
- `AuditLog` (id, user_id, action, entity_type, entity_id, old_values [JSON], new_values [JSON], ip_address, user_agent, created_at)
- `Notification` (id, recipient_id, notification_type, title, message, related_entity_type/id, is_read, email_sent, email_sent_at, created_at) — CHECK constraint on notification_type

**Step 5: Wire up models/__init__.py**

```python
# backend/app/models/__init__.py
from app.models.user import User, RefreshToken, InvitationLink, AllowedDomain
from app.models.case import Case, CaseICDCode
from app.models.report import WeeklyReport, YearlySummary
from app.models.audit import AuditLog, ImportLog, Notification

__all__ = [
    "User", "RefreshToken", "InvitationLink", "AllowedDomain",
    "Case", "CaseICDCode",
    "WeeklyReport", "YearlySummary",
    "AuditLog", "ImportLog", "Notification",
]
```

**Step 6: Verify models compile**

```bash
cd /home/frontend/dak_c2s/backend
source venv/bin/activate
python -c "from app.models import *; print('All models loaded OK')"
```

**Step 7: Commit**

```bash
git add backend/app/models/
git commit -m "feat: SQLAlchemy models for users, cases, reports, audit"
```

---

### Task 3: Alembic Migrations

**Files:**
- Create: `backend/alembic.ini`
- Create: `backend/alembic/env.py`
- Create: `backend/alembic/versions/` (auto-generated)

**Step 1: Initialize Alembic**

```bash
cd /home/frontend/dak_c2s/backend
source venv/bin/activate
alembic init alembic
```

**Step 2: Configure alembic/env.py**

- Import `Base` from `app.database` and all models from `app.models`
- Set `target_metadata = Base.metadata`
- Read `sqlalchemy.url` from `app.config.get_settings().database_url`

**Step 3: Generate initial migration**

```bash
alembic revision --autogenerate -m "initial schema"
```

**Step 4: Review generated migration, then apply**

```bash
alembic upgrade head
```

**Step 5: Seed allowed_domains**

Write `backend/scripts/init_db.py`:
- Insert `AllowedDomain(domain='dak.de', role='dak_mitarbeiter')` if not exists
- Run: `python -m scripts.init_db`

**Step 6: Verify tables exist on Hetzner DB**

```bash
python -c "
from app.database import engine
from sqlalchemy import inspect
insp = inspect(engine)
print(insp.get_table_names())
"
# Expected: ['users', 'refresh_tokens', 'invitation_links', 'allowed_domains', 'cases', 'case_icd_codes', 'weekly_reports', 'yearly_summary', 'import_log', 'audit_log', 'notifications']
```

**Step 7: Commit**

```bash
git add alembic.ini backend/alembic/ backend/scripts/
git commit -m "feat: Alembic migrations, initial schema deployed"
```

---

### Task 4: Core Security & Dependencies

**Files:**
- Create: `backend/app/core/security.py`
- Create: `backend/app/core/dependencies.py`
- Create: `backend/app/core/exceptions.py`

**Step 1: Write security.py**

```python
# backend/app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from passlib.context import CryptContext
import pyotp
import secrets
from app.config import get_settings

settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

def create_access_token(user_id: int, role: str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    return jwt.encode({"sub": str(user_id), "role": role, "exp": expire}, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)

def create_refresh_token() -> str:
    return secrets.token_urlsafe(64)

def decode_access_token(token: str) -> dict:
    return jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])

def generate_mfa_secret() -> str:
    return pyotp.random_base32()

def verify_mfa_code(secret: str, code: str) -> bool:
    totp = pyotp.TOTP(secret)
    return totp.verify(code)

def get_mfa_uri(secret: str, email: str) -> str:
    totp = pyotp.TOTP(secret)
    return totp.provisioning_uri(name=email, issuer_name=settings.APP_NAME)
```

**Step 2: Write dependencies.py**

```python
# backend/app/core/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.database import get_db
from app.core.security import decode_access_token
from app.models.user import User
from jose import JWTError

security = HTTPBearer()

def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_db),
) -> User:
    try:
        payload = decode_access_token(credentials.credentials)
        user_id = int(payload["sub"])
    except (JWTError, KeyError, ValueError):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
    user = db.query(User).filter(User.id == user_id, User.is_active == True).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
    return user

def require_admin(user: User = Depends(get_current_user)) -> User:
    if user.role != "admin":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
    return user
```

**Step 3: Write exceptions.py**

Custom exceptions: `CaseNotFound`, `DuplicateCase`, `InvalidImportFile`, `ICDValidationError`. Each maps to appropriate HTTP status codes.

**Step 4: Write tests for security functions**

`backend/tests/test_security.py`:
- `test_hash_and_verify_password`
- `test_create_and_decode_access_token`
- `test_invalid_token_raises`
- `test_mfa_secret_and_verify`

```bash
cd /home/frontend/dak_c2s/backend
pytest tests/test_security.py -v
```

**Step 5: Commit**

```bash
git add backend/app/core/ backend/tests/test_security.py
git commit -m "feat: JWT auth, bcrypt, MFA, dependency injection"
```

---

### Task 5: Auth Schemas & API

**Files:**
- Create: `backend/app/schemas/auth.py`
- Create: `backend/app/schemas/user.py`
- Create: `backend/app/api/auth.py`
- Create: `backend/app/services/auth_service.py`
- Modify: `backend/app/main.py` (add router)

**Step 1: Write auth schemas**

`backend/app/schemas/auth.py`:
- `LoginRequest` (email, password, mfa_code: Optional)
- `TokenResponse` (access_token, refresh_token, token_type, user: UserResponse)
- `RegisterRequest` (username, email, password, invitation_token: Optional)
- `RefreshRequest` (refresh_token)
- `MFASetupResponse` (secret, qr_uri)
- `MFAVerifyRequest` (code)

`backend/app/schemas/user.py`:
- `UserResponse` (id, username, email, role, mfa_enabled, is_active, last_login, created_at)
- `UserCreate` (username, email, password, role)
- `UserUpdate` (username, email, role, is_active — all Optional)

**Step 2: Write auth_service.py**

Business logic:
- `authenticate_user(db, email, password, mfa_code)` — check credentials, account lock (5 attempts, 30 min), MFA verification, update last_login
- `register_user(db, data)` — domain whitelist check OR invitation token validation
- `create_tokens(db, user)` — generate access + refresh, store refresh hash in DB
- `refresh_access_token(db, refresh_token)` — validate, return new access token
- `revoke_refresh_token(db, refresh_token)` — mark as revoked

**Step 3: Write auth API router**

`backend/app/api/auth.py`:
- `POST /api/auth/login` → authenticate, return tokens
- `POST /api/auth/register` → domain check or invitation, create user
- `POST /api/auth/refresh` → new access token
- `POST /api/auth/logout` → revoke refresh token
- `POST /api/auth/mfa/setup` → generate secret + QR URI (admin only)
- `POST /api/auth/mfa/verify` → verify TOTP code, enable MFA

**Step 4: Register router in main.py**

```python
from app.api.auth import router as auth_router
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
```

**Step 5: Write auth tests**

`backend/tests/test_auth.py`:
- `test_register_with_valid_domain`
- `test_register_with_invalid_domain_rejected`
- `test_register_with_invitation_token`
- `test_login_success`
- `test_login_wrong_password`
- `test_account_lockout_after_5_failures`
- `test_refresh_token_flow`
- `test_logout_revokes_token`

Use `httpx.AsyncClient` with `app` for integration tests. Use a test fixture for DB session.

```bash
pytest tests/test_auth.py -v
```

**Step 6: Commit**

```bash
git add backend/app/schemas/ backend/app/api/auth.py backend/app/services/auth_service.py backend/tests/test_auth.py
git commit -m "feat: auth system — login, register, refresh, MFA, domain whitelist"
```

---

### Task 6: Admin API + Audit Middleware

**Files:**
- Create: `backend/app/api/admin.py`
- Create: `backend/app/services/audit_service.py`
- Create: `backend/app/api/notifications.py`
- Modify: `backend/app/main.py` (add routers + audit middleware)

**Step 1: Write audit_service.py**

```python
# Helper to log actions
def log_action(db, user_id, action, entity_type, entity_id, old_values, new_values, ip, user_agent):
    entry = AuditLog(user_id=user_id, action=action, entity_type=entity_type,
                     entity_id=entity_id, old_values=old_values, new_values=new_values,
                     ip_address=ip, user_agent=user_agent)
    db.add(entry)
    db.commit()
```

**Step 2: Write admin API**

`backend/app/api/admin.py`:
- `GET /api/admin/users` → list users (admin only)
- `POST /api/admin/users` → create user
- `PUT /api/admin/users/{id}` → update role, active status
- `POST /api/admin/invitations` → create invitation link (token + expiry + optional email)
- `GET /api/admin/invitations` → list invitations
- `GET /api/admin/audit-log` → paginated audit log

**Step 3: Write notifications API**

`backend/app/api/notifications.py`:
- `GET /api/notifications` → unread + recent for current user
- `PUT /api/notifications/{id}/read` → mark single as read
- `PUT /api/notifications/read-all` → mark all as read

**Step 4: Write create_admin.py script**

`backend/scripts/create_admin.py` — interactive script to create the first admin user (prompts for username, email, password).

**Step 5: Register routers in main.py, test endpoints**

```bash
uvicorn app.main:app --reload --port 8000
# Test: POST /api/auth/register, POST /api/auth/login
# Test: GET /api/admin/users (with admin token)
```

**Step 6: Commit**

```bash
git add backend/app/api/admin.py backend/app/api/notifications.py backend/app/services/audit_service.py backend/scripts/create_admin.py
git commit -m "feat: admin API, audit logging, notifications, create_admin script"
```

---

## Phase 2: Import & ICD Workflow

### Task 7: Utility Functions

**Files:**
- Create: `backend/app/utils/fallgruppe_map.py`
- Create: `backend/app/utils/kw_utils.py`
- Create: `backend/app/utils/validators.py`

**Step 1: Write fallgruppe_map.py**

```python
MODUL_TO_FALLGRUPPE = {
    "Zweitmeinung Onkologie": "onko",
    "Zweitmeinung Kardiologie": "kardio",
    "Zweitmeinung Intensiv": "intensiv",
    "Zweitmeinung Gallenblase": "galle",
    "Zweitmeinung Schilddrüse": "sd",
}

def map_modul_to_fallgruppe(modul: str) -> str:
    modul = modul.strip()
    if modul in MODUL_TO_FALLGRUPPE:
        return MODUL_TO_FALLGRUPPE[modul]
    modul_lower = modul.lower()
    if "begutachtung" in modul_lower:
        # Derive from context — check for keywords
        for key, val in [("onko", "onko"), ("kardio", "kardio"), ("intensiv", "intensiv"),
                         ("galle", "galle"), ("schilddrüse", "sd")]:
            if key in modul_lower:
                return val
    raise ValueError(f"Cannot map module: {modul}")
```

**Step 2: Write kw_utils.py**

- `date_to_kw(d: date) -> int` — ISO calendar week
- `date_to_jahr(d: date) -> int` — ISO calendar year (can differ from d.year in week 1/53)
- `parse_german_date(s: str) -> date` — handles "DD.MM.YY" and "DD.MM.YYYY", edge cases like "29.08.0196"

**Step 3: Write validators.py**

- `validate_icd(code: str) -> str` — regex `^[A-Z]\d{2}(\.\d{1,2})?$`, normalize uppercase, strip
- `validate_kvnr(kvnr: str) -> str` — format check (letter + 9 digits)
- `normalize_icd_hauptgruppe(code: str) -> str` — extract letter + first 2 digits (e.g., "C50.1" → "C50")

**Step 4: Write tests**

`backend/tests/test_utils.py`:
- Test all mappings including "Begutachtung" edge cases
- Test KW calculation across year boundaries
- Test German date parsing with edge cases ("29.08.0196")
- Test ICD validation (valid, invalid, normalization)
- Test KVNR validation

```bash
pytest tests/test_utils.py -v
```

**Step 5: Commit**

```bash
git add backend/app/utils/ backend/tests/test_utils.py
git commit -m "feat: utility functions — fallgruppe mapping, KW calc, ICD/KVNR validation"
```

---

### Task 8: CRM CSV Parser

**Files:**
- Create: `backend/app/services/csv_parser.py`
- Create: `backend/tests/test_csv_parser.py`

**Step 1: Write failing tests for CSV parser**

Test cases from spec:
- Normal row: `"Tonn | Regina | 28.04.1960 | D410126355"` → nachname=Tonn, vorname=Regina, geburtsdatum=1960-04-28, kvnr=D410126355
- Missing KVNR: `"Daum | Luana | 05.02.2016 |"` → kvnr=None
- Bad date: `"Name | Vorname | 29.08.0196 | X123"` → geburtsdatum=None (log warning)
- Date format: `"02.02.26, 08:50"` → 2026-02-02
- Modul mapping: `"Zweitmeinung Onkologie"` → `"onko"`

```bash
pytest tests/test_csv_parser.py -v
# Expected: FAIL (csv_parser.py doesn't exist yet)
```

**Step 2: Implement csv_parser.py**

```python
# backend/app/services/csv_parser.py
import csv
import io
from dataclasses import dataclass
from datetime import date
from typing import Optional
from app.utils.fallgruppe_map import map_modul_to_fallgruppe
from app.utils.kw_utils import parse_german_date, date_to_kw, date_to_jahr

@dataclass
class ParsedCase:
    nachname: str
    vorname: Optional[str]
    geburtsdatum: Optional[date]
    kvnr: Optional[str]
    thema: str
    fallgruppe: str
    datum: date
    jahr: int
    kw: int
    crm_ticket_id: Optional[str]

def parse_hauptkontakt(raw: str) -> dict:
    """Parse pipe-delimited contact: 'Nachname | Vorname | Geburtsdatum | KVNR'"""
    parts = [p.strip() for p in raw.split("|")]
    result = {"nachname": parts[0] if len(parts) > 0 else ""}
    result["vorname"] = parts[1] if len(parts) > 1 and parts[1] else None
    result["geburtsdatum"] = None
    if len(parts) > 2 and parts[2]:
        try:
            result["geburtsdatum"] = parse_german_date(parts[2])
        except ValueError:
            pass  # Log warning, continue
    result["kvnr"] = parts[3] if len(parts) > 3 and parts[3] else None
    return result

def parse_csv(content: bytes, filename: str) -> list[ParsedCase]:
    """Parse CRM CSV file, return list of ParsedCase."""
    text = content.decode("utf-8-sig")  # Handle BOM
    reader = csv.DictReader(io.StringIO(text))
    cases = []
    for row in reader:
        kontakt = parse_hauptkontakt(row.get("Hauptkontakt", ""))
        datum_str = row.get("Erstellungsdatum", "")
        datum = parse_german_date(datum_str.split(",")[0].strip()) if datum_str else date.today()
        modul = row.get("Modul", "")
        fallgruppe = map_modul_to_fallgruppe(modul)
        cases.append(ParsedCase(
            nachname=kontakt["nachname"],
            vorname=kontakt["vorname"],
            geburtsdatum=kontakt["geburtsdatum"],
            kvnr=kontakt["kvnr"],
            thema=row.get("Thema", ""),
            fallgruppe=fallgruppe,
            datum=datum,
            jahr=date_to_jahr(datum),
            kw=date_to_kw(datum),
            crm_ticket_id=row.get("Name", None),
        ))
    return cases
```

**Step 3: Run tests, verify pass**

```bash
pytest tests/test_csv_parser.py -v
```

**Step 4: Commit**

```bash
git add backend/app/services/csv_parser.py backend/tests/test_csv_parser.py
git commit -m "feat: CRM CSV parser with pipe-delimited contact parsing"
```

---

### Task 9: Import Service + Duplicate Detection

**Files:**
- Create: `backend/app/services/import_service.py`
- Create: `backend/app/schemas/import_schemas.py`
- Create: `backend/tests/test_import.py`

**Step 1: Write import schemas**

`backend/app/schemas/import_schemas.py`:
- `ImportRow` — parsed case data for preview
- `ImportPreview` (total, new_cases, duplicates, rows: list[ImportRow])
- `ImportResult` (imported, skipped, updated, errors: list[str])

**Step 2: Write import_service.py**

Key logic:
- `generate_fall_id(case)` — format: `{YYYY}-{KW:02d}-{fallgruppe}-{nachname}` (must be unique)
- `check_duplicate(db, parsed_case)` — match on (nachname, vorname, geburtsdatum, fallgruppe, datum) or fall_id
- `preview_import(db, parsed_cases)` → `ImportPreview`
- `confirm_import(db, parsed_cases, user_id)` → `ImportResult` — insert new cases, skip duplicates, log to import_log
- `import_icd_xlsx(db, file, user_id)` — parse Excel with ICD column, match cases, update icd field

**Step 3: Write tests**

`backend/tests/test_import.py`:
- `test_generate_fall_id_format`
- `test_duplicate_detection_exact_match`
- `test_duplicate_detection_no_match`
- `test_import_creates_cases_in_db`
- `test_import_skips_duplicates`
- `test_import_logs_created`

**Step 4: Run tests**

```bash
pytest tests/test_import.py -v
```

**Step 5: Commit**

```bash
git add backend/app/services/import_service.py backend/app/schemas/import_schemas.py backend/tests/test_import.py
git commit -m "feat: import service with duplicate detection and fall_id generation"
```

---

### Task 10: ICD Service

**Files:**
- Create: `backend/app/services/icd_service.py`
- Create: `backend/tests/test_icd_service.py`

**Step 1: Write failing tests**

Test ICD normalization:
- `"c50.1"` → `"C50.1"` (uppercase)
- `"C50.1, C79.5"` → `["C50.1", "C79.5"]` (split multi-ICD)
- `"C50.1;C79.5"` → `["C50.1", "C79.5"]` (semicolon separator)
- `"XYZ"` → validation error
- Hauptgruppe: `"C50.1"` → `"C50"`

**Step 2: Implement icd_service.py**

- `normalize_icd(raw: str) -> list[str]` — split by comma/semicolon, strip, uppercase, validate each
- `save_icd_for_case(db, case_id, icd_raw, user_id)` — update `cases.icd`, create `CaseICDCode` entries, set `icd_entered_by/at`
- `get_pending_icd_cases(db, filters)` — cases where `icd IS NULL`
- `generate_coding_template(db, filters) -> bytes` — openpyxl workbook with case ID, name, fallgruppe, empty ICD column

**Step 3: Run tests**

```bash
pytest tests/test_icd_service.py -v
```

**Step 4: Commit**

```bash
git add backend/app/services/icd_service.py backend/tests/test_icd_service.py
git commit -m "feat: ICD service — normalize, split, validate, coding template"
```

---

### Task 11: Import & Cases API Routes

**Files:**
- Create: `backend/app/api/import_router.py`
- Create: `backend/app/api/cases.py`
- Create: `backend/app/schemas/case.py`
- Modify: `backend/app/main.py` (add routers)

**Step 1: Write case schemas**

`backend/app/schemas/case.py`:
- `CaseResponse` — full case representation
- `CaseListResponse` (items: list[CaseResponse], total, page, per_page)
- `CaseUpdate` — partial update fields
- `ICDUpdate` (icd: str)
- `CodingUpdate` (gutachten_typ: str, therapieaenderung: str, ta_diagnosekorrektur, ta_unterversorgung, ta_uebertherapie)

**Step 2: Write import_router.py**

- `POST /api/import/csv` → accept file upload, parse, return ImportPreview
- `POST /api/import/csv/confirm` → confirm import from preview session
- `POST /api/import/icd-xlsx` → upload ICD-coded Excel (DAK role)
- `POST /api/import/historical` → one-time import from Abrechnung_DAK.xlsx (admin only)
- `GET /api/import/log` → import history

**Step 3: Write cases.py**

- `GET /api/cases` → paginated list with filters (jahr, kw, fallgruppe, has_icd, has_coding)
- `GET /api/cases/{id}` → single case
- `PUT /api/cases/{id}` → update case fields (admin)
- `GET /api/cases/pending-icd` → cases without ICD
- `PUT /api/cases/{id}/icd` → set ICD (DAK or admin)
- `GET /api/cases/pending-coding` → gutachten without typ
- `PUT /api/cases/{id}/coding` → set gutachten_typ + therapieaenderung (admin)
- `GET /api/cases/coding-template` → download .xlsx template

**Step 4: Register routers in main.py**

**Step 5: Test endpoints manually**

```bash
uvicorn app.main:app --reload --port 8000
# Upload a CSV, check preview, confirm import
```

**Step 6: Commit**

```bash
git add backend/app/api/import_router.py backend/app/api/cases.py backend/app/schemas/case.py
git commit -m "feat: import and cases API endpoints"
```

---

### Task 12: Historical Excel Import

**Files:**
- Create: `backend/app/services/excel_import.py`
- Create: `backend/scripts/import_historical.py`

**Step 1: Write excel_import.py**

Parse `Abrechnung_DAK.xlsx`:
- Read sheets: "2026", "2025", "2024", "2023", "2020-2022" (skip "Tabelle1")
- Map 39 columns to Case model fields (column positions from spec)
- Handle "2020-2022" sheet which has extra "Jahr" column at position 2
- Convert German date formats, boolean fields ("Ja"/"Nein"/1/0/empty)
- Handle empty rows, merged cells
- Generate fall_id for each imported case
- Deduplicate against existing DB records

**Step 2: Write import_historical.py script**

```python
# backend/scripts/import_historical.py
"""One-time script: Import all cases from Abrechnung_DAK.xlsx into DB."""
# Usage: python -m scripts.import_historical /path/to/Abrechnung_DAK.xlsx
```

- Accept file path as argument
- Print progress per sheet
- Print summary (imported, skipped, errors)
- Log to import_log table

**Step 3: Commit** (actual import runs when data files are provided)

```bash
git add backend/app/services/excel_import.py backend/scripts/import_historical.py
git commit -m "feat: historical Excel import (Abrechnung_DAK.xlsx → DB)"
```

---

### Task 13: Notification Service

**Files:**
- Create: `backend/app/services/notification_service.py`

**Step 1: Implement notification_service.py**

- `send_notification(db, recipient_id, type, title, message, entity_type, entity_id)` — create in-app notification + send email
- `send_email(to, subject, body)` — SMTP via `smtplib.SMTP_SSL` on port 465
- Trigger points:
  - `new_cases_uploaded` → notify all DAK users when admin uploads CSV
  - `icd_entered` → notify admin when DAK user enters ICD
  - `icd_uploaded` → notify admin when DAK user uploads ICD Excel
  - `report_ready` → notify all users when report generated
  - `coding_completed` → notify DAK users when coding done

**Step 2: Commit**

```bash
git add backend/app/services/notification_service.py
git commit -m "feat: notification service — in-app + SMTP email"
```

---

## Phase 3: Reports & Coding

### Task 14: Report Service

**Files:**
- Create: `backend/app/services/report_service.py`
- Create: `backend/app/services/vorjahr_service.py`
- Create: `backend/tests/test_report_service.py`

**Step 1: Write report_service.py**

Implement all 5 sheet calculations (using pandas queries against DB):

**Sheet 1 "Auswertung KW gesamt":**
- Per KW: count Erstberatungen, Unterlagen (unterlagen=1), Ablehnungen (ablehnung=1), Keine Rückmeldung (NOT unterlagen AND NOT ablehnung AND NOT abbruch), Gutachten (gutachten=1)
- Totals row with percentages

**Sheet 2 "Auswertung nach Fachgebieten":**
- Per KW, per fallgruppe (onko, kardio, intensiv, galle, sd): Anzahl, Gutachten, Keine RM/Ablehnung

**Sheet 3 "Auswertung Gutachten":**
- Per KW, per fallgruppe + gesamt: Gutachten count, Alternative (gutachten_typ='Alternative'), Bestätigung (gutachten_typ='Bestätigung')

**Sheet 4 "Auswertung Therapieänderungen":**
- Per KW: Gutachten, TA Ja, TA Nein, Diagnosekorrektur, Unterversorgung, Übertherapie

**Sheet 5 "Auswertung ICD onko":**
- ICD codes from onko cases, normalized uppercase, sorted, with count

**Step 2: Write vorjahr_service.py**

- `get_vorjahr_data(db, jahr)` → aggregated data from previous year for comparison
- Reads from `yearly_summary` table (cached) or calculates live

**Step 3: Write tests**

`backend/tests/test_report_service.py`:
- Insert known test data, verify each sheet calculation returns correct values
- Test year-over-year comparison
- Test edge cases (empty weeks, KW 53)

```bash
pytest tests/test_report_service.py -v
```

**Step 4: Commit**

```bash
git add backend/app/services/report_service.py backend/app/services/vorjahr_service.py backend/tests/test_report_service.py
git commit -m "feat: report service — all 5 sheet calculations + year-over-year"
```

---

### Task 15: Excel Export (Berichtswesen Format)

**Files:**
- Create: `backend/app/services/excel_export.py`
- Create: `backend/scripts/import_berichtswesen.py`

**Step 1: Write excel_export.py**

Using openpyxl, generate `.xlsx` matching the exact Berichtswesen format from spec section 3.3:

- Sheet 1 layout: Row 1 "Gesamtübersicht", Row 2 year headers, Rows 3-7 summary with percentages, Row 10 column headers, Row 11+ data per KW
- Sheet 2 layout: Fallgruppen columns (5 groups × 3 columns)
- Sheet 3 layout: Gutachten breakdown (6 groups × 3 columns)
- Sheet 4 layout: Therapieänderungen (Gutachten, TA Ja/Nein, Diagnosekorrektur, Unterversorgung, Übertherapie)
- Sheet 5 layout: ICD onko (ICD | Anzahl)
- Apply formatting: headers bold, percentage columns formatted, column widths

**Step 2: Write import_berichtswesen.py**

One-time script to import previous years' Berichtswesen data into `yearly_summary` table for year-over-year comparisons.

**Step 3: Commit**

```bash
git add backend/app/services/excel_export.py backend/scripts/import_berichtswesen.py
git commit -m "feat: Excel export in exact Berichtswesen format + historical import"
```

---

### Task 16: Coding Service & Reports API

**Files:**
- Create: `backend/app/services/coding_service.py`
- Create: `backend/app/api/coding.py`
- Create: `backend/app/api/reports.py`
- Create: `backend/app/schemas/report.py`
- Create: `backend/app/services/excel_sync.py`

**Step 1: Write coding_service.py**

- `get_coding_queue(db, filters)` — cases where `gutachten=1 AND gutachten_typ IS NULL`
- `update_coding(db, case_id, gutachten_typ, therapieaenderung, ta_*, user_id)` — set coding fields, log audit
- `batch_update_coding(db, updates: list, user_id)` — mass coding for historical data

**Step 2: Write report schemas**

`backend/app/schemas/report.py`:
- `DashboardKPIs` (total_cases, pending_icd, pending_coding, reports_generated, current_year_stats)
- `WeeklyData` (kw, erstberatungen, unterlagen, ablehnungen, keine_rm, gutachten)
- `ReportMeta` (id, jahr, kw, generated_at, download_url)

**Step 3: Write coding API**

`backend/app/api/coding.py`:
- `GET /api/coding/queue` → paginated coding queue (admin)
- `PUT /api/coding/{id}` → update single case coding (admin)

**Step 4: Write reports API**

`backend/app/api/reports.py`:
- `GET /api/reports/dashboard` → live KPIs + chart data
- `GET /api/reports/weekly/{jahr}/{kw}` → specific week data
- `POST /api/reports/generate` → generate .xlsx, save to disk + DB, return metadata
- `GET /api/reports/download/{id}` → serve generated .xlsx file
- `GET /api/reports/list` → all generated reports

**Step 5: Write excel_sync.py**

- `sync_db_to_excel(db)` → export current DB state to Abrechnung_DAK.xlsx format
- `sync_excel_to_db(db, file)` → import changes from edited Excel back to DB
- Triggered via `POST /api/admin/excel-sync`

**Step 6: Register all new routers in main.py**

**Step 7: Commit**

```bash
git add backend/app/services/coding_service.py backend/app/services/excel_sync.py backend/app/api/coding.py backend/app/api/reports.py backend/app/schemas/report.py
git commit -m "feat: coding queue, reports API, Excel sync"
```

---

### Task 17: Push Backend to GitHub

**Step 1: Create GitHub repo**

```bash
gh repo create complexcaresolutions/dak.c2s --private --source=/home/frontend/dak_c2s --push
```

**Step 2: Push develop branch**

```bash
cd /home/frontend/dak_c2s
git push -u origin develop
```

---

## Phase 4: Frontend

### Task 18: Frontend Setup

**Files:**
- Create: `frontend/` (Vite scaffold)
- Create: `frontend/vite.config.ts`
- Create: `frontend/tailwind.config.js`
- Create: `frontend/src/main.tsx`
- Create: `frontend/src/App.tsx`

**Step 1: Scaffold React + Vite + TypeScript**

```bash
cd /home/frontend/dak_c2s
pnpm create vite frontend --template react-ts
cd frontend
pnpm install
```

**Step 2: Install dependencies**

```bash
pnpm add axios react-router-dom recharts
pnpm add -D tailwindcss @tailwindcss/vite
```

**Step 3: Configure Tailwind**

Add Tailwind via `@tailwindcss/vite` plugin in `vite.config.ts`. Add `@import "tailwindcss"` in CSS.

**Step 4: Configure Vite API proxy**

```typescript
// frontend/vite.config.ts
export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    proxy: {
      '/api': 'http://localhost:8000',
    },
  },
});
```

**Step 5: Initialize shadcn/ui**

```bash
pnpm dlx shadcn@latest init
# Select: TypeScript, default style, CSS variables
```

**Step 6: Verify dev server starts**

```bash
pnpm dev
# Visit http://localhost:5173
```

**Step 7: Commit**

```bash
git add frontend/
git commit -m "feat: frontend setup — React, Vite, TypeScript, Tailwind, shadcn/ui"
```

---

### Task 19: Auth Context & API Client

**Files:**
- Create: `frontend/src/services/api.ts`
- Create: `frontend/src/services/authService.ts`
- Create: `frontend/src/context/AuthContext.tsx`
- Create: `frontend/src/hooks/useAuth.ts`
- Create: `frontend/src/types/index.ts`
- Create: `frontend/src/components/layout/ProtectedRoute.tsx`

**Step 1: Write TypeScript types**

`frontend/src/types/index.ts`:
- `User` (id, username, email, role, mfa_enabled, is_active, last_login)
- `LoginRequest`, `RegisterRequest`, `TokenResponse`
- `Case`, `CaseListResponse`, `ImportPreview`, `ImportResult`
- `DashboardKPIs`, `WeeklyData`, `ReportMeta`
- `Notification`

**Step 2: Write API client with JWT interceptor**

`frontend/src/services/api.ts`:
- Axios instance with base URL `/api`
- Request interceptor: attach access token from localStorage
- Response interceptor: on 401, try refresh token, retry original request
- If refresh fails, redirect to login

**Step 3: Write authService.ts**

- `login(email, password, mfaCode?)` → store tokens, return user
- `register(data)` → create account
- `logout()` → call API, clear tokens
- `refreshToken()` → get new access token

**Step 4: Write AuthContext + useAuth hook**

- `AuthProvider` wraps app, stores user + loading state
- `useAuth()` → `{ user, login, logout, register, isAdmin, isLoading }`
- On mount: check stored token, try refresh

**Step 5: Write ProtectedRoute**

- If not authenticated → redirect to `/login`
- If `requireAdmin` and user is not admin → show 403
- Otherwise render children

**Step 6: Commit**

```bash
git add frontend/src/
git commit -m "feat: auth context, API client with JWT refresh, protected routes"
```

---

### Task 20: Login & Register Pages

**Files:**
- Create: `frontend/src/pages/LoginPage.tsx`
- Create: `frontend/src/pages/RegisterPage.tsx`
- Modify: `frontend/src/App.tsx` (add routes)

**Step 1: Install shadcn components**

```bash
pnpm dlx shadcn@latest add button input label card form
```

**Step 2: Write LoginPage**

- Email + password form
- Optional MFA code field (shown after first login attempt if MFA enabled)
- Error display for invalid credentials / account locked
- Link to register

**Step 3: Write RegisterPage**

- Username, email, password fields
- Optional invitation token field (from URL param)
- Domain validation feedback (@dak.de)

**Step 4: Wire up React Router in App.tsx**

```tsx
<Routes>
  <Route path="/login" element={<LoginPage />} />
  <Route path="/register" element={<RegisterPage />} />
  <Route path="/*" element={<ProtectedRoute><AppLayout /></ProtectedRoute>} />
</Routes>
```

**Step 5: Commit**

```bash
git add frontend/src/pages/ frontend/src/App.tsx
git commit -m "feat: login and register pages with MFA support"
```

---

### Task 21: App Layout

**Files:**
- Create: `frontend/src/components/layout/AppLayout.tsx`
- Create: `frontend/src/components/layout/Sidebar.tsx`
- Create: `frontend/src/components/layout/Header.tsx`

**Step 1: Install shadcn components**

```bash
pnpm dlx shadcn@latest add avatar dropdown-menu separator sheet badge
```

**Step 2: Write Sidebar**

Navigation items (role-aware):
- Dashboard (all)
- Fälle (all)
- Import (admin)
- ICD-Eingabe (dak_mitarbeiter)
- Coding (admin)
- Berichte (all)
- Admin (admin only): Users, Einladungen, Audit-Log

**Step 3: Write Header**

- App title
- NotificationBell (placeholder for now)
- User dropdown (profile, logout)

**Step 4: Write AppLayout**

- Sidebar + Header + `<Outlet />` for page content
- Responsive: collapsible sidebar on mobile

**Step 5: Commit**

```bash
git add frontend/src/components/layout/
git commit -m "feat: app layout with role-aware sidebar and header"
```

---

### Task 22: Dashboard Page

**Files:**
- Create: `frontend/src/pages/DashboardPage.tsx`
- Create: `frontend/src/components/dashboard/KPICards.tsx`
- Create: `frontend/src/components/dashboard/WeeklyChart.tsx`
- Create: `frontend/src/components/dashboard/FallgruppenDonut.tsx`
- Create: `frontend/src/components/dashboard/VorjahresComparison.tsx`
- Create: `frontend/src/hooks/useDashboard.ts`

**Step 1: Install shadcn components**

```bash
pnpm dlx shadcn@latest add card tabs
```

**Step 2: Write useDashboard hook**

- Fetch `GET /api/reports/dashboard`
- Return: kpis, weeklyData, loading, error

**Step 3: Write KPICards**

4 cards: Erstberatungen gesamt, Pending ICD, Pending Coding, Gutachten gesamt. Color-coded.

**Step 4: Write WeeklyChart**

Recharts BarChart showing weekly Erstberatungen + Gutachten trend.

**Step 5: Write FallgruppenDonut**

Recharts PieChart showing distribution across 5 Fallgruppen.

**Step 6: Write VorjahresComparison**

Table comparing current year vs previous year key metrics.

**Step 7: Assemble DashboardPage**

Layout: KPICards (top), WeeklyChart (left) + FallgruppenDonut (right), VorjahresComparison (bottom).

**Step 8: Commit**

```bash
git add frontend/src/pages/DashboardPage.tsx frontend/src/components/dashboard/ frontend/src/hooks/useDashboard.ts
git commit -m "feat: dashboard with KPI cards, weekly chart, fallgruppen donut, year comparison"
```

---

### Task 23: Cases Page

**Files:**
- Create: `frontend/src/pages/CasesPage.tsx`
- Create: `frontend/src/components/cases/CaseTable.tsx`
- Create: `frontend/src/components/cases/CaseDetail.tsx`
- Create: `frontend/src/components/cases/ICDInlineEdit.tsx`
- Create: `frontend/src/hooks/useCases.ts`

**Step 1: Install shadcn components**

```bash
pnpm dlx shadcn@latest add table dialog select pagination
```

**Step 2: Write useCases hook**

- Fetch `GET /api/cases` with pagination + filters
- CRUD operations for individual cases

**Step 3: Write CaseTable**

- Columns: ID, KW, Nachname, Vorname, Fallgruppe, ICD, Gutachten, Status
- Filters: Jahr, KW, Fallgruppe, has_icd, has_coding
- Pagination
- Click row → CaseDetail dialog

**Step 4: Write CaseDetail**

- Full case view in dialog/sheet
- Editable fields (admin): all case fields
- Read-only for dak_mitarbeiter (except ICD)

**Step 5: Write ICDInlineEdit**

- Inline ICD editing in case table or detail view
- Validation feedback (regex check)
- For dak_mitarbeiter: only ICD field editable

**Step 6: Commit**

```bash
git add frontend/src/pages/CasesPage.tsx frontend/src/components/cases/ frontend/src/hooks/useCases.ts
git commit -m "feat: cases page with filterable table, detail view, inline ICD edit"
```

---

### Task 24: Import Pages

**Files:**
- Create: `frontend/src/pages/ImportPage.tsx`
- Create: `frontend/src/components/import/CSVUpload.tsx`
- Create: `frontend/src/components/import/ImportPreview.tsx`
- Create: `frontend/src/components/import/ICDUpload.tsx`

**Step 1: Write CSVUpload**

- File dropzone (accept .csv)
- Upload → POST /api/import/csv → show ImportPreview

**Step 2: Write ImportPreview**

- Table showing parsed rows (new vs duplicate)
- Confirm/Cancel buttons
- On confirm → POST /api/import/csv/confirm → show ImportResult

**Step 3: Write ICDUpload**

- File dropzone (accept .xlsx)
- Upload → POST /api/import/icd-xlsx
- Show result (cases updated, errors)
- Template download link (GET /api/cases/coding-template)

**Step 4: Assemble ImportPage**

Tabs: "CSV Import" | "ICD Upload" | "Import-Log"

**Step 5: Commit**

```bash
git add frontend/src/pages/ImportPage.tsx frontend/src/components/import/
git commit -m "feat: import pages — CSV upload with preview, ICD Excel upload"
```

---

### Task 25: Coding Queue Page

**Files:**
- Create: `frontend/src/pages/CodingPage.tsx`
- Create: `frontend/src/components/coding/CodingQueue.tsx`
- Create: `frontend/src/components/coding/CodingCard.tsx`
- Create: `frontend/src/components/coding/CodingProgress.tsx`

**Step 1: Write CodingQueue**

- List of cases with gutachten=1 but no gutachten_typ
- Each shows: Name, Fallgruppe, Kurzbeschreibung, Fragestellung

**Step 2: Write CodingCard**

- Individual case coding form
- Fields: gutachten_typ (Bestätigung/Alternative), therapieaenderung (Ja/Nein), checkboxes for ta_diagnosekorrektur, ta_unterversorgung, ta_uebertherapie
- Save → PUT /api/coding/{id}

**Step 3: Write CodingProgress**

- Progress bar: X of Y cases coded
- Stats: Bestätigung vs Alternative ratio

**Step 4: Commit**

```bash
git add frontend/src/pages/CodingPage.tsx frontend/src/components/coding/
git commit -m "feat: coding queue with progress tracking"
```

---

### Task 26: Reports Page

**Files:**
- Create: `frontend/src/pages/ReportsPage.tsx`
- Create: `frontend/src/components/reports/ReportList.tsx`
- Create: `frontend/src/components/reports/ReportDownload.tsx`

**Step 1: Write ReportList**

- Table of generated reports (date, KW, generated_by)
- Download button per report → GET /api/reports/download/{id}
- Generate button (admin) → POST /api/reports/generate

**Step 2: Commit**

```bash
git add frontend/src/pages/ReportsPage.tsx frontend/src/components/reports/
git commit -m "feat: reports page with list and download"
```

---

### Task 27: Notifications

**Files:**
- Create: `frontend/src/components/notifications/NotificationBell.tsx`
- Create: `frontend/src/components/notifications/NotificationDropdown.tsx`
- Create: `frontend/src/hooks/useNotifications.ts`

**Step 1: Write useNotifications hook**

- Poll `GET /api/notifications` every 30 seconds
- Return: notifications, unreadCount, markAsRead, markAllAsRead

**Step 2: Write NotificationBell + Dropdown**

- Bell icon in header with unread badge count
- Dropdown: list of recent notifications with timestamps
- Click → mark as read + navigate to related entity

**Step 3: Commit**

```bash
git add frontend/src/components/notifications/ frontend/src/hooks/useNotifications.ts
git commit -m "feat: notification bell with dropdown and polling"
```

---

### Task 28: Admin Pages

**Files:**
- Create: `frontend/src/pages/AdminUsersPage.tsx`
- Create: `frontend/src/pages/AdminInvitationsPage.tsx`
- Create: `frontend/src/pages/AdminAuditPage.tsx`
- Create: `frontend/src/components/admin/UserManagement.tsx`
- Create: `frontend/src/components/admin/InvitationLinks.tsx`
- Create: `frontend/src/components/admin/AuditLog.tsx`

**Step 1: Write UserManagement**

- Table: username, email, role, active, last_login
- Edit button → dialog with role/active toggle
- Create user button

**Step 2: Write InvitationLinks**

- Create invitation form (email optional, expiry date)
- List existing invitations with status (active/used/expired)
- Copy link button

**Step 3: Write AuditLog**

- Paginated table: timestamp, user, action, entity, old/new values (JSON expandable)
- Filter by user, action, date range

**Step 4: Commit**

```bash
git add frontend/src/pages/Admin* frontend/src/components/admin/
git commit -m "feat: admin pages — user management, invitations, audit log"
```

---

## Phase 5: Integration & Deploy

### Task 29: Frontend Production Build & Integration Test

**Step 1: Build frontend**

```bash
cd /home/frontend/dak_c2s/frontend
pnpm build
# Output: frontend/dist/
```

**Step 2: Test full stack locally**

- Backend: `uvicorn app.main:app --port 8000`
- Serve frontend build from dist/
- Test all flows: login → dashboard → import CSV → view cases → enter ICD → coding → generate report → download

**Step 3: Commit**

```bash
git add -A
git commit -m "feat: frontend production build, integration tested"
```

---

### Task 30: GitHub & Deploy to Hetzner 1

**Step 1: Push to GitHub**

```bash
cd /home/frontend/dak_c2s
git push origin develop
git checkout main
git merge develop
git push origin main
git checkout develop
```

**Step 2: Clone on Hetzner 1**

```bash
ssh hetzner1
# As appropriate user:
cd /var/www/vhosts/dak.complexcaresolutions.de/
git clone git@github.com:complexcaresolutions/dak.c2s.git .
# Or: git init + git remote add origin + git pull
```

**Step 3: Setup Python venv on Hetzner**

```bash
cd /var/www/vhosts/dak.complexcaresolutions.de/backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

**Step 4: Configure .env on Hetzner**

- DB_HOST=localhost (MariaDB is local on Hetzner 1)
- All other production values

**Step 5: Run Alembic migrations on production DB**

```bash
cd /var/www/vhosts/dak.complexcaresolutions.de/backend
source venv/bin/activate
alembic upgrade head
```

**Step 6: Create systemd service**

Copy service file from spec section 10 to `/etc/systemd/system/dak-backend.service`.

```bash
sudo systemctl daemon-reload
sudo systemctl enable dak-backend
sudo systemctl start dak-backend
sudo systemctl status dak-backend
```

**Step 7: Configure Plesk Nginx**

Add directives from spec section 9 in Plesk → dak.complexcaresolutions.de → Additional nginx directives.

**Step 8: Build frontend on Hetzner**

```bash
cd /var/www/vhosts/dak.complexcaresolutions.de/frontend
npm install
npm run build
```

**Step 9: Create admin account**

```bash
cd /var/www/vhosts/dak.complexcaresolutions.de/backend
source venv/bin/activate
python -m scripts.create_admin
```

**Step 10: Test SMTP**

```bash
python -c "
from app.services.notification_service import send_email
send_email('test@example.com', 'Test', 'Portal SMTP works')
"
```

**Step 11: Smoke test production**

- Visit https://dak.complexcaresolutions.de
- Login with admin
- Check dashboard loads
- Upload test CSV
- Verify notifications

**Step 12: Invite DAK users**

- Create invitation links via admin panel
- Send to DAK contacts

---

## Dependency Graph

```
Task 1 (Scaffolding) → Task 2 (Models) → Task 3 (Alembic)
                                        → Task 4 (Security) → Task 5 (Auth API) → Task 6 (Admin API)
                     → Task 7 (Utils) → Task 8 (CSV Parser) → Task 9 (Import Service)
                                                              → Task 10 (ICD Service)
                                      → Task 11 (Import/Cases API) ← Task 9, 10
                                      → Task 12 (Historical Import)
                                      → Task 13 (Notifications)
                     → Task 14 (Reports) → Task 15 (Excel Export) → Task 16 (Coding + Reports API)
                     → Task 17 (Push to GitHub)
                     → Task 18 (Frontend Setup) → Task 19 (Auth Context) → Task 20 (Login)
                                                                         → Task 21 (Layout)
                                                → Task 22 (Dashboard)
                                                → Task 23 (Cases)
                                                → Task 24 (Import Pages)
                                                → Task 25 (Coding)
                                                → Task 26 (Reports)
                                                → Task 27 (Notifications)
                                                → Task 28 (Admin Pages)
                     → Task 29 (Integration) → Task 30 (Deploy)
```

## Notes

- **Referenzdaten:** User provides data files to `data/` — historical import (Task 12) runs when available
- **Python compatibility:** Code must work on both 3.13 (dev) and 3.11 (prod) — avoid 3.12+ syntax like `type` statements
- **Testing strategy:** Unit tests for services/utils (pytest), manual integration tests for API endpoints, visual testing for frontend
- **DB:** Single MariaDB instance on Hetzner 1 — dev connects remotely, prod connects locally
