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.
177 lines
10 KiB
TypeScript
177 lines
10 KiB
TypeScript
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>;
|
||
}
|