diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx new file mode 100644 index 0000000..96344d8 --- /dev/null +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -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 ( + +
+ {error ? ( + + +

שגיאה בטעינת התיק

+

{error.message}

+ +
+
+ ) : ( + <> + {isPending ? ( + + + + + + + + ) : ( + + )} + +
+ + + + + סקירה + + מסמכים + {data?.documents && ( + + ({data.documents.length}) + + )} + + פעולות + + + +
+

תוצאה צפויה

+

+ {data?.expected_outcome ?? "לא נקבעה תוצאה צפויה."} +

+
+
+

סיכום מהיר

+
+
מסמכים בתיק
+
+ {data?.documents?.length ?? 0} +
+
בעיבוד
+
+ {data?.processing_count ?? 0} +
+
+
+
+ + + + + + +
+ +

+ מעבר לעורך 12 הבלוקים לכתיבת טיוטה. +

+
+
+
+
+
+ + + +

שלב בתהליך

+ +
+
+
+ + )} +
+
+ ); +} diff --git a/web-ui/src/components/cases/case-header.tsx b/web-ui/src/components/cases/case-header.tsx new file mode 100644 index 0000000..8966990 --- /dev/null +++ b/web-ui/src/components/cases/case-header.tsx @@ -0,0 +1,67 @@ +import Link from "next/link"; +import { Card, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/cases/status-badge"; +import type { CaseDetail } from "@/lib/api/cases"; + +function formatDate(iso?: string | null) { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleDateString("he-IL", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return iso ?? "—"; + } +} + +export function CaseHeader({ data }: { data?: CaseDetail }) { + return ( + + + + +
+
+
+ + ערר {data?.case_number ?? "—"} + + {data?.status && } +
+

+ {data?.title ?? "טוען…"} +

+ {data?.subject && ( +

+ {data.subject} +

+ )} +
+ +
+
+ תאריך דיון +
+
{formatDate(data?.hearing_date)}
+
+ עודכן +
+
{formatDate(data?.updated_at)}
+
+ ועדה +
+
{data?.committee_type ?? "—"}
+
+
+
+
+ ); +} diff --git a/web-ui/src/components/cases/documents-panel.tsx b/web-ui/src/components/cases/documents-panel.tsx new file mode 100644 index 0000000..fa94ead --- /dev/null +++ b/web-ui/src/components/cases/documents-panel.tsx @@ -0,0 +1,73 @@ +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import type { CaseDetail } from "@/lib/api/cases"; + +function formatSize(bytes?: number) { + if (!bytes) return ""; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function categoryTone(category?: string | null) { + switch (category) { + case "appeal": return "bg-info-bg text-info border-info/40"; + case "response": return "bg-gold-wash text-gold-deep border-gold/40"; + case "decision": return "bg-success-bg text-success border-success/40"; + case "protocol": return "bg-warn-bg text-warn border-warn/40"; + default: return "bg-rule-soft text-ink-muted border-rule"; + } +} + +export function DocumentsPanel({ data }: { data?: CaseDetail }) { + const docs = data?.documents ?? []; + + if (docs.length === 0) { + return ( +
+
+

אין מסמכים בתיק זה

+
+ ); + } + + return ( + + + + ); +} diff --git a/web-ui/src/components/cases/workflow-timeline.tsx b/web-ui/src/components/cases/workflow-timeline.tsx new file mode 100644 index 0000000..ffeaf73 --- /dev/null +++ b/web-ui/src/components/cases/workflow-timeline.tsx @@ -0,0 +1,77 @@ +"use client"; + +import type { CaseStatus } from "@/lib/api/cases"; +import { STATUS_LABELS } from "@/components/cases/status-badge"; + +/* + * Vertical RTL workflow timeline showing the 13-status case pipeline. + * Groups the raw statuses into the 5 visual phases used across the app + * (intake → prep → thinking → writing → done) so the user sees + * "where am I in the process" rather than 13 micro-steps. + */ + +type Phase = { + key: string; + label: string; + statuses: CaseStatus[]; +}; + +const PHASES: Phase[] = [ + { key: "intake", label: "קליטה ועיבוד", statuses: ["new", "uploading", "processing"] }, + { key: "prep", label: "הכנת תיק", statuses: ["documents_ready", "outcome_set"] }, + { key: "thinking", label: "ניתוח וכיוון", statuses: ["brainstorming", "direction_approved"] }, + { key: "writing", label: "כתיבת טיוטה", statuses: ["drafting", "qa_review", "drafted"] }, + { key: "done", label: "סגירה", statuses: ["exported", "reviewed", "final"] }, +]; + +function phaseIndexOf(status?: CaseStatus): number { + if (!status) return -1; + return PHASES.findIndex((p) => p.statuses.includes(status)); +} + +export function WorkflowTimeline({ status }: { status?: CaseStatus }) { + const currentIdx = phaseIndexOf(status); + + return ( +
    +
    + {PHASES.map((phase, i) => { + const state = + currentIdx === -1 ? "pending" + : i < currentIdx ? "done" + : i === currentIdx ? "current" + : "pending"; + + const dotTone = + state === "done" ? "bg-success border-success" + : state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]" + : "bg-surface border-rule"; + + const labelTone = + state === "done" ? "text-ink-soft" + : state === "current" ? "text-navy font-semibold" + : "text-ink-muted"; + + return ( +
  1. + +
    + {phase.label} + {state === "current" && status && ( + + {STATUS_LABELS[status]} + + )} +
    +
  2. + ); + })} +
+ ); +} diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index d4bedb4..7a68f58 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -74,10 +74,34 @@ export function useCase(caseNumber: string | undefined) { return useQuery({ queryKey: casesKeys.detail(caseNumber ?? ""), queryFn: ({ signal }) => - apiRequest(`/api/cases/${caseNumber}`, { signal }), + apiRequest(`/api/cases/${caseNumber}/details`, { signal }), enabled: Boolean(caseNumber), /* Replaces the old 5s polling from vanilla index.html */ staleTime: 5_000, refetchInterval: 5_000, }); } + +export type WorkflowStatus = { + case_number?: string; + status?: CaseStatus | string; + current_step?: string; + steps?: Array<{ + key: string; + label?: string; + status: "done" | "current" | "pending" | string; + }>; + /* FastAPI returns free-form JSON; keep it permissive */ + [key: string]: unknown; +}; + +export function useWorkflowStatus(caseNumber: string | undefined) { + return useQuery({ + queryKey: [...casesKeys.all, "workflow", caseNumber ?? ""] as const, + queryFn: ({ signal }) => + apiRequest(`/api/cases/${caseNumber}/status`, { signal }), + enabled: Boolean(caseNumber), + staleTime: 5_000, + refetchInterval: 5_000, + }); +}