From f83ab824ff1284c4775c611fb666dfc464db4304 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 22:46:53 -0300 Subject: [PATCH] F4: replace director report review UI with INF workflow --- web-v2/src/pages/ReportDetailPage.tsx | 267 ++++++++++++++++---------- 1 file changed, 170 insertions(+), 97 deletions(-) diff --git a/web-v2/src/pages/ReportDetailPage.tsx b/web-v2/src/pages/ReportDetailPage.tsx index 2d4b7a3..107c8e1 100644 --- a/web-v2/src/pages/ReportDetailPage.tsx +++ b/web-v2/src/pages/ReportDetailPage.tsx @@ -4,60 +4,97 @@ import { Link, useParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; -import { - approveInspectionReport, - getInspectionReport, - getInspectionReportReview, - inspectionReportRevisionUrl, - inspectionReportWordUrl, - signFinalInspectionReport, - uploadInspectionReportRevision, -} from '../lib/api'; -import type { InspectionReport, InspectionReportReview } from '../lib/api'; +import { SearchableSelect } from '../components/SearchableSelect'; import { formatDate } from '../lib/format'; +import { + addInspectionReportFollowUp, + getInspectionReportF4, + inspectionReportWordDownloadUrl, + listInspectionReportFollowUps, + officializeInspectionReport, + 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 reviewLabel(value: InspectionReport['reviewStatus']): string { - if (value === 'SIGNED') return 'Firmado'; - if (value === 'APPROVED') return 'Aprobado · falta firma'; - return 'Pendiente de revisión'; +function localDateTime(value: Date): string { + const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000); + return local.toISOString().slice(0, 16); } -function reviewClass(value: InspectionReport['reviewStatus']): string { - if (value === 'SIGNED') return 'active'; - if (value === 'APPROVED') return 'observed'; +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 revisionSource(value: 'AUTO' | 'DIRECTOR_UPLOAD'): string { - return value === 'AUTO' ? 'Automática' : 'Corrección cargada'; +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 [report, setReport] = useState(null); - const [review, setReview] = useState(null); + 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 [revisionFile, setRevisionFile] = useState(null); - const [changeSummary, setChangeSummary] = useState(''); - const [approvalNote, setApprovalNote] = useState(''); - const [signatureConfirmed, setSignatureConfirmed] = useState(false); - const canRevise = hasPermission('inspection_reports.revise'); - const canReview = hasPermission('inspection_reports.review'); - const canSign = hasPermission('inspection_reports.sign_final'); + + 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 [followUpType, setFollowUpType] = useState('COMPANY_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, nextReview] = await Promise.all([getInspectionReport(id), getInspectionReportReview(id)]); + const [nextReport, nextFollowUps] = await Promise.all([ + getInspectionReportF4(id), + listInspectionReportFollowUps(id), + ]); setReport(nextReport); - setReview(nextReview); + setFollowUps(nextFollowUps); + setExecutiveSummary(nextReport.executiveSummary ?? ''); + setReportDescription(nextReport.reportDescription ?? ''); + setGedoIfIdentifier(nextReport.gedoIfIdentifier ?? ''); + if (nextReport.gedoOfficializedAt) { + setGedoOfficializedAt(localDateTime(new Date(nextReport.gedoOfficializedAt))); + } }; useEffect(() => { @@ -69,19 +106,19 @@ export function ReportDetailPage() { .finally(() => setLoading(false)); }, [id]); - const uploadRevision = async (event: FormEvent) => { + const saveNarrative = async (event: FormEvent) => { event.preventDefault(); - if (!id || !revisionFile || changeSummary.trim().length < 5) return; + if (!id || report?.status !== 'WORKING') return; setWorking(true); setError(''); setSuccess(''); try { - const next = await uploadInspectionReportRevision(id, revisionFile, changeSummary.trim()); - setReview(next); - setRevisionFile(null); - setChangeSummary(''); - setSuccess(`Versión ${next.currentRevisionNumber} agregada al historial.`); + 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 { @@ -89,17 +126,21 @@ export function ReportDetailPage() { } }; - const approve = async () => { - if (!id) return; + const officialize = async (event: FormEvent) => { + event.preventDefault(); + if (!id || !gedoFile || !gedoIfIdentifier.trim() || !gedoOfficializedAt) return; setWorking(true); setError(''); setSuccess(''); try { - const next = await approveInspectionReport(id, approvalNote); - setReview(next); - setApprovalNote(''); - setSuccess(`Versión ${next.currentRevisionNumber} aprobada por el Director.`); + await officializeInspectionReport(id, { + gedoIfIdentifier: gedoIfIdentifier.trim(), + gedoOfficializedAt: new Date(gedoOfficializedAt).toISOString(), + file: gedoFile, + }); + setGedoFile(null); await reload(); + setSuccess('IF oficial de GEDO registrado. El PDF y su hash quedaron fijados de forma inmutable.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { @@ -107,17 +148,27 @@ export function ReportDetailPage() { } }; - const signFinal = async () => { - if (!id || !signatureConfirmed) return; + 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 signFinalInspectionReport(id); - setReview(next); - setSignatureConfirmed(false); - setSuccess('Informe final firmado electrónicamente. La versión firmada quedó bloqueada.'); - await reload(); + 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 seguimiento del INF. Los registros anteriores permanecen intactos.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { @@ -130,77 +181,99 @@ export function ReportDetailPage() { return
Informes/{report?.code ?? 'Informe'}
-
INFORME DE INSPECCIÓN

{report?.title ?? 'Informe'}

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

- {report &&
{report.wordStatus === 'READY' ? 'Word disponible' : report.wordStatus === 'FAILED' ? 'Word con error' : 'Word pendiente'}{reviewLabel(report.reviewStatus)}
} +
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}
} + {success && {success}} + {report && <> -

Informe Word automático. La versión inicial se genera desde la instantánea congelada del Acta. Las correcciones se agregan como nuevas versiones y nunca sobrescriben la anterior.

+

Un INF corresponde a una sola Acta. El Word puede editarse durante la preparación del Informe. El Acta sellada y los Hallazgos que contiene permanecen inmutables.

+
TRAZABILIDAD DOCUMENTAL

{report.code}

Versión del Acta: {report.actVersion}
Empresa{names(report.companies, 'Sin asignar')}
-
Área{names(report.areas, 'Sin asignar')}
+
Área / Yacimiento{names(report.areas, 'Sin asignar')}
Hallazgos{report.findingCount}
-
Emitido por{report.generatedBy.firstName} {report.generatedBy.lastName}
+
Generado por{report.generatedBy.firstName} {report.generatedBy.lastName}
- {report.wordStatus === 'READY' && }
Inspección{report.visit.code} - Acta incorporada{report.act.code}{report.act.title} - Hallazgos{report.findingCount}Ver seguimiento relacionado + Acta fuente{report.act.code}Contenido inmutable + Hallazgos del Acta{report.findingCount}Seguimiento técnico
-
-
REVISIÓN DIRECTIVA

Versiones y firma final

El Acta y los Hallazgos originales permanecen congelados. La revisión sólo versiona el documento Informe.

{review && {reviewLabel(review.reviewStatus)}}
- {!review ? : <> -
-
1Informe automático{review.revisions.length ? 'Versión inicial registrada' : 'Esperando Word'}
-
1 ? 'complete' : ''}>2Correcciones{review.currentRevisionNumber > 1 ? `${review.currentRevisionNumber - 1} versión/es agregada/s` : 'Sin correcciones'}
-
3Aprobación{review.approvedAt ? formatDate(review.approvedAt) : 'Pendiente del Director'}
-
4Firma final{review.signature ? formatDate(review.signature.signedAt) : 'Pendiente'}
+
+
WORD EDITABLE

Preparación del INF

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

{report.wordStatus === 'READY' && Descargar Word}
+
+