import { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ScanLine, StopCircle, CheckCircle2, AlertTriangle } from 'lucide-react'; import { useEmailScan } from '@/hooks/useEmailScan'; import { useScanEvents } from '@/hooks/useRealtime'; import type { ScanPreset } from '@icc/shared'; interface LogEntry { id: string; time: string; type: 'found' | 'duplicate' | 'info' | 'error'; sender?: string; amount?: string; ref?: string; branch?: string; message?: string; } interface ScanModalProps { onClose: () => void; onMinimize: () => void; preset?: ScanPreset; forceRescan?: boolean; startDate?: string; endDate?: string; } function formatTime(date?: Date): string { const d = date ?? new Date(); return d.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } export default function ScanModal({ onClose, onMinimize, preset = 'today', forceRescan = false, startDate, endDate }: ScanModalProps) { const { t } = useTranslation(); const { startScan, activeJobId, scanStatus, scanError, isStarting } = useEmailScan(); const [logEntries, setLogEntries] = useState([]); const [stats, setStats] = useState({ processed: 0, skipped: 0, errored: 0, total: 0 }); const [status, setStatus] = useState<'starting' | 'scanning' | 'completed' | 'failed'>('starting'); const [dateRange, setDateRange] = useState<{ startDate?: string; endDate?: string }>({}); const logContainerRef = useRef(null); const scanStarted = useRef(false); // Start scan on mount useEffect(() => { if (scanStarted.current) return; scanStarted.current = true; startScan(preset, { forceRescan, startDate, endDate }); }, [preset, forceRescan, startScan]); // Add log entry helper const addLog = useCallback((entry: Omit) => { setLogEntries(prev => { const newEntries = [{ ...entry, id: `${Date.now()}_${Math.random().toString(36).slice(2)}` }, ...prev]; return newEntries.slice(0, 50); }); }, []); // Listen to WebSocket events useScanEvents({ onStarted: useCallback((data: any) => { setStatus('scanning'); setStats(prev => ({ ...prev, total: data.newEmails ?? 0, skipped: data.skipped ?? 0 })); setDateRange({ startDate: data.dateRange?.startDate, endDate: data.dateRange?.endDate }); addLog({ time: formatTime(), type: 'info', message: `${data.totalEmails ?? 0} ${t('scanModal.logEmailsFound')}, ${data.newEmails ?? 0} ${t('scanModal.logNewToProcess')}`, }); if (data.skipped > 0) { addLog({ time: formatTime(), type: 'duplicate', message: `${data.skipped} ${t('scanModal.logAlreadyProcessed')}`, }); } }, [addLog, t]), onProgress: useCallback((data: any) => { setStatus('scanning'); setStats({ processed: data.processed ?? 0, total: data.total ?? 0, skipped: data.skipped ?? 0, errored: data.errored ?? 0, }); if (data.latest) { const tx = data.latest; addLog({ time: formatTime(), type: 'found', sender: tx.sender || 'Inconnu', amount: tx.amount ? `${Number(tx.amount).toFixed(2)}$` : undefined, ref: tx.reference, branch: tx.branch, }); } if (data.error) { addLog({ time: formatTime(), type: 'error', message: data.error.message || 'Erreur de traitement', }); } }, [addLog]), onCompleted: useCallback((data: any) => { setStatus('completed'); addLog({ time: formatTime(), type: 'info', message: `${t('scanModal.logScanComplete')} (${data.duration ?? ''}) — ${data.summary?.parsed ?? 0} ${t('scanModal.logTransactionsExtracted')}`, }); }, [addLog, t]), onError: useCallback((data: any) => { setStatus('failed'); addLog({ time: formatTime(), type: 'error', message: data.message || t('scanModal.logScanError'), }); }, [addLog, t]), }); // Also update from polling status (fallback) useEffect(() => { if (scanStatus) { if (scanStatus.status === 'completed') setStatus('completed'); if (scanStatus.status === 'failed') setStatus('failed'); if (scanStatus.status === 'scanning' || scanStatus.status === 'parsing') setStatus('scanning'); } }, [scanStatus]); // Track error from mutation useEffect(() => { if (scanError) { setStatus('failed'); addLog({ time: formatTime(), type: 'error', message: scanError.message }); } }, [scanError, addLog]); const emailsDone = stats.processed + stats.errored; const progressPercent = stats.total > 0 ? Math.round((emailsDone / stats.total) * 100) : 0; const totalProcessed = emailsDone + stats.skipped; const grandTotal = stats.total + stats.skipped; const totalPercent = grandTotal > 0 ? Math.round((totalProcessed / grandTotal) * 100) : 0; const isActive = status === 'starting' || status === 'scanning'; return (
{/* Header */}
{status === 'completed' ? ( ) : status === 'failed' ? ( ) : ( )} {isActive && (
)}

{status === 'completed' ? t('scanModal.titleComplete') : status === 'failed' ? t('scanModal.titleError') : t('scanModal.title')}

{isActive && } {t('scanModal.engine', 'Scan Engine active')}

{activeJobId && (
{t('scanModal.sessionId')}: #{activeJobId.slice(-8).toUpperCase()}
)}
{/* Progress section */}
{isStarting ? t('scanModal.starting') : `${emailsDone} / ${stats.total} ${t('scanModal.emailsLabel')}`} {dateRange.startDate && dateRange.endDate && ( {t('scanModal.processing')} {dateRange.startDate.slice(0, 10)} {t('scanModal.toDate')} {dateRange.endDate.slice(0, 10)} )}
{/* Current progress */}
{t('scanModal.batchProgress')} {progressPercent}%
{isActive &&
}
{/* Total progress */}
{t('scanModal.totalProgress')} {totalPercent}%
{/* Stats grid */}
{t('scanModal.emailsAnalyzed')}
{stats.processed}
{t('scanModal.duplicatesSkipped')}
{stats.skipped}
0 ? 'bg-red-50 border-red-100' : 'bg-slate-50 border-slate-100'}`}>
0 ? 'text-red-600' : 'text-slate-500'}`}> {t('scanModal.errors')}
0 ? 'text-red-700' : 'text-slate-700'}`}>{stats.errored}
{/* Extraction log */}
{t('scanModal.extractionLog', 'Extraction Log')} {isActive && }
{logEntries.length === 0 ? (
{isStarting ? t('scanModal.logWaiting') : t('scanModal.logEmpty')}
) : ( logEntries.map((entry, index) => (
{entry.time}
{entry.type === 'found' && ( <> {t('scanModal.logFound')}:{' '} {entry.sender} {entry.amount && ( <> | {t('scanModal.logAmount')}:{' '} {entry.amount} )} {entry.ref && ( <> | Ref: {entry.ref} )} )} {entry.type === 'duplicate' && ( <> {t('scanModal.logDuplicate')}:{' '} {entry.message} )} {entry.type === 'info' && ( {entry.message} )} {entry.type === 'error' && ( <> {t('scanModal.logError')}:{' '} {entry.message} )}
)) )}
{/* Action buttons */}
{status === 'completed' || status === 'failed' ? ( ) : ( )}
); }