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.",
|
"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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user