Case archive/restore with Paperclip sync
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
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>
This commit is contained in:
120
web-ui/src/components/cases/case-archive-action.tsx
Normal file
120
web-ui/src/components/cases/case-archive-action.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Archive, RotateCcw, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useArchiveCase, useRestoreCase, type ArchiveResult } from "@/lib/api/cases";
|
||||
|
||||
function paperclipMessage(res: ArchiveResult, action: "archive" | "restore"): string {
|
||||
const verb = action === "archive" ? "אורכן" : "שוחזר";
|
||||
switch (res.paperclip?.status) {
|
||||
case "archived":
|
||||
case "restored":
|
||||
return `התיק ${verb}. גם הפרויקט ב-Paperclip ${verb}.`;
|
||||
case "not_found":
|
||||
return `התיק ${verb}. לא נמצא פרויקט מקביל ב-Paperclip.`;
|
||||
case "error":
|
||||
return `התיק ${verb} ב-legal-ai, אך סנכרון Paperclip נכשל${
|
||||
res.paperclip?.message ? `: ${res.paperclip.message}` : "."
|
||||
}`;
|
||||
default:
|
||||
return `התיק ${verb}.`;
|
||||
}
|
||||
}
|
||||
|
||||
export function CaseArchiveAction({
|
||||
caseNumber,
|
||||
archivedAt,
|
||||
}: {
|
||||
caseNumber: string;
|
||||
archivedAt?: string | null;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const archive = useArchiveCase(caseNumber);
|
||||
const restore = useRestoreCase(caseNumber);
|
||||
|
||||
const isArchived = Boolean(archivedAt);
|
||||
const pending = archive.isPending || restore.isPending;
|
||||
|
||||
function handleArchive() {
|
||||
archive.mutate(undefined, {
|
||||
onSuccess: (res) => {
|
||||
setOpen(false);
|
||||
toast.success(paperclipMessage(res, "archive"));
|
||||
},
|
||||
onError: (err) =>
|
||||
toast.error(err instanceof Error ? err.message : "שגיאה בארכוב"),
|
||||
});
|
||||
}
|
||||
|
||||
function handleRestore() {
|
||||
restore.mutate(undefined, {
|
||||
onSuccess: (res) => {
|
||||
toast.success(paperclipMessage(res, "restore"));
|
||||
},
|
||||
onError: (err) =>
|
||||
toast.error(err instanceof Error ? err.message : "שגיאה בשחזור"),
|
||||
});
|
||||
}
|
||||
|
||||
if (isArchived) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pending}
|
||||
onClick={handleRestore}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2 className="me-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="me-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
שחזר מהארכיון
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={pending}>
|
||||
{pending ? (
|
||||
<Loader2 className="me-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Archive className="me-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
ארכן תיק
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>ארכון תיק {caseNumber}</DialogTitle>
|
||||
<DialogDescription>
|
||||
התיק יוסר מרשימת התיקים הראשית ויעבור לעמוד הארכיון. הפרויקט
|
||||
המקביל ב-Paperclip יסומן גם הוא כארכוב. ניתן לשחזר בכל עת.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">בטל</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleArchive} disabled={pending}>
|
||||
{pending ? "מארכן..." : "ארכן"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user