- 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
231 lines
6.5 KiB
TypeScript
231 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams } from "next/navigation";
|
|
import { api } from "@/lib/api";
|
|
import type { CaseEvent, Case } 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,
|
|
FileText,
|
|
Scale,
|
|
ArrowRightLeft,
|
|
Calendar,
|
|
MessageSquare,
|
|
Gavel,
|
|
Info,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
const EVENT_TYPE_CONFIG: Record<
|
|
string,
|
|
{ label: string; icon: typeof Info; color: string }
|
|
> = {
|
|
status_changed: {
|
|
label: "Statusaenderung",
|
|
icon: ArrowRightLeft,
|
|
color: "bg-blue-50 text-blue-700",
|
|
},
|
|
deadline_created: {
|
|
label: "Frist erstellt",
|
|
icon: Calendar,
|
|
color: "bg-amber-50 text-amber-700",
|
|
},
|
|
deadline_completed: {
|
|
label: "Frist erledigt",
|
|
icon: Calendar,
|
|
color: "bg-emerald-50 text-emerald-700",
|
|
},
|
|
document_uploaded: {
|
|
label: "Dokument hochgeladen",
|
|
icon: FileText,
|
|
color: "bg-violet-50 text-violet-700",
|
|
},
|
|
hearing_scheduled: {
|
|
label: "Verhandlung angesetzt",
|
|
icon: Gavel,
|
|
color: "bg-rose-50 text-rose-700",
|
|
},
|
|
note_added: {
|
|
label: "Notiz hinzugefuegt",
|
|
icon: MessageSquare,
|
|
color: "bg-neutral-100 text-neutral-700",
|
|
},
|
|
case_created: {
|
|
label: "Akte erstellt",
|
|
icon: Scale,
|
|
color: "bg-emerald-50 text-emerald-700",
|
|
},
|
|
};
|
|
|
|
const DEFAULT_EVENT_CONFIG = {
|
|
label: "Ereignis",
|
|
icon: Info,
|
|
color: "bg-neutral-100 text-neutral-600",
|
|
};
|
|
|
|
function DetailSkeleton() {
|
|
return (
|
|
<div>
|
|
<Skeleton className="h-4 w-64" />
|
|
<div className="mt-6 space-y-4">
|
|
<Skeleton className="h-8 w-48" />
|
|
<Skeleton className="h-4 w-32" />
|
|
<Skeleton className="h-32 rounded-lg" />
|
|
<Skeleton className="h-48 rounded-lg" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CaseEventDetailPage() {
|
|
const { id: caseId, eventId } = useParams<{
|
|
id: string;
|
|
eventId: string;
|
|
}>();
|
|
|
|
const { data: caseData } = useQuery({
|
|
queryKey: ["case", caseId],
|
|
queryFn: () => api.get<Case>(`/cases/${caseId}`),
|
|
});
|
|
|
|
const {
|
|
data: event,
|
|
isLoading,
|
|
error,
|
|
} = useQuery({
|
|
queryKey: ["case-event", eventId],
|
|
queryFn: () => api.get<CaseEvent>(`/case-events/${eventId}`),
|
|
});
|
|
|
|
if (isLoading) return <DetailSkeleton />;
|
|
|
|
if (error || !event) {
|
|
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">
|
|
Ereignis nicht gefunden
|
|
</p>
|
|
<p className="mt-1 text-sm text-neutral-500">
|
|
Das Ereignis existiert nicht oder Sie haben keine Berechtigung.
|
|
</p>
|
|
<Link
|
|
href={`/cases/${caseId}`}
|
|
className="mt-4 inline-block text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
|
>
|
|
Zurueck zur Akte
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const typeConfig =
|
|
EVENT_TYPE_CONFIG[event.event_type ?? ""] ?? DEFAULT_EVENT_CONFIG;
|
|
const TypeIcon = typeConfig.icon;
|
|
|
|
return (
|
|
<div className="animate-fade-in">
|
|
<Breadcrumb
|
|
items={[
|
|
{ label: "Dashboard", href: "/dashboard" },
|
|
{ label: "Akten", href: "/cases" },
|
|
{
|
|
label: caseData?.case_number
|
|
? `Az. ${caseData.case_number}`
|
|
: "Akte",
|
|
href: `/cases/${caseId}`,
|
|
},
|
|
{ label: "Verlauf", href: `/cases/${caseId}` },
|
|
{ label: event.title },
|
|
]}
|
|
/>
|
|
|
|
{/* Header */}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<div className={`rounded-lg p-2 ${typeConfig.color}`}>
|
|
<TypeIcon className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
{event.title}
|
|
</h1>
|
|
<p className="text-sm text-neutral-500">
|
|
{event.event_date
|
|
? format(parseISO(event.event_date), "d. MMMM yyyy, HH:mm", {
|
|
locale: de,
|
|
})
|
|
: format(parseISO(event.created_at), "d. MMMM yyyy, HH:mm", {
|
|
locale: de,
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{event.description && (
|
|
<div className="mt-4 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">
|
|
{event.description}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Metadata */}
|
|
<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">
|
|
Metadaten
|
|
</p>
|
|
<dl className="mt-2 space-y-1.5">
|
|
<div className="flex gap-2 text-sm">
|
|
<dt className="text-neutral-500">Typ:</dt>
|
|
<dd>
|
|
<span
|
|
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${typeConfig.color}`}
|
|
>
|
|
{typeConfig.label}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
{event.created_by && (
|
|
<div className="flex gap-2 text-sm">
|
|
<dt className="text-neutral-500">Erstellt von:</dt>
|
|
<dd className="text-neutral-900">{event.created_by}</dd>
|
|
</div>
|
|
)}
|
|
<div className="flex gap-2 text-sm">
|
|
<dt className="text-neutral-500">Erstellt am:</dt>
|
|
<dd className="text-neutral-900">
|
|
{format(parseISO(event.created_at), "d. MMMM yyyy, HH:mm", {
|
|
locale: de,
|
|
})}
|
|
</dd>
|
|
</div>
|
|
{event.metadata &&
|
|
Object.keys(event.metadata).length > 0 &&
|
|
Object.entries(event.metadata).map(([key, value]) => (
|
|
<div key={key} className="flex gap-2 text-sm">
|
|
<dt className="text-neutral-500">{key}:</dt>
|
|
<dd className="text-neutral-900">{String(value)}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div className="mt-6">
|
|
<NotesList parentType="case_event" parentId={eventId} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|