import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useSearchParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { hasAdministratorRole } from '../auth/adminAccess'; import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { AssetDossierPanel } from '../features/assets/AssetDossierPanel'; import { AssetFindingCatalogPanel } from '../features/assets/AssetFindingCatalogPanel'; import { AssetHistoryPanel } from '../features/assets/AssetHistoryPanel'; import { AssetMediaPanel } from '../features/assets/AssetMediaPanel'; import { AssetTechnicalDataPanel } from '../features/assets/AssetTechnicalDataPanel'; import { ASSET_OPERATIONAL_STATUSES, ASSET_STATUSES, assetOperationalStatusLabel, assetStatusClass, assetStatusLabel, } from '../features/assets/assetPresentation'; import { getAssetLineage, listAssetTreeChildren, updateAsset, updateAssetInformationStatus, updateAssetOperationalStatus, } from '../lib/api'; import type { AssetDetail, AssetInformationStatus, AssetLineageItem, AssetListItem, AssetOperationalStatus, } from '../lib/api'; import { getInventoryTechnicalValues } from '../lib/inventoryStructureApi'; import type { InventoryTechnicalValues } from '../lib/inventoryStructureApi'; import './SimpleInventoryDetailPage.css'; const AssetGeometryEditor = lazy(() => import('../features/map/AssetGeometryEditor').then((module) => ({ default: module.AssetGeometryEditor })), ); export type SimpleInventoryKind = | 'DEPARTAMENTO' | 'AREA' | 'YACIMIENTO' | 'INSTALACION' | 'SUBINSTALACION'; type SimpleTab = 'summary' | 'activity' | 'findings' | 'location' | 'photos' | 'history'; function normalized(value: string | null | undefined): string { return (value ?? '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim() .toLowerCase() .replace(/[_\s]+/g, '-'); } export function simpleInventoryKind(asset: AssetDetail): SimpleInventoryKind | null { const code = normalized(asset.type.code); const name = normalized(asset.type.name); const values = new Set([code, name]); if (values.has('departamento')) return 'DEPARTAMENTO'; if (values.has('area')) return 'AREA'; if (values.has('yacimiento')) return 'YACIMIENTO'; if (values.has('subinstalacion')) return 'SUBINSTALACION'; if ( values.has('instalacion') || values.has('instalacion-de-superficie') || values.has('instalacion-superficie') ) return 'INSTALACION'; return null; } export function isSimpleInventoryAsset(asset: AssetDetail): boolean { return simpleInventoryKind(asset) !== null; } const labels: Record = { DEPARTAMENTO: 'Departamento', AREA: 'Área', YACIMIENTO: 'Yacimiento', INSTALACION: 'Instalación', SUBINSTALACION: 'Subinstalación', }; const childLabels: Partial> = { DEPARTAMENTO: 'Área', AREA: 'Yacimiento', YACIMIENTO: 'Instalación', INSTALACION: 'Subinstalación', }; function lineageLabel(item: AssetLineageItem): string { return item.commonName ? `${item.name} · ${item.commonName}` : item.name; } export function SimpleInventoryDetailPage({ initialAsset }: { initialAsset: AssetDetail }) { const [params, setParams] = useSearchParams(); const { user, hasPermission } = useAuth(); const kind = simpleInventoryKind(initialAsset); const [asset, setAsset] = useState(initialAsset); const [lineage, setLineage] = useState([]); const [children, setChildren] = useState([]); const [childrenHasMore, setChildrenHasMore] = useState(false); const [technical, setTechnical] = useState(null); const [loadingRelated, setLoadingRelated] = useState(true); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [name, setName] = useState(initialAsset.name); const [commonName, setCommonName] = useState(initialAsset.commonName ?? ''); const [description, setDescription] = useState(initialAsset.description ?? ''); const [status, setStatus] = useState(initialAsset.informationStatus); const [operationalStatus, setOperationalStatus] = useState(initialAsset.operationalStatus); const [historyRefreshKey, setHistoryRefreshKey] = useState(0); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const canEdit = hasPermission('assets.update'); const canCreate = hasPermission('assets.create'); const canChangeStatus = hasPermission('assets.change_status'); const canChangeOperationalStatus = hasPermission('assets.change_operational_status'); const canReadHistory = hasPermission('assets.read_history'); const canReadMedia = hasPermission('assets.read_media'); const canManageMedia = hasPermission('assets.manage_media'); const canEditGeometry = hasPermission('assets.update_geometry'); 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 canAdvanced = hasAdministratorRole(user); const inspectable = kind === 'YACIMIENTO' || kind === 'INSTALACION' || kind === 'SUBINSTALACION'; const technicalLevel = kind === 'INSTALACION' || kind === 'SUBINSTALACION'; const hasChildren = kind !== 'SUBINSTALACION'; const requestedTab = params.get('tab') as SimpleTab | null; const availableTabs = useMemo(() => new Set([ 'summary', ...(inspectable && canReadDossier ? ['activity' as const] : []), ...(technicalLevel && canReadFindingCatalog ? ['findings' as const] : []), 'location', ...(canReadMedia ? ['photos' as const] : []), ...(canReadHistory ? ['history' as const] : []), ]), [inspectable, technicalLevel, canReadDossier, canReadFindingCatalog, canReadMedia, canReadHistory]); const tab: SimpleTab = requestedTab && availableTabs.has(requestedTab) ? requestedTab : 'summary'; const loadRelated = async () => { setLoadingRelated(true); try { const [loadedLineage, childPage, loadedTechnical] = await Promise.all([ getAssetLineage(asset.id), hasChildren ? listAssetTreeChildren({ parentId: asset.id, limit: 100 }) : Promise.resolve({ data: [] as AssetListItem[], meta: { count: 0, hasMore: false } }), technicalLevel ? getInventoryTechnicalValues(asset.id).catch(() => null) : Promise.resolve(null), ]); setLineage(loadedLineage); setChildren(childPage.data); setChildrenHasMore(childPage.meta.hasMore); setTechnical(loadedTechnical); } catch (requestError) { setError(errorMessage(requestError)); } finally { setLoadingRelated(false); } }; useEffect(() => { void loadRelated(); }, [asset.id]); if (!kind) return Este registro no pertenece al modelo simple de Inventarios.; const save = async (event: FormEvent) => { event.preventDefault(); if (!name.trim()) return; setSaving(true); setError(''); setSuccess(''); try { let saved = asset; if (canEdit) { saved = await updateAsset(asset.id, { name: name.trim(), commonName: commonName.trim() || null, description: description.trim() || null, }); } if (canChangeStatus && saved.informationStatus !== status) { saved = await updateAssetInformationStatus(asset.id, status); } if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) { saved = await updateAssetOperationalStatus(asset.id, operationalStatus); } setAsset(saved); setName(saved.name); setCommonName(saved.commonName ?? ''); setDescription(saved.description ?? ''); setStatus(saved.informationStatus); setOperationalStatus(saved.operationalStatus); setEditing(false); setHistoryRefreshKey((current) => current + 1); setSuccess('Registro actualizado.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const cancelEdit = () => { setEditing(false); setName(asset.name); setCommonName(asset.commonName ?? ''); setDescription(asset.description ?? ''); setStatus(asset.informationStatus); setOperationalStatus(asset.operationalStatus); }; const setTab = (next: SimpleTab) => { const nextParams = new URLSearchParams(params); next === 'summary' ? nextParams.delete('tab') : nextParams.set('tab', next); nextParams.delete('advanced'); setParams(nextParams); }; const childLabel = childLabels[kind]; const parentLabel = kind === 'AREA' ? 'Departamento' : kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : kind === 'SUBINSTALACION' ? 'Instalación' : null; return
{labels[kind].toUpperCase()}

{asset.name}

{asset.code}{asset.commonName && {asset.commonName}}
{assetStatusLabel(asset.informationStatus)} {tab === 'summary' && (canEdit || canChangeStatus || canChangeOperationalStatus) && } {tab === 'summary' && canCreate && childLabel && Agregar {childLabel.toLowerCase()}}
{inspectable ? 'Ficha operativa: identidad, ubicación y actividad de inspección. La información administrativa avanzada queda fuera de la vista cotidiana.' : 'Este nivel organiza la estructura del Inventario. La vista cotidiana muestra sólo los datos necesarios para ubicar y navegar.'}
{error && {error}} {success && {success}} {tab === 'summary' &&
DATOS PRINCIPALES

Identificación

{editing ?
Más datos