import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { SearchableSelect } from '../components/SearchableSelect'; import { useOperationalContext } from '../context/OperationalContext'; import { InspectionActsPanel } from '../features/inspections/InspectionActsPanel'; import { inspectionStatusClass, inspectionVisitStatusLabel, } from '../features/inspections/inspectionPresentation'; import { listInspectionPreventiveCandidates, } from '../features/inspections/preventiveCandidatesApi'; import type { InspectionPreventiveCandidate, } from '../features/inspections/preventiveCandidatesApi'; import { createInspectionVisit, excludeInspectionVisitAsset, generateInspectionVisitChecklist, getInspectionVisit, includeInspectionVisitAsset, listInspectionAssignees, listInspectionPlanningAreas, listInspectionPlanningOperators, replaceInspectionVisitAssets, replaceInspectionVisitTeam, updateInspectionVisit, } from '../lib/api'; import type { InspectionPerson, InspectionPlanningContextAsset, InspectionVisit, InspectionVisitAssetPlanningSource, } from '../lib/api'; import { cancelInspectionVisit, planInspectionVisit, unplanInspectionVisit, } from '../lib/inspectionVisitLifecycleApi'; import { formatDate, formatDateOnly } from '../lib/format'; interface VisitForm { objective: string; operationalAreaId: string; operatorCompanyId: string; plannedStartAt: string; instructions: string; } const emptyForm: VisitForm = { objective: '', operationalAreaId: '', operatorCompanyId: '', plannedStartAt: '', instructions: '', }; function localDateTime(value: string | null): string { if (!value) return ''; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return ''; const local = new Date(parsed.getTime() - parsed.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 16); } function toIso(value: string): string | null { if (!value) return null; const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); } function personName(person: InspectionPerson): string { return `${person.firstName} ${person.lastName}`; } function sourceLabel(source: InspectionVisitAssetPlanningSource): string { if (source === 'AUTOMATIC') return 'Checklist automático'; if (source === 'PREVENTIVE') return 'Preventivo'; if (source === 'VERIFICATION') return 'Verificación'; return 'Plan previo'; } function checklistLabel(kind: string): string { if (kind === 'VERIFICATION_OVERDUE') return 'Control vencido'; if (kind === 'UPCOMING_CONTROL') return 'Próximo control'; return 'Antecedente'; } export function InspectionVisitEditorF4Page() { const { id } = useParams(); const navigate = useNavigate(); const { hasPermission } = useAuth(); const context = useOperationalContext(); const isNew = !id; const canManage = hasPermission('inspections.manage'); const canAssign = hasPermission('inspections.assign'); const [visit, setVisit] = useState(null); const [form, setForm] = useState(emptyForm); const [areas, setAreas] = useState([]); const [operators, setOperators] = useState([]); const [assignees, setAssignees] = useState([]); const [assets, setAssets] = useState([]); const [assetSearch, setAssetSearch] = useState(''); const [newAssetId, setNewAssetId] = useState(''); const [leadInspectorId, setLeadInspectorId] = useState(''); const [memberIds, setMemberIds] = useState>(new Set()); const [loading, setLoading] = useState(!isNew); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [showCancelForm, setShowCancelForm] = useState(false); const [cancelReason, setCancelReason] = useState(''); const applyVisit = (value: InspectionVisit, syncForm = true) => { setVisit(value); setLeadInspectorId(value.leadInspector?.id ?? ''); setMemberIds(new Set(value.team.map((member) => member.id))); if (value.operationalArea?.id) { context.setContext(value.operationalArea.id, value.operatorCompany?.id ?? ''); } if (syncForm) { setForm({ objective: value.objective ?? '', operationalAreaId: value.operationalArea?.id ?? '', operatorCompanyId: value.operatorCompany?.id ?? '', plannedStartAt: localDateTime(value.plannedStartAt), instructions: value.instructions ?? '', }); } }; const reload = async () => { if (!id) return; applyVisit(await getInspectionVisit(id)); }; useEffect(() => { if (isNew) listInspectionPlanningAreas().then(setAreas).catch(() => undefined); if (canAssign) listInspectionAssignees().then(setAssignees).catch(() => undefined); }, [canAssign, isNew]); useEffect(() => { if (!isNew || form.operationalAreaId || !context.areaId) return; setForm((current) => ({ ...current, operationalAreaId: context.areaId, operatorCompanyId: context.companyId, })); }, [isNew, form.operationalAreaId, context.areaId, context.companyId]); useEffect(() => { if (!id) return; setLoading(true); reload() .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, [id]); useEffect(() => { if (!isNew || !form.operationalAreaId) { setOperators([]); return; } listInspectionPlanningOperators(form.operationalAreaId) .then(setOperators) .catch(() => setOperators([])); }, [form.operationalAreaId, isNew]); useEffect(() => { if (!id || !visit) { setAssets([]); return; } const timer = window.setTimeout(() => { listInspectionPreventiveCandidates(id, assetSearch) .then(setAssets) .catch((requestError) => setError(errorMessage(requestError))); }, 220); return () => window.clearTimeout(timer); }, [id, assetSearch, visit?.updatedAt]); const planningEditable = !visit || visit.status === 'DRAFT' || visit.status === 'PLANNED'; const activeAssetIds = useMemo(() => new Set(visit?.assets.map((asset) => asset.id) ?? []), [visit]); const linkedAssetIds = useMemo(() => new Set(visit?.planningAssets.map((asset) => asset.id) ?? []), [visit]); const candidateAssets = assets.filter((asset) => !linkedAssetIds.has(asset.id)); const saveGeneral = async (event: FormEvent) => { event.preventDefault(); setBusy(true); setError(''); setSuccess(''); try { const plannedStartAt = toIso(form.plannedStartAt); if (isNew) { if (!form.operationalAreaId || !form.operatorCompanyId || !plannedStartAt || !leadInspectorId) { setError('Seleccioná Área, Operadora, fecha de inicio e Inspector.'); return; } const created = await createInspectionVisit({ operationalAreaId: form.operationalAreaId, operatorCompanyId: form.operatorCompanyId, plannedStartAt, leadInspectorUserId: leadInspectorId, }); navigate(`/inspecciones/${created.id}`, { replace: true }); } else if (id) { applyVisit(await updateInspectionVisit(id, { objective: form.objective.trim() || null, plannedStartAt, instructions: form.instructions.trim() || null, })); setSuccess('Planificación actualizada. Si cambió la fecha, regenerá el checklist.'); } } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const regenerateChecklist = async () => { if (!id) return; setBusy(true); setError(''); setSuccess(''); try { applyVisit(await generateInspectionVisitChecklist(id), false); setSuccess('Checklist técnico recalculado con antecedentes y controles.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const addPreventiveAsset = async (event: FormEvent) => { event.preventDefault(); if (!id || !newAssetId) return; setBusy(true); setError(''); setSuccess(''); try { applyVisit(await replaceInspectionVisitAssets(id, [...activeAssetIds, newAssetId]), false); setAssets((current) => current.filter((asset) => asset.id !== newAssetId)); setNewAssetId(''); setSuccess('Registro agregado como preventivo.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const excludeAsset = async (assetId: string) => { if (!id) return; const reason = window.prompt('Motivo de exclusión (mínimo 10 caracteres). Quedará auditado:')?.trim() ?? ''; if (reason.length < 10) { if (reason) setError('El motivo debe tener al menos 10 caracteres.'); return; } setBusy(true); setError(''); setSuccess(''); try { applyVisit(await excludeInspectionVisitAsset(id, assetId, reason), false); setSuccess('Registro excluido con trazabilidad.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const reincludeAsset = async (assetId: string) => { if (!id) return; setBusy(true); setError(''); setSuccess(''); try { applyVisit(await includeInspectionVisitAsset(id, assetId), false); setSuccess('Registro reincorporado; la exclusión anterior permanece en historial.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const chooseLead = (value: string) => { setLeadInspectorId(value); if (value) setMemberIds((current) => new Set([...current, value])); }; const saveTeam = async (event: FormEvent) => { event.preventDefault(); if (!id) return; const nextMembers = new Set(memberIds); if (leadInspectorId) nextMembers.add(leadInspectorId); setBusy(true); setError(''); setSuccess(''); try { applyVisit(await replaceInspectionVisitTeam(id, leadInspectorId || null, [...nextMembers]), false); setSuccess('Equipo actualizado.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const lifecycleAction = async (action: 'PLAN' | 'UNPLAN') => { if (!id) return; setBusy(true); setError(''); setSuccess(''); try { let updated: InspectionVisit; if (action === 'PLAN') { updated = await planInspectionVisit(id); setSuccess('Inspección planificada. Ya puede iniciarse desde la APK.'); } else { updated = await unplanInspectionVisit(id); setSuccess('La Inspección volvió a borrador para corregir la planificación.'); } applyVisit(updated); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const cancelCurrentVisit = async (event: FormEvent) => { event.preventDefault(); if (!id) return; const reason = cancelReason.trim(); if (reason.length < 10) { setError('El motivo de cancelación debe tener al menos 10 caracteres.'); return; } if (reason.length > 500) { setError('El motivo de cancelación no puede superar los 500 caracteres.'); return; } setBusy(true); setError(''); setSuccess(''); try { const updated = await cancelInspectionVisit(id, reason); applyVisit(updated); setCancelReason(''); setShowCancelForm(false); setSuccess('Inspección cancelada. El motivo quedó registrado y auditado.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; if (loading) return ; return
Inspecciones/{isNew ? 'Planificar inspección' : visit?.code ?? 'Detalle'}
INSPECCIÓN

{isNew ? 'Planificar inspección' : visit?.code ?? 'Inspección'}

{isNew ? 'Área, Operadora, inicio e Inspector. El identificador se genera automáticamente.' : `${visit?.operationalArea?.name ?? 'Sin Área'} · ${visit?.operatorCompany?.name ?? 'Sin Operadora'} · ${visit?.assetCount ?? 0} registros`}

{visit && {inspectionVisitStatusLabel(visit.status)}}
{error && {error}}{success && {success}}
{isNew ? 'CREACIÓN RÁPIDA' : 'PLANIFICACIÓN'}

Contexto y fecha de inicio

El Área, Yacimiento y Operadora quedan fijados al crear la Inspección. La fecha puede reprogramarse antes del trabajo de campo.

{visit && Actualizado {formatDate(visit.updatedAt)}}
{visit &&
Identificador institucional{visit.code}
}
{isNew ? : } {!isNew && } {isNew ? : } {isNew && }
{!isNew && <>