import { SearchableSelect } from '../components/SearchableSelect'; import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { ASSET_OPERATIONAL_STATUSES, ASSET_STATUSES, assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from '../features/assets/assetPresentation'; import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel'; import { AssetDossierPanel } from '../features/assets/AssetDossierPanel'; import { AssetMediaPanel } from '../features/assets/AssetMediaPanel'; import { AssetProvenancePanel } from '../features/assets/AssetProvenancePanel'; import { AssetOperationalRelationsPanel } from '../features/assets/AssetOperationalRelationsPanel'; import { AssetRegistryPanel } from '../features/assets/AssetRegistryPanel'; import { AssetContextHistoryPanel } from '../features/assets/AssetContextHistoryPanel'; import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel'; import { createAsset, getAsset, getAssetLineage, listAssetParentOptions, listAssetTypes, listCompaniesForArea, listOperationalAreas, updateAsset, updateAssetInformationStatus, updateAssetOperationalStatus, } from '../lib/api'; import type { AssetAttributeDefinition, AssetDetail, AssetInformationStatus, AssetOperationalStatus, AssetLineageItem, AssetListItem, AssetType, OperationalAssetSummary, } from '../lib/api'; const AssetGeometryEditor = lazy(() => import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor }))); type DetailTab = 'summary' | 'dossier' | 'findings' | 'location' | 'registry' | 'files' | 'history'; function localDateTime(value: unknown): string { if (typeof value !== 'string' || !value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return value.slice(0, 16); const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 16); } function normalizeAttributeValues(definitions: AssetAttributeDefinition[], values: Record): Record { const result: Record = {}; definitions.filter((item) => item.isActive).forEach((definition) => { const raw = values[definition.id]; if (definition.dataType === 'BOOLEAN') result[definition.id] = Boolean(raw); else if (raw !== undefined && raw !== null && raw !== '') result[definition.id] = definition.dataType === 'NUMBER' ? Number(raw) : raw; }); return result; } export function AssetEditorPage() { const { id } = useParams(); const editing = Boolean(id); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const contextParentId = !editing ? searchParams.get('parentId') : null; const requestedTab = searchParams.get('tab') as DetailTab | null; const tab: DetailTab = requestedTab && ['summary','dossier','findings','location','registry','files','history'].includes(requestedTab) ? requestedTab : 'summary'; const { hasPermission } = useAuth(); const canEdit = editing ? hasPermission('assets.update') : hasPermission('assets.create'); const canCreate = hasPermission('assets.create'); const canChangeStatus = hasPermission('assets.change_status'); const canChangeOperationalStatus = hasPermission('assets.change_operational_status'); const canEditGeometry = hasPermission('assets.update_geometry'); const canReadHistory = hasPermission('assets.read_history'); const canReadMedia = hasPermission('assets.read_media'); const canManageMedia = hasPermission('assets.manage_media'); const canReadProvenance = hasPermission('assets.read_provenance'); const canManageProvenance = hasPermission('assets.manage_provenance'); const canVerifyProvenance = hasPermission('assets.verify_provenance'); const canReadRelations = hasPermission('asset_relations.read'); const canManageRelations = hasPermission('asset_relations.manage'); const canManageContext = hasPermission('assets.manage_context'); const canReadFindingCatalog = hasPermission('finding_catalog.read'); const canManageFindingCatalog = hasPermission('finding_catalog.manage'); const canReadDossier = hasPermission('inspections.read') && hasPermission('inspection_acts.read') && hasPermission('inspection_findings.read') && hasPermission('inspection_evidence.read') && hasPermission('inspection_communications.read'); const canReadRegistry = hasPermission('asset_registry.read'); const canManageRegistry = hasPermission('asset_registry.manage'); const canSave = canEdit || (editing && (canChangeStatus || canChangeOperationalStatus)); const [types, setTypes] = useState([]); const [asset, setAsset] = useState(null); const [contextParent, setContextParent] = useState(null); const [lineage, setLineage] = useState([]); const [parents, setParents] = useState([]); const [parentSearch, setParentSearch] = useState(''); const [code, setCode] = useState(''); const [name, setName] = useState(''); const [commonName, setCommonName] = useState(''); const [typeId, setTypeId] = useState(''); const [parentId, setParentId] = useState(''); const [operationalAreaId, setOperationalAreaId] = useState(''); const [operatorCompanyId, setOperatorCompanyId] = useState(''); const [operationalAreas, setOperationalAreas] = useState([]); const [operationalCompanies, setOperationalCompanies] = useState([]); const [description, setDescription] = useState(''); const [status, setStatus] = useState('DRAFT'); const [operationalStatus, setOperationalStatus] = useState('UNKNOWN'); const [attributeValues, setAttributeValues] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [historyRefreshKey, setHistoryRefreshKey] = useState(0); const canDirectContextEdit = !editing || Boolean(asset?.dataOrigin === 'FIELD_SURVEY' && asset.informationStatus === 'DRAFT'); const selectedType = types.find((type) => type.id === typeId) ?? null; const definitions = useMemo(() => selectedType?.attributes.filter((item) => item.isActive) ?? [], [selectedType]); useEffect(() => { Promise.all([ listAssetTypes(), id ? getAsset(id) : Promise.resolve(null), id ? getAssetLineage(id) : Promise.resolve([] as AssetLineageItem[]), ]) .then(([loadedTypes, loadedAsset, loadedLineage]) => { setTypes(loadedTypes); setLineage(loadedLineage); if (loadedAsset) { setAsset(loadedAsset); setCode(loadedAsset.code); setName(loadedAsset.name); setCommonName(loadedAsset.commonName ?? ''); setTypeId(loadedAsset.type.id); setParentId(loadedAsset.parent?.id ?? ''); setOperationalAreaId(loadedAsset.operationalArea?.id ?? ''); setOperatorCompanyId(loadedAsset.operatorCompany?.id ?? ''); setDescription(loadedAsset.description ?? ''); setStatus(loadedAsset.informationStatus); setOperationalStatus(loadedAsset.operationalStatus); setAttributeValues(Object.fromEntries(loadedAsset.attributes.map((attribute) => [attribute.definitionId, attribute.dataType === 'DATETIME' ? localDateTime(attribute.value) : attribute.value ?? '']))); } else if (!contextParentId) { const first = loadedTypes.find((type) => type.isActive && type.canBeRoot) ?? loadedTypes.find((type) => type.isActive); if (first) setTypeId(first.id); } }) .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, [id]); useEffect(() => { if (editing || !contextParentId || types.length === 0) return; Promise.all([getAsset(contextParentId), getAssetLineage(contextParentId)]).then(([parent, parentLineage]) => { setContextParent(parent); setLineage(parentLineage); const compatible = types.find((type) => type.isActive && type.allowedParentTypes.some((allowed) => allowed.id === parent.type.id)); if (compatible) { setTypeId(compatible.id); setParentId(parent.id); const parentType = types.find((type) => type.id === parent.type.id); if (parentType?.operationalRole === 'AREA') setOperationalAreaId(parent.id); else if (parent.operationalArea) setOperationalAreaId(parent.operationalArea.id); if (parent.operatorCompany) setOperatorCompanyId(parent.operatorCompany.id); } }).catch((requestError) => setError(errorMessage(requestError))); }, [editing, contextParentId, types]); useEffect(() => { if (!typeId) { setParents([]); return; } const timer = window.setTimeout(() => listAssetParentOptions(typeId, id, parentSearch).then(setParents).catch((requestError) => setError(errorMessage(requestError))), 220); return () => window.clearTimeout(timer); }, [typeId, id, parentSearch]); useEffect(() => { if (!canReadRelations || selectedType?.operationalRole !== 'GENERIC' || !parentId) { setOperationalAreas([]); if (!parentId) { setOperationalAreaId(''); setOperatorCompanyId(''); } return; } listOperationalAreas(parentId).then((loadedAreas) => { setOperationalAreas(loadedAreas); if (operationalAreaId && !loadedAreas.some((area) => area.id === operationalAreaId)) { setOperationalAreaId(''); setOperatorCompanyId(''); } }).catch((requestError) => setError(errorMessage(requestError))); }, [canReadRelations, selectedType?.operationalRole, parentId]); useEffect(() => { if (!canReadRelations || !operationalAreaId || selectedType?.operationalRole !== 'GENERIC') { setOperationalCompanies([]); return; } listCompaniesForArea(operationalAreaId).then(setOperationalCompanies).catch((requestError) => setError(errorMessage(requestError))); }, [canReadRelations, operationalAreaId, selectedType?.operationalRole]); const changeType = (nextTypeId: string) => { setTypeId(nextTypeId); setParentId(''); setOperationalAreaId(''); setOperatorCompanyId(''); setAttributeValues({}); setParentSearch(''); }; const setAttribute = (definitionId: string, value: unknown) => setAttributeValues((current) => ({ ...current, [definitionId]: value })); const save = async (event: FormEvent) => { event.preventDefault(); if (!selectedType) return; setSaving(true); setError(''); setSuccess(''); try { const attributes = normalizeAttributeValues(definitions, attributeValues); let saved: AssetDetail; if (editing && id) { if (canEdit) saved = await updateAsset(id, { typeId: canDirectContextEdit ? typeId : undefined, code, name, commonName: commonName.trim() || null, parentId: canDirectContextEdit ? parentId || null : undefined, operationalAreaId: canDirectContextEdit ? operationalAreaId || null : undefined, operatorCompanyId: canDirectContextEdit ? operatorCompanyId || null : undefined, description: description.trim() || null, attributes }); else if (asset) saved = asset; else return; if (canChangeStatus && saved.informationStatus !== status) saved = await updateAssetInformationStatus(id, status); if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) saved = await updateAssetOperationalStatus(id, operationalStatus); setAsset(saved); setSuccess('Registro actualizado correctamente'); setHistoryRefreshKey((current) => current + 1); } else { saved = await createAsset({ code, name, commonName: commonName.trim() || null, typeId, parentId: parentId || null, operationalAreaId: operationalAreaId || null, operatorCompanyId: operatorCompanyId || null, description: description.trim() || null, informationStatus: canChangeStatus ? status : 'DRAFT', attributes }); navigate(`/inventarios/${saved.id}`, { replace: true }); } } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; if (loading) return ; const detailTabs = [ { key: 'summary', label: 'Resumen', show: true }, { key: 'dossier', label: 'Expediente', show: editing && canReadDossier }, { key: 'findings', label: 'Hallazgos aplicables', show: editing && canReadFindingCatalog }, { key: 'location', label: 'Ubicación', show: editing }, { key: 'registry', label: 'Documentos y registro', show: editing && (canReadRegistry || canReadProvenance) }, { key: 'files', label: 'Archivos', show: editing && canReadMedia }, { key: 'history', label: 'Historial', show: editing && canReadHistory }, ] as const; const breadcrumbSection = asset?.type.code === 'empresa' || contextParent?.type.code === 'empresa' ? 'companies' : 'territory'; return
INVENTARIO

{editing ? asset?.name ?? 'Registro' : contextParent ? `Agregar en ${contextParent.name}` : 'Nuevo registro'}

{editing ? `${asset?.type.name} · ${asset?.code}` : contextParent ? `El sistema heredará el contexto disponible de ${contextParent.code}.` : 'Creá una entidad con identidad, jerarquía y atributos propios.'}

{editing && asset &&
{assetStatusLabel(asset.informationStatus)}{assetOperationalStatusLabel(asset.operationalStatus)}
}{editing && id && canCreate && Agregar registro aquí}
{error && {error}}{success && {success}} {editing && } {(!editing || tab === 'summary') && <> {contextParent && !editing &&
Alta contextual{contextParent.name} · {contextParent.code}
Ver padre
}

Identificación

Conservá el nombre técnico y, cuando exista, agregá el nombre habitual usado en campo.