Files
dh-inspeccion-v2/web-v2/src/layout/AppLayout.tsx
T
admin 35d4630581 F5 · Inventario operativo, territorio y catálogo autorizado (#25)
* fix(web): simplify inventory administration menu

* fix(web): remove legacy imports and function catalog routes

* fix(web): remove redundant inspections lifecycle legend

* feat(inventory): distinguish physical instances from structural records

* fix(dashboard): align inventory and act follow-up metrics

* fix(web): align dashboard summary contract

* fix(web): clarify dashboard act and report concepts

* feat(inventory): mark field-created records as real instances

* feat(inventory): map physical instance flag on asset entity

* fix(inventory): keep field yacimientos structural

* feat(inventory): classify future concrete instances at database level

* fix(inventory): count only installation and subinstallation instances

* feat(inventory): add authoritative F5 source snapshot

* feat(inventory): preload authoritative territory model

* feat(inventory): preload authoritative technical catalog

* fix(findings): use only authoritative F5 family catalog

* fix(inventory): preserve non-hierarchical operator snapshot compatibility

* feat(inventory): add inventory-only asset filter

* feat(inventory): add inventory-only tree filter

* feat(inventory): add inventory browser query contract

* feat(inventory): add area-owned inventory browser

* feat(inventory): expose area-owned inventory browser

* refactor(inventory): remove function catalog and add inventory browser

* fix(inventory): make operator relation temporal and non-owning

* feat(web): add inventory browser API client

* feat(inventory): extend inventory browser filters

* feat(inventory): add real inventory list endpoint logic

* feat(inventory): expose real inventory list

* feat(web): add real inventory list client

* refactor(web): make inventory hierarchy area-owned

* fix(web): show only real inventory instances

* fix(web): style act follow-up tabs and F5 inventory context

* fix(web): load F5 flow styles

* fix(inventory): apply area-owned operational guard on F5 up

* fix(inventory): treat company on asset as non-owning creation snapshot

* fix(inventory): resolve field inventory by area hierarchy, not company ownership

* fix(inventory): preserve custom catalog and apply authoritative universal findings

* fix(inventory): harden authoritative catalog migration checks

* fix(inventory): make authoritative territory preload safely reversible

* feat(inventory): allow independent company master creation

* fix(inventory): make guided creation area-owned and support companies

* feat(web): expose independent company master in inventory setup

* feat(web): create companies independently from physical inventory hierarchy

* fix(inventory): merge by physical area and preserve sealed documents

* test(inventory): lock F5 authoritative model and merge invariants

* feat(inventory): add family administration DTOs

* feat(inventory): administer installation and subinstallation classifications

* feat(inventory): expose family classification administration

* feat(web): add inventory classification administration API

* fix(web): configure finding applicability by inventory classification

* fix(web): redefine inventory configuration around hierarchy classifications and columns

* chore(release): identify F5 inventory model

* chore(release): bump API for F5 inventory model

* test(release): expect F5 health metadata

* chore(release): align WEB package with F5 inventory cut

* chore(release): expose F5 WEB phase

* test(dashboard): expect inspector activity and act follow-up metrics

* test(dashboard): route F5 summary query mocks explicitly

* ci: rehearse all migrations on clean PostGIS before merge

* ci: prove F5 migrations revert and reapply cleanly

* test(f5): align operational navigation contract

* test(f5): align operator lifecycle with area-owned inventory

* test(f5): make merge compatibility area-based

* test(f5): distinguish literal and normalized yacimiento counts

* test(f5): model normalized yacimiento collision explicitly

* ci(f5): bootstrap historical admin prerequisite in clean migration rehearsal

* ci(f5): bypass irreversible historical reset in clean rehearsal

* fix(f5): make territory SQL parameter types explicit

* fix(f5): guarantee canonical inventory hierarchy before territory preload

* ci(f5): include canonical hierarchy migration in rollback gate

* fix(f5): type relation backup markers explicitly

* fix(f5): make catalog SQL text parameter types explicit
2026-09-08 23:18:15 -03:00

131 lines
5.4 KiB
TypeScript

import { useState } from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router';
import { useAuth } from '../auth/AuthContext';
import { APP_PHASE, APP_VERSION } from '../config/version';
import { Icon } from '../components/Icon';
import { OperationalContextBar } from '../components/OperationalContextBar';
import type { IconName } from '../components/Icon';
interface NavItem {
to: string;
label: string;
icon: IconName;
permission?: string;
}
const operational: NavItem[] = [
{ to: '/', label: 'Inicio', icon: 'home', permission: 'dashboard.read' },
{ to: '/inspecciones', label: 'Inspecciones', icon: 'clipboard', permission: 'inspections.read' },
{ to: '/preparacion-campo', label: 'Preparación de campo', icon: 'clipboard', permission: 'inspections.read' },
];
const followUp: NavItem[] = [
{ to: '/seguimiento-actas', label: 'Seguimiento de actas', icon: 'clipboard', permission: 'inspection_acts.read' },
{ to: '/hallazgos', label: 'Hallazgos', icon: 'alert', permission: 'inspection_findings.read' },
];
const documents: NavItem[] = [
{ to: '/actas', label: 'Actas', icon: 'clipboard', permission: 'inspection_acts.read' },
{ to: '/informes', label: 'Informes', icon: 'audit', permission: 'inspection_reports.read' },
];
const master: NavItem[] = [
{ to: '/inventarios', label: 'Inventarios', icon: 'layers', permission: 'assets.read' },
{ to: '/mapa', label: 'Mapa', icon: 'map', permission: 'assets.read' },
];
const administration: NavItem[] = [
{ to: '/admin/users', label: 'Usuarios', icon: 'users', permission: 'users.read' },
{ to: '/admin/roles', label: 'Roles y permisos', icon: 'shield', permission: 'roles.read' },
{ to: '/admin/asset-types', label: 'Configuración de Inventarios', icon: 'layers', permission: 'asset_types.read' },
{ to: '/admin/finding-catalog', label: 'Catálogo de hallazgos', icon: 'alert', permission: 'finding_catalog.manage' },
{ to: '/admin/document-delivery', label: 'Entrega documental', icon: 'audit', permission: 'document_delivery.read' },
];
const system: NavItem[] = [
{ to: '/admin/audit', label: 'Auditoría', icon: 'audit', permission: 'audit.read' },
];
export function AppLayout() {
const { user, hasPermission, logout } = useAuth();
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false);
const canSee = (item: NavItem) => !item.permission || hasPermission(item.permission);
const close = () => setMobileOpen(false);
const handleLogout = async () => {
await logout().catch(() => undefined);
navigate('/login', { replace: true });
};
const renderItems = (items: NavItem[]) => items.filter(canSee).map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
onClick={close}
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
<Icon name={item.icon} />
<span>{item.label}</span>
</NavLink>
));
const hasFollowUp = followUp.some(canSee);
const hasDocuments = documents.some(canSee);
const hasMaster = master.some(canSee);
const hasAdmin = administration.some(canSee);
const hasSystem = system.some(canSee);
return (
<div className="shell">
{mobileOpen && <button className="sidebar-backdrop" aria-label="Cerrar menú" onClick={close} />}
<aside className={`sidebar ${mobileOpen ? 'open' : ''}`}>
<div className="brand">
<span className="brand-mark">DH</span>
<div><strong>DH Inspección</strong><small>Plataforma V2</small></div>
</div>
<nav className="sidebar-nav" aria-label="Navegación principal">
<span className="nav-heading">OPERACIÓN</span>
{renderItems(operational)}
{hasFollowUp && <span className="nav-heading spaced">SEGUIMIENTO</span>}
{renderItems(followUp)}
{hasDocuments && <span className="nav-heading spaced">DOCUMENTOS</span>}
{renderItems(documents)}
{hasMaster && <span className="nav-heading spaced">INVENTARIOS</span>}
{renderItems(master)}
{hasAdmin && <span className="nav-heading spaced">ADMINISTRACIÓN</span>}
{renderItems(administration)}
{hasSystem && <span className="nav-heading spaced">SISTEMA</span>}
{renderItems(system)}
</nav>
<div className="sidebar-user">
<div className="user-avatar">{user?.firstName?.[0]}{user?.lastName?.[0]}</div>
<div className="user-copy">
<strong>{user?.firstName} {user?.lastName}</strong>
<small>{user?.roles.join(' · ') || user?.username}</small>
</div>
<button className="icon-button dark" onClick={handleLogout} title="Cerrar sesión" aria-label="Cerrar sesión">
<Icon name="logout" />
</button>
</div>
</aside>
<div className="main-column">
<header className="mobile-header">
<button className="icon-button" onClick={() => setMobileOpen(true)} aria-label="Abrir menú"><Icon name="menu" /></button>
<strong>DH Inspección</strong>
<span className="mobile-version">v{APP_VERSION}</span>
</header>
<OperationalContextBar />
<main className="content"><Outlet /></main>
<footer className="app-footer" aria-label="Versión de la aplicación">
<span>DH Inspección V2</span><span>·</span><strong>v{APP_VERSION}</strong><span>·</span><span>{APP_PHASE}</span>
</footer>
</div>
</div>
);
}