Files
dh-inspeccion-v2/web-v2/src/layout/AppLayout.tsx
T
admin 205ff1bab8 F1.3 · Vista web de preparación de campo
Completa F1.3 con la vista web imprimible de preparación de campo, selector buscable y lectura Acta → Hallazgos → Inventario.
2026-09-05 17:19:42 -03:00

133 lines
5.6 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' },
{ to: '/importaciones', label: 'Importaciones', icon: 'upload', permission: 'asset_imports.read' },
{ to: '/relevamiento', label: 'Relevamientos', icon: 'clipboard', permission: 'surveys.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>
);
}