diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 3f65e64..756041a 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -747,6 +747,22 @@ async def update_decision(decision_id: UUID, **fields) -> None: await conn.execute(sql, decision_id, *values) +# ── Document deletion ────────────────────────────────────────────── + +async def delete_document(doc_id: UUID) -> bool: + """Delete a document and all its chunks. Returns True if deleted.""" + pool = await get_pool() + async with pool.acquire() as conn: + async with conn.transaction(): + await conn.execute( + "DELETE FROM document_chunks WHERE document_id = $1", doc_id + ) + result = await conn.execute( + "DELETE FROM documents WHERE id = $1", doc_id + ) + return int(result.split()[-1]) > 0 + + # ── Chunks & Vectors ─────────────────────────────────────────────── async def delete_document_chunks(document_id: UUID) -> int: diff --git a/web-ui/src/components/cases/documents-panel.tsx b/web-ui/src/components/cases/documents-panel.tsx index 13550e6..effe15c 100644 --- a/web-ui/src/components/cases/documents-panel.tsx +++ b/web-ui/src/components/cases/documents-panel.tsx @@ -1,8 +1,29 @@ +"use client"; + +import { useState } from "react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { CheckCircle2, Clock, Loader2, XCircle } from "lucide-react"; -import type { CaseDetail } from "@/lib/api/cases"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + CheckCircle2, + Clock, + Eye, + Loader2, + Trash2, + XCircle, +} from "lucide-react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/api/client"; +import { casesKeys } from "@/lib/api/cases"; +import type { CaseDetail, CaseDocument } from "@/lib/api/cases"; /* * Document list for the case detail "מסמכים" tab. Uses the real document @@ -90,8 +111,218 @@ function filenameFromPath(path: string): string { return parts[parts.length - 1] || path; } -export function DocumentsPanel({ data }: { data?: CaseDetail }) { +/* ── Document text preview dialog ──────────────────────────────── */ + +function DocumentPreviewDialog({ + doc, + open, + onOpenChange, +}: { + doc: CaseDocument; + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const [text, setText] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadText = async () => { + if (text !== null) return; // already loaded + setLoading(true); + setError(null); + try { + const res = await apiRequest<{ text: string }>(`/api/documents/${doc.id}/text`); + setText(res.text || "(ריק)"); + } catch { + setError("לא ניתן לטעון את תוכן המסמך"); + } finally { + setLoading(false); + } + }; + + const handleOpenChange = (v: boolean) => { + if (v) loadText(); + if (!v) { + setText(null); + setError(null); + } + onOpenChange(v); + }; + + const displayName = doc.title || filenameFromPath(doc.file_path); + + return ( + + + + {displayName} + +
+ {loading && ( +
+ + טוען מסמך... +
+ )} + {error && ( +
{error}
+ )} + {text !== null && !loading && ( + +
+                {text}
+              
+
+ )} +
+ +
+
+ ); +} + +/* ── Delete confirmation dialog ────────────────────────────────── */ + +function DeleteConfirmDialog({ + doc, + caseNumber, + open, + onOpenChange, +}: { + doc: CaseDocument; + caseNumber: string; + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const qc = useQueryClient(); + const deleteMutation = useMutation({ + mutationFn: () => + apiRequest(`/api/cases/${caseNumber}/documents/${doc.id}`, { + method: "DELETE", + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: casesKeys.all }); + onOpenChange(false); + }, + }); + + const displayName = doc.title || filenameFromPath(doc.file_path); + + return ( + + + + מחיקת מסמך + +

+ האם למחוק את המסמך “{displayName}”? +
+ פעולה זו אינה ניתנת לביטול. +

+ + + + +
+
+ ); +} + +/* ── Single document row ───────────────────────────────────────── */ + +function DocumentRow({ + doc, + caseNumber, +}: { + doc: CaseDocument; + caseNumber: string; +}) { + const [previewOpen, setPreviewOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const displayName = doc.title || filenameFromPath(doc.file_path); + const canPreview = + doc.extraction_status === "completed" || doc.extraction_status === "proofread"; + + return ( + <> +
  • + + + {doc.doc_type && ( + + {doctypeLabel(doc.doc_type)} + + )} + +
  • + {previewOpen && ( + + )} + {deleteOpen && ( + + )} + + ); +} + +/* ── Main panel ────────────────────────────────────────────────── */ + +export function DocumentsPanel({ + data, +}: { + data?: CaseDetail; +}) { const docs = data?.documents ?? []; + const caseNumber = data?.case_number ?? ""; if (docs.length === 0) { return ( @@ -154,38 +385,11 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) { )} - +
      - {sorted.map((doc) => { - const displayName = doc.title || filenameFromPath(doc.file_path); - return ( -
    • - -
      -
      - {displayName} -
      -
      - {doc.page_count != null && ( - {doc.page_count} עמ׳ - )} - {doc.created_at && {formatDate(doc.created_at)}} -
      -
      - {doc.doc_type && ( - - {doctypeLabel(doc.doc_type)} - - )} -
    • - ); - })} + {sorted.map((doc) => ( + + ))}
    diff --git a/web/app.py b/web/app.py index e352a54..5a93fb2 100644 --- a/web/app.py +++ b/web/app.py @@ -2436,6 +2436,31 @@ async def api_reprocess_document(case_number: str, doc_id: str): return {"status": "reprocessing"} +@app.delete("/api/cases/{case_number}/documents/{doc_id}") +async def api_delete_document(case_number: str, doc_id: str): + """Delete a single document from a case (including its chunks and file).""" + case = await db.get_case_by_number(case_number) + if not case: + raise HTTPException(404, f"תיק {case_number} לא נמצא") + + case_id = UUID(case["id"]) + document_id = UUID(doc_id) + doc = await db.get_document(document_id) + if not doc or UUID(doc["case_id"]) != case_id: + raise HTTPException(404, "מסמך לא נמצא בתיק") + + # Try to remove the physical file + file_path = doc.get("file_path") + if file_path: + import pathlib + p = pathlib.Path(file_path) + if p.exists(): + p.unlink(missing_ok=True) + + await db.delete_document(document_id) + return {"deleted": True, "doc_id": doc_id} + + # ── Chair feedback endpoints ──────────────────────────────────────