Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
		| @@ -1,32 +1,37 @@ | ||||
| import React, { type ReactNode, useState, useEffect } from 'react'; | ||||
| // src/layouts/MainLayout.tsx | ||||
| import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo | ||||
|     from 'react'; | ||||
| import { | ||||
|     Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper, | ||||
|     IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider // Nuevas importaciones | ||||
|     IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider, | ||||
|     Button | ||||
| } from '@mui/material'; | ||||
| import AccountCircle from '@mui/icons-material/AccountCircle'; // Icono de usuario | ||||
| import LockResetIcon from '@mui/icons-material/LockReset'; // Icono para cambiar contraseña | ||||
| import LogoutIcon from '@mui/icons-material/Logout'; // Icono para cerrar sesión | ||||
| import AccountCircle from '@mui/icons-material/AccountCircle'; | ||||
| import LockResetIcon from '@mui/icons-material/LockReset'; | ||||
| import LogoutIcon from '@mui/icons-material/Logout'; | ||||
| import { useAuth } from '../contexts/AuthContext'; | ||||
| import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; | ||||
| import { useNavigate, useLocation } from 'react-router-dom'; | ||||
| import { usePermissions } from '../hooks/usePermissions'; // <<--- AÑADIR ESTA LÍNEA | ||||
|  | ||||
| interface MainLayoutProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
|  | ||||
| const modules = [ | ||||
|     { label: 'Inicio', path: '/' }, | ||||
|     { label: 'Distribución', path: '/distribucion' }, | ||||
|     { label: 'Contables', path: '/contables' }, | ||||
|     { label: 'Impresión', path: '/impresion' }, | ||||
|     { label: 'Reportes', path: '/reportes' }, | ||||
|     { label: 'Radios', path: '/radios' }, | ||||
|     { label: 'Usuarios', path: '/usuarios' }, | ||||
| // Definición original de módulos | ||||
| const allAppModules = [ | ||||
|     { label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible | ||||
|     { label: 'Distribución', path: '/distribucion', requiredPermission: 'SS001' }, | ||||
|     { label: 'Contables', path: '/contables', requiredPermission: 'SS002' }, | ||||
|     { label: 'Impresión', path: '/impresion', requiredPermission: 'SS003' }, | ||||
|     { label: 'Reportes', path: '/reportes', requiredPermission: 'SS004' }, | ||||
|     { label: 'Radios', path: '/radios', requiredPermission: 'SS005' }, | ||||
|     { label: 'Usuarios', path: '/usuarios', requiredPermission: 'SS006' }, | ||||
| ]; | ||||
|  | ||||
| const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|     const { | ||||
|         user, | ||||
|         user, // user ya está disponible aquí | ||||
|         logout, | ||||
|         isAuthenticated, | ||||
|         isPasswordChangeForced, | ||||
| @@ -35,24 +40,40 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|         passwordChangeCompleted | ||||
|     } = useAuth(); | ||||
|  | ||||
|     const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS | ||||
|     const navigate = useNavigate(); | ||||
|     const location = useLocation(); | ||||
|  | ||||
|     const [selectedTab, setSelectedTab] = useState<number | false>(false); | ||||
|     const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); // Estado para el menú de usuario | ||||
|     const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); | ||||
|  | ||||
|     // --- INICIO DE CAMBIO: Filtrar módulos basados en permisos --- | ||||
|     const accessibleModules = useMemo(() => { | ||||
|         if (!isAuthenticated) return []; // Si no está autenticado, ningún módulo excepto quizás login (que no está aquí) | ||||
|         return allAppModules.filter(module => { | ||||
|             if (module.requiredPermission === null) return true; // Inicio siempre accesible | ||||
|             return isSuperAdmin || tienePermiso(module.requiredPermission); | ||||
|         }); | ||||
|     }, [isAuthenticated, isSuperAdmin, tienePermiso]); | ||||
|     // --- FIN DE CAMBIO --- | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const currentModulePath = modules.findIndex(module => | ||||
|         // --- INICIO DE CAMBIO: Usar accessibleModules para encontrar el tab --- | ||||
|         const currentModulePath = accessibleModules.findIndex(module => | ||||
|             location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/')) | ||||
|         ); | ||||
|         if (currentModulePath !== -1) { | ||||
|             setSelectedTab(currentModulePath); | ||||
|         } else if (location.pathname === '/') { | ||||
|             setSelectedTab(0); // Asegurar que la pestaña de Inicio se seleccione para la ruta raíz | ||||
|             // Asegurar que Inicio se seleccione si es accesible | ||||
|             const inicioIndex = accessibleModules.findIndex(m => m.path === '/'); | ||||
|             if (inicioIndex !== -1) setSelectedTab(inicioIndex); | ||||
|             else setSelectedTab(false); | ||||
|         } else { | ||||
|             setSelectedTab(false); // Ninguna pestaña seleccionada si no coincide | ||||
|             setSelectedTab(false); | ||||
|         } | ||||
|     }, [location.pathname]); | ||||
|         // --- FIN DE CAMBIO --- | ||||
|     }, [location.pathname, accessibleModules]); // << CAMBIO: dependencia a accessibleModules | ||||
|  | ||||
|     const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => { | ||||
|         setAnchorElUserMenu(event.currentTarget); | ||||
| @@ -69,7 +90,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|  | ||||
|     const handleLogoutClick = () => { | ||||
|         logout(); | ||||
|         handleCloseUserMenu(); // Cierra el menú antes de desloguear completamente | ||||
|         handleCloseUserMenu(); | ||||
|     }; | ||||
|  | ||||
|     const handleModalClose = (passwordChangedSuccessfully: boolean) => { | ||||
| @@ -77,23 +98,27 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|             passwordChangeCompleted(); | ||||
|         } else { | ||||
|             if (isPasswordChangeForced) { | ||||
|                 logout(); | ||||
|                 logout(); // Si es forzado y cancela/falla, desloguear | ||||
|             } else { | ||||
|                 setShowForcedPasswordChangeModal(false); | ||||
|                  setShowForcedPasswordChangeModal(false); // Si no es forzado, solo cerrar modal | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { | ||||
|         setSelectedTab(newValue); | ||||
|         navigate(modules[newValue].path); | ||||
|         // --- INICIO DE CAMBIO: Navegar usando accessibleModules --- | ||||
|         if (accessibleModules[newValue]) { | ||||
|             setSelectedTab(newValue); | ||||
|             navigate(accessibleModules[newValue].path); | ||||
|         } | ||||
|         // --- FIN DE CAMBIO --- | ||||
|     }; | ||||
|  | ||||
|     // Determinar si el módulo actual es el de Reportes | ||||
|     const isReportesModule = location.pathname.startsWith('/reportes'); | ||||
|  | ||||
|     if (showForcedPasswordChangeModal && isPasswordChangeForced) { | ||||
|         return ( | ||||
|         // ... (sin cambios) | ||||
|          return ( | ||||
|             <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> | ||||
|                 <ChangePasswordModal | ||||
|                     open={showForcedPasswordChangeModal} | ||||
| @@ -104,17 +129,31 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // Si no hay módulos accesibles después del login (y no es el cambio de clave forzado) | ||||
|     // Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio. | ||||
|     // Deberías redirigir a login o mostrar un mensaje de "Sin acceso". | ||||
|     if (isAuthenticated && !isPasswordChangeForced && accessibleModules.length === 0) { | ||||
|         return ( | ||||
|             <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}> | ||||
|                 <Typography variant="h6">No tiene acceso a ninguna sección del sistema.</Typography> | ||||
|                 <Button onClick={logout} sx={{ mt: 2 }}>Cerrar Sesión</Button> | ||||
|             </Box> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> | ||||
|             <AppBar position="sticky" elevation={1} /* Elevation sutil para AppBar */> | ||||
|             <AppBar position="sticky" elevation={1}> | ||||
|                 <Toolbar sx={{ display: 'flex', justifyContent: 'space-between' }}> | ||||
|                     <Typography variant="h6" component="div" noWrap sx={{ cursor: 'pointer' }} onClick={() => navigate('/')}> | ||||
|                         Sistema de Gestión - El Día | ||||
|                     </Typography> | ||||
|  | ||||
|                     <Box sx={{ display: 'flex', alignItems: 'center' }}> | ||||
|                         {user && ( | ||||
|                             <Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} /* Ocultar en pantallas muy pequeñas */> | ||||
|                         {/* ... (Menú de usuario sin cambios) ... */} | ||||
|                          {user && ( | ||||
|                             <Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} > | ||||
|                                 Hola, {user.nombreCompleto} | ||||
|                             </Typography> | ||||
|                         )} | ||||
| @@ -125,9 +164,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|                                     aria-label="Cuenta del usuario" | ||||
|                                     aria-controls="menu-appbar" | ||||
|                                     aria-haspopup="true" | ||||
|                                     sx={{ | ||||
|                                         padding: '15px', | ||||
|                                     }} | ||||
|                                     sx={{ padding: '15px' }} | ||||
|                                     onClick={handleOpenUserMenu} | ||||
|                                     color="inherit" | ||||
|                                 > | ||||
| @@ -143,15 +180,14 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|                                     onClose={handleCloseUserMenu} | ||||
|                                     sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }} | ||||
|                                 > | ||||
|                                     {user && ( // Mostrar info del usuario en el menú | ||||
|                                         <Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' /* Para que no sea clickeable */ }}> | ||||
|                                     {user && ( | ||||
|                                         <Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' }}> | ||||
|                                             <Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>{user.nombreCompleto}</Typography> | ||||
|                                             <Typography variant="body2" color="text.secondary">{user.username}</Typography> | ||||
|                                         </Box> | ||||
|                                     )} | ||||
|                                     {user && <Divider sx={{ mb: 1 }} />} | ||||
|  | ||||
|                                     {!isPasswordChangeForced && ( // No mostrar si ya está forzado a cambiarla | ||||
|                                     {!isPasswordChangeForced && ( | ||||
|                                         <MenuItem onClick={handleChangePasswordClick}> | ||||
|                                             <ListItemIcon><LockResetIcon fontSize="small" /></ListItemIcon> | ||||
|                                             <ListItemText>Cambiar Contraseña</ListItemText> | ||||
| @@ -166,48 +202,45 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|                         )} | ||||
|                     </Box> | ||||
|                 </Toolbar> | ||||
|                 <Paper square elevation={0} > | ||||
|                     <Tabs | ||||
|                         value={selectedTab} | ||||
|                         onChange={handleTabChange} | ||||
|                         indicatorColor="secondary" // O 'primary' si prefieres el mismo color que el fondo | ||||
|                         textColor="inherit" // El texto de la pestaña hereda el color (blanco sobre fondo oscuro) | ||||
|                         variant="scrollable" | ||||
|                         scrollButtons="auto" | ||||
|                         allowScrollButtonsMobile | ||||
|                         aria-label="módulos principales" | ||||
|                         sx={{ | ||||
|                             backgroundColor: 'primary.main', // Color de fondo de las pestañas | ||||
|                             color: 'white', // Color del texto de las pestañas | ||||
|                             '& .MuiTabs-indicator': { | ||||
|                                 height: 3, // Un indicador un poco más grueso | ||||
|                             }, | ||||
|                             '& .MuiTab-root': { // Estilo para cada pestaña | ||||
|                                 minWidth: 100, // Ancho mínimo para cada pestaña | ||||
|                                 textTransform: 'none', // Evitar MAYÚSCULAS por defecto | ||||
|                                 fontWeight: 'normal', | ||||
|                                 opacity: 0.85, // Ligeramente transparentes si no están seleccionadas | ||||
|                                 '&.Mui-selected': { | ||||
|                                     fontWeight: 'bold', | ||||
|                                     opacity: 1, | ||||
|                                     // color: 'secondary.main' // Opcional: color diferente para la pestaña seleccionada | ||||
|                                 }, | ||||
|                             } | ||||
|                         }} | ||||
|                     > | ||||
|                         {modules.map((module) => ( | ||||
|                             <Tab key={module.path} label={module.label} /> | ||||
|                         ))} | ||||
|                     </Tabs> | ||||
|                 </Paper> | ||||
|                 {/* --- INICIO DE CAMBIO: Renderizar Tabs solo si hay módulos accesibles y está autenticado --- */} | ||||
|                 {isAuthenticated && accessibleModules.length > 0 && ( | ||||
|                     <Paper square elevation={0} > | ||||
|                         <Tabs | ||||
|                             value={selectedTab} | ||||
|                             onChange={handleTabChange} | ||||
|                             indicatorColor="secondary" | ||||
|                             textColor="inherit" | ||||
|                             variant="scrollable" | ||||
|                             scrollButtons="auto" | ||||
|                             allowScrollButtonsMobile | ||||
|                             aria-label="módulos principales" | ||||
|                             sx={{ | ||||
|                                 backgroundColor: 'primary.main', | ||||
|                                 color: 'white', | ||||
|                                 '& .MuiTabs-indicator': { height: 3 }, | ||||
|                                 '& .MuiTab-root': { | ||||
|                                     minWidth: 100, textTransform: 'none', | ||||
|                                     fontWeight: 'normal', opacity: 0.85, | ||||
|                                     '&.Mui-selected': { fontWeight: 'bold', opacity: 1 }, | ||||
|                                 } | ||||
|                             }} | ||||
|                         > | ||||
|                             {/* Mapear sobre accessibleModules en lugar de allAppModules */} | ||||
|                             {accessibleModules.map((module) => ( | ||||
|                                 <Tab key={module.path} label={module.label} /> | ||||
|                             ))} | ||||
|                         </Tabs> | ||||
|                     </Paper> | ||||
|                 )} | ||||
|                 {/* --- FIN DE CAMBIO --- */} | ||||
|             </AppBar> | ||||
|  | ||||
|             <Box | ||||
|                 component="main" | ||||
|                 sx={{ | ||||
|                 sx={{ /* ... (estilos sin cambios) ... */ | ||||
|                     flexGrow: 1, | ||||
|                     py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding vertical responsivo | ||||
|                     px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding horizontal responsivo | ||||
|                     py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, | ||||
|                     px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, | ||||
|                     display: 'flex', | ||||
|                     flexDirection: 'column' | ||||
|                 }} | ||||
| @@ -215,17 +248,19 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|                 {children} | ||||
|             </Box> | ||||
|  | ||||
|             <Box component="footer" sx={{ p: 1, backgroundColor: 'grey.200' /* Un gris más claro */, color: 'text.secondary', textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}> | ||||
|             <Box component="footer" sx={{ /* ... (estilos sin cambios) ... */  | ||||
|                  p: 1, backgroundColor: 'grey.200', color: 'text.secondary',  | ||||
|                  textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}` | ||||
|             }}> | ||||
|                 <Typography variant="caption"> | ||||
|                     {/* Puedes usar caption para un texto más pequeño en el footer */} | ||||
|                     Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)} | ||||
|                 </Typography> | ||||
|             </Box> | ||||
|  | ||||
|             <ChangePasswordModal | ||||
|                 open={showForcedPasswordChangeModal && !isPasswordChangeForced} // Solo mostrar si no es el forzado inicial | ||||
|                 onClose={() => handleModalClose(false)} // Asumir que si se cierra sin cambiar, no fue exitoso | ||||
|                 isFirstLogin={false} // Este modal no es para el primer login forzado | ||||
|                 open={showForcedPasswordChangeModal && !isPasswordChangeForced} | ||||
|                 onClose={() => handleModalClose(false)} | ||||
|                 isFirstLogin={false} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user