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 <noreply@anthropic.com>
This commit is contained in:
1 parent
899fa41b77
commit
9289dc1957
8 files changed
+1194
-71
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)}
|
||||
@@ -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<string, number>;
|
||||
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<string | null>(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 (
|
||||
<div className="mx-auto max-w-md p-6">
|
||||
<h1 className="mb-4 text-2xl font-semibold text-slate-800">Neuer Patient</h1>
|
||||
<div className="mx-auto max-w-lg p-6">
|
||||
<h1 className="mb-1 text-2xl font-semibold text-slate-800">Neuer Patient</h1>
|
||||
<p className="mb-4 text-sm text-slate-500">
|
||||
Kurzformular für Phase 1 – der ausführliche Interview-Assistent folgt in Phase 2.
|
||||
Ein kurzes Interview – ohne KI, nur einfache Fragen zur Pflegesituation.
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} className="flex flex-col gap-3">
|
||||
<input
|
||||
required
|
||||
placeholder="Vorname"
|
||||
value={form.vorname}
|
||||
onChange={(e) => setForm((f) => ({ ...f, vorname: e.target.value }))}
|
||||
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
<input
|
||||
required
|
||||
placeholder="Nachname"
|
||||
value={form.nachname}
|
||||
onChange={(e) => setForm((f) => ({ ...f, nachname: e.target.value }))}
|
||||
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
<StepDots current={state.step} />
|
||||
|
||||
<label className="text-xs font-medium text-slate-500">Geburtsdatum</label>
|
||||
<DateWheelPicker
|
||||
value={form.geburtsdatum}
|
||||
onChange={(iso) => setForm((f) => ({ ...f, geburtsdatum: iso }))}
|
||||
fallbackDate={vorEinemJahrhundert()}
|
||||
label="Geburtsdatum"
|
||||
/>
|
||||
|
||||
<label className="text-xs font-medium text-slate-500">Zielgruppe</label>
|
||||
<select
|
||||
value={form.zielgruppe}
|
||||
onChange={(e) => setForm((f) => ({ ...f, zielgruppe: e.target.value }))}
|
||||
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="alterspflege">Altersbedingt</option>
|
||||
<option value="beduerftige">Bedürftige</option>
|
||||
<option value="behinderte">Behinderung</option>
|
||||
</select>
|
||||
|
||||
<label className="text-xs font-medium text-slate-500">Pflegegrad</label>
|
||||
<select
|
||||
value={form.pflegegrad}
|
||||
onChange={(e) => setForm((f) => ({ ...f, pflegegrad: Number(e.target.value) }))}
|
||||
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
>
|
||||
{[0, 1, 2, 3, 4, 5].map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g === 0 ? "Noch kein Pflegegrad" : `Pflegegrad ${g}`}
|
||||
</option>
|
||||
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
|
||||
{state.step === 1 && <Step1 state={state} dispatch={dispatch} />}
|
||||
{state.step === 2 && <Step2 state={state} dispatch={dispatch} />}
|
||||
{state.step === 3 && <Step3 state={state} dispatch={dispatch} />}
|
||||
{state.step === 4 &&
|
||||
(fragenLoading || !fragenData ? (
|
||||
<p className="text-slate-400">Lädt…</p>
|
||||
) : (
|
||||
<Step4 state={state} dispatch={dispatch} fragenData={fragenData} total={total} />
|
||||
))}
|
||||
</select>
|
||||
|
||||
{anlegen.isError && (
|
||||
<p className="text-sm text-red-600">{(anlegen.error as Error).message}</p>
|
||||
{state.step === 5 && (
|
||||
<Step5 state={state} dispatch={dispatch} schaetzung={schaetzung.data} />
|
||||
)}
|
||||
{state.step === 6 && <Step6 state={state} />}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={anlegen.isPending}
|
||||
className="mt-2 rounded-lg bg-slate-800 px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={back}
|
||||
disabled={state.step === 1 && state.q4sub === 0}
|
||||
className="rounded-lg border border-slate-300 px-4 py-2 text-sm text-slate-600 disabled:opacity-0"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
{state.step < 6 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={next}
|
||||
className="rounded-lg bg-slate-800 px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={speichern}
|
||||
disabled={anlegen.isPending}
|
||||
className="rounded-lg bg-slate-800 px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
|
||||
>
|
||||
Patient speichern
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepDots({ current }: { current: number }) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{STEP_TITEL.map((t, i) => {
|
||||
const n = i + 1;
|
||||
const done = n < current;
|
||||
const on = n === current;
|
||||
return (
|
||||
<div key={t} className="flex flex-1 items-center last:flex-none">
|
||||
<div
|
||||
title={t}
|
||||
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-medium ${
|
||||
on
|
||||
? "bg-slate-800 text-white"
|
||||
: done
|
||||
? "bg-emerald-500 text-white"
|
||||
: "border border-slate-300 text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{done ? "✓" : n}
|
||||
</div>
|
||||
{n < STEP_TITEL.length && <div className="mx-1 h-px flex-1 bg-slate-200" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
required,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">
|
||||
{label}
|
||||
{required && " *"}
|
||||
</label>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step1({
|
||||
state,
|
||||
dispatch,
|
||||
}: {
|
||||
state: WizardState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
Wie heißt die Person, die Sie anmelden möchten?
|
||||
</p>
|
||||
<Field
|
||||
label="Vorname"
|
||||
required
|
||||
value={state.data.vorname}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "vorname", value: v })}
|
||||
/>
|
||||
<Field
|
||||
label="Nachname"
|
||||
required
|
||||
value={state.data.nachname}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "nachname", value: v })}
|
||||
/>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">Geburtsdatum</label>
|
||||
<DateWheelPicker
|
||||
value={state.data.geburtsdatum}
|
||||
onChange={(iso) => dispatch({ type: "SET_FIELD", field: "geburtsdatum", value: iso })}
|
||||
fallbackDate={vor75Jahren()}
|
||||
label="Geburtsdatum"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step2({ state, dispatch }: { state: WizardState; dispatch: React.Dispatch<Action> }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Field
|
||||
label="Straße"
|
||||
value={state.data.strasse}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "strasse", value: v })}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-1/3">
|
||||
<Field
|
||||
label="PLZ"
|
||||
value={state.data.plz}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "plz", value: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Field
|
||||
label="Ort"
|
||||
value={state.data.ort}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "ort", value: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Field
|
||||
label="Telefon"
|
||||
value={state.data.telefon}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "telefon", value: v })}
|
||||
/>
|
||||
<Field
|
||||
label="E-Mail"
|
||||
value={state.data.email}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "email", value: v })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step3({ state, dispatch }: { state: WizardState; dispatch: React.Dispatch<Action> }) {
|
||||
const [suchtext, setSuchtext] = useState(state.data.krankenkasse);
|
||||
const { data: treffer } = useKrankenkassenSuche(suchtext);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="relative">
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">Krankenkasse</label>
|
||||
<input
|
||||
value={suchtext}
|
||||
onChange={(e) => {
|
||||
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 && (
|
||||
<ul className="absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-slate-200 bg-white shadow-lg">
|
||||
{treffer.slice(0, 8).map((k) => (
|
||||
<li key={k.name}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSuchtext(k.name);
|
||||
dispatch({ type: "SET_FIELD", field: "krankenkasse", value: k.name });
|
||||
}}
|
||||
className="block w-full px-3 py-2 text-left text-sm hover:bg-slate-50"
|
||||
>
|
||||
{k.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<Field
|
||||
label="Versichertennummer"
|
||||
value={state.data.versichertennummer}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "versichertennummer", value: v })}
|
||||
/>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">Versicherungsart</label>
|
||||
<select
|
||||
value={state.data.versicherungsart}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: "SET_FIELD", field: "versicherungsart", value: e.target.value })
|
||||
}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
>
|
||||
Patient anlegen
|
||||
</button>
|
||||
</form>
|
||||
<option value="GKV">Gesetzlich (GKV)</option>
|
||||
<option value="PKV">Privat (PKV)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step4({
|
||||
state,
|
||||
dispatch,
|
||||
fragenData,
|
||||
total,
|
||||
}: {
|
||||
state: WizardState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
fragenData: { fragen: { key: string; frage: string; optionen: { label: string; wert: number }[] }[]; zielgruppen: { label: string; zg: string; hinweis: string }[] };
|
||||
total: number;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex gap-1">
|
||||
{Array.from({ length: total }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-1 flex-1 rounded-full ${i <= state.q4sub ? "bg-slate-800" : "bg-slate-200"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{state.q4sub === 0 ? (
|
||||
<div>
|
||||
<p className="mb-3 text-sm font-medium text-slate-700">
|
||||
Worum geht es bei der Pflegesituation in erster Linie?
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{fragenData.zielgruppen.map((o) => (
|
||||
<button
|
||||
key={o.zg}
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "SET_ZIELGRUPPE", zg: o.zg, total })}
|
||||
className={`rounded-lg border px-3 py-2 text-left text-sm ${
|
||||
state.zielgruppe === o.zg
|
||||
? "border-slate-800 bg-slate-50"
|
||||
: "border-slate-300"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-slate-800">{o.label}</div>
|
||||
<div className="text-xs text-slate-500">{o.hinweis}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const frage = fragenData.fragen[state.q4sub - 1];
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-3 text-sm font-medium text-slate-700">{frage.frage}</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{frage.optionen.map((o) => (
|
||||
<button
|
||||
key={o.wert}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({ type: "SET_ANTWORT", key: frage.key, wert: o.wert, total })
|
||||
}
|
||||
className={`rounded-lg border px-3 py-2 text-left text-sm ${
|
||||
state.antworten[frage.key] === o.wert
|
||||
? "border-slate-800 bg-slate-50"
|
||||
: "border-slate-300"
|
||||
}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step5({
|
||||
state,
|
||||
dispatch,
|
||||
schaetzung,
|
||||
}: {
|
||||
state: WizardState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
schaetzung?: { summe: number; von: number; bis: number; vorschlag: number };
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-sm text-amber-800">
|
||||
{schaetzung ? (
|
||||
<>
|
||||
Grober Anhaltspunkt: <strong>Pflegegrad {schaetzung.von}–{schaetzung.bis}</strong>.
|
||||
Das ist <strong>keine offizielle Feststellung</strong> – nur der Medizinische Dienst
|
||||
kann per Begutachtung einen verbindlichen Pflegegrad bescheinigen.
|
||||
</>
|
||||
) : (
|
||||
"Schätzung wird berechnet…"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">
|
||||
Tatsächlicher / vermuteter Pflegegrad
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[0, 1, 2, 3, 4, 5].map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "SET_FIELD", field: "pflegegrad", value: g })}
|
||||
className={`rounded-full px-3 py-1 text-sm ${
|
||||
state.data.pflegegrad === g
|
||||
? "bg-slate-800 text-white"
|
||||
: "border border-slate-300 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{g === 0 ? "keiner" : g}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label="Pflegeperson"
|
||||
value={state.data.pflegeperson}
|
||||
onChange={(v) => dispatch({ type: "SET_FIELD", field: "pflegeperson", value: v })}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-slate-500">
|
||||
Notizen / Diagnosen
|
||||
</label>
|
||||
<textarea
|
||||
value={state.data.notizen}
|
||||
onChange={(e) => dispatch({ type: "SET_FIELD", field: "notizen", value: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step6({ state }: { state: WizardState }) {
|
||||
const d = state.data;
|
||||
return (
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<p className="mb-2 font-medium text-slate-700">Bitte prüfen, bevor gespeichert wird:</p>
|
||||
<SummaryRow label="Name" value={`${d.vorname} ${d.nachname}`} />
|
||||
<SummaryRow label="Geburtsdatum" value={d.geburtsdatum ?? "–"} />
|
||||
<SummaryRow label="Adresse" value={[d.strasse, [d.plz, d.ort].filter(Boolean).join(" ")].filter(Boolean).join(", ") || "–"} />
|
||||
<SummaryRow label="Krankenkasse" value={d.krankenkasse || "–"} />
|
||||
<SummaryRow label="Pflegegrad" value={d.pflegegrad === 0 ? "keiner" : String(d.pflegegrad)} />
|
||||
<SummaryRow label="Pflegeperson" value={d.pflegeperson || "–"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex justify-between border-b border-slate-100 py-1.5">
|
||||
<span className="text-slate-500">{label}</span>
|
||||
<span className="font-medium text-slate-800">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { api } from "./api";
|
||||
|
||||
export type PflegegradOption = { label: string; wert: number };
|
||||
export type PflegegradFrage = { key: string; frage: string; optionen: PflegegradOption[] };
|
||||
export type ZielgruppenOption = { label: string; zg: string; hinweis: string };
|
||||
export type PflegegradFragenOut = { fragen: PflegegradFrage[]; zielgruppen: ZielgruppenOption[] };
|
||||
export type Schaetzung = { summe: number; von: number; bis: number; vorschlag: number };
|
||||
|
||||
export function usePflegegradFragen() {
|
||||
return useQuery<PflegegradFragenOut>({
|
||||
queryKey: ["pflegegrad-fragen"],
|
||||
queryFn: () => api<PflegegradFragenOut>("/api/pflegegrad/fragen"),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSchaetzungMutation() {
|
||||
return useMutation({
|
||||
mutationFn: (antworten: Record<string, number>) =>
|
||||
api<Schaetzung>("/api/pflegegrad/schaetzen", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ antworten }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type Krankenkasse = {
|
||||
name: string;
|
||||
kurzname: string;
|
||||
typ: string;
|
||||
land: string;
|
||||
strasse: string;
|
||||
plz: string;
|
||||
ort: string;
|
||||
};
|
||||
|
||||
export function useKrankenkassenSuche(q: string) {
|
||||
return useQuery<Krankenkasse[]>({
|
||||
queryKey: ["krankenkassen", q],
|
||||
queryFn: () => api<Krankenkasse[]>(`/api/krankenkassen?q=${encodeURIComponent(q)}`),
|
||||
enabled: q.length >= 2,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user