Files
legal-ai/web-ui/src/app/archive/page.tsx
Chaim 2b7f291928
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
Case archive/restore with Paperclip sync
Adds a comprehensive archive flow for closed cases — separate /archive
screen in the UI, archive/restore actions on the case detail page, and
automatic two-way sync with Paperclip.

Backend (web/app.py + mcp-server/services/db.py):
- New SCHEMA_V6 migration: cases.archived_at TIMESTAMPTZ + partial index
- list_cases gains include_archived/archived_only flags; default excludes
  archived rows so the main /api/cases list hides closed cases
- archive_case / restore_case helpers in db.py
- POST /api/cases/{n}/archive sets archived_at and calls
  pc_archive_project (sets Paperclip projects.archived_at via direct DB)
- POST /api/cases/{n}/restore clears archived_at and calls
  pc_restore_project (clears Paperclip archived_at)
- archive_project / restore_project in paperclip_client.py — name-based
  match consistent with create_project's lookup

Frontend (web-ui):
- cases.ts: scope param ("active"|"archived"|"all") on useCases;
  useArchiveCase / useRestoreCase mutations
- /archive page (new): table of archived cases with restore button +
  search, sort, empty state matching the editorial aesthetic of /
- case-archive-action.tsx: button on case detail header. Active case →
  confirm dialog → archive. Archived case → restore (no confirm).
  Toast announces both legal-ai and Paperclip outcomes (synced, not
  found in pc, error)
- case-header shows "בארכיון" badge when archived_at is set
- Nav: ארכיון link added to AppShell after בית

Tested end-to-end against the live DB:
- 1130-25 archive → list_cases(include_archived=False) excludes it,
  list_cases(archived_only=True) includes it, restore reverses
- pc archive/restore on 1194-25 verified via direct DB lookup
- TypeScript compiles clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:54:52 +00:00

269 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type SortingState,
} from "@tanstack/react-table";
import { toast } from "sonner";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useCases, useRestoreCase, type Case } from "@/lib/api/cases";
import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area";
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;
}
}
function RestoreButton({ caseNumber }: { caseNumber: string }) {
const restore = useRestoreCase(caseNumber);
return (
<Button
variant="outline"
size="sm"
disabled={restore.isPending}
onClick={() => {
restore.mutate(undefined, {
onSuccess: (res) => {
const pc = res.paperclip?.status;
if (pc === "restored") {
toast.success(`התיק שוחזר. גם הפרויקט ב-Paperclip שוחזר.`);
} else if (pc === "not_found") {
toast.success("התיק שוחזר. לא נמצא פרויקט מתאים ב-Paperclip.");
} else if (pc === "error") {
toast.warning(
`התיק שוחזר ב-legal-ai, אך סנכרון Paperclip נכשל: ${res.paperclip?.message ?? ""}`,
);
} else {
toast.success("התיק שוחזר.");
}
},
onError: (err) =>
toast.error(err instanceof Error ? err.message : "שגיאה בשחזור"),
});
}}
>
{restore.isPending ? "משחזר..." : "שחזר"}
</Button>
);
}
const columns: ColumnDef<Case>[] = [
{
accessorKey: "case_number",
header: "מס׳ ערר",
cell: ({ row }) => (
<Link
href={`/cases/${row.original.case_number}`}
className="text-navy font-semibold hover:text-gold-deep tabular-nums"
>
{row.original.case_number}
</Link>
),
},
{
accessorKey: "title",
header: "כותרת",
cell: ({ row }) => (
<div className="text-ink max-w-[420px] truncate" title={row.original.title}>
{row.original.title}
</div>
),
},
{
accessorKey: "appeal_subtype",
header: "תחום",
cell: ({ row }) => {
const s = row.original.appeal_subtype;
if (!s || s === "unknown")
return <span className="text-ink-muted"></span>;
return (
<span className="text-ink-soft text-sm">{APPEAL_SUBTYPE_LABELS[s]}</span>
);
},
},
{
accessorKey: "archived_at",
header: "תאריך ארכוב",
cell: ({ row }) => (
<span className="text-ink-muted text-sm tabular-nums">
{formatDate(row.original.archived_at)}
</span>
),
},
{
id: "actions",
header: "",
cell: ({ row }) => <RestoreButton caseNumber={row.original.case_number} />,
},
];
export default function ArchivePage() {
const { data, isPending, error } = useCases(true, "archived");
const [sorting, setSorting] = useState<SortingState>([
{ id: "archived_at", desc: true },
]);
const [globalFilter, setGlobalFilter] = useState("");
const rows = useMemo(() => data ?? [], [data]);
const table = useReactTable({
data: rows,
columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: (row, _colId, filterValue: string) => {
if (!filterValue) return true;
const needle = filterValue.toLowerCase();
return (
row.original.case_number.toLowerCase().includes(needle) ||
row.original.title.toLowerCase().includes(needle)
);
},
});
return (
<AppShell>
<section className="space-y-8">
<header className="space-y-1.5">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
ארכיון תיקי ערר
</div>
<h1 className="text-navy">תיקים סגורים</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
תיקים שסגרו את הטיפול בהם. שחזור מחזיר את התיק לרשימה הראשית
ופותח מחדש את הפרויקט המקביל ב-Paperclip.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3">
<Input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="חיפוש לפי מס׳ ערר או כותרת…"
className="max-w-sm bg-surface"
dir="rtl"
/>
<span className="text-sm text-ink-muted me-auto">
{table.getFilteredRowModel().rows.length} תיקים בארכיון
</span>
</div>
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-rule-soft/60">
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="border-rule">
{hg.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="text-navy font-semibold cursor-pointer select-none text-right"
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{ asc: " ▲", desc: " ▼" }[
header.column.getIsSorted() as string
] ?? ""}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isPending ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i} className="border-rule">
{columns.map((_c, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))
) : error ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-danger py-8"
>
שגיאה בטעינת ארכיון: {error.message}
</TableCell>
</TableRow>
) : table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-ink-muted py-12"
>
<div className="text-gold text-2xl mb-2" aria-hidden>
</div>
{globalFilter
? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"}
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="border-rule hover:bg-gold-wash/40 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</section>
</AppShell>
);
}