feat: Phase C — detail pages for deadlines, appointments, events, creation forms
This commit is contained in:
230
frontend/src/app/(app)/cases/[id]/ereignisse/[eventId]/page.tsx
Normal file
230
frontend/src/app/(app)/cases/[id]/ereignisse/[eventId]/page.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user