From 9289dc1957a1decf62ea925edc4ece07939aa0e4 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 19 Jul 2026 14:09:01 +0200 Subject: [PATCH] Pflegepilot v3 Phase 2: 6-Schritte-Interview-Wizard fuer neue Patienten Regelbasierte (nicht KI-gestuetzte) Pflegegrad-Schaetzung 1:1 aus der alten App portiert (NEU_FRAGEN/NEU_ZG_OPTIONEN/neuSchaetzung -> pflegegrad_regeln.py + POST /api/pflegegrad/schaetzen), damit die Regel nur einmal zentral im Backend existiert statt dupliziert im Frontend. Krankenkassen-Stammdaten (~86 Versicherer DE/AT/CH) 1:1 portiert fuer die Autocomplete in Schritt 3. Frontend: kompletter Wizard als useReducer-State-Maschine (Schritte 1-6, verschachtelter Sub-Wizard in Schritt 4 mit Auto-Advance je Antwort ausser bei der letzten Frage, Zurueck-Navigation landet korrekt auf der letzten Frage), ersetzt das Kurzformular aus Phase 1. Co-Authored-By: Claude Sonnet 5 --- pflegepilot_v3/backend/app/main.py | 4 +- .../backend/app/routers/pflegegrad.py | 17 + .../backend/app/routers/stammdaten.py | 20 + pflegepilot_v3/backend/app/schemas.py | 43 ++ .../backend/app/static_data/krankenkassen.py | 407 ++++++++++++ .../app/static_data/pflegegrad_regeln.py | 99 +++ .../src/app/(app)/patienten/neu/page.tsx | 629 ++++++++++++++++-- pflegepilot_v3/frontend/src/lib/pflegegrad.ts | 46 ++ 8 files changed, 1194 insertions(+), 71 deletions(-) create mode 100644 pflegepilot_v3/backend/app/routers/pflegegrad.py create mode 100644 pflegepilot_v3/backend/app/routers/stammdaten.py create mode 100644 pflegepilot_v3/backend/app/static_data/krankenkassen.py create mode 100644 pflegepilot_v3/backend/app/static_data/pflegegrad_regeln.py create mode 100644 pflegepilot_v3/frontend/src/lib/pflegegrad.ts diff --git a/pflegepilot_v3/backend/app/main.py b/pflegepilot_v3/backend/app/main.py index c6291b5..324f8ad 100644 --- a/pflegepilot_v3/backend/app/main.py +++ b/pflegepilot_v3/backend/app/main.py @@ -6,7 +6,7 @@ from starlette.middleware.sessions import SessionMiddleware from app.config import settings from app.pdf import render_pdf -from app.routers import auth, dashboard, patienten +from app.routers import auth, dashboard, patienten, pflegegrad, stammdaten app = FastAPI(title="Pflegepilot API") @@ -39,6 +39,8 @@ async def security_headers(request, call_next): app.include_router(auth.router) app.include_router(patienten.router) app.include_router(dashboard.router) +app.include_router(pflegegrad.router) +app.include_router(stammdaten.router) @app.get("/api/health") diff --git a/pflegepilot_v3/backend/app/routers/pflegegrad.py b/pflegepilot_v3/backend/app/routers/pflegegrad.py new file mode 100644 index 0000000..8775a47 --- /dev/null +++ b/pflegepilot_v3/backend/app/routers/pflegegrad.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends + +from app.deps import require_login +from app.schemas import PflegegradFragenOut, SchaetzungOut, SchaetzungRequest +from app.static_data.pflegegrad_regeln import FRAGEN, ZIELGRUPPEN_OPTIONEN, schaetzen + +router = APIRouter(prefix="/api/pflegegrad", tags=["pflegegrad"]) + + +@router.get("/fragen", response_model=PflegegradFragenOut) +def fragen(_=Depends(require_login)): + return PflegegradFragenOut(fragen=FRAGEN, zielgruppen=ZIELGRUPPEN_OPTIONEN) + + +@router.post("/schaetzen", response_model=SchaetzungOut) +def schaetzen_route(payload: SchaetzungRequest, _=Depends(require_login)): + return schaetzen(payload.antworten) diff --git a/pflegepilot_v3/backend/app/routers/stammdaten.py b/pflegepilot_v3/backend/app/routers/stammdaten.py new file mode 100644 index 0000000..231507b --- /dev/null +++ b/pflegepilot_v3/backend/app/routers/stammdaten.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Depends + +from app.deps import require_login +from app.schemas import KrankenkasseOut +from app.static_data.krankenkassen import search as kk_search + +router = APIRouter(prefix="/api/krankenkassen", tags=["stammdaten"]) + + +@router.get("", response_model=list[KrankenkasseOut]) +def liste(q: str = "", land: str | None = None, _=Depends(require_login)): + if not q: + from app.static_data.krankenkassen import KRANKENKASSEN + + ergebnis = KRANKENKASSEN + if land: + ergebnis = [k for k in ergebnis if k.get("land", "DE") == land] + else: + ergebnis = kk_search(q, land) + return ergebnis diff --git a/pflegepilot_v3/backend/app/schemas.py b/pflegepilot_v3/backend/app/schemas.py index 034a67c..89942e9 100644 --- a/pflegepilot_v3/backend/app/schemas.py +++ b/pflegepilot_v3/backend/app/schemas.py @@ -107,3 +107,46 @@ class DashboardOut(BaseModel): offene_antraege: int offene_aufgaben: int letzte_patienten: list[PatientOut] + + +class PflegegradOption(BaseModel): + label: str + wert: int + + +class PflegegradFrage(BaseModel): + key: str + frage: str + optionen: list[PflegegradOption] + + +class ZielgruppenOption(BaseModel): + label: str + zg: str + hinweis: str + + +class PflegegradFragenOut(BaseModel): + fragen: list[PflegegradFrage] + zielgruppen: list[ZielgruppenOption] + + +class SchaetzungRequest(BaseModel): + antworten: dict[str, int] + + +class SchaetzungOut(BaseModel): + summe: int + von: int + bis: int + vorschlag: int + + +class KrankenkasseOut(BaseModel): + name: str + kurzname: str + typ: str + land: str = "DE" + strasse: str + plz: str + ort: str diff --git a/pflegepilot_v3/backend/app/static_data/krankenkassen.py b/pflegepilot_v3/backend/app/static_data/krankenkassen.py new file mode 100644 index 0000000..1a95ca0 --- /dev/null +++ b/pflegepilot_v3/backend/app/static_data/krankenkassen.py @@ -0,0 +1,407 @@ +""" +Pflegepilot – Krankenkassen-Stammdaten +1:1 aus der alten App (pflegepilot_alpha_v2/pflegepilot/krankenkassen.py) portiert. +Adressen Stand 2025. Vor Verwendung bitte auf Aktualität pruefen. +Pflegegrad-Bescheide werden meist ueber die zustaendige Pflegekasse abgewickelt, +die in der Regel identisch mit der Krankenkasse ist. +""" + +KRANKENKASSEN = [ + + # ── GKV: AOK ────────────────────────────────────────────────────────────── + {"name": "AOK Bayern – Die Gesundheitskasse", + "kurzname": "AOK Bayern", "typ": "GKV", + "strasse": "Carl-Wery-Straße 28", "plz": "81739", "ort": "München"}, + + {"name": "AOK Baden-Württemberg", + "kurzname": "AOK BW", "typ": "GKV", + "strasse": "Presselstraße 19", "plz": "70191", "ort": "Stuttgart"}, + + {"name": "AOK Berlin-Brandenburg", + "kurzname": "AOK BE/BB", "typ": "GKV", + "strasse": "Wilhelmstraße 1", "plz": "10963", "ort": "Berlin"}, + + {"name": "AOK Bremen/Bremerhaven", + "kurzname": "AOK Bremen", "typ": "GKV", + "strasse": "Bürgermeister-Smidt-Straße 95", "plz": "28195", "ort": "Bremen"}, + + {"name": "AOK Hessen", + "kurzname": "AOK Hessen", "typ": "GKV", + "strasse": "Berner Straße 2", "plz": "60437", "ort": "Frankfurt am Main"}, + + {"name": "AOK Niedersachsen", + "kurzname": "AOK Niedersachsen", "typ": "GKV", + "strasse": "Hildesheimer Straße 273", "plz": "30519", "ort": "Hannover"}, + + {"name": "AOK Nordost – Die Gesundheitskasse", + "kurzname": "AOK Nordost", "typ": "GKV", + "strasse": "Warnowufer 54", "plz": "18057", "ort": "Rostock"}, + + {"name": "AOK NordWest – Die Gesundheitskasse", + "kurzname": "AOK NordWest", "typ": "GKV", + "strasse": "Kopenhagener Straße 1", "plz": "44269", "ort": "Dortmund"}, + + {"name": "AOK PLUS – Die Gesundheitskasse für Sachsen und Thüringen", + "kurzname": "AOK PLUS", "typ": "GKV", + "strasse": "Sternplatz 7", "plz": "01067", "ort": "Dresden"}, + + {"name": "AOK Rheinland/Hamburg – Die Gesundheitskasse", + "kurzname": "AOK Rheinland/HH", "typ": "GKV", + "strasse": "Kaiserstraße 80", "plz": "40479", "ort": "Düsseldorf"}, + + {"name": "AOK Rheinland-Pfalz/Saarland – Die Gesundheitskasse", + "kurzname": "AOK RLP/SL", "typ": "GKV", + "strasse": "Virchowstraße 30", "plz": "67304", "ort": "Eisenberg"}, + + {"name": "AOK Sachsen-Anhalt – Die Gesundheitskasse", + "kurzname": "AOK ST", "typ": "GKV", + "strasse": "Doctor-Eisenbart-Ring 2", "plz": "39120", "ort": "Magdeburg"}, + + # ── GKV: Große Kassen ───────────────────────────────────────────────────── + {"name": "Barmer", + "kurzname": "Barmer", "typ": "GKV", + "strasse": "Lichtscheider Straße 89", "plz": "42285", "ort": "Wuppertal"}, + + {"name": "DAK-Gesundheit", + "kurzname": "DAK", "typ": "GKV", + "strasse": "Nagelsweg 27–31", "plz": "20097", "ort": "Hamburg"}, + + {"name": "Techniker Krankenkasse", + "kurzname": "TK", "typ": "GKV", + "strasse": "Bramfelder Straße 140", "plz": "22305", "ort": "Hamburg"}, + + {"name": "KKH – Kaufmännische Krankenkasse", + "kurzname": "KKH", "typ": "GKV", + "strasse": "Karl-Wiechert-Allee 61", "plz": "30625", "ort": "Hannover"}, + + {"name": "HEK – Hanseatische Krankenkasse", + "kurzname": "HEK", "typ": "GKV", + "strasse": "Wandsbeker Zollstraße 86–90", "plz": "22041", "ort": "Hamburg"}, + + {"name": "hkk – Handelskrankenkasse", + "kurzname": "hkk", "typ": "GKV", + "strasse": "Martinistraße 26", "plz": "28195", "ort": "Bremen"}, + + {"name": "Knappschaft", + "kurzname": "Knappschaft", "typ": "GKV", + "strasse": "Pieperstraße 14–28", "plz": "44789", "ort": "Bochum"}, + + # ── GKV: IKK ────────────────────────────────────────────────────────────── + {"name": "IKK classic", + "kurzname": "IKK classic", "typ": "GKV", + "strasse": "Tannenstraße 4", "plz": "01099", "ort": "Dresden"}, + + {"name": "IKK gesund plus", + "kurzname": "IKK gesund plus", "typ": "GKV", + "strasse": "Guerickestraße 2", "plz": "39104", "ort": "Magdeburg"}, + + {"name": "IKK Nord", + "kurzname": "IKK Nord", "typ": "GKV", + "strasse": "Heßstraße 35", "plz": "20097", "ort": "Hamburg"}, + + {"name": "IKK Südwest", + "kurzname": "IKK Südwest", "typ": "GKV", + "strasse": "Europaallee 9", "plz": "67657", "ort": "Kaiserslautern"}, + + # ── GKV: BKK ────────────────────────────────────────────────────────────── + {"name": "BKK VBU", + "kurzname": "BKK VBU", "typ": "GKV", + "strasse": "Koppenstraße 4–46", "plz": "10243", "ort": "Berlin"}, + + {"name": "BKK firmus", + "kurzname": "BKK firmus", "typ": "GKV", + "strasse": "Gewerbering 4", "plz": "27356", "ort": "Rotenburg (Wümme)"}, + + {"name": "BKK ProVita", + "kurzname": "BKK ProVita", "typ": "GKV", + "strasse": "Am Westpark 8", "plz": "81373", "ort": "München"}, + + {"name": "energie BKK", + "kurzname": "energie BKK", "typ": "GKV", + "strasse": "Lübeckertordamm 1–3", "plz": "20099", "ort": "Hamburg"}, + + {"name": "mhplus BKK", + "kurzname": "mhplus", "typ": "GKV", + "strasse": "Eisenbahnstraße 66", "plz": "73207", "ort": "Plochingen"}, + + {"name": "Novitas BKK", + "kurzname": "Novitas", "typ": "GKV", + "strasse": "Wintgensstraße 27", "plz": "47058", "ort": "Duisburg"}, + + {"name": "Pronova BKK", + "kurzname": "Pronova BKK", "typ": "GKV", + "strasse": "Harrlachweg 1", "plz": "68163", "ort": "Mannheim"}, + + {"name": "R+V Betriebskrankenkasse", + "kurzname": "R+V BKK", "typ": "GKV", + "strasse": "Raiffeisenplatz 1", "plz": "65189", "ort": "Wiesbaden"}, + + {"name": "SBK – Siemens-Betriebskrankenkasse", + "kurzname": "SBK", "typ": "GKV", + "strasse": "Heimeranstraße 31", "plz": "80339", "ort": "München"}, + + {"name": "Vivida BKK", + "kurzname": "Vivida BKK", "typ": "GKV", + "strasse": "Wilhelm-Beck-Straße 6–8", "plz": "88662", "ort": "Überlingen"}, + + {"name": "mkk – meine krankenkasse", + "kurzname": "mkk", "typ": "GKV", + "strasse": "Maximilianstraße 2", "plz": "83435", "ort": "Bad Reichenhall"}, + + {"name": "WMF BKK", + "kurzname": "WMF BKK", "typ": "GKV", + "strasse": "Eberhardstraße 14", "plz": "73312", "ort": "Geislingen an der Steige"}, + + # ── GKV: Weitere ────────────────────────────────────────────────────────── + {"name": "BIG direkt gesund", + "kurzname": "BIG", "typ": "GKV", + "strasse": "Westhellweg 41", "plz": "44137", "ort": "Dortmund"}, + + {"name": "Heimat Krankenkasse", + "kurzname": "Heimat KK", "typ": "GKV", + "strasse": "Bielefeldstraße 1", "plz": "33606", "ort": "Bielefeld"}, + + {"name": "Viactiv Krankenkasse", + "kurzname": "Viactiv", "typ": "GKV", + "strasse": "Huyssenallee 52–56", "plz": "45128", "ort": "Essen"}, + + {"name": "actimonda BKK", + "kurzname": "actimonda", "typ": "GKV", + "strasse": "Brunnenstraße 1", "plz": "45128", "ort": "Essen"}, + + {"name": "Audi BKK", + "kurzname": "Audi BKK", "typ": "GKV", + "strasse": "Ettingerstraße 1", "plz": "85045", "ort": "Ingolstadt"}, + + {"name": "BMW BKK", + "kurzname": "BMW BKK", "typ": "GKV", + "strasse": "Moosacher Straße 90", "plz": "80809", "ort": "München"}, + + # ── PKV: Private Krankenversicherung ────────────────────────────────────── + {"name": "Allianz Private Krankenversicherungs-AG", + "kurzname": "Allianz PKV", "typ": "PKV", + "strasse": "Königinstraße 28", "plz": "80802", "ort": "München"}, + + {"name": "AXA Krankenversicherung AG", + "kurzname": "AXA", "typ": "PKV", + "strasse": "Colonia-Allee 10–20", "plz": "51067", "ort": "Köln"}, + + {"name": "Barmenia Krankenversicherung a.G.", + "kurzname": "Barmenia", "typ": "PKV", + "strasse": "Kronprinzenallee 12–18", "plz": "42094", "ort": "Wuppertal"}, + + {"name": "Continentale Krankenversicherung a.G.", + "kurzname": "Continentale", "typ": "PKV", + "strasse": "Ruhrallee 92", "plz": "44139", "ort": "Dortmund"}, + + {"name": "Debeka Krankenversicherungsverein a.G.", + "kurzname": "Debeka", "typ": "PKV", + "strasse": "Ferdinand-Sauerbruch-Straße 18", "plz": "56058", "ort": "Koblenz"}, + + {"name": "DKV Deutsche Krankenversicherung AG", + "kurzname": "DKV", "typ": "PKV", + "strasse": "Aachener Straße 300", "plz": "50933", "ort": "Köln"}, + + {"name": "Gothaer Krankenversicherung AG", + "kurzname": "Gothaer", "typ": "PKV", + "strasse": "Gothaer Allee 1", "plz": "50969", "ort": "Köln"}, + + {"name": "Hallesche Krankenversicherung a.G.", + "kurzname": "Hallesche", "typ": "PKV", + "strasse": "Reinsburgstraße 10", "plz": "70178", "ort": "Stuttgart"}, + + {"name": "HanseMerkur Krankenversicherung AG", + "kurzname": "HanseMerkur", "typ": "PKV", + "strasse": "Siegfried-Wedells-Platz 1", "plz": "20354", "ort": "Hamburg"}, + + {"name": "Inter Krankenversicherung AG", + "kurzname": "Inter", "typ": "PKV", + "strasse": "Erzbergerstraße 9–15", "plz": "68165", "ort": "Mannheim"}, + + {"name": "LVM Krankenversicherung AG", + "kurzname": "LVM", "typ": "PKV", + "strasse": "Kolde-Ring 21", "plz": "48126", "ort": "Münster"}, + + {"name": "Münchener Verein Krankenversicherung a.G.", + "kurzname": "Münchener Verein", "typ": "PKV", + "strasse": "Pettenkoferstraße 19", "plz": "80336", "ort": "München"}, + + {"name": "NÜRNBERGER Krankenversicherung AG", + "kurzname": "NÜRNBERGER", "typ": "PKV", + "strasse": "Ostendstraße 100", "plz": "90334", "ort": "Nürnberg"}, + + {"name": "ottonova Krankenversicherung AG", + "kurzname": "ottonova", "typ": "PKV", + "strasse": "Maximiliansplatz 5", "plz": "80333", "ort": "München"}, + + {"name": "R+V Krankenversicherung AG", + "kurzname": "R+V PKV", "typ": "PKV", + "strasse": "Raiffeisenplatz 1", "plz": "65189", "ort": "Wiesbaden"}, + + {"name": "SDK – Süddeutsche Krankenversicherung a.G.", + "kurzname": "SDK", "typ": "PKV", + "strasse": "Raiffeisenplatz 1", "plz": "70736", "ort": "Fellbach"}, + + {"name": "Signal Iduna Krankenversicherung AG", + "kurzname": "Signal Iduna", "typ": "PKV", + "strasse": "Joseph-Scherer-Straße 3", "plz": "44139", "ort": "Dortmund"}, + + {"name": "UNIVERSA Krankenversicherung a.G.", + "kurzname": "UNIVERSA", "typ": "PKV", + "strasse": "Sulzbacher Straße 1–7", "plz": "90489", "ort": "Nürnberg"}, + + {"name": "Württembergische Krankenversicherung AG", + "kurzname": "WKV", "typ": "PKV", + "strasse": "Gutenbergstraße 30", "plz": "70176", "ort": "Stuttgart"}, + + {"name": "Bayerische Beamtenkrankenkasse AG", + "kurzname": "BBKK", "typ": "PKV", + "strasse": "Maximilianstraße 53", "plz": "81537", "ort": "München"}, + + {"name": "Mecklenburgische Krankenversicherung AG", + "kurzname": "Mecklenburgische", "typ": "PKV", + "strasse": "Platz der Göttinger Sieben 1", "plz": "30169", "ort": "Hannover"}, + + # ── Österreich: Sozialversicherung (SV) ─────────────────────────────────── + {"name": "Österreichische Gesundheitskasse", + "kurzname": "ÖGK", "typ": "SV", "land": "AT", + "strasse": "Kundmanngasse 21", "plz": "1031", "ort": "Wien"}, + + {"name": "ÖGK Landesstelle Wien", + "kurzname": "ÖGK Wien", "typ": "SV", "land": "AT", + "strasse": "Wienerbergstraße 15–19", "plz": "1100", "ort": "Wien"}, + + {"name": "ÖGK Landesstelle Niederösterreich", + "kurzname": "ÖGK NÖ", "typ": "SV", "land": "AT", + "strasse": "Kremser Landstraße 3", "plz": "3100", "ort": "St. Pölten"}, + + {"name": "ÖGK Landesstelle Oberösterreich", + "kurzname": "ÖGK OÖ", "typ": "SV", "land": "AT", + "strasse": "Gruberstraße 77", "plz": "4021", "ort": "Linz"}, + + {"name": "ÖGK Landesstelle Steiermark", + "kurzname": "ÖGK Stmk", "typ": "SV", "land": "AT", + "strasse": "Josef-Pongratz-Platz 1", "plz": "8011", "ort": "Graz"}, + + {"name": "ÖGK Landesstelle Kärnten", + "kurzname": "ÖGK Ktn", "typ": "SV", "land": "AT", + "strasse": "Kempfstraße 8", "plz": "9021", "ort": "Klagenfurt"}, + + {"name": "ÖGK Landesstelle Salzburg", + "kurzname": "ÖGK Sbg", "typ": "SV", "land": "AT", + "strasse": "Engelbert-Weiß-Weg 10", "plz": "5020", "ort": "Salzburg"}, + + {"name": "ÖGK Landesstelle Tirol", + "kurzname": "ÖGK Tirol", "typ": "SV", "land": "AT", + "strasse": "Klara-Pölt-Weg 2", "plz": "6020", "ort": "Innsbruck"}, + + {"name": "ÖGK Landesstelle Vorarlberg", + "kurzname": "ÖGK Vbg", "typ": "SV", "land": "AT", + "strasse": "Jahngasse 4", "plz": "6850", "ort": "Dornbirn"}, + + {"name": "ÖGK Landesstelle Burgenland", + "kurzname": "ÖGK Bgld", "typ": "SV", "land": "AT", + "strasse": "Esterházyplatz 3", "plz": "7001", "ort": "Eisenstadt"}, + + {"name": "Sozialversicherungsanstalt der Selbständigen", + "kurzname": "SVS", "typ": "SV", "land": "AT", + "strasse": "Wiedner Hauptstraße 84–86", "plz": "1051", "ort": "Wien"}, + + {"name": "Versicherungsanstalt öffentlich Bediensteter, Eisenbahnen und Bergbau", + "kurzname": "BVAEB", "typ": "SV", "land": "AT", + "strasse": "Josefstädter Straße 80", "plz": "1080", "ort": "Wien"}, + + # ── Schweiz: Krankenversicherung nach KVG ───────────────────────────────── + {"name": "Helsana Versicherungen AG", + "kurzname": "Helsana", "typ": "KVG", "land": "CH", + "strasse": "Zürichstrasse 130", "plz": "8600", "ort": "Dübendorf"}, + + {"name": "CSS Versicherung AG", + "kurzname": "CSS", "typ": "KVG", "land": "CH", + "strasse": "Tribschenstraße 21", "plz": "6005", "ort": "Luzern"}, + + {"name": "SWICA Krankenversicherung AG", + "kurzname": "SWICA", "typ": "KVG", "land": "CH", + "strasse": "Römerstraße 38", "plz": "8401", "ort": "Winterthur"}, + + {"name": "Sanitas Krankenversicherung", + "kurzname": "Sanitas", "typ": "KVG", "land": "CH", + "strasse": "Jägergasse 3", "plz": "8021", "ort": "Zürich"}, + + {"name": "Concordia Schweizerische Kranken- und Unfallversicherung AG", + "kurzname": "Concordia", "typ": "KVG", "land": "CH", + "strasse": "Bundesplatz 15", "plz": "6003", "ort": "Luzern"}, + + {"name": "Visana Versicherungen AG", + "kurzname": "Visana", "typ": "KVG", "land": "CH", + "strasse": "Weltpoststrasse 19", "plz": "3015", "ort": "Bern"}, + + {"name": "KPT Krankenkasse AG", + "kurzname": "KPT", "typ": "KVG", "land": "CH", + "strasse": "Wankdorfallee 3", "plz": "3014", "ort": "Bern"}, + + {"name": "Assura-Basis SA", + "kurzname": "Assura", "typ": "KVG", "land": "CH", + "strasse": "Avenue Charles-Ferdinand-Ramuz 70", "plz": "1009", "ort": "Pully"}, + + {"name": "Groupe Mutuel Versicherungen GmbH", + "kurzname": "Groupe Mutuel", "typ": "KVG", "land": "CH", + "strasse": "Rue des Cèdres 5", "plz": "1919", "ort": "Martigny"}, + + {"name": "Atupri Gesundheitsversicherung", + "kurzname": "Atupri", "typ": "KVG", "land": "CH", + "strasse": "Zieglerstrasse 29", "plz": "3007", "ort": "Bern"}, + + {"name": "Sympany AG", + "kurzname": "Sympany", "typ": "KVG", "land": "CH", + "strasse": "Peter Merian-Weg 4", "plz": "4052", "ort": "Basel"}, + + {"name": "EGK Gesundheitskasse", + "kurzname": "EGK", "typ": "KVG", "land": "CH", + "strasse": "Brislachstrasse 2", "plz": "4242", "ort": "Laufen"}, + + {"name": "Aquilana Versicherungen", + "kurzname": "Aquilana", "typ": "KVG", "land": "CH", + "strasse": "Bruggerstrasse 46", "plz": "5401", "ort": "Baden"}, + + {"name": "Galenos Krankenversicherung AG", + "kurzname": "Galenos", "typ": "KVG", "land": "CH", + "strasse": "Militärstrasse 36", "plz": "8004", "ort": "Zürich"}, + + {"name": "Sana24 AG", + "kurzname": "Sana24", "typ": "KVG", "land": "CH", + "strasse": "Weltpoststrasse 19", "plz": "3015", "ort": "Bern"}, + + {"name": "Kolping Krankenkasse AG", + "kurzname": "Kolping", "typ": "KVG", "land": "CH", + "strasse": "Raemistrasse 5", "plz": "8024", "ort": "Zürich"}, +] + + +def get_all(land=None): + if land: + return [k for k in KRANKENKASSEN if k.get("land", "DE") == land] + return KRANKENKASSEN + + +def get_by_land(land): + return [k for k in KRANKENKASSEN if k.get("land", "DE") == land] + + +def get_by_name(name): + name_l = name.lower() + for k in KRANKENKASSEN: + if k["name"].lower() == name_l or k["kurzname"].lower() == name_l: + return k + return None + + +def search(q, land=None): + q = q.lower() + results = [k for k in KRANKENKASSEN + if q in k["name"].lower() + or q in (k["kurzname"] or "").lower() + or q in k["ort"].lower()] + if land: + results = [k for k in results if k.get("land", "DE") == land] + return results diff --git a/pflegepilot_v3/backend/app/static_data/pflegegrad_regeln.py b/pflegepilot_v3/backend/app/static_data/pflegegrad_regeln.py new file mode 100644 index 0000000..70cfde8 --- /dev/null +++ b/pflegepilot_v3/backend/app/static_data/pflegegrad_regeln.py @@ -0,0 +1,99 @@ +""" +Regelbasierte (NICHT KI-gestuetzte) Pflegegrad-Einschaetzung fuer den +Interview-Assistenten. 1:1 aus der alten App (NEU_FRAGEN/NEU_ZG_OPTIONEN/ +neuSchaetzung() in templates/index.html) portiert - dort lag die Logik im +Frontend-JS, hier lebt sie zentral im Backend, damit Frontend und +Schaetzungs-Endpunkt nie auseinanderlaufen koennen. + +Die Schaetzung ist bewusst grob (Summe aus 5 Fragen zu je 0-3 Punkten) und +ersetzt keine Begutachtung durch den Medizinischen Dienst - das muss die +UI immer deutlich als unverbindlichen Vorschlag kennzeichnen. +""" + +FRAGEN = [ + { + "key": "mobilitaet", + "frage": "Wie bewegt sich die Person im Alltag?", + "optionen": [ + {"label": "Ohne Einschränkungen unterwegs", "wert": 0}, + {"label": "Nutzt Hilfsmittel (Stock, Rollator) und kommt allein zurecht", "wert": 1}, + {"label": "Braucht regelmäßig Unterstützung beim Gehen oder Aufstehen", "wert": 2}, + {"label": "Ist bettlägerig oder braucht beim Bewegen fremde Hilfe", "wert": 3}, + ], + }, + { + "key": "selbstversorgung", + "frage": "Wie viel Hilfe braucht die Person beim Waschen, Anziehen und Essen?", + "optionen": [ + {"label": "Keine Hilfe nötig", "wert": 0}, + {"label": "Braucht bei einzelnen Schritten Hilfe", "wert": 1}, + {"label": "Braucht bei den meisten Schritten Hilfe", "wert": 2}, + {"label": "Braucht bei fast allem vollständige Hilfe", "wert": 3}, + ], + }, + { + "key": "kognition", + "frage": "Gibt es Gedächtnisprobleme, Verwirrtheit oder auffälliges Verhalten (z. B. bei Demenz)?", + "optionen": [ + {"label": "Nein", "wert": 0}, + {"label": "Leicht, kommt selten vor", "wert": 1}, + {"label": "Deutlich spürbar, kommt öfter vor", "wert": 2}, + {"label": "Stark ausgeprägt, fast durchgehend", "wert": 3}, + ], + }, + { + "key": "medizin", + "frage": "Sind regelmäßig medizinische Maßnahmen nötig (Medikamente, Verbände, Spritzen, Wundversorgung)?", + "optionen": [ + {"label": "Nein", "wert": 0}, + {"label": "Gelegentlich", "wert": 1}, + {"label": "Täglich", "wert": 2}, + {"label": "Mehrmals täglich oder aufwendig", "wert": 3}, + ], + }, + { + "key": "alltag", + "frage": "Kann die Person ihren Tag noch selbst gestalten (Termine, Kontakte, Beschäftigung)?", + "optionen": [ + {"label": "Ja, ganz selbstständig", "wert": 0}, + {"label": "Braucht ab und zu Anleitung", "wert": 1}, + {"label": "Braucht viel Anleitung oder Begleitung", "wert": 2}, + {"label": "Ist dabei komplett auf Hilfe angewiesen", "wert": 3}, + ], + }, +] + +ZIELGRUPPEN_OPTIONEN = [ + { + "label": "Altersbedingte Unterstützung", + "zg": "alterspflege", + "hinweis": "z. B. altersbedingter Kraft- oder Gedächtnisverlust", + }, + { + "label": "Behinderung (angeboren oder erworben)", + "zg": "behinderte", + "hinweis": "z. B. körperliche, geistige oder seelische Behinderung", + }, + { + "label": "Andere Bedürftigkeit oder soziale Notlage", + "zg": "beduerftige", + "hinweis": "z. B. vorübergehende Notlage ohne dauerhafte Behinderung", + }, +] + + +def schaetzen(antworten: dict[str, int]) -> dict: + summe = sum(antworten.get(f["key"], 0) for f in FRAGEN) + if summe <= 1: + von, bis = 0, 1 + elif summe <= 4: + von, bis = 1, 2 + elif summe <= 7: + von, bis = 2, 3 + elif summe <= 10: + von, bis = 3, 4 + elif summe <= 13: + von, bis = 4, 4 + else: + von, bis = 5, 5 + return {"summe": summe, "von": von, "bis": bis, "vorschlag": round((von + bis) / 2)} diff --git a/pflegepilot_v3/frontend/src/app/(app)/patienten/neu/page.tsx b/pflegepilot_v3/frontend/src/app/(app)/patienten/neu/page.tsx index bb442af..e7f44a0 100644 --- a/pflegepilot_v3/frontend/src/app/(app)/patienten/neu/page.tsx +++ b/pflegepilot_v3/frontend/src/app/(app)/patienten/neu/page.tsx @@ -1,11 +1,118 @@ "use client"; -import { useState } from "react"; +import { useEffect, useReducer, useState } from "react"; import { useRouter } from "next/navigation"; import { usePatientMutations } from "@/lib/patienten"; +import { + usePflegegradFragen, + useSchaetzungMutation, + useKrankenkassenSuche, +} from "@/lib/pflegegrad"; import { DateWheelPicker } from "@/components/DateWheelPicker"; -const vorEinemJahrhundert = () => { +const STEP_TITEL = [ + "Persönliche Daten", + "Kontakt & Adresse", + "Krankenversicherung", + "Pflegesituation", + "Einschätzung & Pflegeperson", + "Übersicht", +]; + +type WizardData = { + vorname: string; + nachname: string; + geburtsdatum: string | null; + strasse: string; + plz: string; + ort: string; + telefon: string; + email: string; + krankenkasse: string; + versichertennummer: string; + versicherungsart: string; + pflegeperson: string; + notizen: string; + pflegegrad: number; +}; + +type WizardState = { + step: number; + q4sub: number; + data: WizardData; + antworten: Record; + zielgruppe: string; +}; + +const initialState: WizardState = { + step: 1, + q4sub: 0, + data: { + vorname: "", + nachname: "", + geburtsdatum: null, + strasse: "", + plz: "", + ort: "", + telefon: "", + email: "", + krankenkasse: "", + versichertennummer: "", + versicherungsart: "GKV", + pflegeperson: "", + notizen: "", + pflegegrad: 0, + }, + antworten: {}, + zielgruppe: "alterspflege", +}; + +type Action = + | { type: "SET_FIELD"; field: keyof WizardData; value: string | number | null } + | { type: "SET_ZIELGRUPPE"; zg: string; total: number } + | { type: "SET_ANTWORT"; key: string; wert: number; total: number } + | { type: "NEXT"; total: number } + | { type: "BACK"; total: number }; + +function reducer(state: WizardState, action: Action): WizardState { + switch (action.type) { + case "SET_FIELD": + return { ...state, data: { ...state.data, [action.field]: action.value } }; + case "SET_ZIELGRUPPE": { + const q4sub = state.q4sub < action.total - 1 ? state.q4sub + 1 : state.q4sub; + return { ...state, zielgruppe: action.zg, q4sub }; + } + case "SET_ANTWORT": { + const antworten = { ...state.antworten, [action.key]: action.wert }; + const q4sub = state.q4sub < action.total - 1 ? state.q4sub + 1 : state.q4sub; + return { ...state, antworten, q4sub }; + } + case "NEXT": { + if (state.step === 4 && state.q4sub < action.total - 1) { + return { ...state, q4sub: state.q4sub + 1 }; + } + if (state.step < STEP_TITEL.length) { + const step = state.step + 1; + return { ...state, step, q4sub: step === 4 ? 0 : state.q4sub }; + } + return state; + } + case "BACK": { + if (state.step === 4 && state.q4sub > 0) { + return { ...state, q4sub: state.q4sub - 1 }; + } + if (state.step > 1) { + const step = state.step - 1; + return { ...state, step, q4sub: step === 4 ? action.total - 1 : state.q4sub }; + } + return state; + } + default: + return state; + } +} + +const vor75Jahren = () => { const d = new Date(); d.setFullYear(d.getFullYear() - 75); return d; @@ -14,87 +121,469 @@ const vorEinemJahrhundert = () => { export default function NeuerPatientPage() { const router = useRouter(); const { anlegen } = usePatientMutations(); - const [form, setForm] = useState({ - vorname: "", - nachname: "", - geburtsdatum: null as string | null, - zielgruppe: "alterspflege", - pflegegrad: 0, - }); + const { data: fragenData, isLoading: fragenLoading } = usePflegegradFragen(); + const schaetzung = useSchaetzungMutation(); - const submit = async (e: React.FormEvent) => { - e.preventDefault(); - const patient = await anlegen.mutateAsync(form); + const [state, dispatch] = useReducer(reducer, initialState); + const total = 1 + (fragenData?.fragen.length ?? 5); + + const [error, setError] = useState(null); + + const next = () => { + if (state.step === 1 && (!state.data.vorname.trim() || !state.data.nachname.trim())) { + setError("Bitte Vor- und Nachname eintragen"); + return; + } + setError(null); + dispatch({ type: "NEXT", total }); + }; + const back = () => dispatch({ type: "BACK", total }); + + useEffect(() => { + if (state.step === 5 && Object.keys(state.antworten).length > 0) { + schaetzung.mutate(state.antworten); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.step]); + + useEffect(() => { + if (schaetzung.data && state.data.pflegegrad === 0) { + dispatch({ type: "SET_FIELD", field: "pflegegrad", value: schaetzung.data.vorschlag }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [schaetzung.data]); + + const speichern = async () => { + const patient = await anlegen.mutateAsync({ + vorname: state.data.vorname, + nachname: state.data.nachname, + geburtsdatum: state.data.geburtsdatum ?? undefined, + strasse: state.data.strasse || undefined, + plz: state.data.plz || undefined, + ort: state.data.ort || undefined, + telefon: state.data.telefon || undefined, + email: state.data.email || undefined, + krankenkasse: state.data.krankenkasse || undefined, + versichertennummer: state.data.versichertennummer || undefined, + versicherungsart: state.data.versicherungsart, + pflegeperson: state.data.pflegeperson || undefined, + notizen: state.data.notizen || undefined, + pflegegrad: state.data.pflegegrad, + zielgruppe: state.zielgruppe, + }); router.push(`/patienten/${patient.id}`); }; return ( -
-

Neuer Patient

+
+

Neuer Patient

- Kurzformular für Phase 1 – der ausführliche Interview-Assistent folgt in Phase 2. + Ein kurzes Interview – ohne KI, nur einfache Fragen zur Pflegesituation.

-
- setForm((f) => ({ ...f, vorname: e.target.value }))} - className="rounded-lg border border-slate-300 px-3 py-2 text-sm" - /> - setForm((f) => ({ ...f, nachname: e.target.value }))} - className="rounded-lg border border-slate-300 px-3 py-2 text-sm" - /> + - - setForm((f) => ({ ...f, geburtsdatum: iso }))} - fallbackDate={vorEinemJahrhundert()} - label="Geburtsdatum" - /> - - - - - - - - {anlegen.isError && ( -

{(anlegen.error as Error).message}

+ {state.step === 5 && ( + )} + {state.step === 6 && } - + {state.step < 6 ? ( + + ) : ( + + )} +
+
+ + ); +} + +function StepDots({ current }: { current: number }) { + return ( +
+ {STEP_TITEL.map((t, i) => { + const n = i + 1; + const done = n < current; + const on = n === current; + return ( +
+
+ {done ? "✓" : n} +
+ {n < STEP_TITEL.length &&
} +
+ ); + })} +
+ ); +} + +function Field({ + label, + value, + onChange, + required, +}: { + label: string; + value: string; + onChange: (v: string) => void; + required?: boolean; +}) { + return ( +
+ + onChange(e.target.value)} + className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" + /> +
+ ); +} + +function Step1({ + state, + dispatch, +}: { + state: WizardState; + dispatch: React.Dispatch; +}) { + return ( +
+

+ Wie heißt die Person, die Sie anmelden möchten? +

+ dispatch({ type: "SET_FIELD", field: "vorname", value: v })} + /> + dispatch({ type: "SET_FIELD", field: "nachname", value: v })} + /> +
+ + dispatch({ type: "SET_FIELD", field: "geburtsdatum", value: iso })} + fallbackDate={vor75Jahren()} + label="Geburtsdatum" + /> +
+
+ ); +} + +function Step2({ state, dispatch }: { state: WizardState; dispatch: React.Dispatch }) { + return ( +
+ dispatch({ type: "SET_FIELD", field: "strasse", value: v })} + /> +
+
+ dispatch({ type: "SET_FIELD", field: "plz", value: v })} + /> +
+
+ dispatch({ type: "SET_FIELD", field: "ort", value: v })} + /> +
+
+ dispatch({ type: "SET_FIELD", field: "telefon", value: v })} + /> + dispatch({ type: "SET_FIELD", field: "email", value: v })} + /> +
+ ); +} + +function Step3({ state, dispatch }: { state: WizardState; dispatch: React.Dispatch }) { + const [suchtext, setSuchtext] = useState(state.data.krankenkasse); + const { data: treffer } = useKrankenkassenSuche(suchtext); + + return ( +
+
+ + { + setSuchtext(e.target.value); + dispatch({ type: "SET_FIELD", field: "krankenkasse", value: e.target.value }); + }} + placeholder="Name eingeben…" + className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" + /> + {treffer && treffer.length > 0 && suchtext.length >= 2 && ( +
    + {treffer.slice(0, 8).map((k) => ( +
  • + +
  • + ))} +
+ )} +
+ dispatch({ type: "SET_FIELD", field: "versichertennummer", value: v })} + /> +
+ + +
+
+ ); +} + +function Step4({ + state, + dispatch, + fragenData, + total, +}: { + state: WizardState; + dispatch: React.Dispatch; + fragenData: { fragen: { key: string; frage: string; optionen: { label: string; wert: number }[] }[]; zielgruppen: { label: string; zg: string; hinweis: string }[] }; + total: number; +}) { + return ( +
+
+ {Array.from({ length: total }, (_, i) => ( +
+ ))} +
+ + {state.q4sub === 0 ? ( +
+

+ Worum geht es bei der Pflegesituation in erster Linie? +

+
+ {fragenData.zielgruppen.map((o) => ( + + ))} +
+
+ ) : ( + (() => { + const frage = fragenData.fragen[state.q4sub - 1]; + return ( +
+

{frage.frage}

+
+ {frage.optionen.map((o) => ( + + ))} +
+
+ ); + })() + )} +
+ ); +} + +function Step5({ + state, + dispatch, + schaetzung, +}: { + state: WizardState; + dispatch: React.Dispatch; + schaetzung?: { summe: number; von: number; bis: number; vorschlag: number }; +}) { + return ( +
+
+ {schaetzung ? ( + <> + Grober Anhaltspunkt: Pflegegrad {schaetzung.von}–{schaetzung.bis}. + Das ist keine offizielle Feststellung – nur der Medizinische Dienst + kann per Begutachtung einen verbindlichen Pflegegrad bescheinigen. + + ) : ( + "Schätzung wird berechnet…" + )} +
+ +
+ +
+ {[0, 1, 2, 3, 4, 5].map((g) => ( + + ))} +
+
+ + dispatch({ type: "SET_FIELD", field: "pflegeperson", value: v })} + /> + +
+ +