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:
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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<string, string> = 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 (
|
||||
<AppShell>
|
||||
@@ -77,7 +85,7 @@ export default function CaseDetailPage({
|
||||
<div>
|
||||
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
|
||||
<p className="text-ink-soft text-sm leading-relaxed">
|
||||
{data?.expected_outcome ?? "לא נקבעה תוצאה צפויה."}
|
||||
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -100,18 +108,13 @@ export default function CaseDetailPage({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="actions" className="mt-5">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||
<Link href={`/cases/${caseNumber}/compose`}>
|
||||
פתח בעורך ההחלטה
|
||||
</Link>
|
||||
</Button>
|
||||
{data && <CaseEditDialog data={data} />}
|
||||
</div>
|
||||
<p className="text-xs text-ink-muted">
|
||||
עריכת פרטי התיק נשמרת מיד דרך PUT /api/cases/{caseNumber}.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||
<Link href={`/cases/${caseNumber}/compose`}>
|
||||
פתח בעורך ההחלטה
|
||||
</Link>
|
||||
</Button>
|
||||
{data && <CaseEditDialog data={data} />}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -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<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) {
|
||||
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<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 }) {
|
||||
const docs = data?.documents ?? [];
|
||||
|
||||
if (docs.length === 0) {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
@@ -34,39 +72,43 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) {
|
||||
return (
|
||||
<ScrollArea className="max-h-[520px]">
|
||||
<ul className="divide-y divide-rule">
|
||||
{docs.map((doc) => (
|
||||
<li
|
||||
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"
|
||||
>
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<div className="text-ink font-medium truncate" title={doc.filename}>
|
||||
{doc.filename}
|
||||
{docs.map((doc) => {
|
||||
const displayName = doc.title || filenameFromPath(doc.file_path);
|
||||
const statusDone =
|
||||
doc.extraction_status === "completed" ||
|
||||
doc.extraction_status === "proofread";
|
||||
return (
|
||||
<li
|
||||
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"
|
||||
>
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<div className="text-ink font-medium truncate" title={displayName}>
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3 flex-wrap">
|
||||
{doc.page_count != null && (
|
||||
<span className="tabular-nums">{doc.page_count} עמ׳</span>
|
||||
)}
|
||||
{doc.created_at && <span>{formatDate(doc.created_at)}</span>}
|
||||
{!statusDone && doc.extraction_status && (
|
||||
<span className="text-warn">
|
||||
{STATUS_LABELS[doc.extraction_status] ?? doc.extraction_status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3">
|
||||
{doc.size_bytes && (
|
||||
<span className="tabular-nums">{formatSize(doc.size_bytes)}</span>
|
||||
)}
|
||||
{doc.uploaded_at && (
|
||||
<span>
|
||||
{new Date(doc.uploaded_at).toLocaleDateString("he-IL")}
|
||||
</span>
|
||||
)}
|
||||
{doc.status && doc.status !== "ready" && (
|
||||
<span className="text-warn">{doc.status}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{doc.category && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`rounded-full px-2 py-0.5 text-[0.7rem] ${categoryTone(doc.category)}`}
|
||||
>
|
||||
{doc.category}
|
||||
</Badge>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
{doc.doc_type && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`rounded-full px-2 py-0.5 text-[0.7rem] shrink-0 ${doctypeTone(doc.doc_type)}`}
|
||||
>
|
||||
{doctypeLabel(doc.doc_type)}
|
||||
</Badge>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
@@ -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 }>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user