# US-FUND-007: Navegación y Routing **Epic:** MAI-001 - Fundamentos del Sistema **Story Points:** 5 **Prioridad:** Baja **Dependencias:** - US-FUND-004 (Infraestructura Base) - US-FUND-001 (Autenticación JWT) **Estado:** Pendiente **Asignado a:** Frontend Lead --- ## 📋 Historia de Usuario **Como** usuario del sistema **Quiero** navegar de forma fluida entre las diferentes secciones de la aplicación **Para** acceder rápidamente a las funcionalidades que necesito según mi rol. --- ## 🎯 Contexto y Objetivos ### Contexto Este documento define la estructura de navegación y routing de la aplicación frontend. Incluye: - **Estructura de rutas** por módulo - **Layouts** reutilizables (Auth, Dashboard) - **Rutas protegidas** (autenticación requerida) - **Guards por rol** (acceso basado en permisos) - **Navegación lateral** (sidebar con menú dinámico) - **Breadcrumbs** para orientación del usuario - **404 y manejo de errores** de navegación ### Objetivos 1. ✅ Rutas organizadas por módulo de negocio 2. ✅ Protección de rutas que requieren autenticación 3. ✅ Restricción de rutas por rol (RBAC) 4. ✅ Sidebar dinámico según rol del usuario 5. ✅ Breadcrumbs actualizados automáticamente 6. ✅ Deep linking funcional (URLs compartibles) 7. ✅ 404 page para rutas inexistentes --- ## ✅ Criterios de Aceptación ### CA-1: Estructura de Rutas **Dado** la aplicación frontend **Cuando** se examinan las rutas configuradas **Entonces**: - ✅ Rutas públicas (no requieren auth): - `/login` - `/register` - `/forgot-password` - `/reset-password/:token` - ✅ Rutas protegidas (requieren auth): - `/dashboard` - `/profile` - `/projects` - `/projects/:id` - `/budgets` - `/purchases` - `/hr` (solo HR) - `/finance` (solo Finance) - `/post-sales` (solo Post-Sales) --- ### CA-2: Rutas Protegidas por Autenticación **Dado** un usuario no autenticado **Cuando** intenta acceder a `/dashboard` **Entonces**: - ✅ Es redirigido a `/login` - ✅ URL de destino se guarda en query param: `/login?redirect=/dashboard` - ✅ Después de login, es redirigido a `/dashboard` --- ### CA-3: Rutas Protegidas por Rol **Dado** un usuario con rol `resident` **Cuando** intenta acceder a `/budgets` (solo Director/Engineer) **Entonces**: - ✅ Es redirigido a `/dashboard` - ✅ Toast muestra: "No tienes permisos para acceder a esta sección" - ✅ No se muestra contenido de la ruta restringida --- ### CA-4: Sidebar Dinámico **Dado** un usuario autenticado **Cuando** visualiza el sidebar **Entonces**: - ✅ Solo muestra secciones permitidas para su rol - ✅ Sección activa está resaltada - ✅ Iconos representativos para cada sección - ✅ Sidebar colapsable en pantallas pequeñas **Ejemplo para rol `engineer`:** - ✅ Dashboard ✔️ - ✅ Proyectos ✔️ - ✅ Presupuestos ✔️ - ✅ Compras ✔️ - ❌ Finanzas (oculto) - ❌ RRHH (oculto) - ❌ Post-venta (oculto) --- ### CA-5: Breadcrumbs **Dado** un usuario en `/projects/123e4567-e89b-12d3-a456-426614174000` **Cuando** visualiza los breadcrumbs **Entonces**: - ✅ Muestra: `Dashboard > Proyectos > Residencial Las Palmas` - ✅ Cada nivel es clickeable (excepto el actual) - ✅ Click en "Proyectos" navega a `/projects` - ✅ Click en "Dashboard" navega a `/dashboard` --- ### CA-6: Deep Linking **Dado** un usuario autenticado **Cuando** accede directamente a `/projects/123e4567-e89b-12d3-a456-426614174000` (URL copiada) **Entonces**: - ✅ La página carga correctamente - ✅ Sidebar muestra "Proyectos" como activo - ✅ Breadcrumbs muestra la ruta completa - ✅ Datos del proyecto se cargan desde la API --- ### CA-7: 404 Not Found **Dado** un usuario autenticado **Cuando** accede a `/ruta-inexistente` **Entonces**: - ✅ Se muestra página 404 personalizada - ✅ Mensaje: "La página que buscas no existe" - ✅ Botón "Ir al Dashboard" redirige a `/dashboard` - ✅ URL en el navegador sigue siendo `/ruta-inexistente` --- ## 🔧 Especificación Técnica Detallada ### 1. Estructura de Rutas **Archivo:** `apps/frontend/src/routes/routes.tsx` ```typescript import { Routes, Route, Navigate } from 'react-router-dom'; // Layouts import { AuthLayout } from '@/components/layout/AuthLayout'; import { DashboardLayout } from '@/components/layout/DashboardLayout'; // Guards import { ProtectedRoute } from '@/components/guards/ProtectedRoute'; import { RoleGuard } from '@/components/guards/RoleGuard'; // Pages - Auth import { LoginPage } from '@/features/auth/pages/LoginPage'; import { RegisterPage } from '@/features/auth/pages/RegisterPage'; import { ForgotPasswordPage } from '@/features/auth/pages/ForgotPasswordPage'; import { ResetPasswordPage } from '@/features/auth/pages/ResetPasswordPage'; // Pages - Dashboard import { DashboardPage } from '@/features/dashboard/pages/DashboardPage'; import { ProfilePage } from '@/features/profile/pages/ProfilePage'; // Pages - Projects import { ProjectsListPage } from '@/features/projects/pages/ProjectsListPage'; import { ProjectDetailPage } from '@/features/projects/pages/ProjectDetailPage'; import { CreateProjectPage } from '@/features/projects/pages/CreateProjectPage'; // Pages - Budgets import { BudgetsListPage } from '@/features/budgets/pages/BudgetsListPage'; import { BudgetDetailPage } from '@/features/budgets/pages/BudgetDetailPage'; // Pages - Purchases import { PurchasesListPage } from '@/features/purchases/pages/PurchasesListPage'; import { SuppliersPage } from '@/features/purchases/pages/SuppliersPage'; // Pages - HR import { HrDashboardPage } from '@/features/hr/pages/HrDashboardPage'; import { EmployeesPage } from '@/features/hr/pages/EmployeesPage'; import { AttendancePage } from '@/features/hr/pages/AttendancePage'; // Pages - Finance import { FinanceDashboardPage } from '@/features/finance/pages/FinanceDashboardPage'; import { CashFlowPage } from '@/features/finance/pages/CashFlowPage'; // Pages - Post-Sales import { PostSalesDashboardPage } from '@/features/post-sales/pages/PostSalesDashboardPage'; import { WarrantiesPage } from '@/features/post-sales/pages/WarrantiesPage'; // Pages - Errors import { NotFoundPage } from '@/features/errors/pages/NotFoundPage'; export function AppRoutes() { return ( {/* ==================== PUBLIC ROUTES ==================== */} }> } /> } /> } /> } /> {/* ==================== PROTECTED ROUTES ==================== */} } > {/* Dashboard - Todos los roles */} } /> } /> {/* Projects - Director, Engineer, Resident */} } /> } /> } /> {/* Budgets - Director, Engineer */} } /> } /> {/* Purchases - Director, Engineer, Purchases */} } /> } /> {/* HR - Director, HR */} } /> } /> } /> {/* Finance - Director, Finance */} } /> } /> {/* Post-Sales - Director, Post-Sales */} } /> } /> {/* ==================== REDIRECTS ==================== */} } /> {/* ==================== 404 NOT FOUND ==================== */} } /> ); } ``` --- ### 2. Protected Route Guard **Archivo:** `apps/frontend/src/components/guards/ProtectedRoute.tsx` ```typescript import { Navigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '@/stores/useAuthStore'; interface ProtectedRouteProps { children: React.ReactNode; } export function ProtectedRoute({ children }: ProtectedRouteProps) { const { isAuthenticated } = useAuthStore(); const location = useLocation(); if (!isAuthenticated) { // Redirigir a login con URL de retorno return ; } return <>{children}; } ``` --- ### 3. Role Guard **Archivo:** `apps/frontend/src/components/guards/RoleGuard.tsx` ```typescript import { Navigate } from 'react-router-dom'; import { toast } from 'sonner'; import { useEffect } from 'react'; import { useAuthStore } from '@/stores/useAuthStore'; interface RoleGuardProps { allowedRoles: string[]; children: React.ReactNode; } export function RoleGuard({ allowedRoles, children }: RoleGuardProps) { const { user } = useAuthStore(); useEffect(() => { if (user && !allowedRoles.includes(user.role)) { toast.error('No tienes permisos para acceder a esta sección'); } }, [user, allowedRoles]); if (!user || !allowedRoles.includes(user.role)) { return ; } return <>{children}; } ``` --- ### 4. Dashboard Layout con Sidebar **Archivo:** `apps/frontend/src/components/layout/DashboardLayout.tsx` ```typescript import { Outlet } from 'react-router-dom'; import { Sidebar } from './Sidebar'; import { Header } from './Header'; import { Breadcrumbs } from './Breadcrumbs'; export function DashboardLayout() { return (
{/* Sidebar */} {/* Main Content */}
{/* Header */}
{/* Breadcrumbs */}
{/* Page Content */}
); } ``` --- ### 5. Sidebar con Menú Dinámico **Archivo:** `apps/frontend/src/components/layout/Sidebar.tsx` ```typescript import { NavLink } from 'react-router-dom'; import { LayoutDashboard, FolderKanban, FileText, ShoppingCart, Users, DollarSign, Headphones, } from 'lucide-react'; import { useAuthStore } from '@/stores/useAuthStore'; import { cn } from '@/lib/utils'; interface MenuItem { label: string; path: string; icon: React.ElementType; allowedRoles: string[]; } const menuItems: MenuItem[] = [ { label: 'Dashboard', path: '/dashboard', icon: LayoutDashboard, allowedRoles: ['director', 'engineer', 'resident', 'purchases', 'finance', 'hr', 'post_sales'], }, { label: 'Proyectos', path: '/projects', icon: FolderKanban, allowedRoles: ['director', 'engineer', 'resident'], }, { label: 'Presupuestos', path: '/budgets', icon: FileText, allowedRoles: ['director', 'engineer'], }, { label: 'Compras', path: '/purchases', icon: ShoppingCart, allowedRoles: ['director', 'engineer', 'purchases'], }, { label: 'RRHH', path: '/hr', icon: Users, allowedRoles: ['director', 'hr'], }, { label: 'Finanzas', path: '/finance', icon: DollarSign, allowedRoles: ['director', 'finance'], }, { label: 'Post-venta', path: '/post-sales', icon: Headphones, allowedRoles: ['director', 'post_sales'], }, ]; export function Sidebar() { const { user } = useAuthStore(); // Filtrar menú según rol const visibleItems = menuItems.filter((item) => item.allowedRoles.includes(user?.role || ''), ); return ( ); } ``` --- ### 6. Breadcrumbs Component **Archivo:** `apps/frontend/src/components/layout/Breadcrumbs.tsx` ```typescript import { Link, useLocation } from 'react-router-dom'; import { ChevronRight, Home } from 'lucide-react'; import { useMemo } from 'react'; interface BreadcrumbItem { label: string; path: string; } // Mapa de rutas a labels const routeLabels: Record = { dashboard: 'Dashboard', projects: 'Proyectos', budgets: 'Presupuestos', purchases: 'Compras', suppliers: 'Proveedores', hr: 'RRHH', employees: 'Empleados', attendance: 'Asistencias', finance: 'Finanzas', 'cash-flow': 'Flujo de Caja', 'post-sales': 'Post-venta', warranties: 'Garantías', profile: 'Mi Perfil', new: 'Nuevo', }; export function Breadcrumbs() { const location = useLocation(); const breadcrumbs = useMemo(() => { const paths = location.pathname.split('/').filter(Boolean); const items: BreadcrumbItem[] = []; let currentPath = ''; paths.forEach((segment, index) => { currentPath += `/${segment}`; // Si es un UUID, obtener nombre del recurso (requiere data del contexto) const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( segment, ); if (isUUID) { // TODO: Obtener nombre real del recurso desde el store o API items.push({ label: 'Detalles', path: currentPath, }); } else { items.push({ label: routeLabels[segment] || segment, path: currentPath, }); } }); return items; }, [location.pathname]); return ( ); } ``` --- ### 7. 404 Not Found Page **Archivo:** `apps/frontend/src/features/errors/pages/NotFoundPage.tsx` ```typescript import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Home, ArrowLeft } from 'lucide-react'; export function NotFoundPage() { const navigate = useNavigate(); return (

404

Página no encontrada

Lo sentimos, la página que buscas no existe o ha sido movida.

); } ``` --- ### 8. Login Redirect After Auth **Archivo:** `apps/frontend/src/features/auth/pages/LoginPage.tsx` (fragmento) ```typescript import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAuthStore } from '@/stores/useAuthStore'; export function LoginPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { setTokens } = useAuthStore(); const handleLogin = async (credentials: LoginDto) => { try { const response = await apiService.post('/auth/login', credentials); setTokens(response.accessToken, response.refreshToken); // Redirigir a la URL original o al dashboard const redirectUrl = searchParams.get('redirect') || '/dashboard'; navigate(redirectUrl); toast.success('Inicio de sesión exitoso'); } catch (error) { toast.error('Credenciales inválidas'); } }; return ( // ... form ); } ``` --- ## 🧪 Test Cases ### TC-NAV-001: Redirección a Login **Pre-condiciones:** - Usuario no autenticado **Pasos:** 1. Navegar a `/dashboard` **Resultado esperado:** - ✅ Redirige a `/login?redirect=/dashboard` - ✅ No se muestra contenido del dashboard --- ### TC-NAV-002: Login con Redirect **Pre-condiciones:** - Usuario en `/login?redirect=/projects/123` **Pasos:** 1. Completar login exitoso **Resultado esperado:** - ✅ Redirige a `/projects/123` - ✅ Página de proyecto carga correctamente --- ### TC-NAV-003: Restricción por Rol **Pre-condiciones:** - Usuario con rol `resident` autenticado **Pasos:** 1. Navegar a `/budgets` (solo Director/Engineer) **Resultado esperado:** - ✅ Redirige a `/dashboard` - ✅ Toast: "No tienes permisos para acceder a esta sección" --- ### TC-NAV-004: Sidebar Dinámico **Pre-condiciones:** - Usuario con rol `engineer` autenticado **Pasos:** 1. Observar el sidebar **Resultado esperado:** - ✅ Visible: Dashboard, Proyectos, Presupuestos, Compras - ❌ Oculto: RRHH, Finanzas, Post-venta --- ### TC-NAV-005: Navegación Activa **Pre-condiciones:** - Usuario en `/projects` **Pasos:** 1. Observar el sidebar **Resultado esperado:** - ✅ Item "Proyectos" resaltado con bg-primary - ✅ Otros items con estilo normal --- ### TC-NAV-006: Breadcrumbs **Pre-condiciones:** - Usuario en `/projects/123/budgets/456` **Pasos:** 1. Observar breadcrumbs **Resultado esperado:** - ✅ Muestra: `Inicio > Proyectos > Detalles > Presupuestos > Detalles` - ✅ "Inicio" clickeable → navega a `/dashboard` - ✅ "Proyectos" clickeable → navega a `/projects` - ✅ Último elemento NO clickeable (activo) --- ### TC-NAV-007: 404 Page **Pre-condiciones:** - Usuario autenticado **Pasos:** 1. Navegar a `/ruta-que-no-existe` **Resultado esperado:** - ✅ Se muestra página 404 - ✅ Título: "404" - ✅ Botón "Volver atrás" navega a página anterior - ✅ Botón "Ir al Dashboard" navega a `/dashboard` --- ## 📋 Tareas de Implementación ### Frontend - [ ] **NAV-FE-001:** Configurar React Router v6 - Estimado: 1h - [ ] **NAV-FE-002:** Crear archivo de rutas (routes.tsx) - Estimado: 2h - [ ] **NAV-FE-003:** Implementar ProtectedRoute guard - Estimado: 1h - [ ] **NAV-FE-004:** Implementar RoleGuard - Estimado: 1.5h - [ ] **NAV-FE-005:** Crear DashboardLayout con sidebar - Estimado: 2h - [ ] **NAV-FE-006:** Crear AuthLayout - Estimado: 1h - [ ] **NAV-FE-007:** Implementar Sidebar con menú dinámico - Estimado: 2h - [ ] **NAV-FE-008:** Implementar Breadcrumbs component - Estimado: 2h - [ ] **NAV-FE-009:** Crear NotFoundPage - Estimado: 1h - [ ] **NAV-FE-010:** Implementar redirect después de login - Estimado: 1h ### Testing - [ ] **NAV-TEST-001:** Unit tests para guards - Estimado: 2h - [ ] **NAV-TEST-002:** E2E tests para navegación - Estimado: 2h **Total estimado:** ~18.5 horas --- ## 🔗 Dependencias ### Depende de - ✅ US-FUND-004 (Infraestructura Base) - ✅ US-FUND-001 (Autenticación JWT) ### Bloqueante para - Todas las páginas y features del sistema - UX completa --- ## 📊 Definición de Hecho (DoD) - ✅ React Router v6 configurado - ✅ Rutas públicas y protegidas definidas - ✅ ProtectedRoute guard funcional - ✅ RoleGuard funcional - ✅ Sidebar muestra menú dinámico según rol - ✅ Breadcrumbs actualizados automáticamente - ✅ 404 page implementada - ✅ Deep linking funcional - ✅ Redirect después de login funcional - ✅ Todos los test cases (TC-NAV-001 a TC-NAV-007) pasan - ✅ Navegación fluida sin flickering --- ## 📝 Notas Adicionales ### Mobile Responsive - ✅ Sidebar colapsable en pantallas < 768px - ✅ Menú hamburguesa en mobile - ✅ Breadcrumbs ocultos en mobile (opcional) ### Accesibilidad - ✅ Navegación con teclado (Tab, Enter) - ✅ ARIA labels en links - ✅ Focus visible en elementos ### Performance - ✅ Lazy loading de páginas con React.lazy() - ✅ Code splitting por ruta - ✅ Suspense boundaries para cargas asíncronas --- **Fecha de creación:** 2025-11-17 **Última actualización:** 2025-11-17 **Versión:** 1.0