200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
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 {
|
||
listInventoryAreas,
|
||
listInventoryChildren,
|
||
} from '../../lib/inventoryBrowserApi';
|
||
import type {
|
||
InventoryBrowserArea,
|
||
InventoryBrowserItem,
|
||
InventoryQuery,
|
||
} from '../../lib/inventoryBrowserApi';
|
||
import { assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from './assetPresentation';
|
||
|
||
function navigationHref(base: URLSearchParams, parentId?: string) {
|
||
const params = new URLSearchParams(base);
|
||
params.delete('view');
|
||
params.delete('page');
|
||
params.delete('section');
|
||
params.delete('companyId');
|
||
parentId ? params.set('parentId', parentId) : params.delete('parentId');
|
||
return `/inventarios${params.size ? `?${params}` : ''}`;
|
||
}
|
||
|
||
function AreaCard({ area, href }: { area: InventoryBrowserArea; href: string }) {
|
||
return <Link className="asset-browser-item" to={href}>
|
||
<span className="asset-browser-item-icon"><Icon name="map" size={17} /></span>
|
||
<span className="asset-browser-item-main">
|
||
<strong>{area.name}</strong>
|
||
<small>
|
||
{area.code} · {area.yacimientoCount} yacimiento{area.yacimientoCount === 1 ? '' : 's'}
|
||
{area.currentOperator ? ` · Operadora vigente: ${area.currentOperator.name}` : ' · Sin operadora vigente'}
|
||
</small>
|
||
</span>
|
||
<span className="asset-browser-item-status">
|
||
<strong>{area.inventoryCount}</strong>
|
||
<small>instancia{area.inventoryCount === 1 ? '' : 's'} real{area.inventoryCount === 1 ? '' : 'es'}</small>
|
||
</span>
|
||
<Icon name="chevron" size={16} />
|
||
</Link>;
|
||
}
|
||
|
||
function InventoryCard({ item, href }: { item: InventoryBrowserItem; href: string }) {
|
||
const structural = !item.isInventoryInstance;
|
||
return <Link className="asset-browser-item" to={href}>
|
||
<span className="asset-browser-item-icon"><Icon name={structural ? 'map' : 'layers'} size={17} /></span>
|
||
<span className="asset-browser-item-main">
|
||
<strong>{item.name}</strong>
|
||
<small>
|
||
{item.code} · {item.type.name}
|
||
{item.inventoryFamily ? ` · ${item.inventoryFamily.name}` : ''}
|
||
{item.commonName ? ` · ${item.commonName}` : ''}
|
||
</small>
|
||
</span>
|
||
<span className="asset-browser-item-status">
|
||
{structural
|
||
? <><span className="tag">Contexto</span><small>{item.childrenCount} nivel{item.childrenCount === 1 ? '' : 'es'} inferior{item.childrenCount === 1 ? '' : 'es'}</small></>
|
||
: <><span className={`status-badge ${assetStatusClass(item.informationStatus)}`}>{assetStatusLabel(item.informationStatus)}</span><small>{assetOperationalStatusLabel(item.operationalStatus)}</small></>}
|
||
</span>
|
||
<Icon name="chevron" size={16} />
|
||
</Link>;
|
||
}
|
||
|
||
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 parentId = searchParams.get('parentId') ?? '';
|
||
const [areas, setAreas] = useState<InventoryBrowserArea[]>([]);
|
||
const [children, setChildren] = useState<InventoryBrowserItem[]>([]);
|
||
const [lineage, setLineage] = useState<AssetLineageItem[]>([]);
|
||
const [hasMore, setHasMore] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState('');
|
||
|
||
useEffect(() => {
|
||
let active = true;
|
||
setLoading(true);
|
||
setError('');
|
||
setAreas([]);
|
||
setChildren([]);
|
||
setLineage([]);
|
||
setHasMore(false);
|
||
|
||
const run = async () => {
|
||
if (!parentId) {
|
||
const response = await listInventoryAreas({
|
||
search: filters.search,
|
||
operationalAreaId: filters.operationalAreaId,
|
||
operatorCompanyId: filters.operatorCompanyId,
|
||
});
|
||
if (active) setAreas(response.data);
|
||
return;
|
||
}
|
||
|
||
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));
|
||
return () => { active = false; };
|
||
}, [parentId, filters.search, filters.operationalAreaId, filters.operatorCompanyId]);
|
||
|
||
if (loading) return <LoadingBlock label="Cargando inventarios…" />;
|
||
|
||
if (!parentId) {
|
||
const realTotal = areas.reduce((sum, area) => sum + Number(area.inventoryCount ?? 0), 0);
|
||
return <div className="asset-browser-panel">
|
||
{error && <Alert>{error}</Alert>}
|
||
<div className="asset-browser-section-heading">
|
||
<div>
|
||
<span className="eyebrow">ESTRUCTURA TERRITORIAL</span>
|
||
<h2>Áreas</h2>
|
||
<p>Las Áreas y Yacimientos son contexto de navegación. El Inventario real comienza en las Instalaciones/Subinstalaciones efectivamente registradas.</p>
|
||
</div>
|
||
<div className="asset-browser-current-actions">
|
||
<span className="count-pill">{realTotal} Inventario real</span>
|
||
<span className="count-pill">{areas.length} Áreas</span>
|
||
</div>
|
||
</div>
|
||
{areas.length === 0
|
||
? <EmptyState title="No hay Áreas para mostrar" text="Probá con otra búsqueda o revisá el contexto seleccionado." />
|
||
: <div className="asset-browser-list">{areas.map((area) => <AreaCard key={area.id} area={area} href={navigationHref(searchParams, area.id)} />)}</div>}
|
||
<div className="asset-browser-levels" aria-label="Estructura de Inventarios">
|
||
<div><span>1</span><strong>Área</strong><small>Ancla territorial</small></div><i>›</i>
|
||
<div><span>2</span><strong>Yacimiento</strong><small>Contexto dentro del Área</small></div><i>›</i>
|
||
<div><span>3</span><strong>Instalación</strong><small>Inventario real</small></div><i>›</i>
|
||
<div><span>4</span><strong>Subinstalación</strong><small>Inventario real</small></div>
|
||
</div>
|
||
</div>;
|
||
}
|
||
|
||
const current = lineage.at(-1) ?? null;
|
||
const breadcrumb = <nav className="asset-browser-breadcrumb" aria-label="Ruta del Inventario">
|
||
<Link to="/inventarios">Inventarios</Link>
|
||
{lineage.map((item,index) => {
|
||
const isLast=index===lineage.length-1;
|
||
return <span className="asset-browser-crumb-part" key={item.id}>
|
||
<span>›</span>
|
||
{isLast ? <strong>{item.name}</strong> : <Link to={navigationHref(searchParams,item.id)}>{item.name}</Link>}
|
||
</span>;
|
||
})}
|
||
</nav>;
|
||
|
||
return <div className="asset-browser-panel">
|
||
{breadcrumb}
|
||
{error && <Alert>{error}</Alert>}
|
||
{hasMore && <Alert type="info">Este nivel tiene más de 200 registros. Usá la búsqueda para acotar el resultado.</Alert>}
|
||
<div className="asset-browser-current-heading">
|
||
<div>
|
||
<span className="eyebrow">{current?.type.name ?? 'INVENTARIO'}</span>
|
||
<h2>{current?.name ?? 'Nivel de Inventario'}</h2>
|
||
<p>{current?.code ?? ''}</p>
|
||
</div>
|
||
<div className="asset-browser-current-actions">
|
||
{current && <Link className="button secondary" to={`/inventarios/${current.id}`}>Ver ficha</Link>}
|
||
{canCreate && current?.type.code.toLowerCase() !== 'subinstalacion' && <Link className="button primary" to={`/inventarios/nuevo?parentId=${parentId}`}><Icon name="plus" />Agregar aquí</Link>}
|
||
</div>
|
||
</div>
|
||
|
||
<section className="asset-browser-group">
|
||
<div className="asset-browser-group-heading">
|
||
<div><h3>{nextLevelLabel(current?.type.code)}</h3><p>La jerarquía permitida es Área → Yacimiento → Instalación → Subinstalación.</p></div>
|
||
<span>{children.length}</span>
|
||
</div>
|
||
{children.length === 0
|
||
? <EmptyState
|
||
title={current?.type.code.toLowerCase() === 'subinstalacion' ? 'Fin de la jerarquía' : 'No hay registros en este nivel'}
|
||
text={current?.type.code.toLowerCase() === 'yacimiento'
|
||
? 'Todavía no hay Instalaciones reales registradas en este Yacimiento.'
|
||
: current?.type.code.toLowerCase() === 'instalacion'
|
||
? 'Todavía no hay Subinstalaciones registradas en esta Instalación.'
|
||
: 'No hay registros que coincidan con la búsqueda actual.'}
|
||
/>
|
||
: <div className="asset-browser-list">{children.map((item) => <InventoryCard key={item.id} item={item} href={navigationHref(searchParams,item.id)} />)}</div>}
|
||
</section>
|
||
</div>;
|
||
}
|