mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 21:53:41 +00:00
- 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>
1738 lines
51 KiB
Markdown
1738 lines
51 KiB
Markdown
# 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
|