import { useEffect, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { SearchableSelect } from '../components/SearchableSelect'; import { formatDate, formatDateOnly } from '../lib/format'; import { addInspectionReportCompanyResponse, addInspectionReportFollowUp, getInspectionReportF4, inspectionReportCompanyResponseDownloadUrl, inspectionReportConsolidatedWordDownloadUrl, inspectionReportFollowUpDownloadUrl, inspectionReportGedoPdfDownloadUrl, inspectionReportWordDownloadUrl, listInspectionReportFollowUps, officializeInspectionReport, setInspectionReportResponseDeadline, updateInspectionReportNarrative, } from '../lib/reportWorkflowApi'; import type { InspectionReportDetailF4, InspectionReportFollowUp, InspectionReportFollowUpType, } from '../lib/reportWorkflowApi'; function names(items: Array<{ name: string }>, empty: string): string { return items.length ? items.map((item) => item.name).join(' · ') : empty; } function localDateTime(value: Date): string { const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 16); } function localDate(value = new Date()): string { const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 10); } function reportStatusLabel(status: InspectionReportDetailF4['status']): string { if (status === 'WORKING') return 'En preparación'; if (status === 'OFFICIALIZED') return 'Oficializado en GEDO'; if (status === 'CANCELLED') return 'Cancelado'; return 'Legado congelado'; } function reportStatusClass(status: InspectionReportDetailF4['status']): string { if (status === 'OFFICIALIZED') return 'active'; if (status === 'CANCELLED') return 'danger'; return 'pending'; } function followUpLabel(type: InspectionReportFollowUpType): string { if (type === 'COMPANY_NOTE') return 'Presentación / nota de empresa'; if (type === 'COMPANY_DOCUMENT') return 'Documento de empresa'; if (type === 'INTERNAL_NOTE') return 'Nota interna'; if (type === 'VERIFICATION') return 'Verificación'; return 'Otro antecedente'; } function fileSize(value: number | null): string { if (!value || value < 1) return ''; if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${Math.round(value / 1024)} KB`; return `${(value / (1024 * 1024)).toFixed(1)} MB`; } export function ReportDetailPage() { const { id } = useParams(); const { hasPermission } = useAuth(); const canManage = hasPermission('inspection_reports.generate'); const [report, setReport] = useState(null); const [followUps, setFollowUps] = useState([]); const [loading, setLoading] = useState(true); const [working, setWorking] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [executiveSummary, setExecutiveSummary] = useState(''); const [reportDescription, setReportDescription] = useState(''); const [gedoIfIdentifier, setGedoIfIdentifier] = useState(''); const [gedoOfficializedAt, setGedoOfficializedAt] = useState(localDateTime(new Date())); const [gedoFile, setGedoFile] = useState(null); const [responseDueOn, setResponseDueOn] = useState(''); const [deadlineReason, setDeadlineReason] = useState(''); const [companyReceivedOn, setCompanyReceivedOn] = useState(localDate()); const [companyDetails, setCompanyDetails] = useState(''); const [companyCommittedOn, setCompanyCommittedOn] = useState(''); const [companyContactName, setCompanyContactName] = useState(''); const [companyContactEmail, setCompanyContactEmail] = useState(''); const [companyResponseFile, setCompanyResponseFile] = useState(null); const [followUpType, setFollowUpType] = useState('INTERNAL_NOTE'); const [followUpOccurredAt, setFollowUpOccurredAt] = useState(localDateTime(new Date())); const [followUpReference, setFollowUpReference] = useState(''); const [followUpDescription, setFollowUpDescription] = useState(''); const [followUpFile, setFollowUpFile] = useState(null); const reload = async () => { if (!id) return; const [nextReport, nextFollowUps] = await Promise.all([ getInspectionReportF4(id), listInspectionReportFollowUps(id), ]); setReport(nextReport); setFollowUps(nextFollowUps); setExecutiveSummary(nextReport.executiveSummary ?? ''); setReportDescription(nextReport.reportDescription ?? ''); setGedoIfIdentifier(nextReport.gedoIfIdentifier ?? ''); if (nextReport.gedoOfficializedAt) setGedoOfficializedAt(localDateTime(new Date(nextReport.gedoOfficializedAt))); setResponseDueOn(nextReport.responseDueOn?.slice(0, 10) ?? ''); }; useEffect(() => { if (!id) return; setLoading(true); setError(''); reload().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, [id]); const saveNarrative = async (event: FormEvent) => { event.preventDefault(); if (!id || report?.status !== 'WORKING') return; setWorking(true); setError(''); setSuccess(''); try { await updateInspectionReportNarrative(id, { executiveSummary: executiveSummary.trim() || null, description: reportDescription.trim() || null }); await reload(); setSuccess('Contenido editable del INF actualizado. El Acta fuente y sus Hallazgos no fueron modificados.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); } }; const officialize = async (event: FormEvent) => { event.preventDefault(); if (!id || !gedoFile || !gedoIfIdentifier.trim() || !gedoOfficializedAt) return; setWorking(true); setError(''); setSuccess(''); try { await officializeInspectionReport(id, { gedoIfIdentifier: gedoIfIdentifier.trim(), gedoOfficializedAt: new Date(gedoOfficializedAt).toISOString(), file: gedoFile }); setGedoFile(null); await reload(); setSuccess('PDF oficial e identificador IF de GEDO cargados manualmente. El Informe quedó oficializado.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); } }; const saveDeadline = async (event: FormEvent) => { event.preventDefault(); if (!id || report?.status !== 'OFFICIALIZED' || !responseDueOn) return; setWorking(true); setError(''); setSuccess(''); try { await setInspectionReportResponseDeadline(id, { responseDueOn, reason: deadlineReason.trim() || null }); setDeadlineReason(''); await reload(); setSuccess(`Vencimiento ${formatDateOnly(responseDueOn)} aplicado a todos los Hallazgos del Acta ${report.act.code}.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); } }; const addCompanyResponse = async (event: FormEvent) => { event.preventDefault(); if (!id || report?.status !== 'OFFICIALIZED' || !companyReceivedOn) return; if (!companyDetails.trim() && !companyResponseFile) return; setWorking(true); setError(''); setSuccess(''); try { await addInspectionReportCompanyResponse(id, { receivedOn: companyReceivedOn, details: companyDetails.trim() || undefined, committedCorrectionOn: companyCommittedOn || undefined, contactName: companyContactName.trim() || undefined, contactEmail: companyContactEmail.trim() || undefined, file: companyResponseFile, }); setCompanyDetails(''); setCompanyCommittedOn(''); setCompanyContactName(''); setCompanyContactEmail(''); setCompanyResponseFile(null); setCompanyReceivedOn(localDate()); await reload(); setSuccess(`Respuesta de empresa registrada y vinculada al Informe ${report.code} y al Acta ${report.act.code}.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); } }; const addFollowUp = async (event: FormEvent) => { event.preventDefault(); if (!id || !followUpOccurredAt) return; if (!followUpDescription.trim() && !followUpReference.trim() && !followUpFile) return; setWorking(true); setError(''); setSuccess(''); try { const next = await addInspectionReportFollowUp(id, { type: followUpType, occurredAt: new Date(followUpOccurredAt).toISOString(), externalReference: followUpReference.trim() || null, description: followUpDescription.trim() || null, file: followUpFile, }); setFollowUps(next); setFollowUpReference(''); setFollowUpDescription(''); setFollowUpFile(null); setFollowUpOccurredAt(localDateTime(new Date())); setSuccess('Antecedente agregado al historial del Informe.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); } }; if (loading) return ; const timeline: Array<{ id: string; date: string; title: string; description: string; href?: string; fileName?: string }> = report ? [ { id: 'act-start', date: report.act.occurredAt, title: `Inspección y Acta ${report.act.code}`, description: `${report.findingCount} hallazgo${report.findingCount === 1 ? '' : 's'} registrados`, href: `/inspecciones/actas/${report.actId}` }, ...(report.act.sealedAt ? [{ id: 'act-sealed', date: report.act.sealedAt, title: 'Acta firmada y cerrada', description: report.act.code, href: `/inspecciones/actas/${report.actId}` }] : []), { id: 'report-issued', date: report.generatedAt, title: `Informe ${report.code} preparado`, description: 'Documento técnico vinculado al Acta' }, ...(report.gedoOfficializedAt ? [{ id: 'gedo', date: report.gedoOfficializedAt, title: 'PDF oficial de GEDO cargado', description: report.gedoIfIdentifier ?? '' }] : []), ...report.deadlines.map((item) => ({ id: `deadline-${item.id}`, date: item.createdAt, title: 'Vencimiento general del Acta', description: `${formatDateOnly(item.responseDueOn)} · ${item.reason}` })), ...report.companyResponses.map((item) => ({ id: `response-${item.id}`, date: `${item.receivedOn}T12:00:00`, title: `Respuesta de empresa · Acta ${report.act.code}`, description: item.details ?? item.originalName ?? 'Respuesta registrada', href: item.originalName ? inspectionReportCompanyResponseDownloadUrl(report.id, item.id) : undefined, fileName: item.originalName ?? undefined })), ...followUps.map((item) => ({ id: item.id, date: item.occurredAt, title: followUpLabel(item.type), description: item.description || item.externalReference || item.originalName || 'Antecedente registrado', href: item.originalName ? inspectionReportFollowUpDownloadUrl(report.id, item.id) : undefined, fileName: item.originalName ?? undefined })), ].sort((a, b) => b.date.localeCompare(a.date)) : []; return
Informes/{report?.code ?? 'Informe'}
INFORME DE INSPECCIÓN

{report?.code ?? 'Informe'}

{report ? `Acta ${report.act.code} · generado ${formatDate(report.generatedAt)}` : 'Consulta del informe.'}

{report &&
{report.wordStatus === 'READY' ? 'Word disponible' : report.wordStatus === 'FAILED' ? 'Word con error' : 'Word pendiente'}{reportStatusLabel(report.status)}
}
{error && {error}} {success && {success}} {report && <>

Un Informe corresponde a una sola Acta. GEDO no se consulta automáticamente: la oficialización se registra manualmente cargando el IF y su PDF oficial. Esa carga no genera una respuesta de empresa ni define un vencimiento por sí sola.

TRAZABILIDAD DOCUMENTAL

{report.code}

Versión del Acta: {report.actVersion}
Empresa{names(report.companies, 'Sin asignar')}
Área / Yacimiento{names(report.areas, 'Sin asignar')}
Hallazgos{report.findingCount}
Generado por{report.generatedBy.firstName} {report.generatedBy.lastName}
Inspección{report.visit.code}Acta fuente{report.act.code}Contenido inmutableHallazgos del Acta{report.findingCount}Seguimiento técnico
WORD EDITABLE

Preparación del INF

El Inspector puede revisar y ajustar el texto antes de enviarlo a GEDO. Esta edición no altera el Acta fuente.

Descargar Word del informe{report.wordStatus === 'READY' && Word anterior}