# 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
} />
} />
} />
```
**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 + `` 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