F1.4: integrar pendientes en la planificación de inspección

This commit is contained in:
2026-09-05 17:24:42 -03:00
parent 3cd9cdd104
commit 37795e6586
@@ -0,0 +1,97 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
import { Icon } from '../../components/Icon';
import type { InspectionVisit } from '../../lib/api';
import { formatDate, formatDateOnly } from '../../lib/format';
import {
getInspectionFieldBriefing,
type FieldBriefing,
type FieldBriefingActState,
} from './fieldBriefingApi';
function stateLabel(state: FieldBriefingActState): string {
if (state === 'ACT_RESPONSE_OVERDUE') return 'Respuesta del Acta vencida';
if (state === 'ACT_RESPONSE_DUE_SOON') return 'Respuesta próxima a vencer';
if (state === 'COMPANY_COMMITMENT_OVERDUE') return 'Compromiso de empresa vencido';
if (state === 'VERIFICATION_PENDING') return 'Verificación pendiente';
if (state === 'RESPONSE_RECEIVED') return 'Respuesta recibida';
return 'Esperando respuesta';
}
function stateClass(state: FieldBriefingActState): string {
if (state === 'ACT_RESPONSE_OVERDUE' || state === 'COMPANY_COMMITMENT_OVERDUE') return 'danger';
if (state === 'ACT_RESPONSE_DUE_SOON' || state === 'VERIFICATION_PENDING') return 'warning';
return 'pending';
}
export function FieldBriefingPanel({ visit }: { visit: InspectionVisit }) {
const [briefing, setBriefing] = useState<FieldBriefing | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let active = true;
setLoading(true);
setError('');
getInspectionFieldBriefing(visit.id)
.then((value) => { if (active) setBriefing(value); })
.catch((requestError) => { if (active) setError(errorMessage(requestError)); })
.finally(() => { if (active) setLoading(false); });
return () => { active = false; };
}, [visit.id, visit.updatedAt]);
return <section className="panel inspection-checklist-panel">
<div className="panel-heading">
<div>
<span className="eyebrow">ANTES DE CONFIRMAR LA SALIDA</span>
<h2>Pendientes para revisar en campo</h2>
<p className="section-copy">Actas anteriores del mismo Área y Operadora que requieren atención durante esta inspección. El plazo administrativo pertenece al Acta y los hallazgos se mantienen dentro de su expediente de origen.</p>
</div>
<div className="form-actions">
<Link className="button secondary" to="/preparacion-campo"><Icon name="clipboard" />Vista imprimible</Link>
</div>
</div>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Preparando pendientes de campo…" /> : briefing && <>
<div className="inspection-checklist-metrics">
<div><strong>{briefing.summary.acts}</strong><span>actas con pendientes</span></div>
<div><strong>{briefing.summary.findings}</strong><span>hallazgos a revisar</span></div>
<div><strong>{briefing.summary.inventoryItems}</strong><span>registros involucrados</span></div>
<div><strong>{briefing.summary.responseOverdue}</strong><span>respuestas vencidas</span></div>
<div><strong>{briefing.summary.commitmentOverdue}</strong><span>compromisos vencidos</span></div>
<div><strong>{briefing.summary.verificationPending}</strong><span>verificaciones pendientes</span></div>
</div>
<div className="temporal-notice">
<Icon name="clipboard" />
<p><strong>Contexto de salida:</strong> {briefing.inspection.area?.name ?? 'Área sin definir'} · {briefing.inspection.operatorCompany?.name ?? 'Operadora sin definir'} · {formatDateOnly(briefing.plannedOn)}.</p>
</div>
{briefing.acts.length === 0
? <EmptyState title="Sin pendientes anteriores" text="No hay Actas anteriores con acciones de campo pendientes para este Área y Operadora." />
: <div>{briefing.acts.map((act) => <div className="table-panel" key={act.actId}>
<div className="table-summary">
<div><strong>{act.actCode}</strong><span>{formatDate(act.occurredAt)} · {act.findings.length} hallazgo{act.findings.length === 1 ? '' : 's'}</span></div>
<span className={`status-badge ${stateClass(act.adminState)}`}>{stateLabel(act.adminState)}</span>
</div>
<div className="table-scroll"><table>
<thead><tr><th>Hallazgo</th><th>Inventario</th><th>Gravedad</th><th>Control</th><th /></tr></thead>
<tbody>{act.findings.map((finding) => <tr key={finding.id}>
<td><Link className="history-asset-link" to={`/hallazgos/${finding.id}`}><strong>{finding.title}</strong><small>{finding.code} · {finding.status}</small></Link></td>
<td><Link className="history-asset-link" to={`/inventarios/${finding.asset.id}`}><strong>{finding.asset.name}</strong><small>{finding.asset.code} · {finding.asset.typeName}</small></Link></td>
<td>{finding.severity ?? '—'}</td>
<td>{finding.nextControlOn ? formatDateOnly(finding.nextControlOn) : 'A definir'}</td>
<td className="action-cell"><Link className="button secondary compact" to={`/seguimiento-actas/${act.actId}`}>Ver Acta</Link></td>
</tr>)}</tbody>
</table></div>
<div className="table-summary">
<span>Plazo empresa: <strong>{act.responseDueOn ? formatDateOnly(act.responseDueOn) : 'sin definir'}</strong></span>
<span>Respuesta: <strong>{act.responseReceivedOn ? formatDateOnly(act.responseReceivedOn) : 'pendiente'}</strong></span>
<span>Compromiso: <strong>{act.committedCorrectionOn ? formatDateOnly(act.committedCorrectionOn) : '—'}</strong></span>
</div>
</div>)}</div>}
</>}
</section>;
}