import { SearchableSelect } from '../components/SearchableSelect'; import { useEffect, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useSearchParams } from 'react-router'; import { PermissionGate } from '../auth/PermissionGate'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { useOperationalContext } from '../context/OperationalContext'; import { OperationalFilters } from '../features/inspections/OperationalFilters'; import { listInspectionFindingsGlobal } from '../lib/api'; import type { InspectionFinding, InspectionFindingWorkflow, InspectionFindingWorkflowCounters, PageMeta, } from '../lib/api'; import { formatDateOnly } from '../lib/format'; const workflows: Array<{ value: InspectionFindingWorkflow; label: string }> = [ { value: 'OPEN', label: 'Abiertos' }, { value: 'WAITING_COMPANY', label: 'Esperando empresa' }, { value: 'COMPANY_OVERDUE', label: 'Respuesta vencida' }, { value: 'TO_SCHEDULE_VERIFICATION', label: 'Programar verificación' }, { value: 'TO_VERIFY', label: 'Para verificar' }, { value: 'VERIFICATION_OVERDUE', label: 'Verificación vencida' }, { value: 'READY_TO_CLOSE', label: 'Listos para cerrar' }, { value: 'CLOSED', label: 'Cerrados' }, { value: 'ALL', label: 'Todos' }, ]; function todayInput(): string { const now = new Date(); const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 10); } function attentionLabel(finding: InspectionFinding): { label: string; className: string } { const today = todayInput(); if (finding.latestVerification?.outcome === 'RESOLVED' && finding.latestVerification.visitStatus === 'CLOSED') return { label: 'Listo para cerrar', className: 'active' }; if (finding.status === 'CLOSED') return { label: 'Cerrado', className: 'active' }; if (!finding.companyResponseReceivedOn) { if (finding.correctionDueOn && finding.correctionDueOn < today) return { label: 'Respuesta vencida', className: 'danger' }; return { label: 'Esperando empresa', className: 'pending' }; } if (!finding.nextControlOn) return { label: 'Programar verificación', className: 'observed' }; if (finding.nextControlOn < today) return { label: 'Verificación vencida', className: 'danger' }; return { label: 'Para verificar', className: 'blue' }; } function deadlineText(finding: InspectionFinding): { title: string; value: string | null; overdue: boolean } { const today = todayInput(); if (finding.latestVerification?.outcome === 'RESOLVED' && finding.latestVerification.visitStatus === 'CLOSED') return { title: 'Verificado conforme', value: finding.latestVerification.verifiedAt, overdue: false }; if (!finding.companyResponseReceivedOn) { return { title: 'Respuesta de empresa', value: finding.correctionDueOn, overdue: Boolean(finding.correctionDueOn && finding.correctionDueOn < today), }; } return { title: 'Verificación DH', value: finding.nextControlOn, overdue: Boolean(finding.nextControlOn && finding.nextControlOn < today), }; } const emptyCounters: InspectionFindingWorkflowCounters = { open: 0, waitingCompany: 0, companyOverdue: 0, companyDueNext7Days: 0, awaitingVerificationSchedule: 0, toVerify: 0, verificationOverdue: 0, verificationNext30Days: 0, readyToClose: 0, closed: 0, }; export function FindingsPage() { const [params, setParams] = useSearchParams(); const operationalContext = useOperationalContext(); const [items, setItems] = useState([]); const [meta, setMeta] = useState({ page: 1, pageSize: 25, total: 0, totalPages: 0 }); const [counters, setCounters] = useState(emptyCounters); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const workflowValue = params.get('workflow') ?? 'OPEN'; const workflow = workflows.some((item) => item.value === workflowValue) ? workflowValue as InspectionFindingWorkflow : 'OPEN'; const search = params.get('search') ?? ''; const [draftSearch, setDraftSearch] = useState(search); const companyId = operationalContext.companyId || params.get('companyId') || ''; const areaId = operationalContext.areaId || params.get('areaId') || ''; const inspectorId = params.get('inspectorId') ?? ''; const dateFrom = params.get('dateFrom') ?? ''; const dateTo = params.get('dateTo') ?? ''; const page = Math.max(1, Number(params.get('page') ?? 1) || 1); useEffect(() => { setLoading(true); setError(''); listInspectionFindingsGlobal({ page, pageSize: 25, search, workflow, companyId, areaId, inspectorId, dateFrom, dateTo }) .then((response) => { setItems(response.data); setMeta(response.meta); setCounters(response.counters); }) .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, [page, search, workflow, companyId, areaId, inspectorId, dateFrom, dateTo]); const setFilter = (key: string, value: string) => { const next = new URLSearchParams(params); if (key === 'areaId') { operationalContext.setAreaId(value); next.delete('areaId'); next.delete('companyId'); } else if (key === 'companyId') { operationalContext.setCompanyId(value); next.delete('companyId'); } else { value ? next.set(key, value) : next.delete(key); } next.delete('page'); setParams(next); }; const applySearch = (event: FormEvent) => { event.preventDefault(); setFilter('search', draftSearch.trim()); }; const setPage = (value: number) => { const next = new URLSearchParams(params); value > 1 ? next.set('page', String(value)) : next.delete('page'); setParams(next); }; return
SEGUIMIENTO

Hallazgos

Bandeja de trabajo para respuestas de empresas, vencimientos, verificaciones y cierres.

Planificar verificaciones
1Vencimiento administrativoFecha límite para recibir la respuesta de la empresa.
2Respuesta recibidaSe carga la presentación y su documentación.
3Vencimiento de verificaciónFecha para volver a inspeccionar o verificar la solución.
4CierreEl hallazgo se cierra cuando la solución queda verificada.
{error && {error}} {loading ? : items.length === 0 ? :
{meta.total} hallazgo{meta.total === 1 ? '' : 's'}Página {page} de {Math.max(meta.totalPages, 1)}
{items.map((finding) => { const attention = attentionLabel(finding); const deadline = deadlineText(finding); return ; })}
HallazgoEmpresa / áreaElementoSituaciónPróximo vencimientoDocumentación
{finding.title}{finding.code} · {finding.document.actCode}
{finding.asset.operatorCompany?.name ?? 'Empresa sin asignar'}{finding.asset.operationalArea?.name ?? 'Área sin asignar'}
{finding.asset.name}{finding.asset.code} {attention.label}
{deadline.title}{formatDateOnly(deadline.value)}
{finding.companyResponseReceivedOn ? `Respuesta ${formatDateOnly(finding.companyResponseReceivedOn)}` : 'Sin respuesta'}
{meta.total ? `${(page - 1) * meta.pageSize + 1}–${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}
}
; }