Files
dh-inspeccion-v2/web-v2/src/features/assets/AssetHierarchyView.tsx
T
admin 7338f277ad F5.1 · Inventario limpio, jerarquía manual y Hallazgos filtrados (#28)
Reset controlado de datos operativos e históricos, nueva jerarquía manual Departamento → Área → Yacimiento → Instalación → Subinstalación, filtros y administración inline de Hallazgos, y corrección de alta con Área sin Operadora.
2026-09-09 08:55:01 -03:00

177 lines
10 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 } 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';
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 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 }: { filters: InventoryQuery }) {
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">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 className="asset-browser-current-actions"><span className="count-pill">{departments.length} Departamentos</span><span className="count-pill">{companies.length} Empresas</span></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><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>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>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>
{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>}
</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>;
}