diff --git a/pflegepilot_v3/frontend/src/app/(app)/patienten/neu-senioren/page.tsx b/pflegepilot_v3/frontend/src/app/(app)/patienten/neu-senioren/page.tsx deleted file mode 100644 index ed6fbc0..0000000 --- a/pflegepilot_v3/frontend/src/app/(app)/patienten/neu-senioren/page.tsx +++ /dev/null @@ -1,1223 +0,0 @@ -"use client"; - -// TEST-VARIANTE (Design B) - grosse Schrift, hoher Kontrast, groessere -// Bedienflaechen fuer die aeltere Nutzergruppe. Bewusst eine komplette Kopie -// von (app)/patienten/neu/page.tsx statt einer gemeinsamen Abstraktion, damit -// die produktive Seite garantiert unangetastet bleibt - reine Testroute, -// absichtlich nirgends in der Navigation verlinkt (nur per direktem Link -// erreichbar: /patienten/neu-senioren). Gleiche Fach-/Speicherlogik wie das -// Original, nur die JSX-/Style-Ebene wurde angepasst. Skalierung ueber CSS -// `zoom` (nicht `transform:scale`) auf dem Aussen-Container, damit auch die -// importierten Unterkomponenten (DateInput, EgkScan, KrankenkasseAutocomplete, -// InfoTooltip) automatisch mitskalieren, ohne dass diese fuer beide Varianten -// dupliziert werden muessten - `zoom` veraendert (anders als `transform`) -// echte Layout-/Boxgroessen, dadurch kein Ueberlappen/Abschneiden. - -import { useEffect, useReducer, useState } from "react"; -import { useRouter } from "next/navigation"; -import { usePatientMutations } from "@/lib/patienten"; -import { useAuth } from "@/lib/auth"; -import { usePflegegradFragen, useSchaetzungMutation, useKrankenkasseLand } from "@/lib/pflegegrad"; -import { pruefeVersichertennummer } from "@/lib/versichertennummer"; -import { useKatalog, useRechnerMutation, type RechnerErgebnis } from "@/lib/katalog"; -import { LEISTUNGSERKLAERUNGEN } from "@/lib/leistungserklaerungen"; -import { api } from "@/lib/api"; -import { usePlzSync } from "@/lib/plz"; -import { DateInput } from "@/components/DateInput"; -import { KrankenkasseAutocomplete } from "@/components/KrankenkasseAutocomplete"; -import { InfoTooltip } from "@/components/InfoTooltip"; -import { EgkScan } from "@/components/EgkScan"; -import type { EgkScanErgebnis } from "@/lib/egk"; - -const STEP_TITEL = [ - "Persönliche Daten", - "Kontakt & Adresse", - "Krankenversicherung", - "Pflegesituation", - "Einschätzung & Pflegeperson", - "Leistungen", - "Übersicht", -]; - -// Groessere, kontrastreichere Bedienelemente fuer diese Design-Variante - -// bewusst lokal statt der geteilten buttonStyles aus lib/theme.ts, damit hier -// durchgaengig groessere Schrift/Touch-Flaechen gelten koennen, ohne die dort -// zentral genutzten (kleineren) Stile fuer die restliche App zu veraendern. -const seniorBtn = { - primary: - "inline-flex items-center justify-center gap-2 rounded-xl bg-slate-800 px-7 py-4 text-xl font-bold text-white transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50", - secondary: - "inline-flex items-center justify-center gap-2 rounded-xl border-2 border-slate-500 bg-white px-7 py-4 text-xl font-bold text-slate-800 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-0", - success: - "inline-flex items-center justify-center gap-2 rounded-xl border-2 border-emerald-400 bg-emerald-50 px-5 py-3 text-lg font-bold text-emerald-800 transition hover:bg-emerald-100", - small: - "inline-flex items-center justify-center gap-2 rounded-xl border-2 border-slate-400 bg-white px-4 py-2.5 text-base font-bold text-slate-700 transition hover:bg-slate-50", -}; - -type WizardData = { - vorname: string; - nachname: string; - geburtsdatum: string | null; - strasse: string; - plz: string; - ort: string; - telefon: string; - email: string; - krankenkasse: string; - versichertennummer: string; - rentenversicherungsnummer: string; - versicherungsart: string; - pflegeperson: string; - notizen: string; - pflegegrad: number; -}; - -type WizardState = { - step: number; - q4sub: number; - data: WizardData; - antworten: Record; - zielgruppe: string; - ausgewaehlteLeistungen: Set; -}; - -const initialState: WizardState = { - step: 1, - q4sub: 0, - ausgewaehlteLeistungen: new Set(), - data: { - vorname: "", - nachname: "", - geburtsdatum: null, - strasse: "", - plz: "", - ort: "", - telefon: "", - email: "", - krankenkasse: "", - versichertennummer: "", - rentenversicherungsnummer: "", - 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 } - | { type: "SET_ANTWORT"; key: string; wert: number } - | { type: "TOGGLE_LEISTUNG"; id: string } - | { type: "SET_LEISTUNGEN"; add: string[]; remove: string[] } - | { 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 "TOGGLE_LEISTUNG": { - const next = new Set(state.ausgewaehlteLeistungen); - next.has(action.id) ? next.delete(action.id) : next.add(action.id); - return { ...state, ausgewaehlteLeistungen: next }; - } - case "SET_LEISTUNGEN": { - const next = new Set(state.ausgewaehlteLeistungen); - action.remove.forEach((id) => next.delete(id)); - action.add.forEach((id) => next.add(id)); - return { ...state, ausgewaehlteLeistungen: next }; - } - case "SET_ZIELGRUPPE": - return { ...state, zielgruppe: action.zg }; - case "SET_ANTWORT": { - const antworten = { ...state.antworten, [action.key]: action.wert }; - return { ...state, antworten }; - } - case "NEXT": { - if (state.step === 4 && state.zielgruppe !== "beduerftige" && 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.zielgruppe !== "beduerftige" && state.q4sub > 0) { - return { ...state, q4sub: state.q4sub - 1 }; - } - if (state.step > 1) { - const step = state.step - 1; - return { - ...state, - step, - q4sub: step === 4 ? (state.zielgruppe === "beduerftige" ? 0 : action.total - 1) : state.q4sub, - }; - } - return state; - } - default: - return state; - } -} - -export default function NeuerPatientSeniorenPage() { - const router = useRouter(); - const { anlegen } = usePatientMutations(); - const { meQuery } = useAuth(); - const { data: fragenData, isLoading: fragenLoading } = usePflegegradFragen(); - const schaetzung = useSchaetzungMutation(); - const rechner = useRechnerMutation(); - - const [state, dispatch] = useReducer(reducer, initialState); - const total = 1 + (fragenData?.fragen.length ?? 5); - - const [error, setError] = useState(null); - const [berechtigungBestaetigt, setBerechtigungBestaetigt] = useState(false); - const [versichertennummerBeruehrt, setVersichertennummerBeruehrt] = useState(false); - const [hatEigeneEmail, setHatEigeneEmail] = useState(true); - const kontoEmail = meQuery.data?.email ?? null; - - const eigeneEmailUmschalten = (checked: boolean) => { - setHatEigeneEmail(checked); - if (checked) { - dispatch({ type: "SET_FIELD", field: "email", value: "" }); - } else if (kontoEmail) { - dispatch({ type: "SET_FIELD", field: "email", value: kontoEmail }); - } - }; - - const [adresseWieKonto, setAdresseWieKonto] = useState(false); - const me = meQuery.data; - const kontoAdresse = - me?.rg_strasse && me?.rg_plz && me?.rg_ort - ? { strasse: me.rg_strasse, plz: me.rg_plz, ort: me.rg_ort } - : null; - - const adresseWieKontoUmschalten = (checked: boolean) => { - setAdresseWieKonto(checked); - if (checked && kontoAdresse) { - dispatch({ type: "SET_FIELD", field: "strasse", value: kontoAdresse.strasse }); - dispatch({ type: "SET_FIELD", field: "plz", value: kontoAdresse.plz }); - dispatch({ type: "SET_FIELD", field: "ort", value: kontoAdresse.ort }); - } else if (!checked) { - dispatch({ type: "SET_FIELD", field: "strasse", value: "" }); - dispatch({ type: "SET_FIELD", field: "plz", value: "" }); - dispatch({ type: "SET_FIELD", field: "ort", value: "" }); - } - }; - - const krankenkasseLand = useKrankenkasseLand(state.data.krankenkasse); - const versichertennummerFehler = pruefeVersichertennummer( - state.data.versichertennummer, - krankenkasseLand, - state.data.versicherungsart - ); - - const next = () => { - if (state.step === 1 && (!state.data.vorname.trim() || !state.data.nachname.trim())) { - setError("Bitte Vor- und Nachname eintragen"); - return; - } - if (state.step === 3 && versichertennummerFehler) { - setVersichertennummerBeruehrt(true); - 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 (state.step === 7) { - rechner.mutate({ pflegegrad: state.data.pflegegrad, zielgruppe: state.zielgruppe }); - } - // 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 () => { - setError(null); - try { - await speichernIntern(); - } catch (e) { - setError(e instanceof Error ? e.message : "Speichern fehlgeschlagen"); - } - }; - - const speichernIntern = 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, - rentenversicherungsnummer: state.data.rentenversicherungsnummer || undefined, - versicherungsart: state.data.versicherungsart, - pflegeperson: state.data.pflegeperson || undefined, - notizen: state.data.notizen || undefined, - pflegegrad: state.data.pflegegrad, - zielgruppe: state.zielgruppe, - berechtigung_bestaetigt: berechtigungBestaetigt, - }); - - let leistungenFehler = false; - if (state.ausgewaehlteLeistungen.size > 0) { - try { - await api(`/api/patienten/${patient.id}/leistungen/katalog`, { - method: "POST", - body: JSON.stringify({ katalog_ids: [...state.ausgewaehlteLeistungen] }), - }); - } catch { - leistungenFehler = true; - } - } - router.push(`/patienten/${patient.id}${leistungenFehler ? "?leistungen_fehler=1" : ""}`); - }; - - return ( -
-
- Testversion „Große Schrift“ – nur zum internen Ausprobieren, nicht verlinkt. -
- - {/* zoom statt transform:scale, damit importierte Unterkomponenten - (DateInput, EgkScan, KrankenkasseAutocomplete, InfoTooltip) ohne - eigene Kopie automatisch mitskalieren und die Boxgroesse im Layout - korrekt mitwaechst (kein Ueberlappen wie bei transform:scale). */} -
-

Neuer Patient

-

- Ein kurzes Interview – ohne KI, nur einfache Fragen zur Pflegesituation. -

- - - -
- {state.step === 1 && } - {state.step === 2 && ( - - )} - {state.step === 3 && ( - setVersichertennummerBeruehrt(true)} - /> - )} - {state.step === 4 && - (fragenLoading || !fragenData ? ( -

Lädt…

- ) : ( - - ))} - {state.step === 5 && ( - - )} - {state.step === 6 && } - {state.step === 7 && ( - - )} - - {error &&

{error}

} - -
- - {state.step < 7 ? ( - - ) : ( - - )} -
-
-
-
- ); -} - -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 &&
} -
- ); - })} -
- {/* Aktueller Schrittname sichtbar als Text statt nur als Hover-Tooltip - (title-Attribut) - fuer Touch-Bedienung/aeltere Nutzer nicht - verlaesslich erreichbar. */} -

- Schritt {current} von {STEP_TITEL.length}: {STEP_TITEL[current - 1]} -

-
- ); -} - -function Field({ - label, - value, - onChange, - required, - maxLength, - name, - autoComplete = "off", -}: { - label: string; - value: string; - onChange: (v: string) => void; - required?: boolean; - maxLength?: number; - name?: string; - autoComplete?: string; -}) { - return ( -
- - onChange(e.target.value)} - maxLength={maxLength} - className="w-full rounded-xl border-2 border-slate-400 px-4 py-3 text-lg text-slate-900" - /> -
- ); -} - -function Step1({ - state, - dispatch, -}: { - state: WizardState; - dispatch: React.Dispatch; -}) { - const egkUebernehmen = (ergebnis: EgkScanErgebnis) => { - if (ergebnis.vorname) dispatch({ type: "SET_FIELD", field: "vorname", value: ergebnis.vorname }); - if (ergebnis.nachname) dispatch({ type: "SET_FIELD", field: "nachname", value: ergebnis.nachname }); - if (ergebnis.geburtsdatum) - dispatch({ type: "SET_FIELD", field: "geburtsdatum", value: ergebnis.geburtsdatum }); - if (ergebnis.krankenkasse) - dispatch({ type: "SET_FIELD", field: "krankenkasse", value: ergebnis.krankenkasse }); - if (ergebnis.versichertennummer) - dispatch({ - type: "SET_FIELD", - field: "versichertennummer", - value: ergebnis.versichertennummer, - }); - }; - - 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 })} - label="Geburtsdatum" - /> -
- ); -} - -function Step2({ - state, - dispatch, - hatEigeneEmail, - onHatEigeneEmailChange, - kontoEmail, - adresseWieKonto, - onAdresseWieKontoChange, - kontoAdresse, -}: { - state: WizardState; - dispatch: React.Dispatch; - hatEigeneEmail: boolean; - onHatEigeneEmailChange: (checked: boolean) => void; - kontoEmail: string | null; - adresseWieKonto: boolean; - onAdresseWieKontoChange: (checked: boolean) => void; - kontoAdresse: { strasse: string; plz: string; ort: string } | null; -}) { - usePlzSync( - state.data.plz, - (v) => dispatch({ type: "SET_FIELD", field: "ort", value: v }), - !adresseWieKonto - ); - - return ( -
- {kontoAdresse && ( - - )} - - {adresseWieKonto && kontoAdresse ? ( -
- Es wird Ihre eigene Adresse verwendet:{" "} - - {kontoAdresse.strasse}, {kontoAdresse.plz} {kontoAdresse.ort} - -
- ) : ( - <> - dispatch({ type: "SET_FIELD", field: "strasse", value: v })} - /> -
-
- dispatch({ type: "SET_FIELD", field: "plz", value: v })} - maxLength={5} - /> -
-
- dispatch({ type: "SET_FIELD", field: "ort", value: v })} - /> -
-
- - )} - dispatch({ type: "SET_FIELD", field: "telefon", value: v })} - /> - - - - {!hatEigeneEmail && kontoEmail && ( -
- Es wird stattdessen Ihre eigene E-Mail-Adresse verwendet:{" "} - {kontoEmail} -
- )} - - {(hatEigeneEmail || !kontoEmail) && ( - dispatch({ type: "SET_FIELD", field: "email", value: v })} - /> - )} - - {!hatEigeneEmail && !kontoEmail && ( -

- Für Ihr Konto ist keine E-Mail-Adresse hinterlegt – bitte hier eine E-Mail-Adresse - eintragen, unter der der Patient bzw. Sie erreichbar sind. -

- )} -
- ); -} - -function Step3({ - state, - dispatch, - fehler, - onVersichertennummerBlur, -}: { - state: WizardState; - dispatch: React.Dispatch; - fehler: string | null; - onVersichertennummerBlur: () => void; -}) { - return ( -
- dispatch({ type: "SET_FIELD", field: "krankenkasse", value: v })} - /> -
- - - dispatch({ type: "SET_FIELD", field: "versichertennummer", value: e.target.value }) - } - onBlur={onVersichertennummerBlur} - className={`w-full rounded-xl border-2 px-4 py-3 text-lg text-slate-900 ${ - fehler ? "border-red-500" : "border-slate-400" - }`} - /> - {fehler ? ( -

{fehler}

- ) : ( -

- Format hängt von der Krankenkasse ab (DE z. B. A123456789). -

- )} -
- dispatch({ type: "SET_FIELD", field: "rentenversicherungsnummer", value: v })} - /> -
- - -
-
- ); -} - -function TeilschritteAuswahl({ - frage, - antwortWert, - onChange, -}: { - frage: { key: string; frage: string; teilschritte: string[] }; - antwortWert: number | undefined; - onChange: (wert: number) => void; -}) { - const [checked, setChecked] = useState>(() => { - if (antwortWert === undefined) return new Set(); - const anzahlAngekreuzt = frage.teilschritte.length - antwortWert; - return new Set(Array.from({ length: anzahlAngekreuzt }, (_, i) => i)); - }); - - const toggle = (i: number) => { - const next = new Set(checked); - if (next.has(i)) next.delete(i); - else next.add(i); - setChecked(next); - onChange(frage.teilschritte.length - next.size); - }; - - return ( -
-

{frage.frage}

-

Was kann die Person noch selbstständig?

-
- {frage.teilschritte.map((t, i) => ( - - ))} -
-
- ); -} - -function Step4({ - state, - dispatch, - fragenData, - total, -}: { - state: WizardState; - dispatch: React.Dispatch; - fragenData: { fragen: { key: string; frage: string; teilschritte: string[] }[]; zielgruppen: { label: string; zg: string; hinweis: string }[] }; - total: number; -}) { - return ( -
- {state.zielgruppe !== "beduerftige" && ( -
- {Array.from({ length: total }, (_, i) => ( -
- ))} -
- )} - - {state.q4sub === 0 ? ( -
-

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

-
- {fragenData.zielgruppen.map((o) => ( - - ))} -
- {state.zielgruppe === "beduerftige" && ( -

- Für diese Auswahl ist keine Pflegegrad-Einschätzung vorgesehen – „Weiter“ - führt direkt zum nächsten Schritt. -

- )} -
- ) : ( - (() => { - const frage = fragenData.fragen[state.q4sub - 1]; - return ( - dispatch({ type: "SET_ANTWORT", key: frage.key, wert })} - /> - ); - })() - )} -
- ); -} - -function Step5({ - state, - dispatch, - schaetzung, -}: { - state: WizardState; - dispatch: React.Dispatch; - schaetzung?: { summe: number; von: number; bis: number; vorschlag: number }; -}) { - return ( -
-
- {state.zielgruppe === "beduerftige" ? ( - <> - Für diese Zielgruppe ist in der Regel keine Pflegegrad-Einschätzung{" "} - nach SGB XI vorgesehen – hier geht es meist um Grundsicherung oder andere - Sozialleistungen. Falls trotzdem bereits ein Pflegegrad festgestellt wurde, kann er - unten eingetragen werden. - - ) : 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 })} - /> - -
- -