Phase 3b: Case Detail view with workflow timeline

New dynamic route /cases/[caseNumber] wired to the FastAPI details
endpoint, using Next 16's async params pattern with React's use()
hook. TanStack Query handles 5s refetchInterval so the page
self-updates during long-running processing without manual polling.

New components: CaseHeader (breadcrumb + meta), WorkflowTimeline
(5-phase RTL pipeline view), DocumentsPanel (categorized list).
Tabs split overview/documents/actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 16:06:27 +00:00
parent 51064f3a03
commit d0daa0efe8
5 changed files with 367 additions and 1 deletions

View File

@@ -0,0 +1,125 @@
"use client";
import { use } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { CaseHeader } from "@/components/cases/case-header";
import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { useCase } from "@/lib/api/cases";
/*
* Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component.
*/
export default function CaseDetailPage({
params,
}: {
params: Promise<{ caseNumber: string }>;
}) {
const { caseNumber } = use(params);
const { data, isPending, error } = useCase(caseNumber);
return (
<AppShell>
<section className="space-y-6">
{error ? (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center space-y-3">
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
<p className="text-sm text-ink-muted">{error.message}</p>
<Button asChild variant="outline">
<Link href="/">חזרה לרשימת התיקים</Link>
</Button>
</CardContent>
</Card>
) : (
<>
{isPending ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-6 w-96" />
</CardContent>
</Card>
) : (
<CaseHeader data={data} />
)}
<div className="grid gap-6 lg:grid-cols-[1fr_280px]">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<Tabs defaultValue="overview" dir="rtl">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="overview">סקירה</TabsTrigger>
<TabsTrigger value="documents">
מסמכים
{data?.documents && (
<span className="ms-1.5 text-[0.7rem] text-ink-muted tabular-nums">
({data.documents.length})
</span>
)}
</TabsTrigger>
<TabsTrigger value="actions">פעולות</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-5 space-y-4">
<div>
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
<p className="text-ink-soft text-sm leading-relaxed">
{data?.expected_outcome ?? "לא נקבעה תוצאה צפויה."}
</p>
</div>
<div>
<h3 className="text-navy text-base mb-2">סיכום מהיר</h3>
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
<dt className="text-ink-muted">מסמכים בתיק</dt>
<dd className="text-ink tabular-nums">
{data?.documents?.length ?? 0}
</dd>
<dt className="text-ink-muted">בעיבוד</dt>
<dd className="text-ink tabular-nums">
{data?.processing_count ?? 0}
</dd>
</dl>
</div>
</TabsContent>
<TabsContent value="documents" className="mt-5">
<DocumentsPanel data={data} />
</TabsContent>
<TabsContent value="actions" className="mt-5">
<div className="space-y-3">
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href={`/cases/${caseNumber}/compose`}>
פתח בעורך ההחלטה
</Link>
</Button>
<p className="text-xs text-ink-muted">
מעבר לעורך 12 הבלוקים לכתיבת טיוטה.
</p>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm h-fit">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
<WorkflowTimeline status={data?.status} />
</CardContent>
</Card>
</div>
</>
)}
</section>
</AppShell>
);
}