diff --git a/web-v2/src/pages/FindingDetailPage.tsx b/web-v2/src/pages/FindingDetailPage.tsx index 7d20d84..0a97a67 100644 --- a/web-v2/src/pages/FindingDetailPage.tsx +++ b/web-v2/src/pages/FindingDetailPage.tsx @@ -1,30 +1,26 @@ import { useEffect, useState } from 'react'; +import type { FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; -import { FindingOfficeWorkspace } from '../features/inspections/FindingOfficeWorkspace'; import { InspectionEvidencePanel } from '../features/inspections/InspectionEvidencePanel'; -import { getInspectionFinding } from '../lib/api'; +import { closeInspectionFinding, getInspectionFinding } from '../lib/api'; import type { InspectionFinding } from '../lib/api'; import { formatDate, formatDateOnly } from '../lib/format'; -function todayInput(): string { - const now = new Date(); - const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000); - return local.toISOString().slice(0, 10); -} - export function FindingDetailPage() { const { id } = useParams(); const navigate = useNavigate(); const { hasPermission } = useAuth(); const [finding, setFinding] = useState(null); const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(false); const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [closureNotes, setClosureNotes] = useState(''); const [evidenceRefreshKey, setEvidenceRefreshKey] = useState(0); - const canFollowUp = hasPermission('inspection_findings.follow_up'); const canClose = hasPermission('inspection_findings.close'); const canReadEvidence = hasPermission('inspection_evidence.read'); const canCreateEvidence = hasPermission('inspection_evidence.create'); @@ -41,14 +37,9 @@ export function FindingDetailPage() { .finally(() => setLoading(false)); }, [id]); - if (loading) return ; + if (loading) return ; if (!finding) return
{error || 'Hallazgo no encontrado'}
; - const responseOverdue = finding.status === 'OPEN' - && !finding.companyResponseReceivedOn - && Boolean(finding.correctionDueOn && finding.correctionDueOn < todayInput()); - const verificationOverdue = finding.status === 'OPEN' - && Boolean(finding.companyResponseReceivedOn && finding.nextControlOn && finding.nextControlOn < todayInput()); const latestVerification = finding.latestVerification; const verificationReadyToClose = latestVerification?.outcome === 'RESOLVED' && latestVerification.visitStatus === 'CLOSED'; @@ -60,48 +51,118 @@ export function FindingDetailPage() { ? 'Requiere nueva fecha' : 'Sin resultado'; + const closeFinding = async (event: FormEvent) => { + event.preventDefault(); + if (!closureNotes.trim()) return; + setBusy(true); setError(''); setSuccess(''); + try { + const updated = await closeInspectionFinding(finding.id, closureNotes.trim()); + setFinding(updated); + setClosureNotes(''); + setSuccess('Hallazgo cerrado. La conclusión quedó incorporada al historial sin modificar el Hallazgo original.'); + } catch (requestError) { + setError(errorMessage(requestError)); + } finally { + setBusy(false); + } + }; + return
-
{finding.code}
+
+ + {finding.code} +
+
-
{finding.code}

{finding.title}

{finding.asset.operatorCompany?.name ?? 'Empresa sin asignar'} · {finding.asset.operationalArea?.name ?? 'Área sin asignar'}

- {finding.status === 'OPEN' ? 'Abierto' : 'Cerrado'} +
+ {finding.code} +

{finding.title}

+

{finding.asset.operatorCompany?.name ?? 'Empresa sin asignar'} · {finding.asset.operationalArea?.name ?? 'Área sin asignar'}

+
+ + {finding.status === 'OPEN' ? 'Abierto' : 'Cerrado'} +
{error && {error}} + {success && {success}} + +
+
+
+ CONTENIDO ORIGINAL +

Descripción

+

Texto libre registrado por el inspector. Una vez bloqueada el Acta, este contenido es inmutable.

+
+
+

{finding.description || 'Sin descripción registrada.'}

+ {finding.legalBasis &&

Base legal / normativa: {finding.legalBasis}

} +
-
Elemento del inventario{finding.asset.name}{finding.asset.code} · {finding.asset.typeName}
-
Acta / inspección{finding.document.actCode}{finding.document.visitCode}
-
Vencimiento administrativo{formatDateOnly(finding.correctionDueOn)}{finding.companyResponseReceivedOn ? `Respondido ${formatDateOnly(finding.companyResponseReceivedOn)}` : 'Esperando respuesta de empresa'}
-
Fecha de verificación{formatDateOnly(finding.nextControlOn)}{finding.nextControlOn ? 'Planificación de inspección o control' : 'Todavía sin definir'}
-
Gravedad{finding.severity ? `${finding.severity}/10` : 'Sin calificar'}{finding.suggestedSeverity ? `Sugerida por catálogo: ${finding.suggestedSeverity}/10` : finding.catalogItemId ? 'El catálogo no define una sugerencia' : 'Hallazgo OTROS'}
+
+ Elemento del Inventario + {finding.asset.name} + {finding.asset.code} · {finding.asset.typeName} +
+
+ Acta / Inspección + {finding.document.actCode} + {finding.document.visitCode} +
+
+ Gravedad + {finding.severity ? `${finding.severity}/10` : 'Sin calificar'} + {finding.suggestedSeverity ? `Sugerida por catálogo: ${finding.suggestedSeverity}/10` : finding.catalogItemId ? 'El catálogo no define una sugerencia' : 'Hallazgo OTROS'} +
+
+ Reincidencia + {finding.isRecurrence ? 'Sí' : 'No'} + {finding.recurrenceOfFindingCode ? `Antecedente: ${finding.recurrenceOfFindingCode}` : finding.isRecurrence ? 'Antecedente vinculado' : 'Sin antecedente marcado'} +
+
+ Próxima verificación + {formatDateOnly(finding.nextControlOn)} + {finding.verificationVisit ? `Planificada en ${finding.verificationVisit.code}` : finding.nextControlOn ? 'Pendiente de planificación' : 'Sin fecha definida'} +
{finding.verificationVisit &&
-
VERIFICACIÓN PLANIFICADA

{finding.verificationVisit.code}

Este hallazgo ya está incorporado a una visita de verificación.

Abrir visita
-
FECHA OBJETIVO{formatDateOnly(finding.nextControlOn)}

Fecha definida en el seguimiento del hallazgo.

VISITA{formatDate(finding.verificationVisit.plannedStartAt)}

{finding.verificationVisit.code}

+
+
+ VERIFICACIÓN PLANIFICADA +

{finding.verificationVisit.code}

+

Este Hallazgo está incorporado a una Inspección de verificación.

+
+ Abrir Inspección +
+
+
FECHA OBJETIVO{formatDateOnly(finding.nextControlOn)}

Fecha técnica de control del Hallazgo.

+
INSPECCIÓN{formatDate(finding.verificationVisit.plannedStartAt)}

{finding.verificationVisit.code}

+
} {latestVerification?.outcome &&
-
ÚLTIMA VERIFICACIÓN DE CAMPO

{verificationOutcomeLabel}

Resultado registrado por el inspector durante {latestVerification.visitCode}.

Abrir verificación
+
+
ÚLTIMA VERIFICACIÓN DE CAMPO

{verificationOutcomeLabel}

Resultado registrado durante {latestVerification.visitCode}.

+ Abrir verificación +
RESULTADO{verificationOutcomeLabel}

{latestVerification.resultNotes || 'Sin observaciones adicionales.'}

-
VERIFICADO{formatDate(latestVerification.verifiedAt)}

{latestVerification.evidenceCount} foto{latestVerification.evidenceCount === 1 ? '' : 's'} de verificación.

- {latestVerification.rescheduledControlOn &&
NUEVA FECHA{formatDateOnly(latestVerification.rescheduledControlOn)}

El hallazgo vuelve a la planificación operativa.

} +
VERIFICADO{formatDate(latestVerification.verifiedAt)}

{latestVerification.evidenceCount} evidencia{latestVerification.evidenceCount === 1 ? '' : 's'} de verificación.

+ {latestVerification.rescheduledControlOn &&
NUEVA FECHA{formatDateOnly(latestVerification.rescheduledControlOn)}

El Hallazgo vuelve a planificación de control.

}
- {verificationReadyToClose && finding.status === 'OPEN' && La verificación de campo indicó que el hallazgo está solucionado y quedó listo para revisión de oficina.} - {latestVerification.outcome === 'NOT_RESOLVED' && finding.status === 'OPEN' && La verificación indicó que el hallazgo continúa abierto. Debe definirse una nueva fecha de control.} + {verificationReadyToClose && finding.status === 'OPEN' && La verificación indicó que el Hallazgo está solucionado y quedó listo para cierre técnico.} + {latestVerification.outcome === 'NOT_RESOLVED' && finding.status === 'OPEN' && La verificación indicó que el Hallazgo continúa abierto.}
} - setEvidenceRefreshKey((value) => value + 1)} - /> + {verificationReadyToClose && finding.status === 'OPEN' && canClose &&
+
CIERRE TÉCNICO

Cerrar Hallazgo

El cierre agrega una conclusión al historial; nunca modifica la descripción original.

+
+