Rutas y Layouts
UNIMAST ERP utiliza Next.js 15 App Router con un sistema sofisticado de layouts anidados, navegación dinámica basada en permisos y componentes de UI especializados.
🏗️ Arquitectura de Routing
Estructura de Directorios
app/
├── app/ # Next.js App Router
│ ├── layout.tsx # Layout raíz de la aplicación
│ ├── (dashboard)/ # Grupo de rutas del dashboard
│ │ ├── layout.tsx # Layout principal del dashboard
│ │ ├── page.tsx # Página de inicio
│ │ ├── invoices/ # Módulo de facturas
│ │ ├── transactions/ # Módulo de transacciones
│ │ ├── ecf/ # Módulo de facturación electrónica
│ │ ├── settings/ # Configuraciones del sistema
│ │ └── layout/ # Componentes específicos del layout
│ ├── api/ # Endpoints de API
│ └── login/ # Página de autenticaciónConvenciones de Next.js 15
- Grupos de rutas:
(dashboard)agrupa rutas bajo un layout común - Layouts anidados: Cada directorio puede tener su propio
layout.tsx - Páginas server-first: Renderizado en servidor por defecto
- Componentes de cliente: Solo cuando es necesario interactividad
🔐 Layout Raíz
Configuración Base (app/app/layout.tsx)
import './globals.css';
import { Analytics } from '@vercel/analytics/react';
import { Toaster } from '@/components/ui/sonner';
export const metadata = {
title: 'Unimast ERP',
description: 'Un sistema de gestión empresarial'
};
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="flex min-h-screen w-full flex-col">
<Toaster
richColors={false}
toastOptions={{
classNames: {
title: 'text-sm font-medium',
description: 'text-muted-foreground text-xs'
}
}}
theme="light"
/>
{children}
</body>
<Analytics />
</html>
);
}Características del Layout Raíz
- Metadatos globales para SEO y configuración
- Toaster global para notificaciones del sistema
- Analytics integrado con Vercel
- CSS global y estilos base
- Estructura HTML consistente
🎯 Layout del Dashboard
Estructura Principal (app/app/(dashboard)/layout.tsx)
export default async function DashboardLayout({
children
}: {
children: React.ReactNode;
}) {
// 1. Verificación de autenticación
const session = await getSession();
if (!session) {
redirect('/login');
}
// 2. Verificación de scopes especiales
const isTerminalOp = await hasRequiredScopes([SCOPE_FLAGS.TERMINAL_OP]);
// 3. Definición de rutas con permisos
const routes = [
{
href: '/',
label: 'Inicio',
icon: <Home className="h-5 w-5" />,
children: []
},
{
href: '#',
label: 'Finanzas',
expanded: true,
icon: <HandCoins className="h-5 w-5" />,
children: [
{
href: '/invoices',
label: 'Facturas',
icon: <FileClock className="h-5 w-5" />,
requiredScopes: [SCOPE.READ_INVOICES],
children: []
}
// ... más rutas
]
}
];
return (
<SessionProvider session={session}>
<Providers>
<TooltipProvider>
<main className="flex min-h-screen w-full flex-col bg-muted/40">
<SidebarProvider>
<CollapsibleSidebar routes={routes} />
<SidebarContentWrapper>
<Header />
<main className="grid flex-1 items-start gap-2 p-4 md:gap-4 bg-muted/40">
{children}
</main>
</SidebarContentWrapper>
</SidebarProvider>
<Analytics />
</main>
</TooltipProvider>
</Providers>
</SessionProvider>
);
}Características del Layout del Dashboard
- Autenticación obligatoria con redirección automática
- Control de scopes para autorización granular
- Navegación dinámica basada en permisos del usuario
- Providers anidados para estado global
- Sidebar responsive con rutas organizadas
🧭 Sistema de Navegación
Estructura de Rutas
interface Route {
href: string;
label: string;
icon?: React.ReactNode;
expanded?: boolean;
requiredScopes?: string[];
children: Route[];
}Categorías de Navegación
1. Inicio
- Ruta:
/ - Acceso: Todos los usuarios autenticados
- Funcionalidad: Dashboard principal con resumen del sistema
2. Finanzas
- Ruta:
#(grupo expandible) - Acceso: Basado en scopes específicos
- Submódulos:
- Facturas (
/invoices) - RequiereSCOPE.READ_INVOICES - Pago (
/customer-pay) - RequiereSCOPE.CREATE_PAYMENTS - Histórico de Turnos - Requiere
SCOPE.READ_CASH_SHIFT - Transacciones (
/transactions) - RequiereSCOPE.READ_CASH_SHIFT
- Facturas (
3. eCF (Facturación Electrónica)
- Ruta:
#(grupo expandible) - Acceso: Basado en scopes de documentos fiscales
- Submódulos:
- Comprobantes Emitidos (
/ecf/issued) - RequiereSCOPE.READ_TAX_DOCUMENT - Comprobantes Recibidos (
/ecf/received) - RequiereSCOPE.READ_RECEIVED_TAX_DOCUMENT
- Comprobantes Emitidos (
4. Configuración
- Ruta:
#(grupo expandible) - Acceso: Solo usuarios con permisos administrativos
- Submódulos:
- Usuarios (
/settings/users) - Gestión de usuarios y roles - Terminales (
/settings/terminals) - Configuración de POS - Métodos de Pago (
/settings/payment-methods) - Configuración de pagos - Roles (
/settings/roles) - Gestión de permisos
- Usuarios (
Control de Acceso por Scopes
// Verificación de scopes en tiempo de renderizado
const isTerminalOp = await hasRequiredScopes([SCOPE_FLAGS.TERMINAL_OP]);
// Rutas condicionales basadas en permisos
{
href: isTerminalOp ? '/shift-history' : '/cash-shifts',
label: 'Histórico de turno',
requiredScopes: [SCOPE.READ_CASH_SHIFT]
}🧩 Componentes de Layout
Sidebar Colapsible (CollapsibleSidebar)
interface CollapsibleSidebarProps {
routes: Route[];
}
export function CollapsibleSidebar({ routes }: CollapsibleSidebarProps) {
return (
<aside className="...">
<nav className="...">
{routes.map((route) => (
<NavItem key={route.href} route={route} />
))}
</nav>
</aside>
);
}Características:
- Navegación jerárquica con grupos expandibles
- Iconos visuales para cada sección
- Control de permisos integrado
- Responsive design con colapso automático
Breadcrumb Inteligente (DashboardBreadcrumb)
const PATHNAME_LABEL: {[key: string]: string} = {
'finances': 'Finanzas',
'customer-pay': 'Pagar',
'transactions': 'Transacciones',
'shift-history': 'Histórico de Turnos',
'invoices': 'Facturas',
'settings': 'Ajustes',
'ecf': 'Facturación Electrónica',
'received': 'Recibidos',
'issued': 'Emitidos'
};
export function DashboardBreadcrumb() {
const paths = usePathname();
const pathNames = paths.split('/').filter((path) => path);
return (
<Breadcrumb className="hidden md:flex">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Inicio</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{pathNames.map((pathname, index) => (
<React.Fragment key={pathname}>
<BreadcrumbSeparator />
{index === pathNames.length - 1 ? (
<BreadcrumbItem>
<BreadcrumbPage>
{PATHNAME_LABEL?.[pathname] || pathname}
</BreadcrumbPage>
</BreadcrumbItem>
) : (
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/${pathNames.slice(0, index + 1).join('/')}`}>
{PATHNAME_LABEL?.[pathname] || pathname}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
)}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
}Características:
- Mapeo automático de rutas a etiquetas legibles
- Navegación jerárquica con enlaces activos
- Responsive design (oculto en móvil)
- Separadores visuales entre niveles
Header del Dashboard
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4">
<DashboardBreadcrumb />
<SearchInput />
<TerminalBadge />
<Payment />
<User />
</header>Componentes del Header:
- Breadcrumb: Navegación de la página actual
- SearchInput: Búsqueda global del sistema
- TerminalBadge: Indicador de terminal activo
- Payment: Acceso rápido a pagos
- User: Menú de usuario y configuración
🔄 Sistema de Providers
Jerarquía de Providers
<SessionProvider session={session}>
<Providers>
<TooltipProvider>
<SidebarProvider>
{/* Contenido del dashboard */}
</SidebarProvider>
</TooltipProvider>
</Providers>
</SessionProvider>Providers Principales
SessionProvider
- Propósito: Gestión de sesión de usuario
- Datos: Información del usuario, roles y scopes
- Acceso: Disponible en toda la aplicación
SidebarProvider
- Propósito: Estado del sidebar (colapsado/expandido)
- Funcionalidad: Control de navegación móvil
- Contexto: Solo en el dashboard
TooltipProvider
- Propósito: Sistema de tooltips global
- Componentes: Todos los elementos con tooltips
- Accesibilidad: Soporte para lectores de pantalla
📱 Navegación Responsive
Sidebar Móvil (MobileNav)
export function MobileNav() {
const { isOpen, toggle } = useSidebar();
return (
<Sheet open={isOpen} onOpenChange={toggle}>
<SheetContent side="left" className="w-80 p-0">
<CollapsibleSidebar routes={routes} />
</SheetContent>
</Sheet>
);
}Características:
- Sheet lateral para dispositivos móviles
- Ancho optimizado para pantallas pequeñas
- Sincronización con estado del sidebar
- Gestos táctiles nativos
Adaptación Automática
- Desktop: Sidebar fijo con navegación completa
- Tablet: Sidebar colapsable con overlay
- Móvil: Navegación en sheet lateral
🛡️ Seguridad y Autorización
Middleware de Autenticación
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('accessToken');
if (!token && !request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}Verificación de Scopes
// Verificación en tiempo de renderizado
const hasAccess = await hasRequiredScopes([SCOPE.READ_INVOICES]);
// Verificación en componentes de cliente
const { hasScope } = useScopes();
const canCreateInvoice = hasScope(SCOPE.CREATE_INVOICES);Protección de Rutas
- Nivel de layout: Verificación automática en
DashboardLayout - Nivel de componente: Verificación granular con hooks
- Nivel de API: Verificación en endpoints
📋 Patrones de Implementación
1. Layouts Anidados
// app/app/(dashboard)/invoices/layout.tsx
export default function InvoicesLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Facturas</h1>
<CreateInvoiceButton />
</div>
{children}
</div>
);
}2. Páginas con Metadata
// app/app/(dashboard)/invoices/page.tsx
export const metadata = {
title: 'Facturas - UNIMAST ERP',
description: 'Gestión de facturas del sistema'
};
export default function InvoicesPage() {
return (
<div className="space-y-4">
<InvoicesTable />
<InvoicesPagination />
</div>
);
}3. Componentes de Navegación
// app/app/(dashboard)/nav-item.tsx
export function NavItem({ route }: { route: Route }) {
const { hasScope } = useScopes();
// Verificar permisos antes de renderizar
if (route.requiredScopes && !route.requiredScopes.every(hasScope)) {
return null;
}
return (
<div className="...">
<Link href={route.href}>
{route.icon}
<span>{route.label}</span>
</Link>
{route.children.length > 0 && (
<NavGroup routes={route.children} />
)}
</div>
);
}🚀 Mejores Prácticas
1. Organización de Rutas
- Agrupar por funcionalidad usando grupos de rutas
- Separar lógica de negocio de componentes de UI
- Usar layouts anidados para funcionalidad común
2. Control de Acceso
- Verificar permisos en el nivel más alto posible
- Usar scopes granulares para control fino
- Implementar fallbacks para usuarios sin permisos
3. Performance
- Lazy loading de componentes pesados
- Code splitting automático con Next.js
- Optimización de imágenes con next/image
4. Accesibilidad
- Navegación por teclado completa
- Labels semánticos para lectores de pantalla
- Contraste adecuado en todos los elementos
5. Responsive Design
- Mobile-first approach en el diseño
- Breakpoints consistentes en toda la aplicación
- Gestos táctiles optimizados para móvil
🔧 Troubleshooting
Problemas Comunes
Rutas no se renderizan
# Verificar que el archivo page.tsx existe
ls app/app/(dashboard)/invoices/page.tsx
# Verificar permisos del usuario
console.log('User scopes:', user.scopes);Sidebar no responde
# Verificar estado del SidebarProvider
const { isOpen, toggle } = useSidebar();
console.log('Sidebar state:', isOpen);Breadcrumb incorrecto
# Verificar mapeo de rutas
console.log('Current pathname:', usePathname());
console.log('PATHNAME_LABEL:', PATHNAME_LABEL);Debug de Navegación
// Agregar logging para debugging
useEffect(() => {
console.log('Current route:', pathname);
console.log('Available routes:', routes);
console.log('User scopes:', user.scopes);
}, [pathname, routes, user.scopes]);¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de autenticación para entender mejor el sistema de permisos, o la documentación de componentes para aprender sobre los componentes de UI utilizados.