import React, { useState, useEffect, useMemo, useRef } from 'react'; import { ExtractedField, FileData } from '../types'; import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, LayoutTemplate, List, Move } from 'lucide-react'; import { createFilledPdf } from '../services/pdfService'; import { jsPDF } from "jspdf"; interface ReviewPanelProps { fields: ExtractedField[]; formFile: FileData; sourceFile: FileData; summary: string; isFillablePdf: boolean; onReset: () => void; } export const ReviewPanel: React.FC = ({ fields: initialFields, formFile, sourceFile, summary, isFillablePdf, onReset }) => { const [fields, setFields] = useState(initialFields); const [activeField, setActiveField] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [filterMode, setFilterMode] = useState<'ALL' | 'ATTENTION'>('ALL'); const [viewMode, setViewMode] = useState<'LIST' | 'FORM'>('LIST'); // Dragging state const [draggingField, setDraggingField] = useState(null); const containerRef = useRef(null); // Derived state for progress const verifiedCount = fields.filter(f => f.isVerified).length; const totalCount = fields.length; const progressPercent = Math.round((verifiedCount / totalCount) * 100); const fieldsRequiresAttention = useMemo(() => fields.filter(f => f.validation?.status !== 'VALID'), [fields]); // Generate preview for PDF download useEffect(() => { let active = true; let timeoutId: ReturnType; const generatePreview = async () => { if (formFile.type === 'application/pdf') { try { const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf); if (!active) return; const blob = new Blob([filledPdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); setPreviewUrl(prev => { if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev); return url; }); } catch (e) { console.error("Failed to generate PDF preview", e); } } else { setPreviewUrl(formFile.previewUrl); } }; // Debounce to avoid excessive PDF generation timeoutId = setTimeout(generatePreview, 600); return () => { active = false; clearTimeout(timeoutId); }; }, [fields, isFillablePdf, formFile]); // Cleanup blob URL on unmount useEffect(() => { return () => { setPreviewUrl(prev => { if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev); return null; }); }; }, []); const handleUpdate = (index: number, newValue: string) => { const newFields = [...fields]; newFields[index] = { ...newFields[index], value: newValue, // Auto-verify when manually edited isVerified: true, validation: { ...newFields[index].validation!, status: 'VALID', message: 'Manually verified' } }; setFields(newFields); }; const handleCoordinateUpdate = (index: number, x: number, y: number) => { const newFields = [...fields]; if (newFields[index].coordinates) { newFields[index] = { ...newFields[index], coordinates: { ...newFields[index].coordinates!, x, y }, isVerified: true // Moving it implies verification }; setFields(newFields); } }; const toggleVerify = (index: number) => { const newFields = [...fields]; newFields[index] = { ...newFields[index], isVerified: !newFields[index].isVerified }; setFields(newFields); }; const applySuggestion = (index: number) => { const field = fields[index]; if (field.validation?.suggestion) { handleUpdate(index, field.validation.suggestion); } }; const handleDownload = async () => { if (formFile.type === 'application/pdf' && previewUrl) { const a = document.createElement('a'); a.href = previewUrl; a.download = `filled_${formFile.file.name}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } else { const doc = new jsPDF(); doc.text("Extracted Data", 20, 20); let y = 40; fields.forEach(f => { doc.text(`${f.label}: ${f.value}`, 20, y); y += 10; }); doc.save("data_report.pdf"); } }; // Drag handlers const handleDragStart = (e: React.MouseEvent, index: number) => { e.stopPropagation(); setDraggingField(index); setActiveField(index); }; const handleMouseMove = (e: React.MouseEvent) => { if (draggingField !== null && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); const relativeX = e.clientX - rect.left; const relativeY = e.clientY - rect.top; // Convert pixels to 0-1000 scale const scaleX = (relativeX / rect.width) * 1000; const scaleY = (relativeY / rect.height) * 1000; // Clamp values const clampedX = Math.max(0, Math.min(1000, scaleX)); const clampedY = Math.max(0, Math.min(1000, scaleY)); handleCoordinateUpdate(draggingField, clampedX, clampedY); } }; const handleMouseUp = () => { setDraggingField(null); }; // Sort: Unverified/Issues first, then verified const displayedFields = fields.map((f, i) => ({ ...f, originalIndex: i })) .sort((a, b) => { // Priority 1: Attention needed const aNeedsAttn = a.validation?.status !== 'VALID'; const bNeedsAttn = b.validation?.status !== 'VALID'; if (aNeedsAttn && !bNeedsAttn) return -1; if (!aNeedsAttn && bNeedsAttn) return 1; // Priority 2: Unverified if (!a.isVerified && b.isVerified) return -1; if (a.isVerified && !b.isVerified) return 1; return a.originalIndex - b.originalIndex; }) .filter(f => filterMode === 'ALL' || (f.validation?.status !== 'VALID' && !f.isVerified)); return (
{/* Header Section */}

Review & Verify

{summary}

{/* View Toggle */}
{/* Verification Progress */}
{verifiedCount} / {totalCount} Verified
{viewMode === 'LIST' ? ( /* ================= LIST VIEW ================= */
{/* Left Column: Preview */}
PDF Preview
{formFile.file.name}
{previewUrl ? ( formFile.type === 'application/pdf' ? (

Unable to display PDF directly.

Download to view
) : (
Form Document
) ) : (

Preview not available

)}
{/* Right Column: Verification List */}

Field Verification

{fieldsRequiresAttention.length > 0 && ( )}
{displayedFields.map((field) => { const idx = field.originalIndex; const status = field.validation?.status || 'VALID'; const isVerified = field.isVerified; let statusBorder = isVerified ? "border-emerald-200" : "border-slate-200"; let statusBg = isVerified ? "bg-emerald-50/30" : "bg-white"; if (!isVerified) { if (status === 'INVALID') { statusBorder = "border-red-200"; statusBg = "bg-red-50/50"; } else if (status === 'WARNING') { statusBorder = "border-amber-200"; statusBg = "bg-amber-50/50"; } } return (
setActiveField(idx)} onBlur={() => setActiveField(null)} >
{!isVerified && status !== 'VALID' && ( {status === 'INVALID' ? : } {status} )}
handleUpdate(idx, e.target.value)} className={`block w-full rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors border ${isVerified ? 'border-emerald-200 text-emerald-900 bg-emerald-50/50' : 'border-slate-300 text-slate-900 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500'}`} placeholder="Empty" />
{(!isVerified && field.validation?.suggestion && status !== 'VALID') && ( )}
); })}
) : ( /* ================= FORM VIEW (Overlay with Drag & Drop) ================= */
{/* Background Image/PDF */} {formFile.previewUrl && ( Form Background )} {/* Fallback info if not visual mode compatible */} {!fields.some(f => f.coordinates) && (

Visual Mode Not Available

Coordinates were not extracted for this document. Please use the List View to edit fields.

)} {/* Overlay Inputs */} {fields.map((field, idx) => { if (!field.coordinates) return null; // Coordinate conversion (0-1000 scale to percentage) const left = (field.coordinates.x / 1000) * 100; const top = (field.coordinates.y / 1000) * 100; const status = field.validation?.status || 'VALID'; let borderColor = 'border-indigo-400 bg-white/60'; if (field.isVerified) borderColor = 'border-emerald-500 bg-emerald-50/70'; else if (status === 'INVALID') borderColor = 'border-red-500 bg-red-50/70'; else if (status === 'WARNING') borderColor = 'border-amber-500 bg-amber-50/70'; const isDragging = draggingField === idx; const isCheckbox = field.value === 'X'; return (
handleDragStart(e, idx)} >
{/* Drag Handle (Visible on Hover) */}
{isCheckbox ? (
X
) : ( handleUpdate(idx, e.target.value)} onFocus={() => setActiveField(idx)} className={` w-full px-1 py-0.5 text-xs font-medium border-2 rounded transition-all shadow-sm focus:ring-2 focus:ring-offset-1 focus:z-10 focus:bg-white ${borderColor} `} style={{ fontFamily: 'Courier, monospace', color: 'black', background: 'rgba(255, 255, 255, 0.7)' }} /> )} {/* Validation Icon Overlay */} {!field.isVerified && status !== 'VALID' && (
{status === 'INVALID' ? : }
)} {/* Tooltip on Hover */}
{field.label}
); })}
)}
); };