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 { Separator } from "@/shared/components/ui/separator";
import { PdfExportButton } from "./pdf-export-button";
@@ -6,13 +10,15 @@ import { PdfExportButton } from "./pdf-export-button";
interface ChecklistItem {
label: string;
fulfilled: boolean;
visible: boolean;
}
export function AntragChecklist() {
const { data: nutzerRows, loading: nutzerLoading } = useNutzer();
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>;
}
@@ -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[] = [
{
label: "Psychotherapeutische Sprechstunde besucht",
fulfilled: nutzer.aktueller_schritt !== "neu",
label: "Erstgespräch durchgeführt",
fulfilled: hasDiagnose,
visible: true,
},
{
label: "Diagnose / Dringlichkeitscode erhalten",
fulfilled: nutzer.dringlichkeitscode,
label: "Dringlichkeitscode erhalten",
fulfilled: hasDringlichkeit,
visible: true,
},
{
label: "Terminservicestelle (TSS) kontaktiert",
fulfilled: nutzer.tss_beantragt,
fulfilled: nutzer.tss_kontaktiert_datum != null,
visible: hasDringlichkeit,
},
{
label: "Eigenständige Therapeutensuche dokumentiert",
fulfilled: stats.absagen + stats.keine_antwort >= 5,
label: "Therapeutensuche dokumentiert",
fulfilled: Number(stats.absagen) + Number(stats.keine_antwort) >= 5,
visible: true,
},
{
label: "Absagenliste exportiert",
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 (
<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
{fulfilledCount} von {visibleItems.length} Voraussetzungen erfüllt
</p>
</div>
<div className="flex flex-col gap-3">
{items.map((item, index) => (
{visibleItems.map((item, index) => (
<Card key={item.label} className="py-4">
<CardContent className="flex items-center gap-4">
{item.fulfilled ? (

View File

@@ -10,14 +10,16 @@ import {
Trash2,
} from "lucide-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 { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card";
import { Label } from "@/shared/components/ui/label";
import { Separator } from "@/shared/components/ui/separator";
import { Switch } from "@/shared/components/ui/switch";
import type { ProzessSchritt } from "@/shared/db/schema";
import { useThemeStore } from "@/shared/hooks/use-theme";
type Theme = "light" | "dark" | "system";
@@ -35,7 +37,7 @@ export function SettingsPage() {
const [devMode, setDevMode] = useState(
() => 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 {
@@ -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"
value={scenarioStep}
onChange={(e) => {
setScenarioStep(e.target.value as ProzessSchritt);
setScenarioStep(e.target.value as Scenario);
setScenarioStatus("idle");
}}
>
<option value="neu">Schritt 1 Erstgespräch</option>
<option value="erstgespraech">
Schritt 1 Erstgespräch
</option>
<option value="diagnose_erhalten">
Schritt 2 Diagnose erhalten
</option>
<option value="tss_beantragt">
<option value="tss_kontaktiert">
Schritt 3 TSS kontaktiert
</option>
<option value="eigensuche">Schritt 4 Eigensuche</option>
<option value="antrag_gestellt">
Schritt 5 Antrag gestellt
<option value="antrag_bereit">
Schritt 5 Antrag bereit
</option>
</select>
<Button
size="sm"
variant="outline"
onClick={async () => {
await seedToStep(scenarioStep);
await seedToScenario(scenarioStep);
setScenarioStatus("done");
}}
>

View File

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

View File

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

View File

@@ -1,2 +1,8 @@
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();
});
});