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