Fix case detail: document fields, expected-outcome label, drop debug note

Three user-reported bugs on /cases/[caseNumber]:

1. Documents tab showed "9 מסמכים" in the count but rendered nothing —
   DocumentsPanel was reading filename/category/status/size_bytes/uploaded_at,
   but the real FastAPI payload (case_get → db.list_documents) returns
   title/doc_type/extraction_status/page_count/created_at. Rewrote the
   panel against the actual document row shape, added a CaseDocument
   type alias in lib/api/cases.ts, mapped doc_type to Hebrew labels
   (כתב ערר / כתב תשובה / ...) and extraction_status likewise.

2. The "פעולות" tab showed a debug-flavoured paragraph "עריכת פרטי התיק
   נשמרת מיד דרך PUT /api/cases/1033-25" — that was internal wording,
   not user copy. Removed.

3. Overview tab showed the raw enum value "full_acceptance" in the
   expected-outcome line. Mapped through the existing expectedOutcomes
   label array so it now reads "קבלה מלאה".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 18:00:24 +00:00
parent cbe9d60901
commit 916360e9b2
4 changed files with 121 additions and 70 deletions

View File

@@ -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.", "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.", "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.", "testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
"status": "in-progress", "status": "done",
"dependencies": [ "dependencies": [
"87" "87"
], ],
"priority": "medium", "priority": "medium",
"subtasks": [], "subtasks": [],
"updatedAt": "2026-04-11T17:40:09.247Z" "updatedAt": "2026-04-11T17:44:08.337Z"
}, },
{ {
"id": "89", "id": "89",
@@ -900,9 +900,9 @@
], ],
"metadata": { "metadata": {
"version": "1.0.0", "version": "1.0.0",
"lastModified": "2026-04-11T17:40:09.248Z", "lastModified": "2026-04-11T17:44:08.337Z",
"taskCount": 59, "taskCount": 59,
"completedCount": 55, "completedCount": 56,
"tags": [ "tags": [
"master" "master"
] ]

View File

@@ -12,8 +12,13 @@ import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { WorkflowTimeline } from "@/components/cases/workflow-timeline"; import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { DocumentsPanel } from "@/components/cases/documents-panel"; import { DocumentsPanel } from "@/components/cases/documents-panel";
import { UploadSheet } from "@/components/documents/upload-sheet"; import { UploadSheet } from "@/components/documents/upload-sheet";
import { expectedOutcomes } from "@/lib/schemas/case";
import { useCase } from "@/lib/api/cases"; import { useCase } from "@/lib/api/cases";
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
expectedOutcomes.map((o) => [o.value, o.label]),
);
/* /*
* Next 16 breaking change: route params are now a Promise. * Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component. * The `use()` hook unwraps them inside a client component.
@@ -25,6 +30,9 @@ export default function CaseDetailPage({
}) { }) {
const { caseNumber } = use(params); const { caseNumber } = use(params);
const { data, isPending, error } = useCase(caseNumber); const { data, isPending, error } = useCase(caseNumber);
const expectedOutcomeLabel = data?.expected_outcome
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
: null;
return ( return (
<AppShell> <AppShell>
@@ -77,7 +85,7 @@ export default function CaseDetailPage({
<div> <div>
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3> <h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
<p className="text-ink-soft text-sm leading-relaxed"> <p className="text-ink-soft text-sm leading-relaxed">
{data?.expected_outcome ?? "לא נקבעה תוצאה צפויה."} {expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
</p> </p>
</div> </div>
<div> <div>
@@ -100,8 +108,7 @@ export default function CaseDetailPage({
</TabsContent> </TabsContent>
<TabsContent value="actions" className="mt-5"> <TabsContent value="actions" className="mt-5">
<div className="space-y-4"> <div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-3">
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment"> <Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href={`/cases/${caseNumber}/compose`}> <Link href={`/cases/${caseNumber}/compose`}>
פתח בעורך ההחלטה פתח בעורך ההחלטה
@@ -109,10 +116,6 @@ export default function CaseDetailPage({
</Button> </Button>
{data && <CaseEditDialog data={data} />} {data && <CaseEditDialog data={data} />}
</div> </div>
<p className="text-xs text-ink-muted">
עריכת פרטי התיק נשמרת מיד דרך PUT /api/cases/{caseNumber}.
</p>
</div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</CardContent> </CardContent>

View File

@@ -2,15 +2,30 @@ import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import type { CaseDetail } from "@/lib/api/cases"; import type { CaseDetail } from "@/lib/api/cases";
function formatSize(bytes?: number) { /*
if (!bytes) return ""; * Document list for the case detail "מסמכים" tab. Uses the real document
if (bytes < 1024) return `${bytes} B`; * row shape returned by the FastAPI case_get endpoint — see db.list_documents
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; * and the `documents` schema in legal_mcp/services/db.py:
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; * id · case_id · doc_type · title · file_path · extraction_status ·
* page_count · created_at · practice_area · appeal_subtype
*/
const DOC_TYPE_LABELS: Record<string, string> = {
appeal: "כתב ערר",
response: "כתב תשובה",
protocol: "פרוטוקול",
decision: "החלטת ועדה מקומית",
plan: "תכנית",
reference: "חומר רקע",
auto: "—",
};
function doctypeLabel(t: string): string {
return DOC_TYPE_LABELS[t] ?? t;
} }
function categoryTone(category?: string | null) { function doctypeTone(t: string): string {
switch (category) { switch (t) {
case "appeal": return "bg-info-bg text-info border-info/40"; case "appeal": return "bg-info-bg text-info border-info/40";
case "response": return "bg-gold-wash text-gold-deep border-gold/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 "decision": return "bg-success-bg text-success border-success/40";
@@ -19,13 +34,36 @@ function categoryTone(category?: string | null) {
} }
} }
const STATUS_LABELS: Record<string, string> = {
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 }) { export function DocumentsPanel({ data }: { data?: CaseDetail }) {
const docs = data?.documents ?? []; const docs = data?.documents ?? [];
if (docs.length === 0) { if (docs.length === 0) {
return ( return (
<div className="text-center py-12 text-ink-muted"> <div className="text-center py-12 text-ink-muted">
<div className="text-gold text-2xl mb-2" aria-hidden></div> <div className="text-gold text-2xl mb-2" aria-hidden="true"></div>
<p className="text-sm">אין מסמכים בתיק זה</p> <p className="text-sm">אין מסמכים בתיק זה</p>
</div> </div>
); );
@@ -34,39 +72,43 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) {
return ( return (
<ScrollArea className="max-h-[520px]"> <ScrollArea className="max-h-[520px]">
<ul className="divide-y divide-rule"> <ul className="divide-y divide-rule">
{docs.map((doc) => ( {docs.map((doc) => {
const displayName = doc.title || filenameFromPath(doc.file_path);
const statusDone =
doc.extraction_status === "completed" ||
doc.extraction_status === "proofread";
return (
<li <li
key={doc.id} key={doc.id}
className="py-3 flex items-start justify-between gap-4 hover:bg-gold-wash/30 transition-colors px-2 -mx-2 rounded" className="py-3 flex items-start justify-between gap-4 hover:bg-gold-wash/30 transition-colors px-2 -mx-2 rounded"
> >
<div className="flex-1 min-w-0 space-y-0.5"> <div className="flex-1 min-w-0 space-y-0.5">
<div className="text-ink font-medium truncate" title={doc.filename}> <div className="text-ink font-medium truncate" title={displayName}>
{doc.filename} {displayName}
</div> </div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3"> <div className="text-[0.72rem] text-ink-muted flex items-center gap-3 flex-wrap">
{doc.size_bytes && ( {doc.page_count != null && (
<span className="tabular-nums">{formatSize(doc.size_bytes)}</span> <span className="tabular-nums">{doc.page_count} עמ׳</span>
)} )}
{doc.uploaded_at && ( {doc.created_at && <span>{formatDate(doc.created_at)}</span>}
<span> {!statusDone && doc.extraction_status && (
{new Date(doc.uploaded_at).toLocaleDateString("he-IL")} <span className="text-warn">
{STATUS_LABELS[doc.extraction_status] ?? doc.extraction_status}
</span> </span>
)} )}
{doc.status && doc.status !== "ready" && (
<span className="text-warn">{doc.status}</span>
)}
</div> </div>
</div> </div>
{doc.category && ( {doc.doc_type && (
<Badge <Badge
variant="outline" variant="outline"
className={`rounded-full px-2 py-0.5 text-[0.7rem] ${categoryTone(doc.category)}`} className={`rounded-full px-2 py-0.5 text-[0.7rem] shrink-0 ${doctypeTone(doc.doc_type)}`}
> >
{doc.category} {doctypeLabel(doc.doc_type)}
</Badge> </Badge>
)} )}
</li> </li>
))} );
})}
</ul> </ul>
</ScrollArea> </ScrollArea>
); );

View File

@@ -46,15 +46,21 @@ export type Case = {
hearing_date?: string | null; 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 & { export type CaseDetail = Case & {
documents?: Array<{ documents?: CaseDocument[];
id: number | string;
filename: string;
category?: string | null;
status?: string;
uploaded_at?: string;
size_bytes?: number;
}>;
blocks?: Array<{ code: string; status?: string; char_count?: number }>; blocks?: Array<{ code: string; status?: string; char_count?: number }>;
}; };