Files
legal-ai/web-ui/src/app/cases/[caseNumber]/page.tsx
Chaim f1e9103723
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s
feat(ui): WS6 — party accordion, full-width compose, status-hero overview (#206)
שלושת שינויי-ה-UI של WS6 לפי המוקאפים המאושרים (25b/03b/18b, X17):

1. טאב "טיעונים" (legal-arguments-panel) — אקורדיון ברמת-הצד, סגור
   כברירת-מחדל (type="multiple" ללא defaultValue). כל trigger מציג
   שם-צד + תת-כותרת + באדג'-מונה. בתוך כל צד נשמר הקיבוץ-לפי-קדימות
   והאקורדיון הפנימי ברמת-הטיעון.

2. טאב "עמדות וטענות" (compose) — רוחב-דף מלא: בוטל ה-grid 1fr/320px,
   ראיל "מסמכי התיק" הוסר (DocumentsPanel בסקירה הוא הבעלים היחיד)
   ועבר לפס-מסמכים מתקפל למעלה (סגור כברירת-מחדל). כרטיסי-הסוגיות
   2-up. פסיקה-מצורפת ושערי-הייצוא/העלאה שומרו (relocate, לא remove).
   מפת-StatusChip המקבילה הוסרה → StatusBadge המשותף (G2).

3. סטטוס רק בסקירה (overview) — StatusHero חדש (stepper אופקי + chip
   שלב-נוכחי + שינוי-סטטוס ידני + meta-strip), הבעלים היחיד של
   הסטטוס. WorkflowTimeline/StatusChanger/AgentStatusWidget ירדו משאר
   הטאבים; הטאבים האחרים רוחב-מלא. StatusBadge read-only נשאר ב-H1.

Invariants: INV-IA1 (בעלים-יחיד לסטטוס=hero), INV-IA3/G10 (כל שער-אנוש
נשמר במקום אחד — שינוי-סטטוס בסקירה, ייצוא/העלאה ב-compose),
INV-IA4 (compose=מרחב-עבודה ממוקד-משימה רוחב-מלא), INV-IA6 (מפת-סטטוסים
progressive-disclosure), INV-UI7/UI8, G2 (StatusBadge משותף, אין
מפת-סטטוס מקבילה).

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

181 lines
6.9 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 } 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 { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { StatusHero } from "@/components/cases/status-hero";
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 { 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 } 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 { 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>
);
}
const tabsList = (
<TabsList
variant="line"
className="gap-6 h-auto p-0 rounded-none -mb-px"
>
{[
["overview", "סקירה"],
["arguments", "טיעונים"],
["decision", "ההחלטה"],
["drafts", "טיוטות והערות"],
["agents", "סוכנים"],
].map(([value, label]) => (
<TabsTrigger
key={value}
value={value}
className="flex-none rounded-none px-0 pb-3.5 pt-0 text-[0.92rem] font-medium text-ink-muted data-active:text-navy data-active:font-semibold data-active:after:bg-gold data-active:after:bottom-0"
>
{label}
</TabsTrigger>
))}
</TabsList>
);
const bandActions = (
<>
{data && <CaseEditDialog data={data} />}
<UploadSheet caseNumber={caseNumber} />
<Button asChild className="bg-gold text-white hover:bg-gold-deep border-transparent">
<Link href={`/cases/${caseNumber}/compose`}>פתח עורך החלטה</Link>
</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 defaultValue="overview" 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 data={data} actions={bandActions} tabs={tabsList} />
)}
{/* full-width tab content — status lives ONLY in the overview hero
(mockup 18b); the other tabs lose the status rail entirely (INV-IA1). */}
<div className="min-w-0 mt-6">
<TabsContent value="overview" className="mt-0 space-y-6">
{/* ★ STATUS HERO — the single home for case status (INV-IA1/INV-UI7) */}
<StatusHero caseNumber={caseNumber} data={data} />
{/* documents + agents two-column below the hero (mockup 18b) */}
<div className="grid gap-6 lg:grid-cols-2 items-start">
<DocumentsPanel data={data} />
<AgentActivityPreview caseNumber={caseNumber} />
</div>
{/* decision-editor CTA moved to the band actions (visible on all tabs) */}
</TabsContent>
<TabsContent value="arguments" className="mt-0">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<LegalArgumentsPanel 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>
);
}