add kostenerstattungs-assistent with checklist, pdf export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { useKontaktStats, useNutzer } from "@/features/prozess/hooks";
|
||||
import { Card, CardContent } from "@/shared/components/ui/card";
|
||||
import { Separator } from "@/shared/components/ui/separator";
|
||||
import { PdfExportButton } from "./pdf-export-button";
|
||||
|
||||
interface ChecklistItem {
|
||||
label: string;
|
||||
fulfilled: boolean;
|
||||
}
|
||||
|
||||
export function AntragChecklist() {
|
||||
const { data: nutzerRows, loading: nutzerLoading } = useNutzer();
|
||||
const { data: statsRows, loading: statsLoading } = useKontaktStats();
|
||||
|
||||
if (nutzerLoading || statsLoading) {
|
||||
return <p className="py-8 text-center text-muted-foreground">Laden…</p>;
|
||||
}
|
||||
|
||||
const nutzer = nutzerRows[0];
|
||||
const stats = statsRows[0];
|
||||
|
||||
if (!nutzer || !stats) {
|
||||
return (
|
||||
<p className="py-8 text-center text-muted-foreground">
|
||||
Keine Daten vorhanden.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const items: ChecklistItem[] = [
|
||||
{
|
||||
label: "Psychotherapeutische Sprechstunde besucht",
|
||||
fulfilled: nutzer.aktueller_schritt !== "neu",
|
||||
},
|
||||
{
|
||||
label: "Diagnose / Dringlichkeitscode erhalten",
|
||||
fulfilled: nutzer.dringlichkeitscode,
|
||||
},
|
||||
{
|
||||
label: "Terminservicestelle (TSS) kontaktiert",
|
||||
fulfilled: nutzer.tss_beantragt,
|
||||
},
|
||||
{
|
||||
label: "Eigenständige Therapeutensuche dokumentiert",
|
||||
fulfilled: stats.absagen + stats.keine_antwort >= 5,
|
||||
},
|
||||
{
|
||||
label: "Absagenliste exportiert",
|
||||
fulfilled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const fulfilledCount = items.filter((i) => i.fulfilled).length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Kostenerstattungs-Assistent</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{fulfilledCount} von {items.length} Voraussetzungen erfüllt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{items.map((item, index) => (
|
||||
<Card key={item.label} className="py-4">
|
||||
<CardContent className="flex items-center gap-4">
|
||||
{item.fulfilled ? (
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">
|
||||
<span className="text-sm font-bold">✓</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<span className="text-sm font-medium">{index + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
item.fulfilled ? "text-sm" : "text-sm text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 text-lg font-semibold">Nächste Schritte</h2>
|
||||
<ol className="list-inside list-decimal space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
Besuche eine psychotherapeutische Sprechstunde und lass dir eine
|
||||
Diagnose sowie einen Dringlichkeitscode geben.
|
||||
</li>
|
||||
<li>
|
||||
Kontaktiere die Terminservicestelle (TSS) deiner Kassenärztlichen
|
||||
Vereinigung unter 116 117.
|
||||
</li>
|
||||
<li>
|
||||
Dokumentiere mindestens 5 erfolglose Kontaktversuche (Absagen oder
|
||||
keine Antwort).
|
||||
</li>
|
||||
<li>
|
||||
Exportiere die Absagenliste als PDF und lege sie deinem Antrag bei.
|
||||
</li>
|
||||
<li>
|
||||
Reiche den Kostenerstattungsantrag bei deiner Krankenkasse ein.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<PdfExportButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { generateAbsagenPdf } from "../pdf";
|
||||
|
||||
export function PdfExportButton() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleExport() {
|
||||
setLoading(true);
|
||||
try {
|
||||
await generateAbsagenPdf();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={handleExport} disabled={loading} className="w-full">
|
||||
{loading ? "Wird erstellt…" : "Absagenliste als PDF exportieren"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AntragChecklist } from "./components/antrag-checklist";
|
||||
export { PdfExportButton } from "./components/pdf-export-button";
|
||||
export { generateAbsagenPdf } from "./pdf";
|
||||
@@ -0,0 +1,132 @@
|
||||
import { jsPDF } from "jspdf";
|
||||
import { getDb } from "@/shared/db/client";
|
||||
import type { KontaktErgebnis, KontaktKanal } from "@/shared/db/schema";
|
||||
import { ERGEBNIS_LABELS, KANAL_LABELS } from "@/shared/lib/constants";
|
||||
|
||||
interface NutzerRow {
|
||||
name: string | null;
|
||||
krankenkasse: string | null;
|
||||
}
|
||||
|
||||
interface KontaktRow {
|
||||
datum: string;
|
||||
therapeut_name: string;
|
||||
stadt: string | null;
|
||||
kanal: KontaktKanal;
|
||||
ergebnis: KontaktErgebnis;
|
||||
}
|
||||
|
||||
export async function generateAbsagenPdf(): Promise<void> {
|
||||
const db = await getDb();
|
||||
|
||||
const nutzerResult = await db.query<NutzerRow>(
|
||||
"SELECT name, krankenkasse FROM nutzer LIMIT 1",
|
||||
);
|
||||
const nutzer = nutzerResult.rows[0];
|
||||
|
||||
const kontaktResult = await db.query<KontaktRow>(`
|
||||
SELECT
|
||||
k.datum,
|
||||
t.name AS therapeut_name,
|
||||
t.stadt,
|
||||
k.kanal,
|
||||
k.ergebnis
|
||||
FROM kontakt k
|
||||
JOIN therapeut t ON t.id = k.therapeut_id
|
||||
ORDER BY k.datum ASC
|
||||
`);
|
||||
const kontakte = kontaktResult.rows;
|
||||
|
||||
const doc = new jsPDF();
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
let y = 20;
|
||||
|
||||
// Header
|
||||
doc.setFontSize(18);
|
||||
doc.text("Dokumentation der Therapeutensuche", 14, y);
|
||||
y += 10;
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text(`Name: ${nutzer?.name ?? "k. A."}`, 14, y);
|
||||
y += 6;
|
||||
doc.text(`Krankenkasse: ${nutzer?.krankenkasse ?? "k. A."}`, 14, y);
|
||||
y += 6;
|
||||
doc.text(`Erstellt am: ${new Date().toLocaleDateString("de-DE")}`, 14, y);
|
||||
y += 10;
|
||||
|
||||
// Summary
|
||||
const totalAbsagen = kontakte.filter((k) => k.ergebnis === "absage").length;
|
||||
const totalKeineAntwort = kontakte.filter(
|
||||
(k) => k.ergebnis === "keine_antwort",
|
||||
).length;
|
||||
doc.setFontSize(12);
|
||||
doc.text("Zusammenfassung", 14, y);
|
||||
y += 7;
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Kontaktversuche gesamt: ${kontakte.length}`, 14, y);
|
||||
y += 5;
|
||||
doc.text(`Absagen: ${totalAbsagen}`, 14, y);
|
||||
y += 5;
|
||||
doc.text(`Keine Antwort: ${totalKeineAntwort}`, 14, y);
|
||||
y += 10;
|
||||
|
||||
// Table header
|
||||
const colX = [14, 40, 90, 130, 160];
|
||||
const colHeaders = ["Datum", "Therapeut:in", "Ort", "Kontaktweg", "Ergebnis"];
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont("helvetica", "bold");
|
||||
for (let i = 0; i < colHeaders.length; i++) {
|
||||
doc.text(colHeaders[i], colX[i], y);
|
||||
}
|
||||
y += 2;
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(14, y, pageWidth - 14, y);
|
||||
y += 5;
|
||||
doc.setFont("helvetica", "normal");
|
||||
|
||||
// Table rows
|
||||
for (const k of kontakte) {
|
||||
if (y > 270) {
|
||||
doc.addPage();
|
||||
y = 20;
|
||||
}
|
||||
|
||||
const datumFormatted = formatDatum(k.datum);
|
||||
doc.text(datumFormatted, colX[0], y);
|
||||
doc.text(truncate(k.therapeut_name, 30), colX[1], y);
|
||||
doc.text(truncate(k.stadt ?? "k. A.", 20), colX[2], y);
|
||||
doc.text(KANAL_LABELS[k.kanal], colX[3], y);
|
||||
doc.text(ERGEBNIS_LABELS[k.ergebnis], colX[4], y);
|
||||
y += 6;
|
||||
}
|
||||
|
||||
// Footer
|
||||
y += 10;
|
||||
if (y > 270) {
|
||||
doc.addPage();
|
||||
y = 20;
|
||||
}
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(120);
|
||||
doc.text(
|
||||
"Dieses Dokument wurde automatisch erstellt und dient als Nachweis der eigenständigen Therapeutensuche.",
|
||||
14,
|
||||
y,
|
||||
);
|
||||
|
||||
doc.save("therapeutensuche-dokumentation.pdf");
|
||||
}
|
||||
|
||||
function formatDatum(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString("de-DE");
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function truncate(text: string, maxLen: number): string {
|
||||
if (text.length <= maxLen) return text;
|
||||
return `${text.slice(0, maxLen - 1)}…`;
|
||||
}
|
||||
Reference in New Issue
Block a user