"use client"; import { useEffect, useState } from "react"; import { Upload, Loader2, CheckCircle2, AlertCircle, Trash2, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { useQueryClient } from "@tanstack/react-query"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Progress } from "@/components/ui/progress"; import { useUploadPrecedent, useUploadInternalDecision, useDeletePrecedent, useRequestHalachotExtraction, libraryKeys, isCommitteeCitation, COMMITTEE_DISTRICTS, type PracticeArea, type SourceType, type CommitteeDistrict, } from "@/lib/api/precedent-library"; import { ApiError } from "@/lib/api/client"; import { useProgress } from "@/lib/api/documents"; import { PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES, } from "./practice-area"; const ACCEPT = ".pdf,.docx,.doc,.rtf,.txt,.md"; type DuplicateConflict = { error: string; case_law_id: string; citation: string; case_name: string; court: string; date: string | null; halacha_extraction_status: string; }; type Props = { open: boolean; onOpenChange: (open: boolean) => void; }; export function PrecedentUploadSheet({ open, onOpenChange }: Props) { const [file, setFile] = useState(null); const [citation, setCitation] = useState(""); const [caseName, setCaseName] = useState(""); const [court, setCourt] = useState(""); const [decisionDate, setDecisionDate] = useState(""); const [sourceType, setSourceType] = useState(""); const [precedentLevel, setPrecedentLevel] = useState(""); const [practiceArea, setPracticeArea] = useState(""); const [appealSubtype, setAppealSubtype] = useState(""); const [subjectTags, setSubjectTags] = useState(""); const [headnote, setHeadnote] = useState(""); const [isBinding, setIsBinding] = useState(true); const [chairName, setChairName] = useState(""); const [district, setDistrict] = useState(""); const [caseNumber, setCaseNumber] = useState(""); const isCommittee = isCommitteeCitation(citation); const [taskId, setTaskId] = useState(null); const [conflict, setConflict] = useState(null); const upload = useUploadPrecedent(); const uploadInternal = useUploadInternalDecision(); const deletePrecedent = useDeletePrecedent(); const requestHalachot = useRequestHalachotExtraction(); const progress = useProgress(taskId); const qc = useQueryClient(); useEffect(() => { if (open) return; // eslint-disable-next-line react-hooks/set-state-in-effect -- reset form on close setFile(null); setCitation(""); setCaseName(""); setCourt(""); setDecisionDate(""); setSourceType(""); setPrecedentLevel(""); setPracticeArea(""); setAppealSubtype(""); setSubjectTags(""); setHeadnote(""); setIsBinding(true); setTaskId(null); setConflict(null); setChairName(""); setDistrict(""); setCaseNumber(""); }, [open]); useEffect(() => { if (progress?.status === "completed") { qc.invalidateQueries({ queryKey: libraryKeys.all }); toast.success("הפסיקה הוכנסה לקורפוס. ההלכות ממתינות לאישור."); const t = window.setTimeout(() => onOpenChange(false), 1200); return () => window.clearTimeout(t); } if (progress?.status === "failed") { qc.invalidateQueries({ queryKey: libraryKeys.all }); toast.error(`כשל בעיבוד: ${progress.error || "שגיאה לא ידועה"}`); } }, [progress, onOpenChange, qc]); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { toast.error("בחר קובץ"); return; } if (!citation.trim()) { toast.error("מראה המקום (citation) חובה"); return; } setConflict(null); const tags = subjectTags.split(",").map((t) => t.trim()).filter(Boolean); try { if (isCommittee) { if (!caseNumber.trim()) { toast.error("מספר תיק חובה להחלטת ועדת ערר"); return; } if (!chairName.trim()) { toast.error("שם יו\"ר חובה להחלטת ועדת ערר"); return; } if (!district) { toast.error("מחוז חובה להחלטת ועדת ערר"); return; } const res = await uploadInternal.mutateAsync({ file, case_number: caseNumber.trim(), citation: citation.trim(), chair_name: chairName.trim(), district, case_name: caseName.trim(), court: court.trim(), decision_date: decisionDate || undefined, practice_area: practiceArea, appeal_subtype: appealSubtype.trim(), subject_tags: tags, is_binding: false, summary: headnote.trim(), }); setTaskId(res.task_id); return; } const res = await upload.mutateAsync({ file, citation: citation.trim(), case_name: caseName.trim(), court: court.trim(), decision_date: decisionDate || undefined, source_type: sourceType || undefined, precedent_level: precedentLevel || undefined, practice_area: practiceArea, appeal_subtype: appealSubtype.trim(), subject_tags: tags, is_binding: isBinding, headnote: headnote.trim(), }); setTaskId(res.task_id); } catch (err) { if (err instanceof ApiError && err.status === 409) { const body = err.body as { detail?: DuplicateConflict } | null; if (body?.detail?.error === "duplicate_external_upload") { setConflict(body.detail!); return; } } toast.error(err instanceof Error ? err.message : "כשל בהעלאה"); } }; const handleDelete = async () => { if (!conflict) return; if (!window.confirm("למחוק את הרשומה הקיימת? פעולה זו תמחק גם את כל ההלכות שחולצו ממנה.")) return; try { await deletePrecedent.mutateAsync(conflict.case_law_id); toast.success("הרשומה נמחקה — כעת ניתן להעלות מחדש"); setConflict(null); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה במחיקה"); } }; const handleReextract = async () => { if (!conflict) return; try { await requestHalachot.mutateAsync(conflict.case_law_id); toast.success("חילוץ הלכות הועמד בתור — ההלכות החדשות יופיעו בתור האישור"); qc.invalidateQueries({ queryKey: libraryKeys.all }); onOpenChange(false); } catch (e) { toast.error(e instanceof Error ? e.message : "שגיאה בהפעלת חילוץ"); } }; const isProcessing = taskId !== null && progress?.status !== "completed" && progress?.status !== "failed"; const isSubmitting = upload.isPending || uploadInternal.isPending; const stage = (progress as { stage?: string; percent?: number; step?: string } | null)?.stage; const percent = (progress as { percent?: number } | null)?.percent ?? 0; return ( העלאת פסיקה לקורפוס הסמכותי הקובץ יעבור חילוץ טקסט, embeddings, וחילוץ אוטומטי של מטא־דאטה (שם, ערכאה, תאריך, תחום, תת־סוג, תגיות) והלכות. ההלכות ימתינו לאישורך לפני שיהיו זמינות לסוכני הכתיבה. {/* ─── Conflict banner ─── */} {conflict && (

הפסיקה כבר קיימת במאגר

{conflict.citation} {conflict.case_name && ` — ${conflict.case_name}`} {conflict.court && ` · ${conflict.court}`} {conflict.date && ` · ${new Date(conflict.date).toLocaleDateString("he-IL")}`}

{conflict.halacha_extraction_status && (

סטטוס חילוץ הלכות: {conflict.halacha_extraction_status}

)}
)}
{/* File */}
setFile(e.target.files?.[0] ?? null)} disabled={isProcessing} />
{/* Citation */}
{ setCitation(e.target.value); setConflict(null); }} placeholder={`עע"מ 3975/22 ב. קרן-נכסים נ' ועדה מקומית`} disabled={isProcessing} dir="rtl" /> {isCommittee && (

זוהתה כהחלטת ועדת ערר — נדרשים שם יו"ר ומחוז (למטה).

)}
{isCommittee && (
setCaseNumber(e.target.value)} placeholder="8027-25" disabled={isProcessing} dir="rtl" />

המזהה הנקי בלבד (למשל 8027-25) — לא המראה-מקום המלא.

setChairName(e.target.value)} placeholder='עו"ד פלוני אלמוני' disabled={isProcessing} dir="rtl" />
)}
אופציונלי — דריסה ידנית של שדות שיחולצו אוטומטית מהמסמך
setCaseName(e.target.value)} placeholder="ב. קרן-נכסים" disabled={isProcessing} />
setCourt(e.target.value)} placeholder='בית משפט עליון / בג"ץ / מנהלי / ועדת ערר' disabled={isProcessing} />
setDecisionDate(e.target.value)} disabled={isProcessing} />
setAppealSubtype(e.target.value)} placeholder="שימוש חורג / סופיות ההחלטה" disabled={isProcessing} />
{PRACTICE_AREAS.map((a) => ( ))}
{!isCommittee && (
)}
setSubjectTags(e.target.value)} placeholder="חניה, קווי בניין, שיקול דעת" disabled={isProcessing} />