233 lines
7.2 KiB
TypeScript
233 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams, usePathname } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { api } from "@/lib/api";
|
|
import type { Case } from "@/lib/types";
|
|
import { Breadcrumb } from "@/components/layout/Breadcrumb";
|
|
import { Skeleton } from "@/components/ui/Skeleton";
|
|
import {
|
|
ArrowLeft,
|
|
Activity,
|
|
Clock,
|
|
FileText,
|
|
Users,
|
|
UserCheck,
|
|
StickyNote,
|
|
AlertTriangle,
|
|
ScrollText,
|
|
} from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
|
|
interface CaseDetail extends Case {
|
|
parties: unknown[];
|
|
deadlines_count: number;
|
|
}
|
|
|
|
const STATUS_BADGE: Record<string, string> = {
|
|
active: "bg-emerald-50 text-emerald-700",
|
|
pending: "bg-amber-50 text-amber-700",
|
|
closed: "bg-neutral-100 text-neutral-600",
|
|
archived: "bg-neutral-100 text-neutral-400",
|
|
};
|
|
|
|
const STATUS_LABEL: Record<string, string> = {
|
|
active: "Aktiv",
|
|
pending: "Anhaengig",
|
|
closed: "Geschlossen",
|
|
archived: "Archiviert",
|
|
};
|
|
|
|
const TABS = [
|
|
{ segment: "verlauf", label: "Verlauf", icon: Activity },
|
|
{ segment: "fristen", label: "Fristen", icon: Clock },
|
|
{ segment: "dokumente", label: "Dokumente", icon: FileText },
|
|
{ segment: "parteien", label: "Parteien", icon: Users },
|
|
{ segment: "mitarbeiter", label: "Mitarbeiter", icon: UserCheck },
|
|
{ segment: "notizen", label: "Notizen", icon: StickyNote },
|
|
{ segment: "protokoll", label: "Protokoll", icon: ScrollText },
|
|
] as const;
|
|
|
|
const TAB_LABELS: Record<string, string> = {
|
|
verlauf: "Verlauf",
|
|
fristen: "Fristen",
|
|
dokumente: "Dokumente",
|
|
parteien: "Parteien",
|
|
mitarbeiter: "Mitarbeiter",
|
|
notizen: "Notizen",
|
|
protokoll: "Protokoll",
|
|
};
|
|
|
|
function CaseDetailSkeleton() {
|
|
return (
|
|
<div>
|
|
<Skeleton className="h-4 w-28" />
|
|
<div className="mt-4 flex items-start justify-between">
|
|
<div>
|
|
<Skeleton className="h-6 w-48" />
|
|
<Skeleton className="mt-2 h-4 w-64" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Skeleton className="h-3 w-24" />
|
|
<Skeleton className="h-3 w-24" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-6 flex gap-4 border-b border-neutral-200 pb-2.5">
|
|
{[1, 2, 3, 4, 5].map((i) => (
|
|
<Skeleton key={i} className="h-4 w-20" />
|
|
))}
|
|
</div>
|
|
<div className="mt-6 space-y-3">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-14 rounded-md" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CaseDetailLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const { id } = useParams<{ id: string }>();
|
|
const pathname = usePathname();
|
|
|
|
const {
|
|
data: caseDetail,
|
|
isLoading,
|
|
error,
|
|
} = useQuery({
|
|
queryKey: ["case", id],
|
|
queryFn: () => api.get<CaseDetail>(`/cases/${id}`),
|
|
});
|
|
|
|
// Determine active tab from pathname
|
|
const segments = pathname.split("/");
|
|
const activeSegment = segments[segments.length - 1] || "verlauf";
|
|
const activeTabLabel = TAB_LABELS[activeSegment];
|
|
|
|
if (isLoading) {
|
|
return <CaseDetailSkeleton />;
|
|
}
|
|
|
|
if (error || !caseDetail) {
|
|
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">
|
|
Akte nicht gefunden
|
|
</p>
|
|
<p className="mt-1 text-sm text-neutral-500">
|
|
Die Akte existiert nicht oder Sie haben keine Berechtigung.
|
|
</p>
|
|
<Link
|
|
href="/cases"
|
|
className="mt-4 inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
|
>
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
Zurueck zu Akten
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const breadcrumbItems = [
|
|
{ label: "Dashboard", href: "/dashboard" },
|
|
{ label: "Akten", href: "/cases" },
|
|
{ label: caseDetail.case_number, href: `/cases/${id}/verlauf` },
|
|
...(activeTabLabel ? [{ label: activeTabLabel }] : []),
|
|
];
|
|
|
|
const partiesCount = Array.isArray(caseDetail.parties)
|
|
? caseDetail.parties.length
|
|
: 0;
|
|
|
|
return (
|
|
<div className="animate-fade-in">
|
|
<Breadcrumb items={breadcrumbItems} />
|
|
|
|
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
{caseDetail.title}
|
|
</h1>
|
|
<span
|
|
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[caseDetail.status] ?? "bg-neutral-100 text-neutral-500"}`}
|
|
>
|
|
{STATUS_LABEL[caseDetail.status] ?? caseDetail.status}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-sm text-neutral-500">
|
|
<span>Az. {caseDetail.case_number}</span>
|
|
{caseDetail.case_type && <span>{caseDetail.case_type}</span>}
|
|
{caseDetail.court && <span>{caseDetail.court}</span>}
|
|
{caseDetail.court_ref && <span>({caseDetail.court_ref})</span>}
|
|
</div>
|
|
</div>
|
|
<div className="text-right text-xs text-neutral-400">
|
|
<p>
|
|
Erstellt:{" "}
|
|
{format(new Date(caseDetail.created_at), "d. MMM yyyy", {
|
|
locale: de,
|
|
})}
|
|
</p>
|
|
<p>
|
|
Aktualisiert:{" "}
|
|
{format(new Date(caseDetail.updated_at), "d. MMM yyyy", {
|
|
locale: de,
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{caseDetail.ai_summary && (
|
|
<div className="mt-4 rounded-md border border-blue-100 bg-blue-50 px-4 py-3 text-sm text-blue-800">
|
|
{caseDetail.ai_summary}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 border-b border-neutral-200">
|
|
<nav className="-mb-px flex gap-1 overflow-x-auto sm:gap-4">
|
|
{TABS.map((tab) => {
|
|
const isActive = activeSegment === tab.segment;
|
|
return (
|
|
<Link
|
|
key={tab.segment}
|
|
href={`/cases/${id}/${tab.segment}`}
|
|
className={`inline-flex shrink-0 items-center gap-1.5 border-b-2 px-1 pb-2.5 text-sm font-medium transition-colors ${
|
|
isActive
|
|
? "border-neutral-900 text-neutral-900"
|
|
: "border-transparent text-neutral-400 hover:text-neutral-600"
|
|
}`}
|
|
>
|
|
<tab.icon className="h-4 w-4" />
|
|
{tab.label}
|
|
{tab.segment === "fristen" &&
|
|
caseDetail.deadlines_count > 0 && (
|
|
<span className="ml-1 rounded-full bg-neutral-100 px-1.5 py-0.5 text-xs text-neutral-500">
|
|
{caseDetail.deadlines_count}
|
|
</span>
|
|
)}
|
|
{tab.segment === "parteien" && partiesCount > 0 && (
|
|
<span className="ml-1 rounded-full bg-neutral-100 px-1.5 py-0.5 text-xs text-neutral-500">
|
|
{partiesCount}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="mt-6">{children}</div>
|
|
</div>
|
|
);
|
|
}
|