Previous fix missed backtick template strings. Fixed 7 more api.*() calls in appointments, deadlines, settings, and einstellungen pages.
281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "@/lib/api";
|
|
import type { Appointment, Case } from "@/lib/types";
|
|
import { format, parseISO } from "date-fns";
|
|
import { X } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { useEffect, useState } from "react";
|
|
|
|
const APPOINTMENT_TYPES = [
|
|
{ value: "hearing", label: "Verhandlung" },
|
|
{ value: "meeting", label: "Besprechung" },
|
|
{ value: "consultation", label: "Beratung" },
|
|
{ value: "deadline_hearing", label: "Fristanhorung" },
|
|
{ value: "other", label: "Sonstiges" },
|
|
];
|
|
|
|
interface AppointmentModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
appointment?: Appointment | null;
|
|
}
|
|
|
|
function toLocalDatetime(iso: string): string {
|
|
const d = parseISO(iso);
|
|
return format(d, "yyyy-MM-dd'T'HH:mm");
|
|
}
|
|
|
|
export function AppointmentModal({ open, onClose, appointment }: AppointmentModalProps) {
|
|
const queryClient = useQueryClient();
|
|
const isEdit = !!appointment;
|
|
|
|
const [title, setTitle] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [startAt, setStartAt] = useState("");
|
|
const [endAt, setEndAt] = useState("");
|
|
const [location, setLocation] = useState("");
|
|
const [appointmentType, setAppointmentType] = useState("");
|
|
const [caseId, setCaseId] = useState("");
|
|
|
|
const { data: cases } = useQuery({
|
|
queryKey: ["cases"],
|
|
queryFn: () => api.get<{ cases: Case[]; total: number }>("/cases"),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (appointment) {
|
|
setTitle(appointment.title);
|
|
setDescription(appointment.description ?? "");
|
|
setStartAt(toLocalDatetime(appointment.start_at));
|
|
setEndAt(appointment.end_at ? toLocalDatetime(appointment.end_at) : "");
|
|
setLocation(appointment.location ?? "");
|
|
setAppointmentType(appointment.appointment_type ?? "");
|
|
setCaseId(appointment.case_id ?? "");
|
|
} else {
|
|
setTitle("");
|
|
setDescription("");
|
|
setStartAt("");
|
|
setEndAt("");
|
|
setLocation("");
|
|
setAppointmentType("");
|
|
setCaseId("");
|
|
}
|
|
}, [appointment]);
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (body: Record<string, unknown>) =>
|
|
api.post<Appointment>("/appointments", body),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["appointments"] });
|
|
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
|
toast.success("Termin erstellt");
|
|
onClose();
|
|
},
|
|
onError: () => toast.error("Fehler beim Erstellen des Termins"),
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: (body: Record<string, unknown>) =>
|
|
api.put<Appointment>(`/appointments/${appointment!.id}`, body),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["appointments"] });
|
|
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
|
toast.success("Termin aktualisiert");
|
|
onClose();
|
|
},
|
|
onError: () => toast.error("Fehler beim Aktualisieren des Termins"),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: () => api.delete(`/appointments/${appointment!.id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["appointments"] });
|
|
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
|
toast.success("Termin geloscht");
|
|
onClose();
|
|
},
|
|
onError: () => toast.error("Fehler beim Loschen des Termins"),
|
|
});
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!title.trim() || !startAt) return;
|
|
|
|
const body: Record<string, unknown> = {
|
|
title: title.trim(),
|
|
start_at: new Date(startAt).toISOString(),
|
|
};
|
|
if (description.trim()) body.description = description.trim();
|
|
if (endAt) body.end_at = new Date(endAt).toISOString();
|
|
if (location.trim()) body.location = location.trim();
|
|
if (appointmentType) body.appointment_type = appointmentType;
|
|
if (caseId) body.case_id = caseId;
|
|
|
|
if (isEdit) {
|
|
updateMutation.mutate(body);
|
|
} else {
|
|
createMutation.mutate(body);
|
|
}
|
|
}
|
|
|
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
|
<div className="w-full max-w-lg rounded-lg border border-neutral-200 bg-white shadow-lg">
|
|
<div className="flex items-center justify-between border-b border-neutral-200 px-5 py-3">
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
{isEdit ? "Termin bearbeiten" : "Neuer Termin"}
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-md p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4 p-5">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Titel *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
required
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
|
placeholder="z.B. Mundliche Verhandlung"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Beginn *
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={startAt}
|
|
onChange={(e) => setStartAt(e.target.value)}
|
|
required
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Ende
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={endAt}
|
|
onChange={(e) => setEndAt(e.target.value)}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Typ
|
|
</label>
|
|
<select
|
|
value={appointmentType}
|
|
onChange={(e) => setAppointmentType(e.target.value)}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400"
|
|
>
|
|
<option value="">Kein Typ</option>
|
|
{APPOINTMENT_TYPES.map((t) => (
|
|
<option key={t.value} value={t.value}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Akte
|
|
</label>
|
|
<select
|
|
value={caseId}
|
|
onChange={(e) => setCaseId(e.target.value)}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400"
|
|
>
|
|
<option value="">Keine Akte</option>
|
|
{cases?.cases?.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.case_number} — {c.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Ort
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={location}
|
|
onChange={(e) => setLocation(e.target.value)}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
|
placeholder="z.B. UPC Munchen, Saal 3"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-600">
|
|
Beschreibung
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
|
placeholder="Optionale Notizen zum Termin"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-2">
|
|
<div>
|
|
{isEdit && (
|
|
<button
|
|
type="button"
|
|
onClick={() => deleteMutation.mutate()}
|
|
disabled={deleteMutation.isPending}
|
|
className="rounded-md px-3 py-1.5 text-sm text-red-600 hover:bg-red-50"
|
|
>
|
|
Loschen
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-50"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isPending || !title.trim() || !startAt}
|
|
className="rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
|
|
>
|
|
{isPending ? "Speichern..." : isEdit ? "Aktualisieren" : "Erstellen"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|