pflegepilot_v3: Antragsstatus-Tracking + automatischer Widerspruchs-Vorschlag (T25)
Periodische Magic-Link-Erinnerungsmail ("ist der Bescheid da?"), bei
Ablehnung eines Pflegegrad-Antrags eine "Keine Sorge"-Seite mit
eingebettetem Mini-Widerspruch-Generator, sowie eine Fristeskalationsmail
kurz vor Ablauf der Widerspruchsfrist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
552570e696
commit
55806bb962
6 files changed
+486
No files matched your search
@@ -0,0 +1,44 @@
|
||||
"""Magic-Link-Token fuer die Antragsstatus-Erinnerungsmail (T25) - gleiches
|
||||
Muster wie der Passwort-Reset-Token in routers/auth.py: ein Klartext-Token
|
||||
geht nur in den E-Mail-Link, persistiert wird ausschliesslich der SHA-256-
|
||||
Hash. Ein Token deckt alle drei Ergebnis-Links einer Mail ab (bewilligt/
|
||||
abgelehnt/offen kommt als eigener Query-Parameter dazu) - Einmalgebrauch:
|
||||
sobald ein Link angeklickt wurde, sind die anderen beiden ungueltig.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Antrag, AntragAktionsToken
|
||||
|
||||
TOKEN_GUELTIGKEIT = timedelta(days=14)
|
||||
|
||||
|
||||
def erstelle_token(db: Session, antrag: Antrag) -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
eintrag = AntragAktionsToken(
|
||||
antrag_id=antrag.id,
|
||||
token_hash=hashlib.sha256(token.encode()).hexdigest(),
|
||||
laeuft_ab=datetime.now(timezone.utc) + TOKEN_GUELTIGKEIT,
|
||||
)
|
||||
db.add(eintrag)
|
||||
db.flush()
|
||||
return token
|
||||
|
||||
|
||||
def token_einloesen(db: Session, token: str) -> Antrag | None:
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
eintrag = (
|
||||
db.query(AntragAktionsToken).filter(AntragAktionsToken.token_hash == token_hash).first()
|
||||
)
|
||||
if (
|
||||
eintrag is None
|
||||
or eintrag.eingeloest_am is not None
|
||||
or eintrag.laeuft_ab < datetime.now(timezone.utc)
|
||||
):
|
||||
return None
|
||||
eintrag.eingeloest_am = datetime.now(timezone.utc)
|
||||
return eintrag.antrag
|
||||
@@ -86,6 +86,7 @@ app.include_router(stammdaten.router)
|
||||
app.include_router(stammdaten.rv_router)
|
||||
app.include_router(katalog.router)
|
||||
app.include_router(antraege.router)
|
||||
app.include_router(antraege.router_public)
|
||||
app.include_router(briefe.router)
|
||||
app.include_router(admin.router)
|
||||
app.include_router(abo.router)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Antragsstatus-Tracking (T25): je Antragstyp unterschiedliches Intervall,
|
||||
nach dem erstmalig per E-Mail nachgefragt wird, ob inzwischen ein Bescheid
|
||||
eingetroffen ist (siehe app/reminder_scheduler.py). Nur Typen, bei denen
|
||||
tatsaechlich ein Bescheid einer Stelle erwartet wird - adressaenderung/
|
||||
vollmacht/patientenverfuegung sind bewusst nicht enthalten (kein Tracking).
|
||||
|
||||
erstantrag/hoeherst orientieren sich an der 25-Arbeitstage-Regelfrist nach
|
||||
§ 18 Abs. 3 SGB XI (plus Puffer fuer Postlaufzeit/Wochenenden), die anderen
|
||||
Werte sind pragmatische Schaetzwerte ohne feste gesetzliche Bearbeitungsfrist.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
ANTRAG_ERINNERUNGS_TAGE: dict[str, int] = {
|
||||
"erstantrag": 35,
|
||||
"hoeherst": 35,
|
||||
"widerspruch": 56,
|
||||
"eingliederung": 42,
|
||||
"grundsicherung": 42,
|
||||
"blindenhilfe": 42,
|
||||
}
|
||||
|
||||
# Wiederholungsintervall, falls nach der ersten Erinnerung immer noch keine
|
||||
# Antwort eingetragen wurde, sowie ein Cutoff (ab wann die Serie ganz
|
||||
# abbricht, um niemanden endlos zu behelligen).
|
||||
ERINNERUNG_WIEDERHOLUNG_TAGE = 21
|
||||
ERINNERUNG_CUTOFF_TAGE = 180
|
||||
|
||||
# Freundliche deutsche Bezeichnung je Antragstyp fuer E-Mail-Texte (Pendant
|
||||
# zum ANTRAG_TYP_LABEL im Frontend, das Dashboard/Patientenakte fuer die
|
||||
# Anzeige nutzen - hier eine eigene, serverseitige Kopie fuer Mailtexte).
|
||||
ANTRAG_TYP_LABEL_DE: dict[str, str] = {
|
||||
"erstantrag": "Erstantrag Pflegegrad",
|
||||
"hoeherst": "Höherstufung Pflegegrad",
|
||||
"widerspruch": "Widerspruch",
|
||||
"adressaenderung": "Adressänderung",
|
||||
"vollmacht": "Vollmacht",
|
||||
"patientenverfuegung": "Patientenverfügung",
|
||||
"eingliederung": "Eingliederungshilfe",
|
||||
"grundsicherung": "Grundsicherung",
|
||||
"blindenhilfe": "Blindenhilfe",
|
||||
}
|
||||
|
||||
|
||||
def berechne_naechste_erinnerung(
|
||||
typ: str, bezugsdatum: datetime.date | None = None
|
||||
) -> datetime.date | None:
|
||||
"""Faelligkeitsdatum der ersten "ist der Bescheid da?"-Mail fuer einen
|
||||
neu angelegten Antrag - None fuer Typen ohne Tracking (siehe oben).
|
||||
bezugsdatum ist das Antragsdatum, falls bekannt (z.B. beim manuellen
|
||||
Nachtragen eines extern gestellten Antrags); ohne Angabe wird das
|
||||
Erstellungsdatum in der App als Basis genommen."""
|
||||
tage = ANTRAG_ERINNERUNGS_TAGE.get(typ)
|
||||
if tage is None:
|
||||
return None
|
||||
basis = bezugsdatum or datetime.date.today()
|
||||
return basis + datetime.timedelta(days=tage)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""antragsstatus-tracking: bewilligter betrag, aktenzeichen, erinnerungen
|
||||
|
||||
Revision ID: a1c4e7f9b3d6
|
||||
Revises: d7f1a3c5e8b2
|
||||
Create Date: 2026-07-31 00:00:00.000000
|
||||
|
||||
Tester-Feedback T25: periodische "ist der Bescheid da?"-Erinnerung per
|
||||
E-Mail (Magic-Link, Einmal-Token), Erfassung des bewilligten Betrags, sowie
|
||||
eine Eskalationserinnerung kurz vor Ablauf der Widerspruchsfrist.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a1c4e7f9b3d6'
|
||||
down_revision = 'd7f1a3c5e8b2'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('antraege', sa.Column('bewilligter_betrag', sa.Numeric(10, 2), nullable=True))
|
||||
op.add_column('antraege', sa.Column('aktenzeichen', sa.String(100), nullable=True))
|
||||
op.add_column('antraege', sa.Column('naechste_statuserinnerung_am', sa.Date(), nullable=True))
|
||||
op.add_column(
|
||||
'antraege',
|
||||
sa.Column(
|
||||
'widerspruch_eskalation_gesendet', sa.Boolean(), nullable=False, server_default=sa.false()
|
||||
),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'antrag_aktions_token',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column(
|
||||
'antrag_id', sa.Integer(), sa.ForeignKey('antraege.id', ondelete='CASCADE'), nullable=False
|
||||
),
|
||||
sa.Column('token_hash', sa.String(64), nullable=False, unique=True),
|
||||
sa.Column('laeuft_ab', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('eingeloest_am', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('antrag_aktions_token')
|
||||
op.drop_column('antraege', 'widerspruch_eskalation_gesendet')
|
||||
op.drop_column('antraege', 'naechste_statuserinnerung_am')
|
||||
op.drop_column('antraege', 'aktenzeichen')
|
||||
op.drop_column('antraege', 'bewilligter_betrag')
|
||||
@@ -0,0 +1,260 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
useAntrag,
|
||||
useAntragMutationen,
|
||||
useWiderspruchGruende,
|
||||
pdfUrl,
|
||||
} from "@/lib/briefe";
|
||||
import { usePatient } from "@/lib/patienten";
|
||||
import { buttonStyles, accentForKey, getAccent } from "@/lib/theme";
|
||||
|
||||
const AUTOMATISIERTE_TYPEN = ["erstantrag", "hoeherst"];
|
||||
|
||||
const HAEUFIGE_GRUENDE = [
|
||||
"Die Begutachtung war zu kurz oder fand nur telefonisch statt, statt bei einem persönlichen Hausbesuch.",
|
||||
"Das Pflegetagebuch oder ärztliche Nachweise wurden bei der Begutachtung nicht berücksichtigt.",
|
||||
"Der tatsächliche Unterstützungsbedarf – z. B. bei Mobilität, Selbstversorgung oder nachts – wurde unterschätzt.",
|
||||
"Der Bescheid ist nicht ausreichend begründet worden, sodass unklar bleibt, worauf die Entscheidung beruht.",
|
||||
];
|
||||
|
||||
function fristInfo(bescheidDatum: string | null, widerspruchFrist: string | null) {
|
||||
const frist = widerspruchFrist
|
||||
? new Date(widerspruchFrist)
|
||||
: bescheidDatum
|
||||
? new Date(new Date(bescheidDatum).getTime() + 31 * 24 * 60 * 60 * 1000)
|
||||
: null;
|
||||
if (!frist) return null;
|
||||
const heute = new Date();
|
||||
const tage = Math.ceil((frist.getTime() - heute.getTime()) / (24 * 60 * 60 * 1000));
|
||||
const farbe = tage <= 3 ? "text-red-700 bg-red-50 border-red-200" : tage <= 14 ? "text-amber-800 bg-amber-50 border-amber-200" : "text-emerald-700 bg-emerald-50 border-emerald-200";
|
||||
return { frist, tage, farbe };
|
||||
}
|
||||
|
||||
export default function KeineSorgePage() {
|
||||
const params = useParams();
|
||||
const aid = Number(params.id);
|
||||
const { data: antrag } = useAntrag(aid);
|
||||
const { data: patient } = usePatient(antrag?.patient_id ?? 0);
|
||||
const { data: gruendeData } = useWiderspruchGruende();
|
||||
const { widerspruchErstellen } = useAntragMutationen(patient?.id ?? 0);
|
||||
|
||||
const [sollGrad, setSollGrad] = useState(3);
|
||||
const [gewaehlteGruende, setGewaehlteGruende] = useState<Set<string>>(new Set());
|
||||
const [beweismittel, setBeweismittel] = useState<Set<string>>(new Set());
|
||||
|
||||
const gruppenNorm = useMemo(() => {
|
||||
if (!gruendeData) return [];
|
||||
const seen = new Map<string, string>();
|
||||
for (const g of gruendeData.gruende) if (!seen.has(g.norm)) seen.set(g.norm, g.group);
|
||||
return [...seen.entries()];
|
||||
}, [gruendeData]);
|
||||
|
||||
const toggleGrund = (id: string) => {
|
||||
setGewaehlteGruende((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const toggleBeweis = (label: string) => {
|
||||
setBeweismittel((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(label) ? next.delete(label) : next.add(label);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
if (!antrag || !patient) {
|
||||
return <div className="mx-auto max-w-3xl p-6 text-slate-500">Lädt…</div>;
|
||||
}
|
||||
|
||||
const info = fristInfo(antrag.bescheid_datum, antrag.widerspruch_frist);
|
||||
const automatisiert = AUTOMATISIERTE_TYPEN.includes(antrag.typ);
|
||||
const ergebnisAccent = getAccent("emerald");
|
||||
|
||||
const patientName = `${patient.vorname} ${patient.nachname}`;
|
||||
const adresse = [patient.strasse, [patient.plz, patient.ort].filter(Boolean).join(", ")]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
const vollformularUrl = () => {
|
||||
// name/adresse/geburtsdatum/versichertennummer/krankenkasse muessen hier
|
||||
// nicht mitgegeben werden - /widerspruch fuellt sie beim Waehlen von
|
||||
// patientId ohnehin aus demselben Patientendatensatz (patientWaehlen()).
|
||||
const p = new URLSearchParams();
|
||||
p.set("patientId", String(patient.id));
|
||||
if (antrag.bescheid_datum) p.set("bescheiddatum", antrag.bescheid_datum);
|
||||
if (antrag.aktenzeichen) p.set("aktenzeichen", antrag.aktenzeichen);
|
||||
if (antrag.zugeteilter_pflegegrad != null) p.set("istGrad", String(antrag.zugeteilter_pflegegrad));
|
||||
p.set("sollGrad", String(sollGrad));
|
||||
if (gewaehlteGruende.size > 0) p.set("grundIds", [...gewaehlteGruende].join(","));
|
||||
if (beweismittel.size > 0) p.set("beweismittel", [...beweismittel].join(","));
|
||||
return `/widerspruch?${p.toString()}`;
|
||||
};
|
||||
|
||||
const erstellen = () => {
|
||||
if (!antrag.bescheid_datum || gewaehlteGruende.size === 0) return;
|
||||
widerspruchErstellen.mutate({
|
||||
patient_id: patient.id,
|
||||
name: patientName,
|
||||
adresse,
|
||||
geburtsdatum: patient.geburtsdatum,
|
||||
versichertennummer: patient.versichertennummer,
|
||||
krankenkasse: patient.krankenkasse,
|
||||
bescheiddatum: antrag.bescheid_datum,
|
||||
aktenzeichen: antrag.aktenzeichen,
|
||||
ist_grad: antrag.zugeteilter_pflegegrad ?? 0,
|
||||
soll_grad: sollGrad,
|
||||
grund_ids: [...gewaehlteGruende],
|
||||
beweismittel: [...beweismittel],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl p-6">
|
||||
<h1 className="mb-2 text-2xl font-semibold text-slate-800">
|
||||
Der Bescheid wurde abgelehnt – aber das ist nicht das Ende
|
||||
</h1>
|
||||
<p className="mb-4 text-sm text-slate-600">
|
||||
Ablehnungen kommen häufig vor und liegen oft an nachvollziehbaren, angreifbaren Gründen – nicht
|
||||
daran, dass tatsächlich kein Bedarf besteht. Ein Widerspruch kostet nichts und lohnt sich in
|
||||
vielen Fällen. Häufige Gründe, warum Bescheide zu Recht angefochten werden:
|
||||
</p>
|
||||
<ul className="mb-4 flex flex-col gap-2">
|
||||
{HAEUFIGE_GRUENDE.map((g) => (
|
||||
<li key={g} className="flex gap-2 rounded-lg border border-slate-200 bg-slate-50 p-2.5 text-sm text-slate-700">
|
||||
<span className="text-slate-400">•</span>
|
||||
{g}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{info && (
|
||||
<p className={`mb-6 rounded-lg border p-3 text-sm font-medium ${info.farbe}`}>
|
||||
Widerspruchsfrist (§ 84 SGG): bis {info.frist.toLocaleDateString("de-DE")}
|
||||
{info.tage >= 0 ? ` – noch ${info.tage} Tage Zeit` : " – die Frist ist bereits abgelaufen, ein Widerspruch kann trotzdem noch sinnvoll sein"}
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!automatisiert ? (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<p className="mb-3 text-sm text-slate-600">
|
||||
Für diese Art von Antrag steht noch kein automatisch vorausgefüllter Widerspruch zur
|
||||
Verfügung. Nutzen Sie das allgemeine Widerspruchs-Formular.
|
||||
</p>
|
||||
<Link href={`/widerspruch?patientId=${patient.id}`} className={buttonStyles.primary}>
|
||||
Zum allgemeinen Widerspruchs-Formular
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-slate-700">Widerspruch vorbereiten</h2>
|
||||
<Link href={vollformularUrl()} className={buttonStyles.small}>
|
||||
Vollständiges Formular öffnen
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mb-3 text-xs text-slate-500">
|
||||
Für: {patientName}
|
||||
{patient.krankenkasse ? ` · ${patient.krankenkasse}` : ""}
|
||||
{antrag.bescheid_datum ? ` · Bescheid vom ${new Date(antrag.bescheid_datum).toLocaleDateString("de-DE")}` : ""}
|
||||
</p>
|
||||
|
||||
<div className="mb-3 flex gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-slate-600">Zugeteilter Grad</label>
|
||||
<p className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700">
|
||||
{antrag.zugeteilter_pflegegrad ? `Grad ${antrag.zugeteilter_pflegegrad}` : "Kein Grad"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-slate-600">Erwarteter Grad</label>
|
||||
<select
|
||||
value={sollGrad}
|
||||
onChange={(e) => setSollGrad(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((g) => (
|
||||
<option key={g} value={g}>
|
||||
Grad {g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-xs font-semibold text-slate-600">Widerspruchsgründe auswählen *</p>
|
||||
<div className="mb-3 flex max-h-72 flex-col gap-3 overflow-y-auto">
|
||||
{gruppenNorm.map(([norm, group]) => {
|
||||
const accent = getAccent(accentForKey(norm));
|
||||
return (
|
||||
<div key={norm} className={`rounded-xl border p-2.5 ${accent.card}`}>
|
||||
<p className={`mb-1.5 inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${accent.badge}`}>
|
||||
{norm} – {group}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{gruendeData!.gruende
|
||||
.filter((g) => g.norm === norm)
|
||||
.map((g) => (
|
||||
<label key={g.id} className="flex items-start gap-2 text-xs text-slate-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={gewaehlteGruende.has(g.id)}
|
||||
onChange={() => toggleGrund(g.id)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
{g.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-xs font-semibold text-slate-600">Beigefügte Beweismittel</p>
|
||||
<div className="mb-4 grid grid-cols-2 gap-1">
|
||||
{gruendeData?.beweismittel_optionen.map((b) => (
|
||||
<label key={b} className="flex items-center gap-2 text-xs text-slate-700">
|
||||
<input type="checkbox" checked={beweismittel.has(b)} onChange={() => toggleBeweis(b)} />
|
||||
{b}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!widerspruchErstellen.data ? (
|
||||
<>
|
||||
<button
|
||||
onClick={erstellen}
|
||||
disabled={widerspruchErstellen.isPending || !antrag.bescheid_datum || gewaehlteGruende.size === 0}
|
||||
className={buttonStyles.primary + " w-full"}
|
||||
>
|
||||
Widerspruch erstellen
|
||||
</button>
|
||||
{widerspruchErstellen.isError && (
|
||||
<p className="mt-2 text-sm text-red-600">{(widerspruchErstellen.error as Error).message}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={`rounded-xl border p-3 ${ergebnisAccent.card}`}>
|
||||
<p className={`mb-2 text-sm font-medium ${ergebnisAccent.heading}`}>Widerspruch erstellt ✓</p>
|
||||
<a
|
||||
href={pdfUrl(patient.id, widerspruchErstellen.data.id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={buttonStyles.primary}
|
||||
>
|
||||
PDF öffnen
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAntragAktionEinloesen, type AntragAktionErgebnis } from "@/lib/briefe";
|
||||
import { ApiError } from "@/lib/api";
|
||||
|
||||
function AntragStatusInner() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get("token") ?? "";
|
||||
const ergebnis = (searchParams.get("ergebnis") ?? "offen") as AntragAktionErgebnis;
|
||||
const einloesen = useAntragAktionEinloesen();
|
||||
const gestartet = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || gestartet.current) return;
|
||||
gestartet.current = true;
|
||||
einloesen.mutate(
|
||||
{ token, ergebnis },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.ergebnis === "bewilligt") {
|
||||
router.replace(`/patienten/${data.patient_id}?antragBetrag=${data.antrag_id}`);
|
||||
} else if (data.ergebnis === "abgelehnt") {
|
||||
router.replace(`/antraege/${data.antrag_id}/keine-sorge`);
|
||||
} else {
|
||||
router.replace(`/patienten/${data.patient_id}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<p className="text-sm text-red-600">
|
||||
Dieser Link ist unvollständig. Bitte den Link direkt aus der E-Mail öffnen.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (einloesen.isError) {
|
||||
const message =
|
||||
einloesen.error instanceof ApiError ? einloesen.error.message : "Unbekannter Fehler";
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-red-600">Link ungültig oder abgelaufen: {message}</p>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="rounded-lg bg-slate-800 px-4 py-2 text-center text-sm font-medium text-white"
|
||||
>
|
||||
Zur Anmeldung
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <p className="text-sm text-slate-500">Einen Moment, wir tragen den Status ein…</p>;
|
||||
}
|
||||
|
||||
export default function AntragStatusPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-sm p-6 py-16">
|
||||
<h1 className="mb-4 text-2xl font-semibold text-slate-800">Antragsstatus</h1>
|
||||
<Suspense fallback={<p className="text-slate-500">Lädt…</p>}>
|
||||
<AntragStatusInner />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user