Files
KanzlAI-mGMT/frontend/src/app/(app)/cases/[id]/layout.tsx

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>
);
}