dak.c2s/Dak_projekt_spezifikation_final.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

35 KiB
Raw Permalink Blame History

DAK Zweitmeinungs-Portal — Implementierungsspezifikation

FINAL v4.0 — Alle Entscheidungen getroffen

Datum: 24.02.2026
Status: IMPLEMENTIERUNGSBEREIT


Entscheidungsprotokoll (komplett)

# Thema Entscheidung
1 Historischer Import Alle Excel-Sheets (2023-2026) → DB
2 Therapieänderungen Eigenes Sheet UND integriert in Gutachten-Sheet
3 Abrechnung_DAK.xlsx Bleibt parallel; DB = Master für Berichtswesen
4 DAK-Registrierung Domain-Whitelist (@dak.de) + Einladungslinks
5 Benachrichtigungen Bidirektional: Admin↔DAK per E-Mail + In-App
6 Masse-Coding Ja, Queue-Interface für historische Nachbearbeitung
7 Datenbank MariaDB 10.11.14 (dak_c2s auf localhost)
8 Deployment Nativ, systemd-Service, Plesk-Nginx
9 Nginx Über Plesk "Additional nginx directives"
10 Dev-DB MariaDB direkt auf sv-frontend
11 Process Manager Manueller systemd-Service
12 Max Upload 20 MB
13 Docker Nein — native Deployment

1. Infrastruktur

Produktion (Hetzner 1)

Server:         Hetzner 1 (Plesk-managed)
Domain:         dak.complexcaresolutions.de (SSL vorhanden)
Datenbank:      MariaDB 10.11.14 → DB: dak_c2s / User: dak_c2s_admin
Python:         3.11.2
Node.js:        vorhanden (Plesk-Modul)
Nginx:          Plesk-verwaltet → "Additional nginx directives"
Process Mgr:    systemd (manuell)
Git:            Plesk-Modul

Entwicklung (sv-frontend)

Server:         Debian-basiert, vollständiger Dev-Server
Python:         installiert
Node.js:        installiert
Claude Code:    läuft
MariaDB:        lokal für Entwicklung
Git:            → GitHub (complexcaresolutions/dak.c2s)

E-Mail

SMTP Host:      smtp.complexcaresolutions.de
Port:           465 (SSL)
Benutzer:       noreply@complexcaresolutions.de
Passwort:       9Vw0!3y6o
Absender:       noreply@complexcaresolutions.de

Git

Organisation:   complexcaresolutions (GitHub Business)
Repository:     dak.c2s (noch anzulegen)
Workflow:       sv-frontend → git push → GitHub → Hetzner 1 git pull

2. Architektur

┌─────────────────────────────────────────────────┐
│              Hetzner 1 (Plesk)                  │
│                                                 │
│  Nginx (Plesk + Additional directives)          │
│  ┌────────────────────────────────────────────┐ │
│  │ dak.complexcaresolutions.de:443 (SSL)      │ │
│  │                                            │ │
│  │  /api/*  → proxy_pass 127.0.0.1:8000      │ │
│  │  /docs   → proxy_pass 127.0.0.1:8000      │ │
│  │  /*      → /frontend/dist/ (React SPA)     │ │
│  └──────────────┬────────────────┬────────────┘ │
│                 │                │               │
│  ┌──────────────▼─────┐  ┌──────▼────────────┐  │
│  │  FastAPI Backend   │  │  React Frontend   │  │
│  │  Gunicorn+Uvicorn  │  │  Static Build     │  │
│  │  systemd managed   │  │  /frontend/dist   │  │
│  │  Port 8000         │  │                   │  │
│  └──────────┬─────────┘  └───────────────────┘  │
│             │                                    │
│  ┌──────────▼─────────┐                         │
│  │  MariaDB 10.11.14  │                         │
│  │  DB: dak_c2s       │                         │
│  │  localhost:3306     │                         │
│  └────────────────────┘                         │
└─────────────────────────────────────────────────┘

Tech-Stack

Layer Technologie Details
Frontend React 18 + TypeScript Vite, Tailwind CSS, shadcn/ui, Recharts
Backend Python 3.11 + FastAPI SQLAlchemy 2.0, Pandas, openpyxl
DB-Treiber PyMySQL + cryptography mysql+pymysql Dialekt
Auth python-jose + passlib + pyotp JWT, bcrypt, TOTP
Migrations Alembic MariaDB-kompatibel
Process Gunicorn + Uvicorn Workers systemd-Service
E-Mail smtplib SSL, Port 465

3. Quelldaten-Analyse

3.1 CRM-CSV (wöchentlicher Import)

Dateiname-Pattern:  YYYY-MM-DD-HHMM.csv
Encoding:           UTF-8-BOM
Delimiter:          Komma
Spalten:            Hauptkontakt, Name, Thema, Erstellungsdatum, Modul

Hauptkontakt (Pipe-delimited):
  "Nachname | Vorname | Geburtsdatum | KVNR"
  Beispiel: "Tonn | Regina | 28.04.1960 | D410126355"
  
  Edge Cases:
  - KVNR kann fehlen: "Daum | Luana | 05.02.2016 |"
  - Geburtsdatum fehlerhaft: "29.08.0196" 
  - Leerzeichen um Pipes

Erstellungsdatum:
  Format: "DD.MM.YY, HH:MM"
  Beispiel: "02.02.26, 08:50"

Modul → Fallgruppe Mapping:
  "Zweitmeinung Onkologie"     → "onko"
  "Zweitmeinung Kardiologie"   → "kardio"
  "Zweitmeinung Intensiv"      → "intensiv"
  "Zweitmeinung Gallenblase"   → "galle"
  "Zweitmeinung Schilddrüse"   → "sd"
  "Begutachtung *"             → aus Kontext ableiten

3.2 Abrechnung_DAK.xlsx (Master-Datei)

Sheets: 2026, 2025, 2024, 2023, 2020-2022, Gutachten, Übersicht, ...
Spalten pro Jahres-Sheet (39):
  ID, KW, Datum, Anrede, Vorname, Nachname, Geburtsdatum, KVNR,
  Versicherung, ICD, Fallgruppe, Straße, PLZ, Ort, E-Mail,
  Ansprechpartner, Telefonnummer, Mobiltelefon, Unterlagen,
  Unterlagen verschickt, Erhalten, Unterlagen erhalten,
  Unterlagen an Gutachter, Gutachten, Gutachter, Gutachten erstellt,
  Gutachten versendet, Schweigepflicht, Ablehnung, Abbruch,
  Abbruch_Datum, Kurzbeschreibung, Fragestellung, Kommentar,
  E-Mail2, Telefon2, Sonstiges, Abgerechnet, Abrechnung_Datum

Sheet "2020-2022" hat zusätzliche Spalte "Jahr" (Position 2)

Datenvolumen:
  2026: 68 Zeilen
  2025: 631 Zeilen  
  2024: 769 Zeilen
  2023: 635 Zeilen
  2020-2022: 1182 Zeilen
  GESAMT: ~3.285 Fälle

Hinweis: "Tabelle1" ignorieren (temporäre Arbeitstabelle)

3.3 Berichtswesen (Referenzformat)

4 Sheets (5 ab jetzt):

Sheet 1 "Auswertung KW gesamt":
  Zeile 1:    "Gesamtübersicht"
  Zeile 2:    [leer] [leer] [AKTUELLES_JAHR] [leer] [VORJAHR] [leer]
  Zeile 3:    "Gesamtzahl an Erstberatungen" [leer] [Wert] [leer] [VJ-Wert] [leer]
  Zeile 4:    "Anzahl Ablehnungen" [leer] [Wert] [Prozent] [VJ-Wert] [VJ-Prozent]
  Zeile 5:    "Anzahl versendeter Unterlagen" [leer] [Wert] [Prozent] [VJ-Wert] [VJ-Prozent]
  Zeile 6:    "Anzahl keine Rückmeldung" [leer] [Wert] [Prozent] [VJ-Wert] [VJ-Prozent]
  Zeile 7:    "Anzahl erstellter Gutachten" [leer] [Wert] [Prozent] [VJ-Wert] [VJ-Prozent]
  Zeile 8-9:  leer
  Zeile 10:   KW | Anzahl Erstberatungen | Unterlagen | Ablehnung | Keine Rückmeldung | Gutachten
  Zeile 11+:  Daten pro KW (1-52)

Sheet 2 "Auswertung nach Fachgebieten":
  Zeile 1:    "Übersicht nach Fallgruppen"
  Zeile 3:    Gruppenköpfe: onko | kardio | intensiv | galle | sd
  Zeile 4:    KW | Anzahl | Gutachten | Keine RM/Ablehnung (×5 Fallgruppen)
  Zeile 5+:   Daten pro KW

  ACHTUNG: 2023 hatte nur 3 Fallgruppen (onko, kardio, intensiv)
           Ab 2024 kamen galle + sd dazu

Sheet 3 "Auswertung Gutachten":
  Zeile 3:    Gruppenköpfe: Gesamt | onko | kardio | intensiv | galle | sd
  Zeile 4:    KW | Gutachten | Alternative | Bestätigung (×6)
  Zeile 5+:   Daten pro KW

Sheet 4 "Auswertung Therapieänderungen" (NEU):
  → Gleiche Struktur wie Sheet 3, aber mit:
     KW | Gutachten | TA Ja | TA Nein | Diagnosekorrektur | Unterversorgung | Übertherapie

Sheet 5 "Auswertung ICD onko":
  Zeile 1:    ICD | Anzahl von ICD
  Zeile 2+:   Daten (ICD normalisiert, uppercase, sortiert)

4. MariaDB Schema

-- ============================================
-- Vorab: Charset sicherstellen
-- ============================================
ALTER DATABASE dak_c2s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- ============================================
-- BENUTZER & AUTH
-- ============================================

CREATE TABLE IF NOT EXISTS users (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'dak_mitarbeiter',
    mfa_secret VARCHAR(255) DEFAULT NULL,
    mfa_enabled TINYINT(1) NOT NULL DEFAULT 0,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    must_change_password TINYINT(1) NOT NULL DEFAULT 0,
    last_login DATETIME DEFAULT NULL,
    failed_login_attempts INT UNSIGNED NOT NULL DEFAULT 0,
    locked_until DATETIME DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_username (username),
    UNIQUE KEY uk_email (email),
    CONSTRAINT chk_role CHECK (role IN ('admin', 'dak_mitarbeiter'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS refresh_tokens (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    token_hash VARCHAR(255) NOT NULL,
    expires_at DATETIME NOT NULL,
    revoked TINYINT(1) NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_token (token_hash),
    CONSTRAINT fk_rt_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS invitation_links (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    token VARCHAR(255) NOT NULL,
    email VARCHAR(255) DEFAULT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'dak_mitarbeiter',
    created_by INT UNSIGNED DEFAULT NULL,
    expires_at DATETIME NOT NULL,
    used_at DATETIME DEFAULT NULL,
    used_by INT UNSIGNED DEFAULT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_token (token),
    CONSTRAINT fk_inv_created FOREIGN KEY (created_by) REFERENCES users(id),
    CONSTRAINT fk_inv_used FOREIGN KEY (used_by) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS allowed_domains (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    domain VARCHAR(255) NOT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'dak_mitarbeiter',
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT IGNORE INTO allowed_domains (domain, role) VALUES ('dak.de', 'dak_mitarbeiter');

-- ============================================
-- KERN: FÄLLE
-- ============================================

CREATE TABLE IF NOT EXISTS cases (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    fall_id VARCHAR(100) DEFAULT NULL,
    crm_ticket_id VARCHAR(20) DEFAULT NULL,
    
    jahr SMALLINT UNSIGNED NOT NULL,
    kw TINYINT UNSIGNED NOT NULL,
    datum DATE NOT NULL,
    
    anrede VARCHAR(20) DEFAULT NULL,
    vorname VARCHAR(100) DEFAULT NULL,
    nachname VARCHAR(100) NOT NULL,
    geburtsdatum DATE DEFAULT NULL,
    kvnr VARCHAR(20) DEFAULT NULL,
    versicherung VARCHAR(50) NOT NULL DEFAULT 'DAK',
    
    icd TEXT DEFAULT NULL,
    fallgruppe VARCHAR(20) NOT NULL,
    
    strasse VARCHAR(255) DEFAULT NULL,
    plz VARCHAR(10) DEFAULT NULL,
    ort VARCHAR(100) DEFAULT NULL,
    email VARCHAR(255) DEFAULT NULL,
    ansprechpartner VARCHAR(200) DEFAULT NULL,
    telefonnummer VARCHAR(50) DEFAULT NULL,
    mobiltelefon VARCHAR(50) DEFAULT NULL,
    email2 VARCHAR(255) DEFAULT NULL,
    telefon2 VARCHAR(50) DEFAULT NULL,
    
    unterlagen TINYINT(1) NOT NULL DEFAULT 0,
    unterlagen_verschickt DATE DEFAULT NULL,
    erhalten TINYINT(1) DEFAULT NULL,
    unterlagen_erhalten DATE DEFAULT NULL,
    unterlagen_an_gutachter DATE DEFAULT NULL,
    gutachten TINYINT(1) NOT NULL DEFAULT 0,
    gutachter VARCHAR(100) DEFAULT NULL,
    gutachten_erstellt DATE DEFAULT NULL,
    gutachten_versendet DATE DEFAULT NULL,
    schweigepflicht TINYINT(1) NOT NULL DEFAULT 0,
    ablehnung TINYINT(1) NOT NULL DEFAULT 0,
    abbruch TINYINT(1) NOT NULL DEFAULT 0,
    abbruch_datum DATE DEFAULT NULL,
    
    gutachten_typ VARCHAR(20) DEFAULT NULL,
    therapieaenderung VARCHAR(5) DEFAULT NULL,
    ta_diagnosekorrektur TINYINT(1) NOT NULL DEFAULT 0,
    ta_unterversorgung TINYINT(1) NOT NULL DEFAULT 0,
    ta_uebertherapie TINYINT(1) NOT NULL DEFAULT 0,
    
    kurzbeschreibung TEXT DEFAULT NULL,
    fragestellung TEXT DEFAULT NULL,
    kommentar TEXT DEFAULT NULL,
    sonstiges TEXT DEFAULT NULL,
    
    abgerechnet TINYINT(1) NOT NULL DEFAULT 0,
    abrechnung_datum DATE DEFAULT NULL,
    
    import_source VARCHAR(255) DEFAULT NULL,
    imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    updated_by INT UNSIGNED DEFAULT NULL,
    icd_entered_by INT UNSIGNED DEFAULT NULL,
    icd_entered_at DATETIME DEFAULT NULL,
    coding_completed_by INT UNSIGNED DEFAULT NULL,
    coding_completed_at DATETIME DEFAULT NULL,
    
    UNIQUE KEY uk_fall_id (fall_id),
    INDEX idx_jahr_kw (jahr, kw),
    INDEX idx_kvnr (kvnr),
    INDEX idx_fallgruppe (fallgruppe),
    INDEX idx_datum (datum),
    INDEX idx_nachname_vorname (nachname, vorname),
    INDEX idx_pending_icd (jahr, kw, fallgruppe, icd(20)),
    INDEX idx_pending_coding (gutachten, gutachten_typ),
    
    CONSTRAINT fk_c_updated FOREIGN KEY (updated_by) REFERENCES users(id),
    CONSTRAINT fk_c_icd_by FOREIGN KEY (icd_entered_by) REFERENCES users(id),
    CONSTRAINT fk_c_coding_by FOREIGN KEY (coding_completed_by) REFERENCES users(id),
    CONSTRAINT chk_fallgruppe CHECK (fallgruppe IN ('onko', 'kardio', 'intensiv', 'galle', 'sd')),
    CONSTRAINT chk_gutachten_typ CHECK (gutachten_typ IS NULL OR gutachten_typ IN ('Bestätigung', 'Alternative')),
    CONSTRAINT chk_ta CHECK (therapieaenderung IS NULL OR therapieaenderung IN ('Ja', 'Nein'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS case_icd_codes (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    case_id INT UNSIGNED NOT NULL,
    icd_code VARCHAR(20) NOT NULL,
    icd_hauptgruppe VARCHAR(10) DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_case (case_id),
    INDEX idx_code (icd_code),
    INDEX idx_haupt (icd_hauptgruppe),
    CONSTRAINT fk_icd_case FOREIGN KEY (case_id) REFERENCES cases(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================
-- BERICHTSWESEN
-- ============================================

CREATE TABLE IF NOT EXISTS weekly_reports (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    jahr SMALLINT UNSIGNED NOT NULL,
    kw TINYINT UNSIGNED NOT NULL,
    report_date DATE NOT NULL,
    report_file_path VARCHAR(500) DEFAULT NULL,
    report_data JSON DEFAULT NULL,
    generated_by INT UNSIGNED DEFAULT NULL,
    generated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_jahr_kw (jahr, kw),
    CONSTRAINT fk_rpt_by FOREIGN KEY (generated_by) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS yearly_summary (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    jahr SMALLINT UNSIGNED NOT NULL,
    kw TINYINT UNSIGNED NOT NULL,
    erstberatungen INT UNSIGNED DEFAULT 0,
    ablehnungen INT UNSIGNED DEFAULT 0,
    unterlagen INT UNSIGNED DEFAULT 0,
    keine_rueckmeldung INT UNSIGNED DEFAULT 0,
    gutachten_gesamt INT UNSIGNED DEFAULT 0,
    gutachten_alternative INT UNSIGNED DEFAULT 0,
    gutachten_bestaetigung INT UNSIGNED DEFAULT 0,
    onko_anzahl INT UNSIGNED DEFAULT 0, onko_gutachten INT UNSIGNED DEFAULT 0, onko_keine_rm INT UNSIGNED DEFAULT 0,
    kardio_anzahl INT UNSIGNED DEFAULT 0, kardio_gutachten INT UNSIGNED DEFAULT 0, kardio_keine_rm INT UNSIGNED DEFAULT 0,
    intensiv_anzahl INT UNSIGNED DEFAULT 0, intensiv_gutachten INT UNSIGNED DEFAULT 0, intensiv_keine_rm INT UNSIGNED DEFAULT 0,
    galle_anzahl INT UNSIGNED DEFAULT 0, galle_gutachten INT UNSIGNED DEFAULT 0, galle_keine_rm INT UNSIGNED DEFAULT 0,
    sd_anzahl INT UNSIGNED DEFAULT 0, sd_gutachten INT UNSIGNED DEFAULT 0, sd_keine_rm INT UNSIGNED DEFAULT 0,
    onko_alternative INT UNSIGNED DEFAULT 0, onko_bestaetigung INT UNSIGNED DEFAULT 0,
    kardio_alternative INT UNSIGNED DEFAULT 0, kardio_bestaetigung INT UNSIGNED DEFAULT 0,
    intensiv_alternative INT UNSIGNED DEFAULT 0, intensiv_bestaetigung INT UNSIGNED DEFAULT 0,
    galle_alternative INT UNSIGNED DEFAULT 0, galle_bestaetigung INT UNSIGNED DEFAULT 0,
    sd_alternative INT UNSIGNED DEFAULT 0, sd_bestaetigung INT UNSIGNED DEFAULT 0,
    ta_ja INT UNSIGNED DEFAULT 0,
    ta_nein INT UNSIGNED DEFAULT 0,
    ta_diagnosekorrektur INT UNSIGNED DEFAULT 0,
    ta_unterversorgung INT UNSIGNED DEFAULT 0,
    ta_uebertherapie INT UNSIGNED DEFAULT 0,
    UNIQUE KEY uk_jahr_kw (jahr, kw)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================
-- LOGGING & BENACHRICHTIGUNGEN
-- ============================================

CREATE TABLE IF NOT EXISTS import_log (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    filename VARCHAR(255) NOT NULL,
    import_type VARCHAR(50) NOT NULL,
    cases_imported INT UNSIGNED NOT NULL DEFAULT 0,
    cases_skipped INT UNSIGNED NOT NULL DEFAULT 0,
    cases_updated INT UNSIGNED NOT NULL DEFAULT 0,
    errors TEXT DEFAULT NULL,
    details JSON DEFAULT NULL,
    imported_by INT UNSIGNED DEFAULT NULL,
    imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_imp_by FOREIGN KEY (imported_by) REFERENCES users(id),
    CONSTRAINT chk_imp_type CHECK (import_type IN ('csv_crm', 'icd_xlsx', 'historical_excel', 'excel_sync'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS audit_log (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED DEFAULT NULL,
    action VARCHAR(100) NOT NULL,
    entity_type VARCHAR(50) DEFAULT NULL,
    entity_id INT UNSIGNED DEFAULT NULL,
    old_values JSON DEFAULT NULL,
    new_values JSON DEFAULT NULL,
    ip_address VARCHAR(45) DEFAULT NULL,
    user_agent TEXT DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_entity (entity_type, entity_id),
    INDEX idx_created (created_at),
    CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS notifications (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    recipient_id INT UNSIGNED NOT NULL,
    notification_type VARCHAR(50) NOT NULL,
    title VARCHAR(255) NOT NULL,
    message TEXT DEFAULT NULL,
    related_entity_type VARCHAR(50) DEFAULT NULL,
    related_entity_id INT UNSIGNED DEFAULT NULL,
    is_read TINYINT(1) NOT NULL DEFAULT 0,
    email_sent TINYINT(1) NOT NULL DEFAULT 0,
    email_sent_at DATETIME DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_recipient (recipient_id, is_read),
    CONSTRAINT fk_notif_user FOREIGN KEY (recipient_id) REFERENCES users(id),
    CONSTRAINT chk_notif CHECK (notification_type IN (
        'new_cases_uploaded', 'icd_entered', 'icd_uploaded',
        'report_ready', 'coding_completed'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

5. Backend-Projektstruktur

dak.c2s/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py                     # FastAPI App, Middleware, CORS, Lifespan
│   │   ├── config.py                   # Pydantic Settings (.env)
│   │   ├── database.py                 # SQLAlchemy engine (mysql+pymysql)
│   │   │
│   │   ├── models/
│   │   │   ├── __init__.py             # Alle Models exportieren
│   │   │   ├── user.py                 # User, RefreshToken, InvitationLink, AllowedDomain
│   │   │   ├── case.py                 # Case, CaseICDCode
│   │   │   ├── report.py              # WeeklyReport, YearlySummary
│   │   │   └── audit.py               # AuditLog, ImportLog, Notification
│   │   │
│   │   ├── schemas/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py                 # LoginRequest, TokenResponse, RegisterRequest, MFASetup
│   │   │   ├── user.py                 # UserResponse, UserCreate, UserUpdate
│   │   │   ├── case.py                 # CaseResponse, CaseList, CaseUpdate, ICDUpdate, CodingUpdate
│   │   │   ├── import_schemas.py       # ImportPreview, ImportResult, ImportRow
│   │   │   ├── report.py              # DashboardKPIs, WeeklyData, ReportMeta
│   │   │   └── notification.py        # NotificationResponse, NotificationList
│   │   │
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py                 # POST login, register, refresh, mfa/setup, mfa/verify
│   │   │   ├── cases.py                # GET/PUT cases, pending-icd, pending-coding
│   │   │   ├── import_router.py        # POST csv, icd-xlsx, historical
│   │   │   ├── coding.py              # GET queue, PUT coding/{id}
│   │   │   ├── reports.py             # GET dashboard, weekly, download; POST generate
│   │   │   ├── notifications.py       # GET list, PUT mark-read
│   │   │   └── admin.py               # Users CRUD, invitations, audit-log
│   │   │
│   │   ├── services/
│   │   │   ├── __init__.py
│   │   │   ├── auth_service.py         # JWT create/verify, password hash/verify, MFA
│   │   │   ├── csv_parser.py           # CRM CSV parsing (pipe-delimited contacts)
│   │   │   ├── import_service.py       # Import orchestration + duplicate detection
│   │   │   ├── icd_service.py          # ICD normalize, split, validate, hauptgruppe
│   │   │   ├── coding_service.py       # Coding queue, batch update
│   │   │   ├── report_service.py       # All 5 sheet calculations (pandas)
│   │   │   ├── excel_export.py        # openpyxl → Berichtswesen format
│   │   │   ├── excel_import.py        # Abrechnung_DAK.xlsx → DB (historical)
│   │   │   ├── vorjahr_service.py     # Year-over-year comparison
│   │   │   ├── notification_service.py # SMTP + in-app notifications
│   │   │   ├── audit_service.py       # Audit log helper
│   │   │   └── excel_sync.py          # Bidirectional Excel ↔ DB sync
│   │   │
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   ├── security.py             # JWT encode/decode, password helpers
│   │   │   ├── dependencies.py        # get_current_user, require_admin, get_db
│   │   │   └── exceptions.py          # Custom HTTP exceptions
│   │   │
│   │   └── utils/
│   │       ├── __init__.py
│   │       ├── fallgruppe_map.py      # Modul string → fallgruppe code
│   │       ├── kw_utils.py            # ISO KW calculation, date helpers
│   │       └── validators.py          # ICD regex, KVNR format, date validation
│   │
│   ├── alembic/
│   │   ├── alembic.ini
│   │   ├── env.py
│   │   └── versions/
│   │
│   ├── scripts/
│   │   ├── init_db.py                  # Create schema + seed data
│   │   ├── import_historical.py       # One-time: Abrechnung → DB
│   │   ├── import_berichtswesen.py    # One-time: Vorjahres-Berichte → yearly_summary
│   │   └── create_admin.py            # Create initial admin user
│   │
│   ├── tests/
│   │   ├── conftest.py
│   │   ├── test_csv_parser.py
│   │   ├── test_import.py
│   │   ├── test_icd_service.py
│   │   ├── test_report_service.py
│   │   └── test_auth.py
│   │
│   ├── requirements.txt
│   ├── .env.example
│   └── .env
│
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── ui/                     # shadcn/ui
│   │   │   ├── layout/                # AppLayout, Sidebar, Header, ProtectedRoute
│   │   │   ├── dashboard/            # KPICards, WeeklyChart, FallgruppenDonut, etc.
│   │   │   ├── cases/                # CaseTable, CaseDetail, ICDInlineEdit
│   │   │   ├── import/               # CSVUpload, ImportPreview, ICDUpload
│   │   │   ├── coding/               # CodingQueue, CodingCard, CodingProgress
│   │   │   ├── reports/              # ReportList, ReportDownload
│   │   │   ├── notifications/        # NotificationBell, NotificationDropdown
│   │   │   └── admin/                # UserManagement, InvitationLinks, AuditLog
│   │   ├── pages/                     # LoginPage, DashboardPage, CasesPage, etc.
│   │   ├── services/                  # api.ts, authService.ts, etc.
│   │   ├── hooks/                     # useAuth, useCases, useNotifications
│   │   ├── types/                     # TypeScript interfaces
│   │   ├── context/                   # AuthContext
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── package.json
│   ├── vite.config.ts
│   ├── tailwind.config.js
│   └── tsconfig.json
│
├── data/                               # Referenzdaten (nicht im Git)
│   ├── Abrechnung_DAK.xlsx
│   ├── Berichtswesen_2024_29122024.xlsx
│   ├── Berichtswesen_2023_31122023.xlsx
│   └── sample_csv/
│       └── 2026-02-06-0406.csv
│
├── .gitignore
├── README.md
└── SPEC.md                             # Diese Datei

6. API-Endpunkte

Auth

POST /api/auth/login                    → {access_token, refresh_token, user}
POST /api/auth/register                 → {user} (Domain-Check oder Invitation-Token)
POST /api/auth/refresh                  → {access_token}
POST /api/auth/mfa/setup               → {secret, qr_uri} (Admin)
POST /api/auth/mfa/verify              → {verified: true}
POST /api/auth/logout                   → Revoke refresh token

Cases

GET    /api/cases                       → Paginated list (filter: jahr, kw, fallgruppe, has_icd, has_coding)
GET    /api/cases/{id}                  → Single case detail
PUT    /api/cases/{id}                  → Update case fields
GET    /api/cases/pending-icd           → Cases without ICD (for DAK)
PUT    /api/cases/{id}/icd              → Set ICD (DAK or Admin)
GET    /api/cases/pending-coding        → Cases with gutachten but no typ (for Admin)
PUT    /api/cases/{id}/coding           → Set gutachten_typ + therapieaenderung (Admin)
GET    /api/cases/coding-template       → Download .xlsx template for ICD entry

Import

POST /api/import/csv                    → Upload CRM CSV → preview
POST /api/import/csv/confirm            → Confirm import from preview
POST /api/import/icd-xlsx               → Upload ICD-coded Excel (DAK)
POST /api/import/historical             → Import from Abrechnung_DAK.xlsx (Admin, one-time)
GET  /api/import/log                    → Import history

Reports

GET    /api/reports/dashboard            → Live KPIs + chart data
GET    /api/reports/weekly/{jahr}/{kw}   → Specific week data
POST   /api/reports/generate             → Generate report .xlsx (Admin)
GET    /api/reports/download/{id}        → Download generated .xlsx
GET    /api/reports/list                 → All generated reports

Notifications

GET    /api/notifications                → Unread + recent
PUT    /api/notifications/{id}/read      → Mark as read
PUT    /api/notifications/read-all       → Mark all as read

Admin

GET    /api/admin/users                  → List users
POST   /api/admin/users                  → Create user
PUT    /api/admin/users/{id}             → Update user (role, active)
POST   /api/admin/invitations            → Create invitation link
GET    /api/admin/invitations            → List invitation links
GET    /api/admin/audit-log              → Paginated audit log
POST   /api/admin/excel-sync             → Trigger Excel ↔ DB sync

7. Implementierungsphasen

Phase 1: Setup + DB + Auth (Woche 1)

Ziel: Lauffähiges Backend mit Auth auf sv-frontend

□ GitHub Repo complexcaresolutions/dak.c2s anlegen
□ Projekt klonen auf sv-frontend
□ MariaDB auf sv-frontend einrichten (dev)
□ Backend-Projektstruktur erstellen
□ Python venv + requirements.txt
□ .env + config.py (Pydantic Settings)
□ database.py (SQLAlchemy mysql+pymysql)
□ Alle SQLAlchemy Models
□ Alembic init + erste Migration
□ Schema deployen (dev)
□ Auth: JWT + bcrypt + MFA
□ API: auth (login, register, refresh, mfa)
□ API: admin (users CRUD)
□ Domain-Whitelist + Einladungslinks
□ Audit-Log Middleware
□ CORS + Error Handling
□ create_admin.py Script
□ Erster Commit + Push

Phase 2: Import + ICD (Woche 2)

Ziel: CSV-Import und ICD-Workflow funktionsfähig

□ csv_parser.py (Pipe-Format, Modul-Mapping)
□ import_service.py (Duplikatprüfung, Fall_ID)
□ icd_service.py (Normalisierung, Validierung)
□ validators.py (ICD-Regex, KVNR, Datum)
□ API: import/csv (Upload + Preview + Confirm)
□ API: import/icd-xlsx (DAK-Rückläufer)
□ API: cases/pending-icd
□ API: cases/{id}/icd
□ Coding-Template Generierung (.xlsx)
□ excel_import.py (Abrechnung → DB)
□ import_historical.py Script
□ Historischen Import durchführen (2023-2026)
□ Import-Logging
□ notification_service.py (SMTP)
□ Tests: csv_parser, import, icd

Phase 3: Berichtswesen + Coding (Woche 3)

Ziel: Berichte generierbar, Coding-Queue nutzbar

□ report_service.py (alle 5 Sheet-Berechnungen)
□ vorjahr_service.py
□ excel_export.py (exaktes Berichtswesen-Format)
□ import_berichtswesen.py (Vorjahres-Cache)
□ API: reports/dashboard
□ API: reports/generate + download
□ coding_service.py (Queue-Logik)
□ API: coding (queue, update)
□ API: cases/pending-coding
□ Benachrichtigungen (alle Trigger)
□ excel_sync.py (DB ↔ Abrechnung)
□ Tests: reports, coding

Phase 4: Frontend Core (Woche 4-5)

Ziel: Funktionsfähiges Web-Interface

Woche 4:
□ React + Vite + TS + Tailwind + shadcn/ui Setup
□ API-Client (axios/fetch + JWT interceptor)
□ AuthContext + ProtectedRoute
□ Login + Register Seiten
□ AppLayout (Sidebar + Header)
□ Dashboard: KPI-Cards
□ Dashboard: WeeklyTrendChart (Recharts)
□ Dashboard: FallgruppenDonut
□ Dashboard: VorjahresComparison

Woche 5:
□ CaseTable + Filter + Pagination
□ ICD-Inline-Edit (DAK)
□ CSVUpload + ImportPreview
□ ICDUpload (Excel)
□ CodingQueue + CodingCard
□ ReportList + Download
□ NotificationBell + Dropdown
□ Admin: UserManagement
□ Admin: InvitationLinks
□ Admin: AuditLogViewer

Phase 5: Deploy + Go-Live (Woche 6)

Ziel: Live auf dak.complexcaresolutions.de

□ Frontend Production Build
□ Git push → GitHub
□ Hetzner 1: git pull
□ Python venv auf Server
□ MariaDB Schema auf Produktion deployen
□ .env auf Server konfigurieren
□ systemd-Service einrichten + starten
□ Plesk: Additional nginx directives
□ SMTP testen (Produktion)
□ Admin-Account anlegen
□ Historische Daten importieren (Produktion)
□ DAK-Mitarbeiter einladen
□ Smoke Tests
□ Go-Live ✓

8. Sicherheit

Authentifizierung

  • bcrypt (12 Rounds) für Passwort-Hashing
  • JWT Access Token: 15 Min Laufzeit
  • JWT Refresh Token: 7 Tage, in DB gespeichert, revokierbar
  • MFA: TOTP (pyotp), optional aktivierbar
  • Account-Sperre nach 5 Fehlversuchen (30 Min)
  • Domain-Whitelist: @dak.de für Selbstregistrierung
  • Einladungslinks: Token + Ablaufdatum + optional E-Mail-Bindung

Autorisierung (RBAC)

  • admin: Voller Zugriff (Import, Coding, Reports, Users)
  • dak_mitarbeiter: Dashboard, ICD-Eingabe, Report-Download

Audit

  • Jede Datenänderung wird geloggt (entity, old/new values, user, IP)
  • Unveränderlich (INSERT only)
  • Einsehbar über Admin-UI

Transport

  • TLS/SSL (Plesk-verwaltet)
  • CORS: nur dak.complexcaresolutions.de

Daten

  • MariaDB auf localhost (kein externer Zugang)
  • Verschlüsselung at rest: MariaDB encryption + Dateisystem
  • Uploads in geschütztem Verzeichnis (nicht öffentlich)
  • Sensible Daten (.env, Passwörter) nicht im Git

9. Nginx Konfiguration (Plesk Additional Directives)

# In Plesk → dak.complexcaresolutions.de → Apache & nginx Settings
# → Additional nginx directives

# API Reverse Proxy
location /api/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 120s;
    client_max_body_size 20M;
}

# FastAPI Docs
location /docs {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
}

location /openapi.json {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
}

# React SPA fallback
location / {
    root /var/www/vhosts/dak.complexcaresolutions.de/frontend/dist;
    index index.html;
    try_files $uri $uri/ /index.html;
}

10. systemd Service

# /etc/systemd/system/dak-backend.service

[Unit]
Description=DAK Portal Backend (FastAPI/Gunicorn)
After=network.target mariadb.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/vhosts/dak.complexcaresolutions.de/backend
Environment="PATH=/var/www/vhosts/dak.complexcaresolutions.de/backend/venv/bin"
EnvironmentFile=/var/www/vhosts/dak.complexcaresolutions.de/backend/.env
ExecStart=/var/www/vhosts/dak.complexcaresolutions.de/backend/venv/bin/gunicorn \
    app.main:app \
    --workers 2 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 127.0.0.1:8000 \
    --access-logfile /var/www/vhosts/dak.complexcaresolutions.de/logs/access.log \
    --error-logfile /var/www/vhosts/dak.complexcaresolutions.de/logs/error.log
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Management:

sudo systemctl daemon-reload
sudo systemctl enable dak-backend
sudo systemctl start dak-backend
sudo systemctl status dak-backend
sudo journalctl -u dak-backend -f    # Live-Logs