Files
dh-inspeccion-v2/web-v2/src/features/assets/AssetHierarchyView.tsx
T

246 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, AssetType } from '../../lib/api';
import {
listInventoryCompanies,
listInventoryDepartments,
listInventoryChildren,
} from '../../lib/inventoryBrowserApi';
import type {
InventoryBrowserCompany,
InventoryBrowserDepartment,
InventoryBrowserItem,
InventoryQuery,
} from '../../lib/inventoryBrowserApi';
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) {
const params = new URLSearchParams(base);
params.delete('view');
params.delete('page');
params.delete('section');
params.delete('companyId');
params.delete('typeId');
parentId ? params.set('parentId', parentId) : params.delete('parentId');
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 }) {
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>{department.name}</strong>
<small>{department.code} · {department.areaCount} Área{department.areaCount === 1 ? '' : 's'}</small>
</span>
<span className="asset-browser-item-status"><span className={`status-badge ${assetStatusClass(department.informationStatus)}`}>{assetStatusLabel(department.informationStatus)}</span></span>
<Icon name="chevron" size={16} />
</Link>;
}
function CompanyCard({ company }: { company: InventoryBrowserCompany }) {
return <Link className="asset-browser-item" to={`/inventarios/${company.id}`}>
<span className="asset-browser-item-icon"><Icon name="users" size={17} /></span>
<span className="asset-browser-item-main"><strong>{company.name}</strong><small>{company.code} · {company.areaCount} Área{company.areaCount === 1 ? '' : 's'} operada{company.areaCount === 1 ? '' : 's'}</small></span>
<span className="asset-browser-item-status"><span className={`status-badge ${assetStatusClass(company.informationStatus)}`}>{assetStatusLabel(company.informationStatus)}</span></span>
<Icon name="chevron" size={16} />
</Link>;
}
function InventoryCard({ item, href }: { item: InventoryBrowserItem; href: string }) {
return <Link className="asset-browser-item" to={href}>
<span className="asset-browser-item-icon"><Icon name={['departamento','area','yacimiento'].includes(item.type.code.toLowerCase()) ? '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">
<span className={`status-badge ${assetStatusClass(item.informationStatus)}`}>{assetStatusLabel(item.informationStatus)}</span>
{item.inventoryFamily
? <small>{item.findingCount} Hallazgo{item.findingCount === 1 ? '' : 's'} asociado{item.findingCount === 1 ? '' : 's'}</small>
: <small>{item.childrenCount} registro{item.childrenCount === 1 ? '' : 's'} inferior{item.childrenCount === 1 ? '' : 'es'}</small>}
</span>
<Icon name="chevron" size={16} />
</Link>;
}
function nextLevelLabel(typeCode: string | undefined) {
switch (typeCode?.toLowerCase()) {
case 'departamento': return 'Áreas';
case 'area': return 'Yacimientos';
case 'yacimiento': return 'Instalaciones';
case 'instalacion': return 'Subinstalaciones';
default: return 'Niveles inferiores';
}
}
export function AssetHierarchyView({ filters, types }: { filters: InventoryQuery; types: AssetType[] }) {
const { hasPermission } = useAuth();
const canCreate = hasPermission('assets.create');
const [searchParams] = useSearchParams();
const parentId = searchParams.get('parentId') ?? '';
const [departments, setDepartments] = useState<InventoryBrowserDepartment[]>([]);
const [companies, setCompanies] = useState<InventoryBrowserCompany[]>([]);
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(''); setDepartments([]); setCompanies([]); setChildren([]); setLineage([]); setHasMore(false);
const run = async () => {
if (!parentId) {
const [loadedDepartments, loadedCompanies] = await Promise.all([
listInventoryDepartments({ search: filters.search, operatorCompanyId: filters.operatorCompanyId }),
listInventoryCompanies({ search: filters.search }),
]);
if (active) { setDepartments(loadedDepartments.data); setCompanies(loadedCompanies.data); }
return;
}
const [loadedLineage, response] = await Promise.all([
getAssetLineage(parentId),
listInventoryChildren(parentId, { search: filters.search }),
]);
if (!active) return;
setLineage(loadedLineage.filter((item) => ['departamento','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.operatorCompanyId]);
if (loading) return <LoadingBlock label="Cargando Inventarios…" />;
if (!parentId) {
return <div className="asset-browser-panel">
{error && <Alert>{error}</Alert>}
<div className="asset-browser-section-heading">
<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>
<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>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>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>
<section className="asset-browser-group" style={{ marginTop: 20 }}>
<div className="asset-browser-group-heading">
<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>
</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{['instalacion','subinstalacion'].includes(current.type.code.toLowerCase()) ? ' y Hallazgos' : ''}</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>Jerarquía: Departamento Á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() === 'subinstalacion' ? 'Abrí la ficha para ver su clasificación y Hallazgos asociados.' : 'Agregá el primer registro de este nivel o revisá 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>;
}