fix(web): show only real inventory instances

This commit is contained in:
2026-09-08 20:05:04 -03:00
parent 3f0148e4b7
commit 4bc54f67fe
+54 -26
View File
@@ -15,14 +15,15 @@ import {
ASSET_OPERATIONAL_STATUSES, ASSET_OPERATIONAL_STATUSES,
ASSET_STATUSES, ASSET_STATUSES,
} from '../features/assets/assetPresentation'; } from '../features/assets/assetPresentation';
import { listAssets, listAssetTree, listAssetTypes } from '../lib/api'; import { listAssetTypes } from '../lib/api';
import type { import type {
AssetInformationStatus, AssetInformationStatus,
AssetListItem,
AssetOperationalStatus, AssetOperationalStatus,
AssetType, AssetType,
PageMeta, PageMeta,
} from '../lib/api'; } from '../lib/api';
import { listRealInventory } from '../lib/inventoryBrowserApi';
import type { InventoryListItem, InventoryQuery } from '../lib/inventoryBrowserApi';
import { formatDate } from '../lib/format'; import { formatDate } from '../lib/format';
const quickViews = [ const quickViews = [
@@ -35,7 +36,7 @@ const quickViews = [
export function AssetsPage() { export function AssetsPage() {
const [urlParams, setUrlParams] = useSearchParams(); const [urlParams, setUrlParams] = useSearchParams();
const operationalContext = useOperationalContext(); const operationalContext = useOperationalContext();
const [assets, setAssets] = useState<AssetListItem[]>([]); const [assets, setAssets] = useState<InventoryListItem[]>([]);
const [types, setTypes] = useState<AssetType[]>([]); const [types, setTypes] = useState<AssetType[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 }); const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -59,25 +60,39 @@ export function AssetsPage() {
const effectiveOperationalStatus = quick === 'out' ? 'OUT_OF_SERVICE' as AssetOperationalStatus : operationalStatus; const effectiveOperationalStatus = quick === 'out' ? 'OUT_OF_SERVICE' as AssetOperationalStatus : operationalStatus;
useEffect(() => { useEffect(() => {
listAssetTypes().then(setTypes).catch(() => undefined); listAssetTypes().then((data) => setTypes(data.filter((type) => ['instalacion','subinstalacion'].includes(type.code.toLowerCase())))).catch(() => undefined);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (view !== 'list') return; if (view !== 'list') return;
setLoading(true); setError(''); setLoading(true);
listAssets({ setError('');
page, pageSize: 25, search, typeId, status, listRealInventory({
page,
pageSize: 25,
search,
typeId,
status,
operationalStatus: effectiveOperationalStatus, operationalStatus: effectiveOperationalStatus,
operationalAreaId, operatorCompanyId, needsValidation, hasGeometry, operationalAreaId,
operatorCompanyId,
needsValidation,
hasGeometry,
}) })
.then((response) => { setAssets(response.data); setMeta(response.meta); }) .then((response) => { setAssets(response.data); setMeta(response.meta); })
.catch((requestError) => setError(errorMessage(requestError))) .catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [view, page, search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]); }, [view, page, search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]);
const filters: Parameters<typeof listAssetTree>[0] = useMemo(() => ({ const filters: InventoryQuery = useMemo(() => ({
search, typeId, status, operationalStatus: effectiveOperationalStatus, search,
operationalAreaId, operatorCompanyId, needsValidation, hasGeometry, typeId,
status,
operationalStatus: effectiveOperationalStatus,
operationalAreaId,
operatorCompanyId,
needsValidation,
hasGeometry,
}), [search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]); }), [search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]);
const update = (changes: Record<string, string | null>) => { const update = (changes: Record<string, string | null>) => {
@@ -91,18 +106,25 @@ export function AssetsPage() {
const clearFilters = () => { const clearFilters = () => {
const next = new URLSearchParams(); const next = new URLSearchParams();
if (view === 'list') next.set('view', 'list'); if (view === 'list') next.set('view', 'list');
['section', 'companyId', 'parentId'].forEach((key) => { const parentId=urlParams.get('parentId');
const value = urlParams.get(key); if (parentId) next.set('parentId',parentId);
if (value) next.set(key, value); setDraftSearch('');
}); setUrlParams(next);
setDraftSearch(''); setUrlParams(next); };
const setPage = (value: number) => {
const next = new URLSearchParams(urlParams);
value > 1 ? next.set('page', String(value)) : next.delete('page');
setUrlParams(next);
}; };
const setPage = (value: number) => { const next = new URLSearchParams(urlParams); value > 1 ? next.set('page', String(value)) : next.delete('page'); setUrlParams(next); };
const advancedActive = Boolean(typeId || status || rawOperationalStatus); const advancedActive = Boolean(typeId || status || rawOperationalStatus);
return <section> return <section>
<div className="page-heading asset-center-heading"> <div className="page-heading asset-center-heading">
<div><span className="eyebrow">INVENTARIOS</span><h1>Inventarios</h1><p>Consultá y administrá el inventario operativo de cada empresa desde un solo lugar.</p></div> <div>
<span className="eyebrow">INVENTARIOS</span>
<h1>Inventarios</h1>
<p>Instancias reales registradas en campo, organizadas por Área Yacimiento Instalación Subinstalación.</p>
</div>
<PermissionGate permission="assets.create"><Link className="button primary" to="/inventarios/nuevo"><Icon name="plus" />Nuevo registro</Link></PermissionGate> <PermissionGate permission="assets.create"><Link className="button primary" to="/inventarios/nuevo"><Icon name="plus" />Nuevo registro</Link></PermissionGate>
</div> </div>
@@ -111,15 +133,15 @@ export function AssetsPage() {
<div className="asset-center-controls"> <div className="asset-center-controls">
<form className="asset-center-search" onSubmit={applySearch}> <form className="asset-center-search" onSubmit={applySearch}>
<Icon name="search" /> <Icon name="search" />
<input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar registro, código o identificación…" /> <input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar Inventario, código o identificación…" />
<button className="button primary" type="submit">Buscar</button> <button className="button primary" type="submit">Buscar</button>
</form> </form>
<div className="quick-view-row" aria-label="Vistas rápidas"> <div className="quick-view-row" aria-label="Vistas rápidas">
{quickViews.map((item) => <button key={item.key} type="button" className={quick === item.key ? 'active' : ''} onClick={() => setQuick(item.key)}>{item.label}</button>)} {quickViews.map((item) => <button key={item.key} type="button" className={quick === item.key ? 'active' : ''} onClick={() => setQuick(item.key)}>{item.label}</button>)}
<button type="button" className={advancedOpen || advancedActive ? 'advanced active' : 'advanced'} onClick={() => setAdvancedOpen((current) => !current)}>Más filtros</button> {view === 'list' && <button type="button" className={advancedOpen || advancedActive ? 'advanced active' : 'advanced'} onClick={() => setAdvancedOpen((current) => !current)}>Más filtros</button>}
</div> </div>
{(advancedOpen || advancedActive) && <div className="advanced-filter-panel"> {view === 'list' && (advancedOpen || advancedActive) && <div className="advanced-filter-panel">
<label className="field compact-field"><span>Tipo</span><SearchableSelect value={typeId} onChange={(event) => update({ typeId: event.target.value || null })}><option value="">Todos</option>{types.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label> <label className="field compact-field"><span>Nivel</span><SearchableSelect value={typeId} onChange={(event) => update({ typeId: event.target.value || null })}><option value="">Instalaciones y Subinstalaciones</option>{types.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)}</SearchableSelect></label>
<label className="field compact-field"><span>Estado del dato</span><SearchableSelect value={status} onChange={(event) => update({ status: event.target.value || null })}><option value="">Todos</option>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label> <label className="field compact-field"><span>Estado del dato</span><SearchableSelect value={status} onChange={(event) => update({ status: event.target.value || null })}><option value="">Todos</option>{ASSET_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label>
<label className="field compact-field"><span>Estado operativo</span><SearchableSelect value={rawOperationalStatus} onChange={(event) => update({ operationalStatus: event.target.value || null, quick: quick === 'out' ? null : quick === 'all' ? null : quick })}><option value="">Todos</option>{ASSET_OPERATIONAL_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label> <label className="field compact-field"><span>Estado operativo</span><SearchableSelect value={rawOperationalStatus} onChange={(event) => update({ operationalStatus: event.target.value || null, quick: quick === 'out' ? null : quick === 'all' ? null : quick })}><option value="">Todos</option>{ASSET_OPERATIONAL_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label>
<button type="button" className="button text filter-clear" onClick={clearFilters}>Limpiar filtros</button> <button type="button" className="button text filter-clear" onClick={clearFilters}>Limpiar filtros</button>
@@ -127,13 +149,19 @@ export function AssetsPage() {
</div> </div>
{error && <Alert>{error}</Alert>} {error && <Alert>{error}</Alert>}
{view === 'hierarchy' ? <AssetHierarchyView filters={filters} /> : loading ? <LoadingBlock label="Cargando inventario…" /> : assets.length === 0 ? <EmptyState title="No encontramos registros" text="Probá con otra búsqueda o cambiá los filtros seleccionados." /> : <div className="table-panel compact-assets-table"> {view === 'hierarchy'
<div className="table-summary"><strong>{meta.total} registro{meta.total === 1 ? '' : 's'}</strong><span>Página {meta.page} de {Math.max(meta.totalPages, 1)}</span></div> ? <AssetHierarchyView filters={filters} />
<div className="table-scroll"><table><thead><tr><th>Registro</th><th>Tipo</th><th>Área / Operadora</th><th>Estado</th><th>Actualizado</th><th aria-label="Acciones" /></tr></thead><tbody> : loading
? <LoadingBlock label="Cargando Inventario real…" />
: assets.length === 0
? <EmptyState title="Todavía no hay Inventario real" text="Las Áreas y Yacimientos precargados son contexto. Las Instalaciones/Subinstalaciones aparecerán aquí cuando sean registradas realmente." />
: <div className="table-panel compact-assets-table">
<div className="table-summary"><strong>{meta.total} instancia{meta.total === 1 ? '' : 's'} real{meta.total === 1 ? '' : 'es'}</strong><span>Página {meta.page} de {Math.max(meta.totalPages, 1)}</span></div>
<div className="table-scroll"><table><thead><tr><th>Inventario</th><th>Nivel</th><th>Área / Operadora vigente</th><th>Estado</th><th>Actualizado</th><th aria-label="Acciones" /></tr></thead><tbody>
{assets.map((asset) => <tr key={asset.id}> {assets.map((asset) => <tr key={asset.id}>
<td><div className="asset-cell"><span className="asset-symbol"><Icon name="layers" size={16} /></span><div><Link to={`/inventarios/${asset.id}`} className="table-primary">{asset.name}</Link><small>{asset.code}{asset.parent ? ` · en ${asset.parent.name}` : ''}</small></div></div></td> <td><div className="asset-cell"><span className="asset-symbol"><Icon name="layers" size={16} /></span><div><Link to={`/inventarios/${asset.id}`} className="table-primary">{asset.name}</Link><small>{asset.code}{asset.parent ? ` · en ${asset.parent.name}` : ''}</small></div></div></td>
<td><span className="tag">{asset.type.name}</span></td> <td><span className="tag">{asset.type.name}</span></td>
<td>{asset.operationalArea || asset.operatorCompany ? <span><strong className="table-primary">{asset.operationalArea?.name ?? 'Sin área'}</strong><small className="cell-subtext">{asset.operatorCompany?.name ?? 'Sin operadora'}</small></span> : <span className="muted">Sin contexto</span>}</td> <td>{asset.operationalArea ? <span><strong className="table-primary">{asset.operationalArea.name}</strong><small className="cell-subtext">{asset.operatorCompany?.name ?? 'Sin operadora vigente'}</small></span> : <span className="muted">Sin Área</span>}</td>
<td><div className="dual-status"><span className={`status-badge ${assetStatusClass(asset.informationStatus)}`}>{assetStatusLabel(asset.informationStatus)}</span><small>{assetOperationalStatusLabel(asset.operationalStatus)}</small></div></td> <td><div className="dual-status"><span className={`status-badge ${assetStatusClass(asset.informationStatus)}`}>{assetStatusLabel(asset.informationStatus)}</span><small>{assetOperationalStatusLabel(asset.operationalStatus)}</small></div></td>
<td>{formatDate(asset.updatedAt)}</td> <td>{formatDate(asset.updatedAt)}</td>
<td className="action-cell"><Link className="icon-button" to={`/inventarios/${asset.id}`} aria-label={`Abrir ${asset.name}`}><Icon name="chevron" /></Link></td> <td className="action-cell"><Link className="icon-button" to={`/inventarios/${asset.id}`} aria-label={`Abrir ${asset.name}`}><Icon name="chevron" /></Link></td>