diff --git a/web-v2/src/pages/InspectionVisitEditorPage.tsx b/web-v2/src/pages/InspectionVisitEditorPage.tsx deleted file mode 100644 index 223e655..0000000 --- a/web-v2/src/pages/InspectionVisitEditorPage.tsx +++ /dev/null @@ -1,410 +0,0 @@ -import { SearchableSelect } from '../components/SearchableSelect'; -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 { useOperationalContext } from '../context/OperationalContext'; -import { - inspectionStatusClass, - inspectionVisitStatusLabel, -} from '../features/inspections/inspectionPresentation'; -import { InspectionActsPanel } from '../features/inspections/InspectionActsPanel'; -import { - createInspectionVisit, - excludeInspectionVisitAsset, - generateInspectionVisitChecklist, - getInspectionVisit, - includeInspectionVisitAsset, - listAssets, - listInspectionAssignees, - listInspectionPlanningAreas, - listInspectionPlanningOperators, - replaceInspectionVisitAssets, - replaceInspectionVisitTeam, - updateInspectionVisit, - updateInspectionVisitStatus, -} from '../lib/api'; -import type { - AssetListItem, - InspectionChecklistItemKind, - InspectionPerson, - InspectionPlanningContextAsset, - InspectionVisit, - InspectionVisitAssetPlanningSource, - InspectionVisitStatus, -} from '../lib/api'; -import { formatDate, formatDateOnly } from '../lib/format'; - -interface VisitForm { - objective: string; - operationalAreaId: string; - operatorCompanyId: string; - plannedStartAt: string; - instructions: string; -} - -const emptyVisit: VisitForm = { - objective: '', - operationalAreaId: '', - operatorCompanyId: '', - plannedStartAt: '', - instructions: '', -}; - -function localDateTime(value: string | null): string { - if (!value) return ''; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return ''; - const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); - return local.toISOString().slice(0, 16); -} - -function isoOrNull(value: string): string | null { - return value ? new Date(value).toISOString() : null; -} - -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: InspectionChecklistItemKind): string { - if (kind === 'COMPANY_OVERDUE') return 'Respuesta vencida'; - if (kind === 'VERIFICATION_OVERDUE') return 'Control vencido'; - if (kind === 'UPCOMING_CONTROL') return 'Próximo control'; - return 'Antecedente'; -} - -export function InspectionVisitEditorPage() { - const { id } = useParams(); - const navigate = useNavigate(); - const { hasPermission } = useAuth(); - const operationalContext = useOperationalContext(); - const isNew = !id; - const canManage = hasPermission('inspections.manage'); - const canAssign = hasPermission('inspections.assign'); - const [visit, setVisit] = useState(null); - const [form, setForm] = useState(emptyVisit); - const [areas, setAreas] = useState([]); - const [operators, setOperators] = useState([]); - const [assets, setAssets] = useState([]); - const [assignees, setAssignees] = 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 applyVisit = (value: InspectionVisit, syncGeneral = true) => { - setVisit(value); - if (value.operationalArea?.id) operationalContext.setContext(value.operationalArea.id, value.operatorCompany?.id ?? ''); - setLeadInspectorId(value.leadInspector?.id ?? ''); - setMemberIds(new Set(value.team.map((member) => member.id))); - if (syncGeneral) { - setForm({ - objective: value.objective ?? '', - operationalAreaId: value.operationalArea?.id ?? '', - operatorCompanyId: value.operatorCompany?.id ?? '', - plannedStartAt: localDateTime(value.plannedStartAt), - instructions: value.instructions ?? '', - }); - } - }; - - useEffect(() => { - listInspectionPlanningAreas().then(setAreas).catch(() => undefined); - }, []); - - useEffect(() => { - if (!isNew || form.operationalAreaId || !operationalContext.areaId) return; - setForm((current) => ({ - ...current, - operationalAreaId: operationalContext.areaId, - operatorCompanyId: operationalContext.companyId, - })); - }, [isNew, form.operationalAreaId, operationalContext.areaId, operationalContext.companyId]); - - useEffect(() => { - if (!id) return; - setLoading(true); - getInspectionVisit(id) - .then((value) => applyVisit(value)) - .catch((requestError) => setError(errorMessage(requestError))) - .finally(() => setLoading(false)); - }, [id]); - - useEffect(() => { - if (!form.operationalAreaId) { - setOperators([]); - return; - } - listInspectionPlanningOperators(form.operationalAreaId) - .then(setOperators) - .catch(() => setOperators([])); - }, [form.operationalAreaId]); - - useEffect(() => { - if (!form.operationalAreaId || !form.operatorCompanyId) { - setAssets([]); - return; - } - const timer = window.setTimeout(() => { - listAssets({ - pageSize: 100, - search: assetSearch.trim(), - operationalAreaId: form.operationalAreaId, - operatorCompanyId: form.operatorCompanyId, - }) - .then((response) => setAssets(response.data)) - .catch(() => undefined); - }, 220); - return () => window.clearTimeout(timer); - }, [assetSearch, form.operationalAreaId, form.operatorCompanyId]); - - useEffect(() => { - if (!canAssign) return; - listInspectionAssignees().then(setAssignees).catch(() => undefined); - }, [canAssign]); - - 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 { - if (isNew) { - const plannedStartAt = isoOrNull(form.plannedStartAt); - 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 || null, - operationalAreaId: form.operationalAreaId || null, - operatorCompanyId: form.operatorCompanyId || null, - plannedStartAt: isoOrNull(form.plannedStartAt), - instructions: form.instructions || null, - })); - setSuccess('Planificación actualizada. Si cambió el contexto o la fecha, regenerá el checklist.'); - } - } catch (requestError) { - setError(errorMessage(requestError)); - } finally { - setBusy(false); - } - }; - - const generateChecklist = async () => { - if (!id) return; - setBusy(true); - setError(''); - setSuccess(''); - try { - applyVisit(await generateInspectionVisitChecklist(id), false); - setSuccess('Checklist recalculado con antecedentes, vencidos y próximos controles.'); - } catch (requestError) { - setError(errorMessage(requestError)); - } finally { - setBusy(false); - } - }; - - const addPreventiveAsset = async (event: FormEvent) => { - event.preventDefault(); - if (!id || !visit || !newAssetId) return; - setBusy(true); - setError(''); - setSuccess(''); - try { - applyVisit(await replaceInspectionVisitAssets(id, [...activeAssetIds, newAssetId]), false); - setNewAssetId(''); - setSuccess('Registro agregado como inspección preventiva.'); - } 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 de exclusión debe tener al menos 10 caracteres.'); - return; - } - setBusy(true); - setError(''); - setSuccess(''); - try { - applyVisit(await excludeInspectionVisitAsset(id, assetId, reason), false); - setSuccess('Registro excluido con motivo auditado.'); - } 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 auditoría.'); - } 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; - setBusy(true); - setError(''); - setSuccess(''); - try { - const nextMembers = new Set(memberIds); - if (leadInspectorId) nextMembers.add(leadInspectorId); - applyVisit(await replaceInspectionVisitTeam( - id, - leadInspectorId || null, - [...nextMembers], - ), false); - setSuccess('Equipo de inspección actualizado.'); - } catch (requestError) { - setError(errorMessage(requestError)); - } finally { - setBusy(false); - } - }; - - const changeStatus = async (status: InspectionVisitStatus) => { - if (!id) return; - let reason: string | null = null; - if (status === 'CANCELLED') { - reason = window.prompt('Indicá el motivo de cancelación (mínimo 10 caracteres):')?.trim() || null; - if (!reason) return; - } - setBusy(true); - setError(''); - setSuccess(''); - try { - applyVisit(await updateInspectionVisitStatus(id, status, reason)); - setSuccess(`Visita ${inspectionVisitStatusLabel(status).toLocaleLowerCase('es-AR')}.`); - } 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 ? 'Cuatro datos y listo. El código y el checklist se generan 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'}

{isNew ? '¿Dónde y cuándo se inspecciona?' : 'Contexto y fecha de inicio'}

{isNew ? 'El código se asigna automáticamente. No hay título ni fecha de fin planificada.' : 'La inspección conserva una única fecha de inicio planificada; el cierre real se registra al finalizar en campo.'}

{visit && Actualizado {formatDate(visit.updatedAt)}}
- {!isNew &&
Código automático{visit?.code}
} -
- - - - {isNew && } -
- {!isNew && <>