diff --git a/src/features/prozess/components/process-stepper.tsx b/src/features/prozess/components/process-stepper.tsx
index 57ec748..d3a1485 100644
--- a/src/features/prozess/components/process-stepper.tsx
+++ b/src/features/prozess/components/process-stepper.tsx
@@ -1,24 +1,25 @@
import { useForm } from "@tanstack/react-form";
import { Link } from "@tanstack/react-router";
+import { ChevronDown, ChevronRight, Plus } from "lucide-react";
import { useState } from "react";
import { useTherapeutenListe } from "@/features/kontakte/hooks";
+import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import { DateInput } from "@/shared/components/ui/date-input";
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 { dbExec } from "@/shared/hooks/use-db";
-import { PROZESS_SCHRITTE } from "@/shared/lib/constants";
+import type { ErstgespraechRow, ProcessStatus } from "../hooks";
+import {
+ addSitzung,
+ createErstgespraech,
+ setTssKontaktiert,
+ updateErstgespraech,
+ useErstgespraeche,
+ useSitzungen,
+} from "../hooks";
import { PhaseCard } from "./phase-card";
-interface ProcessStepperProps {
- aktuellerSchritt: ProzessSchritt;
- kontaktGesamt: number;
- absagen: number;
- onUpdate: () => void;
-}
-
const inputClasses =
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
@@ -27,177 +28,236 @@ function todayISO() {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
+interface ProcessStepperProps {
+ processStatus: ProcessStatus;
+ tssKontaktiertDatum: string | null;
+ kontaktGesamt: number;
+ absagen: number;
+ onUpdate: () => void;
+}
+
export function ProcessStepper({
- aktuellerSchritt,
+ processStatus,
+ tssKontaktiertDatum,
kontaktGesamt,
absagen,
onUpdate,
}: ProcessStepperProps) {
- // Map legacy step to current steps
- const effectiveSchritt =
- aktuellerSchritt === "sprechstunde_absolviert"
- ? "diagnose_erhalten"
- : aktuellerSchritt;
-
- const currentIndex = PROZESS_SCHRITTE.findIndex(
- (s) => s.key === effectiveSchritt,
- );
+ const visibleSteps = processStatus.steps.filter((s) => s.visible);
+ const currentIndex = visibleSteps.findIndex((s) => s.status === "aktuell");
return (
Dein Fortschritt
-
- Schritt {currentIndex + 1} von {PROZESS_SCHRITTE.length}
-
+ {currentIndex >= 0 && (
+
+ Schritt {currentIndex + 1} von {visibleSteps.length}
+
+ )}
- {PROZESS_SCHRITTE.map((schritt, i) => {
- const status =
- i < currentIndex
- ? "erledigt"
- : i === currentIndex
- ? "aktuell"
- : "offen";
-
- return (
-
- {status === "aktuell" && (
-
- )}
-
- );
- })}
+ {visibleSteps.map((step, i) => (
+
+ {step.key === "erstgespraech" && (
+
+ )}
+ {step.key === "tss" && step.status === "aktuell" && (
+
+ )}
+ {step.key === "tss" &&
+ step.status === "erledigt" &&
+ tssKontaktiertDatum && }
+ {step.key === "eigensuche" && (
+
+ )}
+ {step.key === "antrag" && step.status === "aktuell" && (
+
+ )}
+
+ ))}
);
}
-function StepAction({
- schritt,
- kontaktGesamt,
- absagen,
- onUpdate,
-}: {
- schritt: ProzessSchritt;
- kontaktGesamt: number;
- absagen: number;
- onUpdate: () => void;
-}) {
- switch (schritt) {
- case "neu":
- return ;
- case "diagnose_erhalten":
- return (
+function ErstgespraechAction({ onUpdate }: { onUpdate: () => void }) {
+ const { data: erstgespraeche, refetch } = useErstgespraeche();
+ const [showForm, setShowForm] = useState(false);
+
+ const handleUpdate = () => {
+ refetch();
+ onUpdate();
+ };
+
+ return (
+ <>
+ {erstgespraeche.length > 0 && (
+ <>
+
+
+ {erstgespraeche.map((eg) => (
+
+ ))}
+
+ >
+ )}
+ {showForm ? (
+ <>
+
+ {
+ setShowForm(false);
+ handleUpdate();
+ }}
+ onCancel={() => setShowForm(false)}
+ />
+ >
+ ) : (
<>
-
>
- );
- case "tss_beantragt":
- return (
-
- );
- case "eigensuche":
- return (
- <>
-
-
- {kontaktGesamt} Kontaktversuche, davon {absagen} Absagen.{" "}
-
- Kontakte verwalten
-
-
-
- >
- );
- case "antrag_gestellt":
- return (
- <>
-
-
- Dein Kostenerstattungsantrag wurde eingereicht.{" "}
-
- Zum Antrag
-
-
- >
- );
- default:
- return null;
- }
-}
-
-function AdvanceButton({
- nextStep,
- label,
- onDone,
-}: {
- nextStep: ProzessSchritt;
- label: string;
- onDone: () => void;
-}) {
- return (
- <>
-
-
- {
- await dbExec(
- "UPDATE nutzer SET aktueller_schritt = $1, aktualisiert_am = NOW() WHERE id = 1",
- [nextStep],
- );
- onDone();
- }}
- >
- {label}
-
-
+ )}
>
);
}
-function SprechstundeForm({ onDone }: { onDone: () => void }) {
+function ErstgespraechCard({
+ erstgespraech,
+ onUpdate,
+}: {
+ erstgespraech: ErstgespraechRow;
+ onUpdate: () => void;
+}) {
+ const [expanded, setExpanded] = useState(false);
+ const [editing, setEditing] = useState(false);
+
+ return (
+
+
setExpanded(!expanded)}
+ >
+
+ {erstgespraech.therapeut_name}
+ {erstgespraech.therapeut_stadt
+ ? ` (${erstgespraech.therapeut_stadt})`
+ : ""}
+
+
+ {erstgespraech.diagnose && (
+ {erstgespraech.diagnose}
+ )}
+ {erstgespraech.dringlichkeitscode && Dringlichkeit}
+
+ {erstgespraech.sitzung_count}{" "}
+ {Number(erstgespraech.sitzung_count) === 1
+ ? "Sitzung"
+ : "Sitzungen"}
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {expanded && (
+
+
+ {editing ? (
+
{
+ setEditing(false);
+ onUpdate();
+ }}
+ onCancel={() => setEditing(false)}
+ />
+ ) : (
+
+ setEditing(true)}
+ >
+ Diagnose bearbeiten
+
+
+ )}
+
+ )}
+
+ );
+}
+
+function SitzungList({
+ sprechstundeId,
+ onUpdate,
+}: {
+ sprechstundeId: number;
+ onUpdate: () => void;
+}) {
+ const { data: sitzungen, refetch } = useSitzungen(sprechstundeId);
+ const [addingDate, setAddingDate] = useState("");
+
+ return (
+
+
Sitzungen
+
+ {sitzungen.map((s) => (
+ -
+ {new Date(s.datum).toLocaleDateString("de-DE")}
+
+ ))}
+
+
+ setAddingDate(iso)} />
+ {
+ await addSitzung(sprechstundeId, addingDate);
+ setAddingDate("");
+ refetch();
+ onUpdate();
+ }}
+ >
+ Sitzung hinzufügen
+
+
+
+ );
+}
+
+function ErstgespraechForm({
+ onDone,
+ onCancel,
+}: {
+ onDone: () => void;
+ onCancel: () => void;
+}) {
const { data: therapeuten, loading } = useTherapeutenListe();
- const [saved, setSaved] = useState(false);
const form = useForm({
defaultValues: {
@@ -209,145 +269,258 @@ function SprechstundeForm({ onDone }: { onDone: () => void }) {
onSubmit: async ({ value }) => {
const therapeutId = Number(value.therapeut_id);
if (!therapeutId) return;
-
- await dbExec(
- `INSERT INTO sprechstunde (therapeut_id, datum, ergebnis, diagnose, dringlichkeitscode) VALUES ($1, $2, 'erstgespraech', $3, $4)`,
- [
- therapeutId,
- value.datum,
- value.diagnose || null,
- value.dringlichkeitscode,
- ],
+ await createErstgespraech(
+ therapeutId,
+ value.datum,
+ value.diagnose || null,
+ value.dringlichkeitscode,
);
- await dbExec(
- `UPDATE nutzer SET
- aktueller_schritt = 'diagnose_erhalten',
- dringlichkeitscode = $1,
- dringlichkeitscode_datum = CASE WHEN $1 = TRUE THEN $2::date ELSE NULL END,
- aktualisiert_am = NOW()
- WHERE id = 1`,
- [value.dringlichkeitscode, value.datum],
- );
- setSaved(true);
onDone();
},
});
- if (saved) {
- return (
-
- Erstgespräch erfasst.
+ return (
+
+ {(field) => (
+
+
+ {loading ? (
+
Laden…
+ ) : therapeuten.length === 0 ? (
+
+ Lege zuerst unter{" "}
+
+ Kontakte
+ {" "}
+ einen Eintrag an.
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {(field) => (
+
+
+ field.handleChange(iso)}
+ />
+
+ )}
+
+
+
+ {(field) => (
+
+
+ field.handleChange(e.target.value)}
+ />
+
+ )}
+
+
+
+ {(field) => (
+
+
+ field.handleChange(checked)}
+ />
+
+
+
+ )}
+
+
+
+
+ Abbrechen
+
+
+ Erstgespräch durchgeführt
+
+
+
+ );
+}
+
+function EditErstgespraechForm({
+ erstgespraech,
+ onDone,
+ onCancel,
+}: {
+ erstgespraech: ErstgespraechRow;
+ onDone: () => void;
+ onCancel: () => void;
+}) {
+ const form = useForm({
+ defaultValues: {
+ diagnose: erstgespraech.diagnose ?? "",
+ dringlichkeitscode: erstgespraech.dringlichkeitscode,
+ },
+ onSubmit: async ({ value }) => {
+ await updateErstgespraech(
+ erstgespraech.id,
+ value.diagnose || null,
+ value.dringlichkeitscode,
+ );
+ onDone();
+ },
+ });
+
+ return (
+
+ {(field) => (
+
+
+ field.handleChange(e.target.value)}
+ />
+
+ )}
+
+
+
+ {(field) => (
+
+
+ field.handleChange(checked)}
+ />
+
+
+
+ )}
+
+
+
+
+ Abbrechen
+
+
+ Speichern
+
+
+
+ );
+}
+
+function TssAction({ onUpdate }: { onUpdate: () => void }) {
return (
<>
-
- {(field) => (
-
-
- {loading ? (
-
Laden…
- ) : therapeuten.length === 0 ? (
-
- Lege zuerst unter{" "}
-
- Kontakte
- {" "}
- einen Eintrag an.
-
- ) : (
-
- )}
-
- )}
-
-
-
- {(field) => (
-
-
- field.handleChange(iso)}
- />
-
- )}
-
-
-
- {(field) => (
-
-
- field.handleChange(e.target.value)}
- />
-
- )}
-
-
-
- {(field) => (
-
-
- field.handleChange(checked)}
- />
-
-
- {!field.state.value && (
-
- Ohne Dringlichkeitscode kann die TSS dich ggf. nicht
- vermitteln. Frage in der Sprechstunde gezielt danach.
-
- )}
-
- )}
-
-
-
-
- Erstgespräch durchgeführt
-
-
-
+
+ {
+ await setTssKontaktiert();
+ onUpdate();
+ }}
+ >
+ TSS kontaktiert
+
+
+ >
+ );
+}
+
+function TssDone({ datum }: { datum: string }) {
+ return (
+ <>
+
+
+ TSS kontaktiert am {new Date(datum).toLocaleDateString("de-DE")}
+
+ >
+ );
+}
+
+function EigensucheInfo({
+ kontaktGesamt,
+ absagen,
+}: {
+ kontaktGesamt: number;
+ absagen: number;
+}) {
+ return (
+ <>
+
+
+ {kontaktGesamt} Kontaktversuche, davon {absagen} Absagen.{" "}
+
+ Kontakte verwalten
+
+
+ >
+ );
+}
+
+function AntragLink() {
+ return (
+ <>
+
+
+ Dein Kostenerstattungsantrag kann eingereicht werden.{" "}
+
+ Zum Antrag
+
+
>
);
}