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 { listFindingWorklistF4, } from '../lib/findingWorklistF4Api'; import type { FindingWorklistCountersF4, FindingWorklistItemF4, FindingWorklistWorkflowF4, } from '../lib/findingWorklistF4Api'; import { formatDateOnly } from '../lib/format'; const workflows: Array<{ value: FindingWorklistWorkflowF4; label: string }> = [ { value: 'OPEN', label: 'Abiertos' }, { value: 'TO_SCHEDULE_VERIFICATION', label: 'Sin fecha de control' }, { value: 'TO_VERIFY', label: 'Con control programado' }, { value: 'VERIFICATION_OVERDUE', label: 'Verificación vencida' }, { value: 'READY_TO_CLOSE', label: 'Listos para cerrar' }, { value: 'CLOSED', label: 'Cerrados' }, { value: 'ALL', label: 'Todos' }, ]; const emptyCounters: FindingWorklistCountersF4 = { open: 0, withoutControlDate: 0, toVerify: 0, verificationOverdue: 0, verificationNext30Days: 0, readyToClose: 0, closed: 0, }; 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: FindingWorklistItemF4): { label: string; className: string } { const today = todayInput(); if (finding.status === 'CLOSED') return { label: 'Cerrado', className: 'active' }; if (finding.latestVerification?.outcome === 'RESOLVED' && finding.latestVerification.visitStatus === 'CLOSED') { return { label: 'Listo para cerrar', className: 'active' }; } if (!finding.nextControlOn) return { label: 'Sin fecha de control', className: 'observed' }; if (finding.verificationVisit) return { label: 'Verificación planificada', className: 'blue' }; if (finding.nextControlOn < today) return { label: 'Verificación vencida', className: 'danger' }; return { label: 'Control programado', className: 'pending' }; } function deadlineText(finding: FindingWorklistItemF4): { 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 }; } return { title: finding.nextControlOn ? 'Verificación DH' : 'Sin fecha definida', value: finding.nextControlOn, overdue: Boolean(finding.nextControlOn && finding.nextControlOn < today), }; } 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 FindingWorklistWorkflowF4 : '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(''); listFindingWorklistF4({ 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 TÉCNICO

Hallazgos

Observaciones originales, evidencia, fechas de control, verificaciones y cierre técnico. Las presentaciones administrativas de empresa se siguen desde el INF.

Planificar verificaciones
1HallazgoObservación y Descripción registradas en el Acta.
2Fecha de controlSe define cuándo corresponde volver a verificar.
3VerificaciónEl inspector registra el resultado y la evidencia en campo.
4CierreSe cierra cuando la solución queda técnicamente 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ónFecha de controlDescripció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.description}
{meta.total ? `${(page - 1) * meta.pageSize + 1}–${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}
}
; }