// 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 (
e.stopPropagation()}>

Lavoro #{job.id?.slice(-8)}

{canEdit && Modificabile}
{/* Tabs */}
{tabs.map(tab => ( ))}
{activeTab === 'result' && (
{isSuccess && job.result ? (
✓ Registrazione Completata
Username: {job.result.username}
Password: {job.result.password}
) : job.status === 'error' ? (
✕ Errore

{job.message || 'Errore sconosciuto'}

{job.error_details && (
{job.error_details}
)}
) : job.status === 'running' ? (

{job.message || 'Elaborazione in corso...'}

) : (

In attesa nella coda

Puoi modificare i dati nella scheda "Modifica Dati"

)}
)} {activeTab === 'logs' && (
{logs.length === 0 ? (
Nessun log disponibile
) : logs.map((log, i) => (
{new Date(log.timestamp).toLocaleTimeString('it-IT')} [{log.level}] {log.message}
))}
)} {activeTab === 'video' && (
{/* Live view for running jobs */} {isRunning ? (

📺 Vista Live ● In Diretta

Aggiornamento ogni secondo
{liveScreenshot ? ( Live view ) : (

In attesa del primo frame...

)}

Il video completo sarà disponibile al termine del lavoro

) : hasVideo ? (

🎬 Registrazione Automazione {job.status === 'error' ? 'Errore' : job.status === 'completed' ? 'Completato' : 'In corso'}

e.stopPropagation()} > 💾 Scarica Video

Video registrato durante l'esecuzione dell'automazione browser

) : videoLoading ? (

Verifica disponibilità video...

) : (
🎬

Nessun video disponibile

{job.status === 'queued' ? 'Il lavoro non è ancora stato eseguito' : 'La registrazione video non è stata abilitata per questo lavoro'}

)}
)} {activeTab === 'edit' && (
{!canEdit && (
⚠️ Questo lavoro non può essere modificato perché è già in esecuzione o terminato.
)} {/* Job Mode Selector */}

⚙️ Modalità Lavoro

{/* Email Only */}
canEdit && (setCrifMode(false), setCrifOnly(false))} >
📧
Solo Email

Crea solo l'account email

{/* Email + CRIF */}
canEdit && (setCrifMode(true), setCrifOnly(false))} >
📧+📊
Email + CRIF

Crea email e invia richiesta CRIF

{/* CRIF Only */}
canEdit && (setCrifMode(false), setCrifOnly(true))} >
📊
Solo CRIF

Usa email esistente per CRIF

{/* Email Settings - only show for email creation modes */} {!crifOnly && (

📧 Impostazioni Email

handleAdditionalChange('phone_number', e.target.value)} disabled={!canEdit} /> handleAdditionalChange('email', e.target.value)} disabled={!canEdit} />
)} {/* Existing Email - only show for CRIF-only mode */} {crifOnly && (

📧 Email Esistente per CRIF

Inserisci l'indirizzo email esistente da usare per la richiesta CRIF. Non verrà creato un nuovo account email.
setExistingEmail(e.target.value)} disabled={!canEdit} />
)} {/* Form Sections */} {FORM_SECTIONS.filter(section => !section.crifOnly || crifMode || crifOnly).map(section => (

{section.icon} {section.title} {section.required && Obbligatorio} {section.crifOnly && (crifMode || crifOnly) && {crifOnly ? 'Solo CRIF' : 'CRIF'}}

{section.fields.map(field => ( field.type === 'select' ? ( handleFieldChange(field.key, e.target.value)} disabled={!canEdit} /> ) ))}
))} {/* Save Button */} {canEdit && (
)}
)}
); }; window.JobDetailModal = JobDetailModal;