/** * Pagina Output Review. * * Carica i risultati di un job completato e mostra: * - Griglia di PostCard con badge PN e Schwartz * - SlideViewer inline espandibile con edit inline * - Pulsante Download CSV (POST /api/export/{jobId}/csv con edits) * * Lo stato dei post (con le modifiche inline) è mantenuto localmente * e inviato al backend via POST al momento del download CSV. */ import { Download, Loader2, RefreshCw } from 'lucide-react' import { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { useDownloadEditedCsv, useJobResults, useSettingsStatus } from '../api/hooks' import { PostCard } from '../components/PostCard' import type { PostResult } from '../types' export function OutputReview() { const { jobId } = useParams<{ jobId: string }>() const { data: jobData, isLoading, error } = useJobResults(jobId ?? null) const downloadMutation = useDownloadEditedCsv() const { data: settingsStatus } = useSettingsStatus() // Stato locale dei post — viene aggiornato da edit inline e rigenerazione const [localResults, setLocalResults] = useState([]) // Set di indici dei post che sono stati rigenerati const [regeneratedSlots, setRegeneratedSlots] = useState>(new Set()) // Inizializza i risultati e merge con CalendarSlot dal calendario useEffect(() => { if (jobData?.results) { const slots = jobData.calendar?.slots const merged = jobData.results.map((r) => ({ ...r, slot: slots?.find((s) => s.indice === r.slot_index) ?? r.slot, })) setLocalResults(merged) } }, [jobData]) function handleEdit(updated: PostResult) { setLocalResults((prev) => prev.map((r) => (r.slot_index === updated.slot_index ? updated : r)) ) } function handleRegenerated(updated: PostResult) { setLocalResults((prev) => prev.map((r) => (r.slot_index === updated.slot_index ? updated : r)) ) setRegeneratedSlots((prev) => new Set(prev).add(updated.slot_index)) } async function handleDownloadCsv() { if (!jobId || !jobData) return await downloadMutation.mutateAsync({ jobId, results: localResults, campagna: jobData.campagna, }) } // --------------------------------------------------------------------------- // Loading / Error states // --------------------------------------------------------------------------- if (isLoading) { return (
) } if (error || !jobData) { return (
{error?.message ?? 'Risultati non trovati. Il job potrebbe non essere completato.'}
) } const successCount = localResults.filter((r) => r.status === 'success').length const failedCount = localResults.filter((r) => r.status === 'failed').length const regeneratedCount = regeneratedSlots.size // Conteggio post modificati manualmente (diversi dall'originale, esclusi i rigenerati) const editedCount = localResults.filter((r) => { if (r.status !== 'success' || !r.post) return false const original = jobData?.results?.find((o) => o.slot_index === r.slot_index) if (!original?.post) return false return JSON.stringify(r.post) !== JSON.stringify(original.post) }).length const manuallyEditedCount = Math.max(0, editedCount - regeneratedCount) // --------------------------------------------------------------------------- // UI principale // --------------------------------------------------------------------------- return (
{/* Header */}

Output Review

{jobData.campagna}

{localResults.length} post {successCount} generati {failedCount > 0 && ( {failedCount} falliti )} {regeneratedCount > 0 && ( {regeneratedCount} rigenerati )} {manuallyEditedCount > 0 && ( {manuallyEditedCount} modificati )} job: {jobId}
{/* Download CSV */}
{/* Errore download */} {downloadMutation.error && (
Errore download: {downloadMutation.error.message}
)} {/* Info edit inline */}
Clicca su una card per espandere le slide. I campi di testo sono editabili inline. Usa il pulsante per rigenerare singoli post con topic diversi. Le modifiche saranno incluse nel CSV scaricato.
{/* Hint Unsplash — visibile solo se non configurato */} {settingsStatus && !settingsStatus.unsplash_api_key_configured && (
Le colonne immagine contengono keyword testuali. Configura Unsplash per URL immagini reali nel CSV.
)} {/* Griglia post */}
{localResults.length === 0 ? (

Nessun risultato disponibile.

) : ( localResults.map((result) => ( )) )}
{/* Download CSV footer */} {successCount > 0 && (
)}
) }