Files
legal-ai/web-ui/src/app/cases/[caseNumber]/page.tsx
Chaim fdb0c98650
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 32s
Lint — undefined names / undefined-names (pull_request) Successful in 20s
fix(hearing): פאנל "מה קרה בדיון" סגור כברירת-מחדל (#226)
עוטף את HearingChangesPanel באקורדיון single/collapsible סגור-כברירת-מחדל,
זהה לפאנל-האח "טיעונים מאוגדים לפי צד" מעליו (הנחיית חיים). הכותרת+תיאור
עוברים לטריגר-האקורדיון; מוסרת הכותרת הכפולה מהרכיב (כפתור-ההרצה נשמר,
מיושר לקצה בתצוגה המלאה). tsc + eslint ירוקים.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 09:13:13 +00:00

249 lines
11 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 { HearingChangesPanel } from "@/components/cases/hearing-changes-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.
// Spread full-width via justify-between (chair request) — wide gaps with few
// tabs, tightening as more are added; gap-2 is the no-overlap floor.
const tabsList = (
<TabsList
variant="line"
className="!h-auto w-full gap-2 p-0 justify-between"
>
{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} />
{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>
{/* מה קרה בדיון — comparative analysis of the hearing protocol vs.
the written pleadings (#226). Sits below the aggregated arguments
because it speaks to those same arguments in their oral gloss.
Collapsed by default, mirroring the "by-party" accordion above. */}
<Accordion type="single" collapsible>
<AccordionItem
value="hearing"
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">
<HearingChangesPanel 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>
);
}