Files
legal-ai/web-ui/src/app/cases/[caseNumber]/page.tsx
Chaim 39aff8fd45
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(case-ui): overview body — wider documents column (X17 18j)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:59:17 +00:00

228 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { use, useState } 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 {
Accordion, AccordionContent, AccordionItem, AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { CaseHeader } from "@/components/cases/case-header";
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { DraftsPanel } from "@/components/cases/drafts-panel";
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
import { LegalArgumentsPanel } from "@/components/cases/legal-arguments-panel";
import { PositionsPanel } from "@/components/cases/positions-panel";
import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel";
import { AgentActivityFeed } from "@/components/cases/agent-activity-feed";
import { AgentActivityPreview } from "@/components/cases/agent-activity-preview";
import { UploadSheet } from "@/components/documents/upload-sheet";
import { useCase, useStartWorkflow } from "@/lib/api/cases";
import { toast } from "sonner";
import {
Play, Loader2, LayoutGrid, Scale, BadgeCheck, FileText, MessageSquare, Users,
type LucideIcon,
} from "lucide-react";
/*
* 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 [tab, setTab] = useState("overview");
const { data, isPending, error, refetch } = useCase(caseNumber);
const startWorkflow = useStartWorkflow(caseNumber);
const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready";
// Only take over the whole page when there is NO data to show. A transient
// 5xx on the 5s background refetch must not blow away an already-loaded page.
if (error && !data) {
return (
<AppShell>
<section className="space-y-6">
<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>
<div className="flex items-center justify-center gap-2">
<Button variant="outline" onClick={() => refetch()}>
נסה שוב
</Button>
<Button asChild variant="ghost">
<Link href="/">חזרה לרשימת התיקים</Link>
</Button>
</div>
</CardContent>
</Card>
</section>
</AppShell>
);
}
// Workflow order (X17): intake → arguments+positions → verify citations →
// write decision → drafts/final → agents (monitoring).
const tabDefs: [string, string, LucideIcon][] = [
["overview", "סקירה", LayoutGrid],
["arguments", "טיעונים ועמדות", Scale],
["verify", "אימות פסיקה", BadgeCheck],
["decision", "ההחלטה", FileText],
["drafts", "טיוטות והערות", MessageSquare],
["agents", "סוכנים", Users],
];
// V2 "segmented" tab strip (X17): each tab a pill, active = raised white card.
const tabsList = (
<TabsList
variant="line"
className="!h-auto gap-1.5 p-0 flex-wrap justify-start"
>
{tabDefs.map(([value, label, Icon]) => (
<TabsTrigger
key={value}
value={value}
className="flex-none gap-1.5 rounded-lg border border-transparent px-3.5 py-1.5 text-[0.9rem] font-medium text-ink-soft after:hidden hover:bg-parchment/60 data-active:bg-surface data-active:text-navy data-active:font-semibold data-active:border-rule data-active:shadow-sm [&_svg]:text-ink-light data-active:[&_svg]:text-gold-deep"
>
<Icon className="w-4 h-4" />
{label}
</TabsTrigger>
))}
</TabsList>
);
const bandActions = (
<>
{data && <CaseEditDialog data={data} />}
<UploadSheet caseNumber={caseNumber} />
<Button
className="bg-gold text-white hover:bg-gold-deep border-transparent"
onClick={() => setTab("decision")}
>
פתח עורך החלטה
</Button>
{canStartWorkflow && (
<Button
className="bg-gold-deep hover:bg-gold-deep/90 text-parchment"
disabled={startWorkflow.isPending}
onClick={() =>
startWorkflow.mutate(undefined, {
onSuccess: (res) =>
toast.success(`תהליך הופעל — ${res.issue_identifier}`),
onError: (err) => toast.error(`שגיאה: ${err.message}`),
})
}
>
{startWorkflow.isPending ? (
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
) : (
<Play className="w-4 h-4 me-1.5" />
)}
התחל תהליך
</Button>
)}
</>
);
return (
<AppShell>
<Tabs value={tab} onValueChange={setTab} dir="rtl">
{/* parchment band — header (title/chips/parties/actions) + tab strip */}
{isPending ? (
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6 pb-4 space-y-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-6 w-96" />
</div>
) : (
<CaseHeader caseNumber={caseNumber} data={data} actions={bandActions} tabs={tabsList} />
)}
{/* full-width tab content — status now lives in the always-visible band
StatusRail (banner A, X17), so every tab keeps the status context. */}
<div className="min-w-0 mt-6">
<TabsContent value="overview" className="mt-0">
{/* documents (wider) + agent-activity preview (mockup 18j) */}
<div className="grid gap-6 lg:grid-cols-[1.4fr_1fr] items-start">
<DocumentsPanel data={data} />
<AgentActivityPreview caseNumber={caseNumber} />
</div>
</TabsContent>
{/* טיעונים ועמדות — chair positions (editing) over the analyst's
issues, with the aggregated by-party arguments collapsible below
(merged from the deleted /compose editor, mockup 18f). */}
<TabsContent value="arguments" className="mt-0 space-y-4">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<PositionsPanel caseNumber={caseNumber} />
</CardContent>
</Card>
<Accordion type="single" collapsible>
<AccordionItem
value="byparty"
className="overflow-hidden rounded-xl border border-rule bg-surface shadow-sm"
>
<AccordionTrigger className="px-6 py-4 hover:no-underline">
<span className="flex flex-1 flex-col items-start">
<span className="text-navy text-[0.95rem] font-bold">
טיעונים מאוגדים לפי צד
</span>
<span className="text-ink-muted text-[0.78rem]">
תצוגה מסונתזת של הטענות הגולמיות, מקובצת לפי צד וקדימות
</span>
</span>
</AccordionTrigger>
<AccordionContent className="px-6 pb-5 pt-0">
<LegalArgumentsPanel caseNumber={caseNumber} />
</AccordionContent>
</AccordionItem>
</Accordion>
</TabsContent>
{/* אימות פסיקה — per-argument supporting-precedent verify gate (#154),
relocated from the deleted /compose editor to a top-level tab. */}
<TabsContent value="verify" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<CitationVerificationPanel caseNumber={caseNumber} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="decision" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<DecisionBlocksPanel caseNumber={caseNumber} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="drafts" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<DraftsPanel caseNumber={caseNumber} status={data?.status} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="agents" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<AgentActivityFeed caseNumber={caseNumber} />
</CardContent>
</Card>
</TabsContent>
</div>
</Tabs>
</AppShell>
);
}