* 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
131 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|