diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index a988724..1e3069c 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -862,13 +862,13 @@ "description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.", "details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.", "testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.", - "status": "in-progress", + "status": "done", "dependencies": [ "87" ], "priority": "medium", "subtasks": [], - "updatedAt": "2026-04-11T17:40:09.247Z" + "updatedAt": "2026-04-11T17:44:08.337Z" }, { "id": "89", @@ -900,9 +900,9 @@ ], "metadata": { "version": "1.0.0", - "lastModified": "2026-04-11T17:40:09.248Z", + "lastModified": "2026-04-11T17:44:08.337Z", "taskCount": 59, - "completedCount": 55, + "completedCount": 56, "tags": [ "master" ] diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx index 2af35cb..4622667 100644 --- a/web-ui/src/app/cases/[caseNumber]/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -12,8 +12,13 @@ import { CaseEditDialog } from "@/components/cases/case-edit-dialog"; import { WorkflowTimeline } from "@/components/cases/workflow-timeline"; import { DocumentsPanel } from "@/components/cases/documents-panel"; import { UploadSheet } from "@/components/documents/upload-sheet"; +import { expectedOutcomes } from "@/lib/schemas/case"; import { useCase } from "@/lib/api/cases"; +const EXPECTED_OUTCOME_LABELS: Record = Object.fromEntries( + expectedOutcomes.map((o) => [o.value, o.label]), +); + /* * Next 16 breaking change: route params are now a Promise. * The `use()` hook unwraps them inside a client component. @@ -25,6 +30,9 @@ export default function CaseDetailPage({ }) { const { caseNumber } = use(params); const { data, isPending, error } = useCase(caseNumber); + const expectedOutcomeLabel = data?.expected_outcome + ? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome + : null; return ( @@ -77,7 +85,7 @@ export default function CaseDetailPage({

תוצאה צפויה

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

@@ -100,18 +108,13 @@ export default function CaseDetailPage({ -
-
- - {data && } -
-

- עריכת פרטי התיק נשמרת מיד דרך PUT /api/cases/{caseNumber}. -

+
+ + {data && }
diff --git a/web-ui/src/components/cases/documents-panel.tsx b/web-ui/src/components/cases/documents-panel.tsx index fa94ead..7695c05 100644 --- a/web-ui/src/components/cases/documents-panel.tsx +++ b/web-ui/src/components/cases/documents-panel.tsx @@ -2,30 +2,68 @@ 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`; +/* + * Document list for the case detail "מסמכים" tab. Uses the real document + * row shape returned by the FastAPI case_get endpoint — see db.list_documents + * and the `documents` schema in legal_mcp/services/db.py: + * id · case_id · doc_type · title · file_path · extraction_status · + * page_count · created_at · practice_area · appeal_subtype + */ + +const DOC_TYPE_LABELS: Record = { + appeal: "כתב ערר", + response: "כתב תשובה", + protocol: "פרוטוקול", + decision: "החלטת ועדה מקומית", + plan: "תכנית", + reference: "חומר רקע", + auto: "—", +}; + +function doctypeLabel(t: string): string { + return DOC_TYPE_LABELS[t] ?? t; } -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"; +function doctypeTone(t: string): string { + switch (t) { + 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"; } } +const STATUS_LABELS: Record = { + pending: "בהמתנה", + processing: "בעיבוד", + completed: "הושלם", + proofread: "הוגה", + failed: "נכשל", + error: "שגיאה", +}; + +function formatDate(iso: string) { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleDateString("he-IL"); + } catch { + return iso; + } +} + +function filenameFromPath(path: string): string { + const parts = path.split("/"); + return parts[parts.length - 1] || path; +} + export function DocumentsPanel({ data }: { data?: CaseDetail }) { const docs = data?.documents ?? []; if (docs.length === 0) { return (
-
+

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

); @@ -34,39 +72,43 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) { return (
    - {docs.map((doc) => ( -
  • -
    -
    - {doc.filename} + {docs.map((doc) => { + const displayName = doc.title || filenameFromPath(doc.file_path); + const statusDone = + doc.extraction_status === "completed" || + doc.extraction_status === "proofread"; + return ( +
  • +
    +
    + {displayName} +
    +
    + {doc.page_count != null && ( + {doc.page_count} עמ׳ + )} + {doc.created_at && {formatDate(doc.created_at)}} + {!statusDone && doc.extraction_status && ( + + {STATUS_LABELS[doc.extraction_status] ?? doc.extraction_status} + + )} +
    -
    - {doc.size_bytes && ( - {formatSize(doc.size_bytes)} - )} - {doc.uploaded_at && ( - - {new Date(doc.uploaded_at).toLocaleDateString("he-IL")} - - )} - {doc.status && doc.status !== "ready" && ( - {doc.status} - )} -
    -
- {doc.category && ( - - {doc.category} - - )} - - ))} + {doc.doc_type && ( + + {doctypeLabel(doc.doc_type)} + + )} + + ); + })} ); diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index d6dc20e..509898b 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -46,15 +46,21 @@ export type Case = { hearing_date?: string | null; }; +export type CaseDocument = { + id: string; + case_id: string; + doc_type: string; + title: string; + file_path: string; + page_count: number | null; + extraction_status: string; + created_at: string; + practice_area?: PracticeArea; + appeal_subtype?: AppealSubtype; +}; + export type CaseDetail = Case & { - documents?: Array<{ - id: number | string; - filename: string; - category?: string | null; - status?: string; - uploaded_at?: string; - size_bytes?: number; - }>; + documents?: CaseDocument[]; blocks?: Array<{ code: string; status?: string; char_count?: number }>; };