mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 19:33: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>
35 KiB
35 KiB
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)
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 |
| 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