feat: add structured inventory navigation
This commit is contained in:
@@ -4,7 +4,7 @@ import { useAuth } from '../../auth/AuthContext';
|
|||||||
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
|
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||||
import { Icon } from '../../components/Icon';
|
import { Icon } from '../../components/Icon';
|
||||||
import { getAssetLineage } from '../../lib/api';
|
import { getAssetLineage } from '../../lib/api';
|
||||||
import type { AssetLineageItem } from '../../lib/api';
|
import type { AssetLineageItem, AssetType } from '../../lib/api';
|
||||||
import {
|
import {
|
||||||
listInventoryCompanies,
|
listInventoryCompanies,
|
||||||
listInventoryDepartments,
|
listInventoryDepartments,
|
||||||
@@ -18,16 +18,61 @@ import type {
|
|||||||
} from '../../lib/inventoryBrowserApi';
|
} from '../../lib/inventoryBrowserApi';
|
||||||
import { assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from './assetPresentation';
|
import { assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from './assetPresentation';
|
||||||
|
|
||||||
|
const structuredInventorySections = [
|
||||||
|
{
|
||||||
|
code: 'empresa',
|
||||||
|
label: 'Empresas',
|
||||||
|
description: 'Maestro de empresas operadoras. Se vinculan a una o más Áreas sin formar parte del árbol territorial.',
|
||||||
|
icon: 'users' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'departamento',
|
||||||
|
label: 'Departamentos',
|
||||||
|
description: 'Primer nivel territorial. Desde cada Departamento se accede a sus Áreas.',
|
||||||
|
icon: 'map' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'area',
|
||||||
|
label: 'Áreas',
|
||||||
|
description: 'Cada Área pertenece a un Departamento y puede tener una o más Empresas operadoras vinculadas.',
|
||||||
|
icon: 'map' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'yacimiento',
|
||||||
|
label: 'Yacimientos',
|
||||||
|
description: 'Nivel operativo dentro de un Área. Organiza las Instalaciones que pertenecen a ese Yacimiento.',
|
||||||
|
icon: 'map' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'instalacion',
|
||||||
|
label: 'Instalaciones',
|
||||||
|
description: 'Elementos técnicos ubicados dentro de un Yacimiento y clasificados por su familia de Inventario.',
|
||||||
|
icon: 'layers' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'subinstalacion',
|
||||||
|
label: 'Subinstalaciones',
|
||||||
|
description: 'Último nivel estructural. Dependen de una Instalación y conservan su clasificación técnica.',
|
||||||
|
icon: 'layers' as const,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
function navigationHref(base: URLSearchParams, parentId?: string) {
|
function navigationHref(base: URLSearchParams, parentId?: string) {
|
||||||
const params = new URLSearchParams(base);
|
const params = new URLSearchParams(base);
|
||||||
params.delete('view');
|
params.delete('view');
|
||||||
params.delete('page');
|
params.delete('page');
|
||||||
params.delete('section');
|
params.delete('section');
|
||||||
params.delete('companyId');
|
params.delete('companyId');
|
||||||
|
params.delete('typeId');
|
||||||
parentId ? params.set('parentId', parentId) : params.delete('parentId');
|
parentId ? params.set('parentId', parentId) : params.delete('parentId');
|
||||||
return `/inventarios${params.size ? `?${params}` : ''}`;
|
return `/inventarios${params.size ? `?${params}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inventoryTypeForSection(types: AssetType[], code: string) {
|
||||||
|
if (code === 'empresa') return types.find((type) => type.operationalRole === 'COMPANY') ?? null;
|
||||||
|
return types.find((type) => type.code.toLowerCase() === code) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function DepartmentCard({ department, href }: { department: InventoryBrowserDepartment; href: string }) {
|
function DepartmentCard({ department, href }: { department: InventoryBrowserDepartment; href: string }) {
|
||||||
return <Link className="asset-browser-item" to={href}>
|
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-icon"><Icon name="map" size={17} /></span>
|
||||||
@@ -80,7 +125,7 @@ function nextLevelLabel(typeCode: string | undefined) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssetHierarchyView({ filters }: { filters: InventoryQuery }) {
|
export function AssetHierarchyView({ filters, types }: { filters: InventoryQuery; types: AssetType[] }) {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const canCreate = hasPermission('assets.create');
|
const canCreate = hasPermission('assets.create');
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -123,24 +168,48 @@ export function AssetHierarchyView({ filters }: { filters: InventoryQuery }) {
|
|||||||
return <div className="asset-browser-panel">
|
return <div className="asset-browser-panel">
|
||||||
{error && <Alert>{error}</Alert>}
|
{error && <Alert>{error}</Alert>}
|
||||||
<div className="asset-browser-section-heading">
|
<div className="asset-browser-section-heading">
|
||||||
<div><span className="eyebrow">INVENTARIO COMPLETO</span><h2>Departamentos</h2><p>Entrá por Departamento y navegá Área → Yacimiento → Instalación → Subinstalación hasta llegar a su clasificación y Hallazgos asociados.</p></div>
|
<div>
|
||||||
|
<span className="eyebrow">INVENTARIOS ESTRUCTURADOS</span>
|
||||||
|
<h2>Elegí qué querés administrar</h2>
|
||||||
|
<p>La estructura queda fija y fácil de leer: Empresas como maestro de operadoras y, por separado, Departamento → Área → Yacimiento → Instalación → Subinstalación.</p>
|
||||||
|
</div>
|
||||||
<div className="asset-browser-current-actions"><span className="count-pill">{departments.length} Departamentos</span><span className="count-pill">{companies.length} Empresas</span></div>
|
<div className="asset-browser-current-actions"><span className="count-pill">{departments.length} Departamentos</span><span className="count-pill">{companies.length} Empresas</span></div>
|
||||||
</div>
|
</div>
|
||||||
{departments.length === 0
|
|
||||||
? <EmptyState title="Todavía no hay Departamentos" text="Creá el primer registro para comenzar a construir el Inventario manualmente." />
|
|
||||||
: <div className="asset-browser-list">{departments.map((department) => <DepartmentCard key={department.id} department={department} href={navigationHref(searchParams, department.id)} />)}</div>}
|
|
||||||
|
|
||||||
<div className="asset-browser-levels" aria-label="Estructura de Inventarios">
|
<div className="asset-browser-list" aria-label="Secciones estructuradas de Inventarios">
|
||||||
|
{structuredInventorySections.map((section, index) => {
|
||||||
|
const assetType = inventoryTypeForSection(types, section.code);
|
||||||
|
const content = <>
|
||||||
|
<span className="asset-browser-item-icon"><Icon name={section.icon} size={17} /></span>
|
||||||
|
<span className="asset-browser-item-main">
|
||||||
|
<strong>{section.label}</strong>
|
||||||
|
<small>{section.description}</small>
|
||||||
|
</span>
|
||||||
|
<span className="asset-browser-item-status"><small>{section.code === 'empresa' ? 'Maestro independiente' : `Nivel ${index}`}</small></span>
|
||||||
|
{assetType && <Icon name="chevron" size={16} />}
|
||||||
|
</>;
|
||||||
|
return assetType
|
||||||
|
? <Link key={section.code} className="asset-browser-item" to={`/inventarios?view=list&typeId=${assetType.id}`}>{content}</Link>
|
||||||
|
: <div key={section.code} className="asset-browser-item" aria-disabled="true">{content}</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="asset-browser-levels" aria-label="Jerarquía territorial de Inventarios">
|
||||||
<div><span>1</span><strong>Departamento</strong><small>raíz territorial</small></div><i>›</i>
|
<div><span>1</span><strong>Departamento</strong><small>raíz territorial</small></div><i>›</i>
|
||||||
<div><span>2</span><strong>Área</strong><small>dentro del Departamento</small></div><i>›</i>
|
<div><span>2</span><strong>Área</strong><small>operadoras vinculadas</small></div><i>›</i>
|
||||||
<div><span>3</span><strong>Yacimiento</strong><small>dentro del Área</small></div><i>›</i>
|
<div><span>3</span><strong>Yacimiento</strong><small>dentro del Área</small></div><i>›</i>
|
||||||
<div><span>4</span><strong>Instalación</strong><small>clasificación técnica</small></div><i>›</i>
|
<div><span>4</span><strong>Instalación</strong><small>clasificación técnica</small></div><i>›</i>
|
||||||
<div><span>5</span><strong>Subinstalación</strong><small>clasificación técnica</small></div>
|
<div><span>5</span><strong>Subinstalación</strong><small>clasificación técnica</small></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="asset-browser-group" style={{ marginTop: 20 }}>
|
<section className="asset-browser-group" style={{ marginTop: 20 }}>
|
||||||
<div className="asset-browser-group-heading"><div><h3>Empresas</h3><p>Maestro independiente. La Empresa se vincula al Área como operadora sin alterar la estructura física.</p></div><span>{companies.length}</span></div>
|
<div className="asset-browser-group-heading">
|
||||||
{companies.length === 0 ? <div className="inline-empty">Todavía no hay Empresas cargadas.</div> : <div className="asset-browser-list">{companies.map((company) => <CompanyCard key={company.id} company={company} />)}</div>}
|
<div><h3>Recorrido por territorio</h3><p>También podés navegar como árbol: entrá por un Departamento y descendé hasta la Instalación o Subinstalación concreta.</p></div>
|
||||||
|
<span>{departments.length}</span>
|
||||||
|
</div>
|
||||||
|
{departments.length === 0
|
||||||
|
? <EmptyState title="Todavía no hay Departamentos" text="Creá el primer registro para comenzar a construir el Inventario manualmente." />
|
||||||
|
: <div className="asset-browser-list">{departments.map((department) => <DepartmentCard key={department.id} department={department} href={navigationHref(searchParams, department.id)} />)}</div>}
|
||||||
</section>
|
</section>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user