- Deadline detail page (/fristen/[id]) with status badge, due date, case context, complete button, and notes - Appointment detail page (/termine/[id]) with datetime, location, type badge, case link, description, and notes - Case event detail page (/cases/[id]/ereignisse/[eventId]) with event type icon, description, metadata, and notes - Standalone deadline creation (/fristen/neu) with case dropdown - Standalone appointment creation (/termine/neu) with optional case - Reusable Breadcrumb component for navigation hierarchy - Reusable NotesList component with inline create/edit/delete - Added Note and RecentActivity types to lib/types.ts
202 lines
6.1 KiB
TypeScript
202 lines
6.1 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { useParams } from "next/navigation";
|
||
import { api } from "@/lib/api";
|
||
import type { Appointment } from "@/lib/types";
|
||
import { Breadcrumb } from "@/components/layout/Breadcrumb";
|
||
import { NotesList } from "@/components/notes/NotesList";
|
||
import { Skeleton } from "@/components/ui/Skeleton";
|
||
import { format, parseISO } from "date-fns";
|
||
import { de } from "date-fns/locale";
|
||
import {
|
||
AlertTriangle,
|
||
Calendar,
|
||
ExternalLink,
|
||
MapPin,
|
||
} from "lucide-react";
|
||
import Link from "next/link";
|
||
|
||
interface AppointmentDetail extends Appointment {
|
||
case_number?: string;
|
||
case_title?: string;
|
||
}
|
||
|
||
const TYPE_LABELS: Record<string, string> = {
|
||
hearing: "Verhandlung",
|
||
meeting: "Besprechung",
|
||
consultation: "Beratung",
|
||
deadline_hearing: "Fristanhoerung",
|
||
other: "Sonstiges",
|
||
};
|
||
|
||
const TYPE_COLORS: Record<string, string> = {
|
||
hearing: "bg-blue-50 text-blue-700",
|
||
meeting: "bg-violet-50 text-violet-700",
|
||
consultation: "bg-emerald-50 text-emerald-700",
|
||
deadline_hearing: "bg-amber-50 text-amber-700",
|
||
other: "bg-neutral-100 text-neutral-600",
|
||
};
|
||
|
||
function DetailSkeleton() {
|
||
return (
|
||
<div>
|
||
<Skeleton className="h-4 w-48" />
|
||
<div className="mt-6 space-y-4">
|
||
<Skeleton className="h-8 w-64" />
|
||
<Skeleton className="h-4 w-40" />
|
||
<Skeleton className="h-32 rounded-lg" />
|
||
<Skeleton className="h-48 rounded-lg" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function AppointmentDetailPage() {
|
||
const { id } = useParams<{ id: string }>();
|
||
|
||
const {
|
||
data: appointment,
|
||
isLoading,
|
||
error,
|
||
} = useQuery({
|
||
queryKey: ["appointment", id],
|
||
queryFn: () => api.get<AppointmentDetail>(`/appointments/${id}`),
|
||
});
|
||
|
||
if (isLoading) return <DetailSkeleton />;
|
||
|
||
if (error || !appointment) {
|
||
return (
|
||
<div className="py-12 text-center">
|
||
<div className="mx-auto mb-3 w-fit rounded-xl bg-red-50 p-3">
|
||
<AlertTriangle className="h-6 w-6 text-red-500" />
|
||
</div>
|
||
<p className="text-sm font-medium text-neutral-900">
|
||
Termin nicht gefunden
|
||
</p>
|
||
<p className="mt-1 text-sm text-neutral-500">
|
||
Der Termin existiert nicht oder Sie haben keine Berechtigung.
|
||
</p>
|
||
<Link
|
||
href="/termine"
|
||
className="mt-4 inline-block text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
||
>
|
||
Zurueck zu Termine
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const startDate = parseISO(appointment.start_at);
|
||
const typeBadge = appointment.appointment_type
|
||
? TYPE_COLORS[appointment.appointment_type] ?? TYPE_COLORS.other
|
||
: null;
|
||
const typeLabel = appointment.appointment_type
|
||
? TYPE_LABELS[appointment.appointment_type] ?? appointment.appointment_type
|
||
: null;
|
||
|
||
return (
|
||
<div className="animate-fade-in">
|
||
<Breadcrumb
|
||
items={[
|
||
{ label: "Dashboard", href: "/dashboard" },
|
||
{ label: "Termine", href: "/termine" },
|
||
{ label: appointment.title },
|
||
]}
|
||
/>
|
||
|
||
{/* Header */}
|
||
<div>
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<h1 className="text-lg font-semibold text-neutral-900">
|
||
{appointment.title}
|
||
</h1>
|
||
{typeBadge && typeLabel && (
|
||
<span
|
||
className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-medium ${typeBadge}`}
|
||
>
|
||
{typeLabel}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Date & Time */}
|
||
<div className="mt-4 rounded-lg border border-neutral-200 bg-white px-4 py-3">
|
||
<div className="flex items-center gap-2">
|
||
<Calendar className="h-4 w-4 text-neutral-400" />
|
||
<span className="text-sm font-medium text-neutral-900">
|
||
{format(startDate, "EEEE, d. MMMM yyyy", { locale: de })}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 pl-6 text-sm text-neutral-600">
|
||
{format(startDate, "HH:mm", { locale: de })} Uhr
|
||
{appointment.end_at && (
|
||
<>
|
||
{" "}
|
||
– {format(parseISO(appointment.end_at), "HH:mm", { locale: de })}{" "}
|
||
Uhr
|
||
</>
|
||
)}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Location */}
|
||
{appointment.location && (
|
||
<div className="mt-3 rounded-lg border border-neutral-200 bg-white px-4 py-3">
|
||
<div className="flex items-center gap-2">
|
||
<MapPin className="h-4 w-4 text-neutral-400" />
|
||
<span className="text-sm text-neutral-900">
|
||
{appointment.location}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Case context */}
|
||
{appointment.case_id && (
|
||
<div className="mt-3 rounded-lg border border-neutral-200 bg-white px-4 py-3">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||
Akte
|
||
</p>
|
||
<p className="mt-0.5 text-sm text-neutral-900">
|
||
{appointment.case_number
|
||
? `Az. ${appointment.case_number}`
|
||
: "Verknuepfte Akte"}
|
||
{appointment.case_title && ` — ${appointment.case_title}`}
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href={`/cases/${appointment.case_id}`}
|
||
className="flex items-center gap-1 text-xs text-neutral-500 transition-colors hover:text-neutral-700"
|
||
>
|
||
Zur Akte
|
||
<ExternalLink className="h-3 w-3" />
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Description */}
|
||
{appointment.description && (
|
||
<div className="mt-3 rounded-lg border border-neutral-200 bg-white px-4 py-3">
|
||
<p className="text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||
Beschreibung
|
||
</p>
|
||
<p className="mt-1 whitespace-pre-wrap text-sm text-neutral-700">
|
||
{appointment.description}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Notes */}
|
||
<div className="mt-6">
|
||
<NotesList parentType="appointment" parentId={id} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|