update antrag checklist, scenarios, settings, barrel export, delete obsolete test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 19:50:48 +01:00
parent a3a331cc0c
commit 54291f450b
6 changed files with 86 additions and 85 deletions

View File

@@ -1,4 +1,8 @@
import { useKontaktStats, useNutzer } from "@/features/prozess/hooks"; import {
useErstgespraeche,
useKontaktStats,
useNutzer,
} from "@/features/prozess/hooks";
import { Card, CardContent } from "@/shared/components/ui/card"; import { Card, CardContent } from "@/shared/components/ui/card";
import { Separator } from "@/shared/components/ui/separator"; import { Separator } from "@/shared/components/ui/separator";
import { PdfExportButton } from "./pdf-export-button"; import { PdfExportButton } from "./pdf-export-button";
@@ -6,13 +10,15 @@ import { PdfExportButton } from "./pdf-export-button";
interface ChecklistItem { interface ChecklistItem {
label: string; label: string;
fulfilled: boolean; fulfilled: boolean;
visible: boolean;
} }
export function AntragChecklist() { export function AntragChecklist() {
const { data: nutzerRows, loading: nutzerLoading } = useNutzer(); const { data: nutzerRows, loading: nutzerLoading } = useNutzer();
const { data: statsRows, loading: statsLoading } = useKontaktStats(); const { data: statsRows, loading: statsLoading } = useKontaktStats();
const { data: erstgespraeche, loading: egLoading } = useErstgespraeche();
if (nutzerLoading || statsLoading) { if (nutzerLoading || statsLoading || egLoading) {
return <p className="py-8 text-center text-muted-foreground">Laden</p>; return <p className="py-8 text-center text-muted-foreground">Laden</p>;
} }
@@ -27,42 +33,51 @@ export function AntragChecklist() {
); );
} }
const hasDiagnose = erstgespraeche.some((e) => e.diagnose != null);
const hasDringlichkeit = erstgespraeche.some((e) => e.dringlichkeitscode);
const items: ChecklistItem[] = [ const items: ChecklistItem[] = [
{ {
label: "Psychotherapeutische Sprechstunde besucht", label: "Erstgespräch durchgeführt",
fulfilled: nutzer.aktueller_schritt !== "neu", fulfilled: hasDiagnose,
visible: true,
}, },
{ {
label: "Diagnose / Dringlichkeitscode erhalten", label: "Dringlichkeitscode erhalten",
fulfilled: nutzer.dringlichkeitscode, fulfilled: hasDringlichkeit,
visible: true,
}, },
{ {
label: "Terminservicestelle (TSS) kontaktiert", label: "Terminservicestelle (TSS) kontaktiert",
fulfilled: nutzer.tss_beantragt, fulfilled: nutzer.tss_kontaktiert_datum != null,
visible: hasDringlichkeit,
}, },
{ {
label: "Eigenständige Therapeutensuche dokumentiert", label: "Therapeutensuche dokumentiert",
fulfilled: stats.absagen + stats.keine_antwort >= 5, fulfilled: Number(stats.absagen) + Number(stats.keine_antwort) >= 5,
visible: true,
}, },
{ {
label: "Absagenliste exportiert", label: "Absagenliste exportiert",
fulfilled: false, fulfilled: false,
visible: true,
}, },
]; ];
const fulfilledCount = items.filter((i) => i.fulfilled).length; const visibleItems = items.filter((i) => i.visible);
const fulfilledCount = visibleItems.filter((i) => i.fulfilled).length;
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div> <div>
<h1 className="text-xl font-bold">Kostenerstattungs-Assistent</h1> <h1 className="text-xl font-bold">Kostenerstattungs-Assistent</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{fulfilledCount} von {items.length} Voraussetzungen erfüllt {fulfilledCount} von {visibleItems.length} Voraussetzungen erfüllt
</p> </p>
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{items.map((item, index) => ( {visibleItems.map((item, index) => (
<Card key={item.label} className="py-4"> <Card key={item.label} className="py-4">
<CardContent className="flex items-center gap-4"> <CardContent className="flex items-center gap-4">
{item.fulfilled ? ( {item.fulfilled ? (

View File

@@ -10,14 +10,16 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { seedToStep } from "@/features/einstellungen/scenarios"; import {
type Scenario,
seedToScenario,
} from "@/features/einstellungen/scenarios";
import { deleteAllData } from "@/features/kontakte/hooks"; import { deleteAllData } from "@/features/kontakte/hooks";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card"; import { Card, CardContent } from "@/shared/components/ui/card";
import { Label } from "@/shared/components/ui/label"; import { Label } from "@/shared/components/ui/label";
import { Separator } from "@/shared/components/ui/separator"; import { Separator } from "@/shared/components/ui/separator";
import { Switch } from "@/shared/components/ui/switch"; import { Switch } from "@/shared/components/ui/switch";
import type { ProzessSchritt } from "@/shared/db/schema";
import { useThemeStore } from "@/shared/hooks/use-theme"; import { useThemeStore } from "@/shared/hooks/use-theme";
type Theme = "light" | "dark" | "system"; type Theme = "light" | "dark" | "system";
@@ -35,7 +37,7 @@ export function SettingsPage() {
const [devMode, setDevMode] = useState( const [devMode, setDevMode] = useState(
() => localStorage.getItem("tpf-dev-mode") === "true", () => localStorage.getItem("tpf-dev-mode") === "true",
); );
const [scenarioStep, setScenarioStep] = useState<ProzessSchritt>("neu"); const [scenarioStep, setScenarioStep] = useState<Scenario>("erstgespraech");
const [scenarioStatus, setScenarioStatus] = useState<"idle" | "done">("idle"); const [scenarioStatus, setScenarioStatus] = useState<"idle" | "done">("idle");
const { const {
@@ -178,27 +180,29 @@ export function SettingsPage() {
className="flex h-9 flex-1 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" className="flex h-9 flex-1 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={scenarioStep} value={scenarioStep}
onChange={(e) => { onChange={(e) => {
setScenarioStep(e.target.value as ProzessSchritt); setScenarioStep(e.target.value as Scenario);
setScenarioStatus("idle"); setScenarioStatus("idle");
}} }}
> >
<option value="neu">Schritt 1 Erstgespräch</option> <option value="erstgespraech">
Schritt 1 Erstgespräch
</option>
<option value="diagnose_erhalten"> <option value="diagnose_erhalten">
Schritt 2 Diagnose erhalten Schritt 2 Diagnose erhalten
</option> </option>
<option value="tss_beantragt"> <option value="tss_kontaktiert">
Schritt 3 TSS kontaktiert Schritt 3 TSS kontaktiert
</option> </option>
<option value="eigensuche">Schritt 4 Eigensuche</option> <option value="eigensuche">Schritt 4 Eigensuche</option>
<option value="antrag_gestellt"> <option value="antrag_bereit">
Schritt 5 Antrag gestellt Schritt 5 Antrag bereit
</option> </option>
</select> </select>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={async () => { onClick={async () => {
await seedToStep(scenarioStep); await seedToScenario(scenarioStep);
setScenarioStatus("done"); setScenarioStatus("done");
}} }}
> >

View File

@@ -1,4 +1,3 @@
import type { ProzessSchritt } from "@/shared/db/schema";
import { dbExec } from "@/shared/hooks/use-db"; import { dbExec } from "@/shared/hooks/use-db";
const MOCK_VORNAMEN = [ const MOCK_VORNAMEN = [
@@ -80,6 +79,7 @@ function daysAgoISO(days: number) {
} }
async function clearData() { async function clearData() {
await dbExec("DELETE FROM sitzung");
await dbExec("DELETE FROM kontakt"); await dbExec("DELETE FROM kontakt");
await dbExec("DELETE FROM sprechstunde"); await dbExec("DELETE FROM sprechstunde");
await dbExec("DELETE FROM therapeut"); await dbExec("DELETE FROM therapeut");
@@ -88,17 +88,13 @@ async function clearData() {
async function resetNutzer() { async function resetNutzer() {
await dbExec( await dbExec(
`UPDATE nutzer SET `UPDATE nutzer SET
aktueller_schritt = 'neu', tss_kontaktiert_datum = NULL,
dringlichkeitscode = FALSE,
dringlichkeitscode_datum = NULL,
tss_beantragt = FALSE,
tss_beantragt_datum = NULL,
aktualisiert_am = NOW() aktualisiert_am = NOW()
WHERE id = 1`, WHERE id = 1`,
); );
} }
async function seedSprechstunde() { async function seedErstgespraech() {
const result = await dbExec( const result = await dbExec(
`INSERT INTO therapeut (name, stadt, therapieform, telefon, email) `INSERT INTO therapeut (name, stadt, therapieform, telefon, email)
VALUES ($1, $2, $3, $4, $5) RETURNING id`, VALUES ($1, $2, $3, $4, $5) RETURNING id`,
@@ -112,31 +108,26 @@ async function seedSprechstunde() {
); );
const therapeutId = (result.rows[0] as { id: number }).id; const therapeutId = (result.rows[0] as { id: number }).id;
await dbExec( const spResult = await dbExec(
`INSERT INTO sprechstunde (therapeut_id, datum, ergebnis, diagnose, dringlichkeitscode) `INSERT INTO sprechstunde (therapeut_id, diagnose, dringlichkeitscode)
VALUES ($1, $2, 'erstgespraech', 'F32.1', TRUE)`, VALUES ($1, 'F32.1', TRUE) RETURNING id`,
[therapeutId, daysAgoISO(14)], [therapeutId],
); );
const sprechstundeId = (spResult.rows[0] as { id: number }).id;
await dbExec( await dbExec("INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)", [
`UPDATE nutzer SET sprechstundeId,
aktueller_schritt = 'diagnose_erhalten', daysAgoISO(14),
dringlichkeitscode = TRUE, ]);
dringlichkeitscode_datum = $1::date, await dbExec("INSERT INTO sitzung (sprechstunde_id, datum) VALUES ($1, $2)", [
aktualisiert_am = NOW() sprechstundeId,
WHERE id = 1`, daysAgoISO(7),
[daysAgoISO(14)], ]);
);
} }
async function seedTssBeantragt() { async function seedTssKontaktiert() {
await dbExec( await dbExec(
`UPDATE nutzer SET "UPDATE nutzer SET tss_kontaktiert_datum = $1, aktualisiert_am = NOW() WHERE id = 1",
aktueller_schritt = 'tss_beantragt',
tss_beantragt = TRUE,
tss_beantragt_datum = $1::date,
aktualisiert_am = NOW()
WHERE id = 1`,
[daysAgoISO(10)], [daysAgoISO(10)],
); );
} }
@@ -167,48 +158,45 @@ async function seedKontakte() {
const ergebnis = MOCK_ERGEBNISSE[(i + j) % MOCK_ERGEBNISSE.length]; const ergebnis = MOCK_ERGEBNISSE[(i + j) % MOCK_ERGEBNISSE.length];
const kanal = MOCK_KANALE[(i + j) % MOCK_KANALE.length]; const kanal = MOCK_KANALE[(i + j) % MOCK_KANALE.length];
await dbExec( await dbExec(
`INSERT INTO kontakt (therapeut_id, datum, kanal, ergebnis) VALUES ($1, $2, $3, $4)`, "INSERT INTO kontakt (therapeut_id, datum, kanal, ergebnis) VALUES ($1, $2, $3, $4)",
[therapeutId, daysAgoISO(daysBack), kanal, ergebnis], [therapeutId, daysAgoISO(daysBack), kanal, ergebnis],
); );
} }
} }
await dbExec(
`UPDATE nutzer SET aktueller_schritt = 'eigensuche', aktualisiert_am = NOW() WHERE id = 1`,
);
} }
// Steps in order — each builds on the previous export type Scenario =
const STEP_ORDER: ProzessSchritt[] = [ | "erstgespraech"
"neu", | "diagnose_erhalten"
| "tss_kontaktiert"
| "eigensuche"
| "antrag_bereit";
const SCENARIO_ORDER: Scenario[] = [
"erstgespraech",
"diagnose_erhalten", "diagnose_erhalten",
"tss_beantragt", "tss_kontaktiert",
"eigensuche", "eigensuche",
"antrag_gestellt", "antrag_bereit",
]; ];
const STEP_SEEDERS: Record<ProzessSchritt, (() => Promise<void>) | null> = { const SCENARIO_SEEDERS: Record<Scenario, (() => Promise<void>) | null> = {
neu: null, erstgespraech: null,
sprechstunde_absolviert: null, diagnose_erhalten: seedErstgespraech,
diagnose_erhalten: seedSprechstunde, tss_kontaktiert: seedTssKontaktiert,
tss_beantragt: seedTssBeantragt,
eigensuche: seedKontakte, eigensuche: seedKontakte,
antrag_gestellt: async () => { antrag_bereit: null,
await dbExec(
`UPDATE nutzer SET aktueller_schritt = 'antrag_gestellt', aktualisiert_am = NOW() WHERE id = 1`,
);
},
}; };
export async function seedToStep(targetStep: ProzessSchritt) { export async function seedToScenario(target: Scenario) {
await clearData(); await clearData();
await resetNutzer(); await resetNutzer();
const targetIndex = STEP_ORDER.indexOf(targetStep); const targetIndex = SCENARIO_ORDER.indexOf(target);
if (targetIndex < 0) return; if (targetIndex < 0) return;
for (let i = 0; i <= targetIndex; i++) { for (let i = 0; i <= targetIndex; i++) {
const seeder = STEP_SEEDERS[STEP_ORDER[i]]; const seeder = SCENARIO_SEEDERS[SCENARIO_ORDER[i]];
if (seeder) await seeder(); if (seeder) await seeder();
} }
} }

View File

@@ -129,6 +129,7 @@ export async function deleteKontakt(id: number) {
} }
export async function deleteAllData() { export async function deleteAllData() {
await dbExec("DELETE FROM sitzung");
await dbExec("DELETE FROM kontakt"); await dbExec("DELETE FROM kontakt");
await dbExec("DELETE FROM sprechstunde"); await dbExec("DELETE FROM sprechstunde");
await dbExec("DELETE FROM therapeut"); await dbExec("DELETE FROM therapeut");

View File

@@ -1,2 +1,8 @@
export { ProcessStepper } from "./components/process-stepper"; export { ProcessStepper } from "./components/process-stepper";
export { updateSchritt, useKontaktStats, useNutzer } from "./hooks"; export type { ErstgespraechRow, ProcessStatus, StepStatus } from "./hooks";
export {
deriveProcessStatus,
useErstgespraeche,
useKontaktStats,
useNutzer,
} from "./hooks";

View File

@@ -1,13 +0,0 @@
import { describe, expect, it } from "vitest";
import { prozessSchrittEnum } from "./schema";
describe("prozessSchrittEnum", () => {
it("accepts valid steps", () => {
expect(prozessSchrittEnum.parse("neu")).toBe("neu");
expect(prozessSchrittEnum.parse("eigensuche")).toBe("eigensuche");
});
it("rejects invalid steps", () => {
expect(() => prozessSchrittEnum.parse("invalid")).toThrow();
});
});