diff --git a/web-v2/src/features/assets/AssetHierarchyView.tsx b/web-v2/src/features/assets/AssetHierarchyView.tsx index 70ae762..c5f092c 100644 --- a/web-v2/src/features/assets/AssetHierarchyView.tsx +++ b/web-v2/src/features/assets/AssetHierarchyView.tsx @@ -1,108 +1,87 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link, useSearchParams } from 'react-router'; import { useAuth } from '../../auth/AuthContext'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback'; import { Icon } from '../../components/Icon'; +import { getAssetLineage } from '../../lib/api'; +import type { AssetLineageItem } from '../../lib/api'; import { - getAsset, - getAssetLineage, - listAreasForCompany, - listCompaniesForArea, - listAssetTreeChildren, - listOperationalAreas, - listOperationalCompanies, -} from '../../lib/api'; -import type { AssetDetail, AssetLineageItem, AssetListItem, OperationalAssetSummary } from '../../lib/api'; + listInventoryAreas, + listInventoryChildren, +} from '../../lib/inventoryBrowserApi'; +import type { + InventoryBrowserArea, + InventoryBrowserItem, + InventoryQuery, +} from '../../lib/inventoryBrowserApi'; import { assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from './assetPresentation'; -type TreeFilters = Omit[0]>, 'parentId' | 'limit'>; -type NavigationSection = 'companies' | 'territory'; -type ChildGroupKey = 'fields' | 'installations' | 'wells' | 'equipment'; - -const INSTALLATION_CODES = new Set([ - 'estructura_local', 'locacion', 'instalacion', 'planta', 'bateria', 'estacion', 'subestacion', - 'zona_bombas', 'sistema_drenaje', 'sistema_electrico_iluminacion', 'sistema_defensa_incendios', - 'pileta_api', 'cargadero_descargadero', -]); - -const GROUP_ORDER: ChildGroupKey[] = ['fields', 'installations', 'wells', 'equipment']; -const GROUP_LABELS: Record = { - fields: { title: 'Yacimientos', description: 'Unidades territoriales u operativas dentro del Área.' }, - installations: { title: 'Instalaciones y estructura', description: 'Plantas, baterías, estaciones, locaciones y niveles estructurales.' }, - wells: { title: 'Pozos', description: 'Pozos identificados dentro del contexto seleccionado.' }, - equipment: { title: 'Equipos y otros elementos', description: 'Equipos técnicos y demás elementos registrados en el inventario.' }, -}; - -function childGroup(item: AssetListItem): ChildGroupKey { - const code = item.type.code.toLowerCase(); - if (code === 'yacimiento') return 'fields'; - if (INSTALLATION_CODES.has(code)) return 'installations'; - if (code === 'pozo') return 'wells'; - return 'equipment'; -} - -function normalizeSearch(value: string | undefined) { - return value?.trim().toLocaleLowerCase('es-AR') ?? ''; -} - -function matchesSearch(item: { name: string; code: string; commonName?: string | null }, search?: string) { - if (!search) return true; - const term = normalizeSearch(search); - return item.name.toLocaleLowerCase('es-AR').includes(term) || item.code.toLocaleLowerCase('es-AR').includes(term) || Boolean(item.commonName?.toLocaleLowerCase('es-AR').includes(term)); -} - -function navigationHref(base: URLSearchParams, section: NavigationSection, options: { companyId?: string; parentId?: string } = {}) { +function navigationHref(base: URLSearchParams, parentId?: string) { const params = new URLSearchParams(base); params.delete('view'); params.delete('page'); - params.delete('operationalAreaId'); - params.delete('operatorCompanyId'); - params.set('section', section); - options.companyId ? params.set('companyId', options.companyId) : params.delete('companyId'); - options.parentId ? params.set('parentId', options.parentId) : params.delete('parentId'); - return `/inventarios?${params}`; + params.delete('section'); + params.delete('companyId'); + parentId ? params.set('parentId', parentId) : params.delete('parentId'); + return `/inventarios${params.size ? `?${params}` : ''}`; } -function AssetCard({ item, href }: { item: AssetListItem; href: string }) { +function AreaCard({ area, href }: { area: InventoryBrowserArea; href: string }) { return - + - {item.name} - {item.code} · {item.type.name}{item.commonName ? ` · ${item.commonName}` : ''} + {area.name} + + {area.code} · {area.yacimientoCount} yacimiento{area.yacimientoCount === 1 ? '' : 's'} + {area.currentOperator ? ` · Operadora vigente: ${area.currentOperator.name}` : ' · Sin operadora vigente'} + - {assetStatusLabel(item.informationStatus)} - {assetOperationalStatusLabel(item.operationalStatus)} + {area.inventoryCount} + instancia{area.inventoryCount === 1 ? '' : 's'} real{area.inventoryCount === 1 ? '' : 'es'} ; } -function SummaryCard({ item, href, subtitle }: { item: OperationalAssetSummary; href: string; subtitle: string }) { +function InventoryCard({ item, href }: { item: InventoryBrowserItem; href: string }) { + const structural = !item.isInventoryInstance; return - + {item.name} - {item.code} · {subtitle}{item.commonName ? ` · ${item.commonName}` : ''} + + {item.code} · {item.type.name} + {item.inventoryFamily ? ` · ${item.inventoryFamily.name}` : ''} + {item.commonName ? ` · ${item.commonName}` : ''} + + + + {structural + ? <>Contexto{item.childrenCount} nivel{item.childrenCount === 1 ? '' : 'es'} inferior{item.childrenCount === 1 ? '' : 'es'} + : <>{assetStatusLabel(item.informationStatus)}{assetOperationalStatusLabel(item.operationalStatus)}} ; } -export function AssetHierarchyView({ filters }: { filters: TreeFilters }) { +function nextLevelLabel(typeCode: string | undefined) { + switch (typeCode?.toLowerCase()) { + case 'area': return 'Yacimientos'; + case 'yacimiento': return 'Instalaciones'; + case 'instalacion': return 'Subinstalaciones'; + default: return 'Niveles inferiores'; + } +} + +export function AssetHierarchyView({ filters }: { filters: InventoryQuery }) { const { hasPermission } = useAuth(); const canCreate = hasPermission('assets.create'); const [searchParams] = useSearchParams(); - const rawSection = searchParams.get('section'); - const section: NavigationSection | null = rawSection === 'companies' || rawSection === 'territory' ? rawSection : null; - const companyId = searchParams.get('companyId') ?? ''; const parentId = searchParams.get('parentId') ?? ''; - - const [companies, setCompanies] = useState([]); - const [areas, setAreas] = useState([]); - const [company, setCompany] = useState(null); + const [areas, setAreas] = useState([]); + const [children, setChildren] = useState([]); const [lineage, setLineage] = useState([]); - const [children, setChildren] = useState([]); const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -111,146 +90,110 @@ export function AssetHierarchyView({ filters }: { filters: TreeFilters }) { let active = true; setLoading(true); setError(''); - setCompanies([]); setAreas([]); - setCompany(null); - setLineage([]); setChildren([]); + setLineage([]); setHasMore(false); const run = async () => { - if ((!section || section === 'companies') && !companyId && !parentId) { - const loaded = filters.operationalAreaId ? await listCompaniesForArea(filters.operationalAreaId) : await listOperationalCompanies(); - if (active) setCompanies(loaded.filter((item) => (!filters.operatorCompanyId || item.id === filters.operatorCompanyId) && matchesSearch(item, filters.search))); + if (!parentId) { + const response = await listInventoryAreas({ + search: filters.search, + operationalAreaId: filters.operationalAreaId, + operatorCompanyId: filters.operatorCompanyId, + }); + if (active) setAreas(response.data); return; } - if (section === 'companies' && companyId && !parentId) { - const [loadedCompany, loadedAreas] = await Promise.all([getAsset(companyId), listAreasForCompany(companyId)]); - if (!active) return; - setCompany(loadedCompany); - setAreas(loadedAreas.filter((item) => (!filters.operationalAreaId || item.id === filters.operationalAreaId) && matchesSearch(item, filters.search))); - return; - } - - if (section === 'territory' && !parentId) { - const loaded = await listOperationalAreas(); - if (active) setAreas(loaded.filter((item) => (!filters.operationalAreaId || item.id === filters.operationalAreaId) && matchesSearch(item, filters.search))); - return; - } - - if (parentId) { - const [loadedLineage, childResponse, loadedCompany] = await Promise.all([ - getAssetLineage(parentId), - listAssetTreeChildren({ - ...filters, - operatorCompanyId: companyId || filters.operatorCompanyId, - parentId, - limit: 200, - }), - companyId ? getAsset(companyId) : Promise.resolve(null), - ]); - if (!active) return; - setLineage(loadedLineage); - setChildren(childResponse.data); - setHasMore(childResponse.meta.hasMore); - setCompany(loadedCompany); - } + const [loadedLineage, response] = await Promise.all([ + getAssetLineage(parentId), + listInventoryChildren(parentId, { search: filters.search }), + ]); + if (!active) return; + setLineage(loadedLineage.filter((item) => ['area','yacimiento','instalacion','subinstalacion'].includes(item.type.code.toLowerCase()))); + setChildren(response.data); + setHasMore(response.meta.hasMore); }; - run().catch((requestError) => active && setError(errorMessage(requestError))).finally(() => active && setLoading(false)); + run() + .catch((requestError) => active && setError(errorMessage(requestError))) + .finally(() => active && setLoading(false)); return () => { active = false; }; - }, [section, companyId, parentId, JSON.stringify(filters)]); - - const groupedChildren = useMemo(() => { - const groups = new Map(); - children.forEach((item) => { - const key = childGroup(item); - groups.set(key, [...(groups.get(key) ?? []), item]); - }); - return GROUP_ORDER.map((key) => ({ key, items: groups.get(key) ?? [] })).filter((group) => group.items.length > 0); - }, [children]); - - const current = lineage.at(-1) ?? null; - const activeSection: NavigationSection = section ?? 'companies'; + }, [parentId, filters.search, filters.operationalAreaId, filters.operatorCompanyId]); if (loading) return ; - if (!section && !companyId && !parentId) { + if (!parentId) { + const realTotal = areas.reduce((sum, area) => sum + Number(area.inventoryCount ?? 0), 0); return
{error && {error}}
-
INVENTARIOS POR EMPRESA

Elegí una empresa

Cada empresa tiene su propio inventario. Ingresá para recorrer Áreas, Yacimientos, instalaciones y equipos.

-
Vista territorial{companies.length}
+
+ ESTRUCTURA TERRITORIAL +

Áreas

+

Las Áreas y Yacimientos son contexto de navegación. El Inventario real comienza en las Instalaciones/Subinstalaciones efectivamente registradas.

+
+
+ {realTotal} Inventario real + {areas.length} Áreas +
- {companies.length === 0 ? :
{companies.map((item) => )}
} -
-
1EmpresaInventario principal
-
2ÁreaContexto territorial
-
3YacimientoNivel territorial
-
4InstalaciónPlanta, batería, estación…
-
5EquipoEquipo, pozo, tanque…
+ {areas.length === 0 + ? + :
{areas.map((area) => )}
} +
+
1ÁreaAncla territorial
+
2YacimientoContexto dentro del Área
+
3InstalaciónInventario real
+
4SubinstalaciónInventario real
; } - const breadcrumb =