dak.c2s/docs/plans/2026-02-23-dak-portal-implementation.md
CCS Admin 5d57b1f349 feat: project scaffolding with FastAPI, config, database connection
- Initialize project structure with backend/app/ package layout
- Add FastAPI app with CORS middleware and health check endpoint
- Add Pydantic Settings config with DB, JWT, SMTP, and app settings
- Add SQLAlchemy database engine and session management
- Add requirements.txt with all dependencies (FastAPI, SQLAlchemy, Alembic, etc.)
- Add .env.example template and .gitignore
- Add empty frontend/ and backend test scaffolding
- Include project specification and design/implementation plans

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 07:24:00 +00:00

1738 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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