"use client"; import { useRef, useState } from "react"; import Link from "next/link"; import { useQueryClient } from "@tanstack/react-query"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { useExports, useExportDocx, useUploadDraft, useMarkFinal, useDeleteDraft, useActiveDraft, useUploadFinalDecision, useRunFinalLearning, useRunFinalHalacha, type ExportFile, } from "@/lib/api/exports"; import { usePartyClaimsSummary, useGeneratePartyClaimsSummary, useGenerateInterimDraft, useGenerateStatus, } from "@/lib/api/generate"; import { GenerateStatusChip } from "@/components/cases/generate-status-chip"; import { useCaseFeedback, useCreateFeedback, useResolveFeedback, CATEGORY_LABELS, CATEGORY_COLORS, BLOCK_LABELS, type FeedbackCategory, } from "@/lib/api/feedback"; import { useCaseCitations } from "@/lib/api/citations"; import { learningKeys } from "@/lib/api/learning"; import { LearningStatusBadges } from "@/components/cases/learning-status-badges"; import { formatDateTime, formatDateShort } from "@/lib/format-date"; import type { CaseStatus } from "@/lib/api/cases"; import { toast } from "sonner"; import { FileText, Download, Upload, Award, Loader2, FileOutput, Plus, Trash2, Brain, Scale, Stamp, Link2, AlertTriangle, Zap, ClipboardList, Files, ExternalLink, } from "lucide-react"; /* Statuses at which a draft is considered ready */ const DRAFT_READY: CaseStatus[] = [ "drafted", "exported", "reviewed", "final", ]; function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; const kb = bytes / 1024; if (kb < 1024) return `${kb.toFixed(0)} KB`; return `${(kb / 1024).toFixed(1)} MB`; } function formatDate(epoch: number): string { return formatDateTime(epoch * 1000); } /** * Pick the newest export file whose name starts with `prefix`, preferring the * highest v-number and falling back to the latest created_at. Used by the * "הפקת מסמכים" card to surface the most recent טיוטה-טענות_הצדדים_* file (#214). */ function pickLatestVersioned( files: ExportFile[] | undefined, prefix: string, ): (ExportFile & { version: number | null }) | null { const matches = (files ?? []).filter((f) => f.filename.startsWith(prefix)); if (!matches.length) return null; const withVer = matches.map((f) => { // Version is the trailing integer, after either "-v" (final) or "_" // (interim: טיוטה-טענות_הצדדים_{N}). Anchor to the end so the "_" inside // the interim prefix isn't mistaken for the version separator. const m = f.filename.match(/[_v](\d+)(?:\.\w+)?$/); return { ...f, version: m ? parseInt(m[1], 10) : null }; }); withVer.sort((a, b) => { if (a.version != null && b.version != null) return b.version - a.version; return b.created_at - a.created_at; }); return withVer[0]; } /* ── Main component ─────────────────────────────────── */ export function DraftsPanel({ caseNumber, status, }: { caseNumber: string; status?: CaseStatus; }) { const { data: exports, isLoading: exportsLoading } = useExports(caseNumber); const { data: feedbacks, isLoading: feedbackLoading } = useCaseFeedback(caseNumber); const { data: activeDraft } = useActiveDraft(caseNumber); const exportDocx = useExportDocx(caseNumber); const uploadDraft = useUploadDraft(caseNumber); const markFinal = useMarkFinal(caseNumber); const deleteDraft = useDeleteDraft(caseNumber); const resolveMutation = useResolveFeedback(); const uploadFinal = useUploadFinalDecision(caseNumber); const runLearning = useRunFinalLearning(caseNumber); const runHalacha = useRunFinalHalacha(caseNumber); // ── Document generation (#214) — the "הפקת מסמכים" card ── const { data: summary } = usePartyClaimsSummary(caseNumber); const genSummary = useGeneratePartyClaimsSummary(caseNumber); const genInterim = useGenerateInterimDraft(caseNumber); // Server-derived background-run status (#227 ג) — survives navigation/reload. const summaryStatus = useGenerateStatus(caseNumber, "party_claims_summary"); const interimStatus = useGenerateStatus(caseNumber, "interim_draft"); const qc = useQueryClient(); const fileRef = useRef(null); const finalFileRef = useRef(null); const [deleteTarget, setDeleteTarget] = useState(null); const isDraftReady = status && DRAFT_READY.includes(status); const openFeedbacks = feedbacks?.filter((f) => !f.resolved) ?? []; // The chair's signed final exists once a "סופי-" file is present (or any is_final). const hasFinal = Boolean( exports?.some((f) => f.is_final || f.filename.startsWith("סופי-")), ); // Determine draft label based on *actual* v-numbers in filenames (not counts). // "(מתוקנת)" suffix appears when there's at least one עריכה-* file. const draftLabel = (() => { if (!exports?.length) return "טיוטה מוכנה לעיון"; const drafts = exports.filter((f) => f.filename.startsWith("טיוטה-")); const revisions = exports.filter((f) => f.filename.startsWith("עריכה-")); if (!drafts.length) return "טיוטה מוכנה לעיון"; const versions = drafts .map((f) => { const m = f.filename.match(/v(\d+)/); return m ? parseInt(m[1], 10) : 0; }) .filter((n) => n > 0); const maxVer = versions.length ? Math.max(...versions) : drafts.length; const suffix = revisions.length > 0 ? " (מתוקנת)" : ""; return `טיוטה v${maxVer}${suffix} מוכנה לעיון`; })(); // ── "הפקת מסמכים" derived state (#214) ── // Latest interim ("party-claims") partial draft in the exports table — the poll // target for button 2. The CEO writes it as טיוטה-טענות_הצדדים_{N}.docx. const latestInterim = pickLatestVersioned(exports, "טיוטה-טענות_הצדדים_"); const summaryReady = Boolean(summary?.markdown); function handleGenerateSummary() { genSummary.mutate(undefined, { onSuccess: (d) => { if (d.status === "ok") { toast.success("הופעלה הפקת סיכום-המנהלים — רצה ברקע (אופוס)"); } else { toast.warning(`לא הופעלה ההפקה: ${d.reason ?? d.error ?? d.status}`); } }, onError: () => toast.error("שגיאה בהפעלת הפקת סיכום-המנהלים"), }); } function handleGenerateInterim() { genInterim.mutate(undefined, { onSuccess: (d) => { if (d.status === "ok") { toast.success("הופעלה הפקת טיוטת טענות-הצדדים — רצה ברקע (אופוס)"); } else { toast.warning(`לא הופעלה ההפקה: ${d.reason ?? d.error ?? d.status}`); } }, onError: () => toast.error("שגיאה בהפעלת הפקת הטיוטה החלקית"), }); } function handleUpload(file: File) { uploadDraft.mutate(file, { onSuccess: (data) => { const added = data.bookmarks_added?.length ?? 0; const missing = data.missing_blocks?.length ?? 0; const fallback = data.structural_fallback?.length ?? 0; const realDetected = added - fallback; if (data.apply_status === "completed" || data.apply_status === "ok") { if (realDetected > 0) { toast.success(`הועלה: ${data.filename} — זוהו ${realDetected} בלוקי תוכן`); } else { toast.success(`הועלה: ${data.filename}`); } if (missing > 0) { toast.warning( `שימו לב: ${missing} בלוקי תוכן לא זוהו — בדוק את הכותרות`, ); } } else { toast.error(`הועלה אך השילוב נכשל: ${data.apply_status ?? "שגיאה"}`); } }, onError: (err) => toast.error(err instanceof Error ? err.message : "שגיאה בהעלאה"), }); } function handleExport() { exportDocx.mutate(undefined, { onSuccess: () => toast.success("הטיוטה יוצאה בהצלחה"), onError: () => toast.error("שגיאה בייצוא"), }); } function handleUploadFinal(file: File) { uploadFinal.mutate(file, { onSuccess: (data) => { toast.success( `ההחלטה הסופית נקלטה — ${data.final_words} מילים (לעומת ${data.draft_words} בטיוטה)`, ); const lib = data.library; if (lib?.enrolled) { toast.success( `נוספה לספריית-הפסיקה · ${lib.linked ?? 0} ציטוטים קושרו · ` + `${lib.missing_flagged ?? 0} חסרים סומנו להעלאה`, ); } else if (lib?.error) { toast.warning(`לא נוספה לספריית-הפסיקה: ${lib.error}`); } }, onError: (err) => toast.error(err instanceof Error ? err.message : "שגיאה בהעלאה"), }); } function handleRunLearning() { runLearning.mutate(undefined, { onSuccess: (d) => { // Background run — refresh the indicator (the poll picks up later transitions). qc.invalidateQueries({ queryKey: learningKeys.caseStatus(caseNumber) }); if (d.status === "ok") { toast.success("למידת-הקול הופעלה — רצה ברקע (אופוס + פאנל דיפסיק/גמיני)"); } else { toast.warning(`לא הופעלה למידה: ${d.reason ?? d.error ?? d.status}`); } }, onError: () => toast.error("שגיאה בהפעלת למידת-הקול"), }); } function handleRunHalacha() { runHalacha.mutate(undefined, { onSuccess: (d) => { qc.invalidateQueries({ queryKey: learningKeys.caseStatus(caseNumber) }); if (d.status === "ok") { toast.success("אימות-ההלכות הופעל — רץ ברקע (פאנל אופוס/דיפסיק/גמיני)"); } else { toast.warning(`לא הופעל אימות: ${d.reason ?? d.error ?? d.status}`); } }, onError: () => toast.error("שגיאה בהפעלת אימות-ההלכות"), }); } function handleMarkFinal(filename: string) { markFinal.mutate(filename, { onSuccess: () => toast.success("סומן כסופי"), onError: () => toast.error("שגיאה בסימון"), }); } function handleResolve(id: string) { // fold=false: resolving from the per-case panel is bookkeeping only. // Folding the lesson into the knowledge files is driven from /feedback. resolveMutation.mutate( { feedbackId: id, applied_to: [], fold: false }, { onSuccess: () => toast.success("ההערה סומנה כמטופלת"), onError: () => toast.error("שגיאה בעדכון"), }, ); } return ( // flex + per-card `order` so the visual order matches mockup 26: // generate-card → banner → active-draft → drafts → final+learning → feedback.
{/* ── Card: document generation (mockup 26) — 3 deterministic "הפק" buttons ── */}

הפקת מסמכים

הפקה דטרמיניסטית בלחיצה — בלי בקשת-טקסט. כל כפתור מריץ פעולה מדויקת, והקובץ מופיע בטבלת-הטיוטות / לקריאה מקומית.

{/* 1 — executive summary of party claims */}
הפק סיכום מנהלים

תמצית מזוקקת של טענות הצדדים לקראת הדיון — מסמך נפרד מהטיוטה.

{summaryReady && ( <> )}
{/* 2 — partial draft (parties' claims, blocks ה–ט) */}
הפק טיוטה של טענות הצדדים{" "} (טיוטה חלקית)

בלוקים ה–ט (רקע, טענות, הליכים, תכניות) — עד הדיון, בלי דיון והכרעה. נשמרת בטבלת-הטיוטות.

{latestInterim && ( {latestInterim.version ? `v${latestInterim.version} · ` : ""} {formatDate(latestInterim.created_at)} )}
{/* 3 — full decision draft (REUSES useExportDocx; moved from the banner) */}
הפק טיוטת החלטה מלאה

ההחלטה המלאה — כל 12 הבלוקים, כולל דיון והכרעה. נקבעת כמקור-האמת ונשמרת בטבלת-הטיוטות.

{!isDraftReady && ( זמין כשיש בלוקים כתובים )}
{/* ── Banner — draft-ready indicator (export button moved to "הפקת מסמכים") ── */} {isDraftReady && (
{draftLabel}
)} {/* ── Active-draft badge — the DOCX that is the current source of truth ── */} {activeDraft?.filename && (
מקור האמת: {activeDraft.filename}
)} {/* ── Chair's signed final decision — clean upload + staged learning pipeline ── */}
{ const f = e.target.files?.[0]; if (f) handleUploadFinal(f); if (finalFileRef.current) finalFileRef.current.value = ""; }} />

החלטה סופית של היו״ר

{hasFinal && ( נקלטה )}

העלאת ההחלטה החתומה של דפנה (נבדל מ״העלה גרסה מתוקנת״). הקליטה פותחת השוואת טיוטה↔סופי; לאחר מכן הפעל את שני השלבים האוטומטיים — הכל רץ ברקע, ורק מחלוקות בין הסוכנים מוסלמות אליך.

{hasFinal && (
סטטוס הריצה — בדף{" "} התפעול
)} {hasFinal && } {/* precedents cited inside the signed decision — sub-section of the final card (mockup 18h), collapsible inside CitationsSection itself */}
{/* ── Card: draft files (mockup 18h) ── */}

קבצי טיוטה

{ const f = e.target.files?.[0]; if (f) handleUpload(f); if (fileRef.current) fileRef.current.value = ""; }} />
{exportsLoading ? (

טוען...

) : !exports?.length ? (

אין טיוטות עדיין.{" "} {!isDraftReady && "הטיוטה תופיע כאן כשתהיה מוכנה."}

) : (
{exports.map((file) => ( ))}
קובץ גודל תאריך
{file.filename} {file.is_final && ( סופי )} {formatSize(file.size)} {formatDate(file.created_at)}
{!file.is_final && ( <> )}
)} {/* Delete confirmation dialog */} !open && setDeleteTarget(null)} > מחיקת טיוטה אישור מחיקת קובץ הטיוטה

למחוק את הקובץ{" "} {deleteTarget}?
פעולה זו לא ניתנת לביטול.

{/* ── Card: chair feedback (mockup 18h) ── */}

הערות יו״ר {openFeedbacks.length > 0 && ( {openFeedbacks.length} פתוחות )}

{feedbackLoading ? (

טוען...

) : !feedbacks?.length ? (

אין הערות לתיק זה.

) : (
{feedbacks.map((fb) => (
{CATEGORY_LABELS[fb.category]} {BLOCK_LABELS[fb.block_id] ?? fb.block_id} {fb.resolved && ( טופל )} {fb.created_at ? formatDateShort(fb.created_at) : ""}

{fb.feedback_text}

{fb.lesson_extracted && (

לקח שהופק:

{fb.lesson_extracted}

)} {!fb.resolved && (
)}
))}
)}
); } /* ── Precedents cited inside the decision ──────────── */ function CitationsSection({ caseNumber }: { caseNumber: string }) { const { data, isLoading } = useCaseCitations(caseNumber); // Nothing to show until the signed decision is in the precedent library. if (isLoading || !data?.in_library) return null; if (!data.linked.length && !data.missing.length) return null; return (

פסיקה שצוטטה בהחלטה

{data.linked.length} בספרייה · {data.missing.length} חסרות
{data.linked.map((c) => ( {c.citation} {c.case_name && ( — {c.case_name} )} בספרייה ))} {data.missing.map((c) => (
{c.citation} {c.flagged ? "חסרה — סומנה להעלאה" : "חסרה בספרייה"}
))}

פסיקה מקושרת מחזקת את ההלכות שלה (corroboration); חסרות מסומנות אוטומטית להעלאה לספרייה.

); } /* ── New feedback dialog (case-scoped) ─────────────── */ function NewCaseFeedbackDialog({ caseNumber }: { caseNumber: string }) { const [open, setOpen] = useState(false); const createMutation = useCreateFeedback(); const [blockId, setBlockId] = useState("block-yod"); const [category, setCategory] = useState("missing_content"); const [feedbackText, setFeedbackText] = useState(""); const [lesson, setLesson] = useState(""); function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!feedbackText.trim()) return; createMutation.mutate( { case_number: caseNumber, block_id: blockId, feedback_text: feedbackText, category, lesson_extracted: lesson || undefined, }, { onSuccess: () => { toast.success("ההערה נרשמה בהצלחה"); setOpen(false); setFeedbackText(""); setLesson(""); }, onError: () => toast.error("שגיאה ברישום ההערה"), }, ); } return ( הערת יו״ר — תיק {caseNumber} הוספת הערת יו״ר על בלוק בהחלטה