Files
dh-inspeccion-v2/web-v2/src/pages/FindingDetailPage.tsx
T

185 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { InspectionEvidencePanel } from '../features/inspections/InspectionEvidencePanel';
import { closeInspectionFinding, getInspectionFinding } from '../lib/api';
import type { InspectionFinding } from '../lib/api';
import { formatDate, formatDateOnly } from '../lib/format';
export function FindingDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const [finding, setFinding] = useState<InspectionFinding | null>(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 canClose = hasPermission('inspection_findings.close');
const canReadEvidence = hasPermission('inspection_evidence.read');
const canCreateEvidence = hasPermission('inspection_evidence.create');
const canReadCommunications = hasPermission('inspection_communications.read');
const canCreateCommunications = hasPermission('inspection_communications.create');
useEffect(() => {
if (!id) return;
setLoading(true);
setError('');
getInspectionFinding(id)
.then(setFinding)
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <LoadingBlock label="Cargando Hallazgo…" />;
if (!finding) return <section><Alert>{error || 'Hallazgo no encontrado'}</Alert></section>;
const latestVerification = finding.latestVerification;
const verificationReadyToClose = latestVerification?.outcome === 'RESOLVED'
&& latestVerification.visitStatus === 'CLOSED';
const verificationOutcomeLabel = latestVerification?.outcome === 'RESOLVED'
? 'Solucionado'
: latestVerification?.outcome === 'NOT_RESOLVED'
? 'No solucionado'
: latestVerification?.outcome === 'REQUIRES_NEW_DATE'
? '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 <section>
<div className="finding-detail-breadcrumb">
<button type="button" className="text-link" onClick={() => navigate('/hallazgos')}>Hallazgos</button>
<span></span><strong>{finding.code}</strong>
</div>
<div className="page-heading finding-detail-heading">
<div>
<span className="eyebrow">{finding.code}</span>
<h1>{finding.title}</h1>
<p>{finding.asset.operatorCompany?.name ?? 'Empresa sin asignar'} · {finding.asset.operationalArea?.name ?? 'Área sin asignar'}</p>
</div>
<span className={`status-badge large ${finding.status === 'OPEN' ? 'observed' : 'active'}`}>
{finding.status === 'OPEN' ? 'Abierto' : 'Cerrado'}
</span>
</div>
{error && <Alert>{error}</Alert>}
{success && <Alert type="success">{success}</Alert>}
<article className="panel">
<div className="panel-heading">
<div>
<span className="eyebrow">CONTENIDO ORIGINAL</span>
<h2>Descripción</h2>
<p className="section-copy">Texto libre registrado por el inspector. Una vez bloqueada el Acta, este contenido es inmutable.</p>
</div>
</div>
<p style={{ whiteSpace: 'pre-wrap' }}>{finding.description || 'Sin descripción registrada.'}</p>
{finding.legalBasis && <p><strong>Base legal / normativa:</strong> {finding.legalBasis}</p>}
</article>
<div className="finding-detail-summary-grid">
<article>
<small>Elemento del Inventario</small>
<Link to={`/inventarios/${finding.assetId}`}>{finding.asset.name}</Link>
<span>{finding.asset.code} · {finding.asset.typeName}</span>
</article>
<article>
<small>Acta / Inspección</small>
<Link to={`/inspecciones/actas/${finding.actId}`}>{finding.document.actCode}</Link>
<span><Link className="text-link" to={`/inspecciones/${finding.document.visitId}`}>{finding.document.visitCode}</Link></span>
</article>
<article>
<small>Gravedad</small>
<strong>{finding.severity ? `${finding.severity}/10` : 'Sin calificar'}</strong>
<span>{finding.suggestedSeverity ? `Sugerida por catálogo: ${finding.suggestedSeverity}/10` : finding.catalogItemId ? 'El catálogo no define una sugerencia' : 'Hallazgo OTROS'}</span>
</article>
<article>
<small>Reincidencia</small>
<strong>{finding.isRecurrence ? 'Sí' : 'No'}</strong>
<span>{finding.recurrenceOfFindingCode ? `Antecedente: ${finding.recurrenceOfFindingCode}` : finding.isRecurrence ? 'Antecedente vinculado' : 'Sin antecedente marcado'}</span>
</article>
<article>
<small>Próxima verificación</small>
<strong>{formatDateOnly(finding.nextControlOn)}</strong>
<span>{finding.verificationVisit ? `Planificada en ${finding.verificationVisit.code}` : finding.nextControlOn ? 'Pendiente de planificación' : 'Sin fecha definida'}</span>
</article>
</div>
{finding.verificationVisit && <article className="panel verification-linked-visit">
<div className="panel-heading">
<div>
<span className="eyebrow">VERIFICACIÓN PLANIFICADA</span>
<h2>{finding.verificationVisit.code}</h2>
<p className="section-copy">Este Hallazgo está incorporado a una Inspección de verificación.</p>
</div>
<Link className="button secondary" to={`/inspecciones/${finding.verificationVisit.id}`}>Abrir Inspección <Icon name="chevron" /></Link>
</div>
<div className="finding-two-deadlines">
<div><span>FECHA OBJETIVO</span><strong>{formatDateOnly(finding.nextControlOn)}</strong><p>Fecha técnica de control del Hallazgo.</p></div>
<div><span>INSPECCIÓN</span><strong>{formatDate(finding.verificationVisit.plannedStartAt)}</strong><p>{finding.verificationVisit.code}</p></div>
</div>
</article>}
{latestVerification?.outcome && <article className={`panel verification-result-panel ${latestVerification.outcome === 'RESOLVED' ? 'resolved' : ''}`}>
<div className="panel-heading">
<div><span className="eyebrow">ÚLTIMA VERIFICACIÓN DE CAMPO</span><h2>{verificationOutcomeLabel}</h2><p className="section-copy">Resultado registrado durante {latestVerification.visitCode}.</p></div>
<Link className="button secondary" to={`/inspecciones/${latestVerification.visitId}`}>Abrir verificación <Icon name="chevron" /></Link>
</div>
<div className="finding-two-deadlines">
<div><span>RESULTADO</span><strong>{verificationOutcomeLabel}</strong><p>{latestVerification.resultNotes || 'Sin observaciones adicionales.'}</p></div>
<div><span>VERIFICADO</span><strong>{formatDate(latestVerification.verifiedAt)}</strong><p>{latestVerification.evidenceCount} evidencia{latestVerification.evidenceCount === 1 ? '' : 's'} de verificación.</p></div>
{latestVerification.rescheduledControlOn && <div><span>NUEVA FECHA</span><strong>{formatDateOnly(latestVerification.rescheduledControlOn)}</strong><p>El Hallazgo vuelve a planificación de control.</p></div>}
</div>
{verificationReadyToClose && finding.status === 'OPEN' && <Alert type="success">La verificación indicó que el Hallazgo está solucionado y quedó listo para cierre técnico.</Alert>}
{latestVerification.outcome === 'NOT_RESOLVED' && finding.status === 'OPEN' && <Alert>La verificación indicó que el Hallazgo continúa abierto.</Alert>}
</article>}
{verificationReadyToClose && finding.status === 'OPEN' && canClose && <article className="panel" id="cierre-hallazgo">
<div className="panel-heading"><div><span className="eyebrow">CIERRE TÉCNICO</span><h2>Cerrar Hallazgo</h2><p className="section-copy">El cierre agrega una conclusión al historial; nunca modifica la descripción original.</p></div></div>
<form onSubmit={closeFinding}>
<label className="field"><span>Conclusión de cierre</span><textarea value={closureNotes} onChange={(event) => setClosureNotes(event.target.value)} required minLength={3} rows={4} /></label>
<div className="form-actions"><button className="button primary" disabled={busy || !closureNotes.trim()}>{busy ? 'Cerrando…' : 'Cerrar Hallazgo'}</button></div>
</form>
</article>}
<div id="documentos-hallazgo">
<InspectionEvidencePanel
finding={finding}
canReadEvidence={canReadEvidence}
canCreateEvidence={canCreateEvidence}
canCreateFieldEvidence={false}
canReadCommunications={canReadCommunications}
canCreateCommunications={canCreateCommunications}
refreshKey={evidenceRefreshKey}
/>
</div>
{finding.status === 'CLOSED' && <article className="closed-seal finding-closed-seal">
<Icon name="check" />
<div><span className="eyebrow">HALLAZGO CERRADO</span><strong>{formatDate(finding.closedAt)}</strong><p>{finding.closureNotes}</p></div>
</article>}
</section>;
}