F5.1: listar todos los niveles de Inventario

This commit is contained in:
2026-09-09 07:51:33 -03:00
parent 6c958d4d21
commit a96ca279a7
+23 -38
View File
@@ -22,7 +22,7 @@ import type {
AssetType, AssetType,
PageMeta, PageMeta,
} from '../lib/api'; } from '../lib/api';
import { listRealInventory } from '../lib/inventoryBrowserApi'; import { listInventory } from '../lib/inventoryBrowserApi';
import type { InventoryListItem, InventoryQuery } from '../lib/inventoryBrowserApi'; import type { InventoryListItem, InventoryQuery } from '../lib/inventoryBrowserApi';
import { formatDate } from '../lib/format'; import { formatDate } from '../lib/format';
@@ -60,24 +60,19 @@ 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((data) => setTypes(data.filter((type) => ['instalacion','subinstalacion'].includes(type.code.toLowerCase())))).catch(() => undefined); listAssetTypes().then((data) => setTypes(data.filter((type) =>
type.operationalRole === 'COMPANY'
|| ['departamento','area','yacimiento','instalacion','subinstalacion'].includes(type.code.toLowerCase()),
))).catch(() => undefined);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (view !== 'list') return; if (view !== 'list') return;
setLoading(true); setLoading(true); setError('');
setError(''); listInventory({
listRealInventory({ page, pageSize: 25, search, typeId, status,
page,
pageSize: 25,
search,
typeId,
status,
operationalStatus: effectiveOperationalStatus, operationalStatus: effectiveOperationalStatus,
operationalAreaId, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry,
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)))
@@ -85,36 +80,26 @@ export function AssetsPage() {
}, [view, page, search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]); }, [view, page, search, typeId, status, effectiveOperationalStatus, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry]);
const filters: InventoryQuery = useMemo(() => ({ const filters: InventoryQuery = useMemo(() => ({
search, search, typeId, status, operationalStatus: effectiveOperationalStatus,
typeId, operationalAreaId, operatorCompanyId, needsValidation, hasGeometry,
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>) => {
const next = new URLSearchParams(urlParams); const next = new URLSearchParams(urlParams);
Object.entries(changes).forEach(([key, value]) => value ? next.set(key, value) : next.delete(key)); Object.entries(changes).forEach(([key, value]) => value ? next.set(key, value) : next.delete(key));
next.delete('page'); next.delete('page'); setUrlParams(next);
setUrlParams(next);
}; };
const applySearch = (event: FormEvent) => { event.preventDefault(); update({ search: draftSearch.trim() || null }); }; const applySearch = (event: FormEvent) => { event.preventDefault(); update({ search: draftSearch.trim() || null }); };
const setQuick = (key: string) => update({ quick: key === 'all' ? null : key, operationalStatus: key === 'out' ? null : rawOperationalStatus || null }); const setQuick = (key: string) => update({ quick: key === 'all' ? null : key, operationalStatus: key === 'out' ? null : rawOperationalStatus || null });
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');
const parentId=urlParams.get('parentId'); const parentId=urlParams.get('parentId'); if (parentId) next.set('parentId',parentId);
if (parentId) next.set('parentId',parentId); setDraftSearch(''); setUrlParams(next);
setDraftSearch('');
setUrlParams(next);
}; };
const setPage = (value: number) => { const setPage = (value: number) => {
const next = new URLSearchParams(urlParams); const next = new URLSearchParams(urlParams);
value > 1 ? next.set('page', String(value)) : next.delete('page'); value > 1 ? next.set('page', String(value)) : next.delete('page'); setUrlParams(next);
setUrlParams(next);
}; };
const advancedActive = Boolean(typeId || status || rawOperationalStatus); const advancedActive = Boolean(typeId || status || rawOperationalStatus);
@@ -123,7 +108,7 @@ export function AssetsPage() {
<div> <div>
<span className="eyebrow">INVENTARIOS</span> <span className="eyebrow">INVENTARIOS</span>
<h1>Inventarios</h1> <h1>Inventarios</h1>
<p>Instancias reales registradas en campo, organizadas por Área Yacimiento Instalación Subinstalación.</p> <p>Todos los registros: Departamento Área Yacimiento Instalación Subinstalación, más el maestro independiente de Empresas.</p>
</div> </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>
@@ -133,7 +118,7 @@ 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 Inventario, código o identificación…" /> <input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar por nombre, 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">
@@ -141,7 +126,7 @@ export function AssetsPage() {
{view === 'list' && <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>
{view === 'list' && (advancedOpen || advancedActive) && <div className="advanced-filter-panel"> {view === 'list' && (advancedOpen || advancedActive) && <div className="advanced-filter-panel">
<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>Nivel</span><SearchableSelect value={typeId} onChange={(event) => update({ typeId: event.target.value || null })}><option value="">Todos los niveles</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>
@@ -152,16 +137,16 @@ export function AssetsPage() {
{view === 'hierarchy' {view === 'hierarchy'
? <AssetHierarchyView filters={filters} /> ? <AssetHierarchyView filters={filters} />
: loading : loading
? <LoadingBlock label="Cargando Inventario real…" /> ? <LoadingBlock label="Cargando Inventarios…" />
: assets.length === 0 : 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." /> ? <EmptyState title="Todavía no hay registros" text="La base está lista para comenzar la carga manual desde Departamento." />
: <div className="table-panel compact-assets-table"> : <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-summary"><strong>{meta.total} registro{meta.total === 1 ? '' : 's'}</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> <div className="table-scroll"><table><thead><tr><th>Registro</th><th>Nivel</th><th>Ubicación / Operadora</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 ? <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>{asset.operationalArea ? <span><strong className="table-primary">{asset.operationalArea.name}</strong><small className="cell-subtext">{asset.operatorCompany?.name ?? 'Sin operadora vigente'}</small></span> : asset.operatorCompany ? <span>{asset.operatorCompany.name}</span> : <span className="muted">{asset.parent?.name ?? 'Raíz / maestro independiente'}</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>