// Job Detail Modal Component const { useState, useEffect, useRef } = React; const JobDetailModal = ({ job, onClose, onRetry, onJobUpdated }) => { const toast = useToast(); const providers = useProviders(); const [activeTab, setActiveTab] = useState('result'); const [logs, setLogs] = useState([]); const logsEndRef = useRef(); // Edit state for queued jobs const [personalData, setPersonalData] = useState({}); const [additionalData, setAdditionalData] = useState({}); const [crifMode, setCrifMode] = useState(false); const [crifOnly, setCrifOnly] = useState(false); const [existingEmail, setExistingEmail] = useState(''); const [saving, setSaving] = useState(false); // Video state const [videoExists, setVideoExists] = useState(false); const [videoLoading, setVideoLoading] = useState(true); // Live screenshot streaming state const [liveScreenshot, setLiveScreenshot] = useState(null); const isQueued = job?.status === 'queued'; const isCancelled = job?.status === 'cancelled'; const isError = job?.status === 'error'; const isRunning = job?.status === 'running'; const canEdit = isQueued || isCancelled || isError; const { DATI_ANAGRAFICI_FIELDS, DATI_RESIDENZA_FIELDS, DOCUMENTO_FIELDS, FORM_SECTIONS } = FormConstants; // Initialize edit state when job changes useEffect(() => { if (job) { setPersonalData(job.personal_data || {}); setAdditionalData(job.additional_data || {}); setCrifMode(job.additional_data?.crif_mode || false); setCrifOnly(job.additional_data?.crif_only || false); setExistingEmail(job.additional_data?.existing_email || ''); // Default to edit tab for queued/cancelled jobs (editable) if (job.status === 'queued' || job.status === 'cancelled') setActiveTab('edit'); else if (job.status === 'running') setActiveTab('logs'); else setActiveTab('result'); } }, [job]); // Check if video exists for this job useEffect(() => { if (!job) return; setVideoLoading(true); fetch(`/queue/job/${job.id}/video/exists`) .then(res => res.json()) .then(data => { setVideoExists(data.exists); setVideoLoading(false); }) .catch(() => { setVideoExists(false); setVideoLoading(false); }); }, [job]); // Live screenshot streaming for running jobs useEffect(() => { if (!job || job.status !== 'running' || activeTab !== 'video') return; const es = new EventSource(`/queue/job/${job.id}/screenshots/stream`); es.onmessage = (e) => { setLiveScreenshot(e.data); }; es.onerror = () => es.close(); return () => es.close(); }, [job, activeTab]); // Clear logs when job changes useEffect(() => { setLogs([]); }, [job?.id]); useEffect(() => { if (!job || activeTab !== 'logs') return; // Clear logs before connecting to new stream setLogs([]); const es = new EventSource(`/queue/job/${job.id}/logs/stream`); es.onmessage = (e) => { try { setLogs(prev => [...prev, JSON.parse(e.data)]); } catch (err) { console.error(err); } }; es.onerror = () => es.close(); return () => es.close(); }, [job?.id, activeTab]); useEffect(() => { if (logsEndRef.current && activeTab === 'logs') logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); }, [logs, activeTab]); if (!job) return null; const handleFieldChange = (key, value) => setPersonalData(prev => ({ ...prev, [key]: value })); const handleAdditionalChange = (key, value) => setAdditionalData(prev => ({ ...prev, [key]: value })); const handleSave = async () => { // Validate required fields const missing = DATI_ANAGRAFICI_FIELDS.filter(f => f.required && !personalData[f.key]).map(f => f.label); if (missing.length > 0) { toast(`Campi mancanti: ${missing.join(', ')}`, 'error'); return; } // Validate CRIF fields for both CRIF mode and CRIF-only mode if (crifMode || crifOnly) { const missingRes = DATI_RESIDENZA_FIELDS.filter(f => f.required && !personalData[f.key]).map(f => f.label); const missingDoc = DOCUMENTO_FIELDS.filter(f => f.required && !personalData[f.key]).map(f => f.label); if (missingRes.length + missingDoc.length > 0) { toast(`Campi CRIF mancanti: ${[...missingRes, ...missingDoc].join(', ')}`, 'error'); return; } } // For CRIF-only mode: require existing email, don't require phone/recovery email if (crifOnly) { if (!existingEmail || !existingEmail.trim()) { toast('Email esistente obbligatoria per modalità Solo CRIF', 'error'); return; } } else { // For email creation modes: require phone and recovery email if (!additionalData.phone_number) { toast('Numero di telefono obbligatorio', 'error'); return; } if (!additionalData.email) { toast('Email di recupero obbligatoria', 'error'); return; } } setSaving(true); try { const response = await fetch(`/queue/job/${job.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ personal_data: personalData, additional_data: { ...additionalData, crif_mode: crifMode && !crifOnly, // crif_mode is false when crif_only is true crif_only: crifOnly, existing_email: crifOnly ? existingEmail.trim() : undefined, } }) }); const data = await response.json(); if (response.ok && data.status === 'success') { toast('Lavoro aggiornato con successo', 'success'); if (onJobUpdated) onJobUpdated(data.job); onClose(); } else { toast(data.detail || 'Errore durante il salvataggio', 'error'); } } catch (error) { toast(`Errore: ${error.message}`, 'error'); } finally { setSaving(false); } }; const isSuccess = job.result?.status === 'success'; const hasVideo = videoExists && !videoLoading; const tabs = [ { id: 'result', label: isSuccess ? '✅ Risultato' : job.status === 'error' ? '❌ Errore' : job.status === 'running' ? '⚡ Stato' : '⏳ In Coda' }, { id: 'logs', label: '📋 Logs' }, { id: 'video', label: isRunning ? '📺 Live' : '🎬 Video', disabled: !hasVideo && !isRunning }, { id: 'edit', label: canEdit ? '✏️ Modifica Dati' : '📝 Dati' }, ]; return (
{job.message || 'Errore sconosciuto'}
{job.error_details}
)}
{job.message || 'Elaborazione in corso...'}
In attesa nella coda
Puoi modificare i dati nella scheda "Modifica Dati"
In attesa del primo frame...
Il video completo sarà disponibile al termine del lavoro
Video registrato durante l'esecuzione dell'automazione browser
Verifica disponibilità video...
Nessun video disponibile
{job.status === 'queued' ? 'Il lavoro non è ancora stato eseguito' : 'La registrazione video non è stata abilitata per questo lavoro'}
Crea solo l'account email
Crea email e invia richiesta CRIF
Usa email esistente per CRIF