chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,256 @@
import { useEffect, useMemo, 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 {
getAsset,
getAssetLineage,
listAreasForCompany,
listCompaniesForArea,
listAssetTreeChildren,
listOperationalAreas,
listOperationalCompanies,
} from '../../lib/api';
import type { AssetDetail, AssetLineageItem, AssetListItem, OperationalAssetSummary } from '../../lib/api';
import { assetOperationalStatusLabel, assetStatusClass, assetStatusLabel } from './assetPresentation';
type TreeFilters = Omit<NonNullable<Parameters<typeof listAssetTreeChildren>[0]>, 'parentId' | 'limit'>;
type NavigationSection = 'companies' | 'territory';
type ChildGroupKey = 'fields' | 'installations' | 'wells' | 'equipment';
const INSTALLATION_CODES = new Set([
'estructura_local', 'locacion', 'instalacion', 'planta', 'bateria', 'estacion', 'subestacion',
'zona_bombas', 'sistema_drenaje', 'sistema_electrico_iluminacion', 'sistema_defensa_incendios',
'pileta_api', 'cargadero_descargadero',
]);
const GROUP_ORDER: ChildGroupKey[] = ['fields', 'installations', 'wells', 'equipment'];
const GROUP_LABELS: Record<ChildGroupKey, { title: string; description: string }> = {
fields: { title: 'Yacimientos', description: 'Unidades territoriales u operativas dentro del Área.' },
installations: { title: 'Instalaciones y estructura', description: 'Plantas, baterías, estaciones, locaciones y niveles estructurales.' },
wells: { title: 'Pozos', description: 'Pozos identificados dentro del contexto seleccionado.' },
equipment: { title: 'Equipos y otros elementos', description: 'Equipos técnicos y demás elementos registrados en el inventario.' },
};
function childGroup(item: AssetListItem): ChildGroupKey {
const code = item.type.code.toLowerCase();
if (code === 'yacimiento') return 'fields';
if (INSTALLATION_CODES.has(code)) return 'installations';
if (code === 'pozo') return 'wells';
return 'equipment';
}
function normalizeSearch(value: string | undefined) {
return value?.trim().toLocaleLowerCase('es-AR') ?? '';
}
function matchesSearch(item: { name: string; code: string; commonName?: string | null }, search?: string) {
if (!search) return true;
const term = normalizeSearch(search);
return item.name.toLocaleLowerCase('es-AR').includes(term) || item.code.toLocaleLowerCase('es-AR').includes(term) || Boolean(item.commonName?.toLocaleLowerCase('es-AR').includes(term));
}
function navigationHref(base: URLSearchParams, section: NavigationSection, options: { companyId?: string; parentId?: string } = {}) {
const params = new URLSearchParams(base);
params.delete('view');
params.delete('page');
params.delete('operationalAreaId');
params.delete('operatorCompanyId');
params.set('section', section);
options.companyId ? params.set('companyId', options.companyId) : params.delete('companyId');
options.parentId ? params.set('parentId', options.parentId) : params.delete('parentId');
return `/inventarios?${params}`;
}
function AssetCard({ item, href }: { item: AssetListItem; href: string }) {
return <Link className="asset-browser-item" to={href}>
<span className="asset-browser-item-icon"><Icon name="layers" size={17} /></span>
<span className="asset-browser-item-main">
<strong>{item.name}</strong>
<small>{item.code} · {item.type.name}{item.commonName ? ` · ${item.commonName}` : ''}</small>
</span>
<span className="asset-browser-item-status">
<span className={`status-badge ${assetStatusClass(item.informationStatus)}`}>{assetStatusLabel(item.informationStatus)}</span>
<small>{assetOperationalStatusLabel(item.operationalStatus)}</small>
</span>
<Icon name="chevron" size={16} />
</Link>;
}
function SummaryCard({ item, href, subtitle }: { item: OperationalAssetSummary; href: string; subtitle: string }) {
return <Link className="asset-browser-item" to={href}>
<span className="asset-browser-item-icon"><Icon name="layers" size={17} /></span>
<span className="asset-browser-item-main">
<strong>{item.name}</strong>
<small>{item.code} · {subtitle}{item.commonName ? ` · ${item.commonName}` : ''}</small>
</span>
<Icon name="chevron" size={16} />
</Link>;
}
export function AssetHierarchyView({ filters }: { filters: TreeFilters }) {
const { hasPermission } = useAuth();
const canCreate = hasPermission('assets.create');
const [searchParams] = useSearchParams();
const rawSection = searchParams.get('section');
const section: NavigationSection | null = rawSection === 'companies' || rawSection === 'territory' ? rawSection : null;
const companyId = searchParams.get('companyId') ?? '';
const parentId = searchParams.get('parentId') ?? '';
const [companies, setCompanies] = useState<OperationalAssetSummary[]>([]);
const [areas, setAreas] = useState<OperationalAssetSummary[]>([]);
const [company, setCompany] = useState<AssetDetail | null>(null);
const [lineage, setLineage] = useState<AssetLineageItem[]>([]);
const [children, setChildren] = useState<AssetListItem[]>([]);
const [hasMore, setHasMore] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let active = true;
setLoading(true);
setError('');
setCompanies([]);
setAreas([]);
setCompany(null);
setLineage([]);
setChildren([]);
setHasMore(false);
const run = async () => {
if ((!section || section === 'companies') && !companyId && !parentId) {
const loaded = filters.operationalAreaId ? await listCompaniesForArea(filters.operationalAreaId) : await listOperationalCompanies();
if (active) setCompanies(loaded.filter((item) => (!filters.operatorCompanyId || item.id === filters.operatorCompanyId) && matchesSearch(item, filters.search)));
return;
}
if (section === 'companies' && companyId && !parentId) {
const [loadedCompany, loadedAreas] = await Promise.all([getAsset(companyId), listAreasForCompany(companyId)]);
if (!active) return;
setCompany(loadedCompany);
setAreas(loadedAreas.filter((item) => (!filters.operationalAreaId || item.id === filters.operationalAreaId) && matchesSearch(item, filters.search)));
return;
}
if (section === 'territory' && !parentId) {
const loaded = await listOperationalAreas();
if (active) setAreas(loaded.filter((item) => (!filters.operationalAreaId || item.id === filters.operationalAreaId) && matchesSearch(item, filters.search)));
return;
}
if (parentId) {
const [loadedLineage, childResponse, loadedCompany] = await Promise.all([
getAssetLineage(parentId),
listAssetTreeChildren({
...filters,
operatorCompanyId: companyId || filters.operatorCompanyId,
parentId,
limit: 200,
}),
companyId ? getAsset(companyId) : Promise.resolve(null),
]);
if (!active) return;
setLineage(loadedLineage);
setChildren(childResponse.data);
setHasMore(childResponse.meta.hasMore);
setCompany(loadedCompany);
}
};
run().catch((requestError) => active && setError(errorMessage(requestError))).finally(() => active && setLoading(false));
return () => { active = false; };
}, [section, companyId, parentId, JSON.stringify(filters)]);
const groupedChildren = useMemo(() => {
const groups = new Map<ChildGroupKey, AssetListItem[]>();
children.forEach((item) => {
const key = childGroup(item);
groups.set(key, [...(groups.get(key) ?? []), item]);
});
return GROUP_ORDER.map((key) => ({ key, items: groups.get(key) ?? [] })).filter((group) => group.items.length > 0);
}, [children]);
const current = lineage.at(-1) ?? null;
const activeSection: NavigationSection = section ?? 'companies';
if (loading) return <LoadingBlock label="Cargando inventarios…" />;
if (!section && !companyId && !parentId) {
return <div className="asset-browser-panel">
{error && <Alert>{error}</Alert>}
<div className="asset-browser-section-heading">
<div><span className="eyebrow">INVENTARIOS POR EMPRESA</span><h2>Elegí una empresa</h2><p>Cada empresa tiene su propio inventario. Ingresá para recorrer Áreas, Yacimientos, instalaciones y equipos.</p></div>
<div className="asset-browser-current-actions"><Link className="button secondary" to={navigationHref(searchParams, 'territory')}><Icon name="map" />Vista territorial</Link><span className="count-pill">{companies.length}</span></div>
</div>
{companies.length === 0 ? <EmptyState title="No hay inventarios para mostrar" text="Probá con otra búsqueda o revisá los filtros." /> : <div className="asset-browser-list">{companies.map((item) => <SummaryCard key={item.id} item={item} subtitle="Inventario de empresa" href={navigationHref(searchParams, 'companies', { companyId: item.id })} />)}</div>}
<div className="asset-browser-levels" aria-label="Estructura de los inventarios">
<div><span>1</span><strong>Empresa</strong><small>Inventario principal</small></div><i></i>
<div><span>2</span><strong>Área</strong><small>Contexto territorial</small></div><i></i>
<div><span>3</span><strong>Yacimiento</strong><small>Nivel territorial</small></div><i></i>
<div><span>4</span><strong>Instalación</strong><small>Planta, batería, estación</small></div><i></i>
<div><span>5</span><strong>Equipo</strong><small>Equipo, pozo, tanque</small></div>
</div>
</div>;
}
const breadcrumb = <nav className="asset-browser-breadcrumb" aria-label="Ruta del inventario">
<Link to="/inventarios">Inventarios</Link>
{activeSection === 'territory' && <><span></span><Link to={navigationHref(searchParams, 'territory')}>Vista territorial</Link></>}
{company && <><span></span>{parentId ? <Link to={navigationHref(searchParams, 'companies', { companyId: company.id })}>{company.name}</Link> : <strong>{company.name}</strong>}</>}
{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, activeSection, { companyId: companyId || undefined, parentId: item.id })}>{item.name}</Link>}</span>;
})}
</nav>;
if (section === 'companies' && !companyId) {
return <div className="asset-browser-panel">
{breadcrumb}
{error && <Alert>{error}</Alert>}
<div className="asset-browser-section-heading"><div><span className="eyebrow">INVENTARIOS POR EMPRESA</span><h2>Elegí una empresa</h2><p>Ingresá al inventario de una empresa para ver sus Áreas y continuar hacia Yacimientos, instalaciones y equipos.</p></div><span className="count-pill">{companies.length}</span></div>
{companies.length === 0 ? <EmptyState title="No hay empresas para mostrar" text="Probá con otra búsqueda o revisá los filtros." /> : <div className="asset-browser-list">{companies.map((item) => <SummaryCard key={item.id} item={item} subtitle="Inventario de empresa" href={navigationHref(searchParams, 'companies', { companyId: item.id })} />)}</div>}
</div>;
}
if (section === 'companies' && companyId && !parentId) {
return <div className="asset-browser-panel">
{breadcrumb}
{error && <Alert>{error}</Alert>}
<div className="asset-browser-current-heading"><div><span className="eyebrow">INVENTARIO DE EMPRESA</span><h2>{company?.name ?? 'Organización'}</h2><p>{company?.code} · Áreas con registros asociados a este inventario.</p></div>{company && <Link className="button secondary" to={`/inventarios/${company.id}`}>Ver ficha</Link>}</div>
<div className="asset-browser-group">
<div className="asset-browser-group-heading"><div><h3>Áreas del inventario</h3><p>Seleccioná un Área para continuar hacia Yacimientos, instalaciones y equipos.</p></div><span>{areas.length}</span></div>
{areas.length === 0 ? <EmptyState title="Sin Áreas en el inventario" text="No hay Áreas con registros asignados a esta empresa para los filtros actuales." /> : <div className="asset-browser-list">{areas.map((item) => <SummaryCard key={item.id} item={item} subtitle="Área" href={navigationHref(searchParams, 'companies', { companyId, parentId: item.id })} />)}</div>}
</div>
</div>;
}
if (section === 'territory' && !parentId) {
return <div className="asset-browser-panel">
{breadcrumb}
{error && <Alert>{error}</Alert>}
<div className="asset-browser-section-heading"><div><span className="eyebrow">TERRITORIO</span><h2>Áreas y yacimientos</h2><p>Ingresá por un Área para navegar su estructura física.</p></div><span className="count-pill">{areas.length}</span></div>
{areas.length === 0 ? <EmptyState title="No hay Áreas para mostrar" text="Probá con otra búsqueda o revisá los filtros." /> : <div className="asset-browser-list">{areas.map((item) => <SummaryCard key={item.id} item={item} subtitle="Área" href={navigationHref(searchParams, 'territory', { parentId: item.id })} />)}</div>}
</div>;
}
if (parentId && current) {
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 o los filtros para acotar los resultados.</Alert>}
<div className="asset-browser-current-heading">
<div><span className="eyebrow">{current.type.name}</span><h2>{current.name}</h2><p>{current.code}{current.commonName ? ` · ${current.commonName}` : ''}{company ? ` · Contexto: ${company.name}` : ''}</p></div>
<div className="asset-browser-current-actions"><Link className="button secondary" to={`/inventarios/${current.id}`}>Ver ficha</Link>{canCreate && <Link className="button primary" to={`/inventarios/nuevo?parentId=${current.id}`}><Icon name="plus" />Agregar aquí</Link>}</div>
</div>
{groupedChildren.length === 0 ? <EmptyState title="No hay niveles inferiores" text="Este nivel no tiene registros inferiores que coincidan con los filtros actuales." /> : <div className="asset-browser-groups">
{groupedChildren.map(({ key, items }) => <section className={`asset-browser-group group-${key}`} key={key}>
<div className="asset-browser-group-heading"><div><h3>{GROUP_LABELS[key].title}</h3><p>{GROUP_LABELS[key].description}</p></div><span>{items.length}</span></div>
<div className="asset-browser-list">{items.map((item) => <AssetCard key={item.id} item={item} href={navigationHref(searchParams, activeSection, { companyId: companyId || undefined, parentId: item.id })} />)}</div>
</section>)}
</div>}
</div>;
}
return <>{error && <Alert>{error}</Alert>}<EmptyState title="No se pudo abrir la estructura" text="Volvé al inicio de Inventarios e intentá nuevamente." /></>;
}