All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
332 lines
15 KiB
TypeScript
332 lines
15 KiB
TypeScript
import React, { type ReactNode, useState, useEffect, useMemo } from 'react';
|
|
import {
|
|
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
|
|
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
|
|
Button, Badge
|
|
} from '@mui/material';
|
|
import AccountCircle from '@mui/icons-material/AccountCircle';
|
|
import LockResetIcon from '@mui/icons-material/LockReset';
|
|
import LogoutIcon from '@mui/icons-material/Logout';
|
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { usePermissions } from '../hooks/usePermissions';
|
|
|
|
interface MainLayoutProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
// --- Helper para dar nombres legibles a los tipos de alerta ---
|
|
const getTipoAlertaLabel = (tipoAlerta: string): string => {
|
|
switch (tipoAlerta) {
|
|
case 'DevolucionAnomala': return 'Devoluciones Anómalas';
|
|
case 'ComportamientoSistema': return 'Anomalías del Sistema';
|
|
case 'FaltaDeDatos': return 'Falta de Datos';
|
|
default: return tipoAlerta;
|
|
}
|
|
};
|
|
|
|
// 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' },
|
|
{ label: 'Auditoría', path: '/auditoria', requiredPermission: null, onlySuperAdmin: true },
|
|
];
|
|
|
|
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
|
// Obtenemos todo lo necesario del AuthContext, INCLUYENDO LAS ALERTAS
|
|
const {
|
|
user, logout, isAuthenticated, isPasswordChangeForced,
|
|
showForcedPasswordChangeModal, setShowForcedPasswordChangeModal,
|
|
passwordChangeCompleted,
|
|
alertas
|
|
} = useAuth();
|
|
|
|
// El resto de los hooks locales no cambian
|
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
|
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null);
|
|
const [anchorElAlertasMenu, setAnchorElAlertasMenu] = useState<null | HTMLElement>(null);
|
|
|
|
// --- Agrupación de alertas para el menú ---
|
|
const gruposDeAlertas = useMemo(() => {
|
|
if (!alertas || !Array.isArray(alertas)) return [];
|
|
|
|
const groups = alertas.reduce((acc, alerta) => {
|
|
const label = getTipoAlertaLabel(alerta.tipoAlerta);
|
|
acc[label] = (acc[label] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>);
|
|
|
|
return Object.entries(groups); // Devuelve [['Devoluciones Anómalas', 5], ...]
|
|
}, [alertas]);
|
|
|
|
const numAlertas = alertas.length;
|
|
|
|
const accessibleModules = useMemo(() => {
|
|
if (!isAuthenticated) return [];
|
|
return allAppModules.filter(module => {
|
|
if (module.onlySuperAdmin) { // Si el módulo es solo para SuperAdmin
|
|
return isSuperAdmin;
|
|
}
|
|
if (module.requiredPermission === null) return true;
|
|
return isSuperAdmin || tienePermiso(module.requiredPermission);
|
|
});
|
|
}, [isAuthenticated, isSuperAdmin, tienePermiso]);
|
|
|
|
useEffect(() => {
|
|
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 === '/') {
|
|
const inicioIndex = accessibleModules.findIndex(m => m.path === '/');
|
|
if (inicioIndex !== -1) setSelectedTab(inicioIndex);
|
|
else setSelectedTab(false);
|
|
} else {
|
|
// Si la ruta actual no coincide con ningún módulo accesible,
|
|
// y no es la raíz, podría ser una subruta de un módulo no accesible.
|
|
// O podría ser una ruta inválida.
|
|
// Podríamos intentar encontrar el módulo base y ver si es accesible.
|
|
const basePath = "/" + (location.pathname.split('/')[1] || "");
|
|
const parentModuleIndex = accessibleModules.findIndex(m => m.path === basePath);
|
|
if (parentModuleIndex !== -1) {
|
|
setSelectedTab(parentModuleIndex);
|
|
} else {
|
|
setSelectedTab(false);
|
|
}
|
|
}
|
|
}, [location.pathname, accessibleModules]);
|
|
|
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorElUserMenu(event.currentTarget);
|
|
};
|
|
|
|
const handleCloseUserMenu = () => {
|
|
setAnchorElUserMenu(null);
|
|
};
|
|
|
|
// Handlers para el nuevo menú de alertas
|
|
const handleOpenAlertasMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorElAlertasMenu(event.currentTarget);
|
|
};
|
|
|
|
const handleCloseAlertasMenu = () => {
|
|
setAnchorElAlertasMenu(null);
|
|
};
|
|
|
|
const handleNavigateToAlertas = () => { navigate('/anomalias/alertas'); handleCloseAlertasMenu(); };
|
|
|
|
const handleChangePasswordClick = () => {
|
|
setShowForcedPasswordChangeModal(true);
|
|
handleCloseUserMenu();
|
|
};
|
|
|
|
const handleLogoutClick = () => {
|
|
logout();
|
|
handleCloseUserMenu();
|
|
};
|
|
|
|
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
|
|
if (passwordChangedSuccessfully) {
|
|
passwordChangeCompleted();
|
|
} else {
|
|
if (isPasswordChangeForced) {
|
|
logout(); // Si es forzado y cancela/falla, desloguear
|
|
} else {
|
|
setShowForcedPasswordChangeModal(false); // Si no es forzado, solo cerrar modal
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
if (accessibleModules[newValue]) {
|
|
setSelectedTab(newValue);
|
|
navigate(accessibleModules[newValue].path);
|
|
}
|
|
};
|
|
|
|
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
|
<ChangePasswordModal
|
|
open={showForcedPasswordChangeModal}
|
|
onClose={handleModalClose}
|
|
isFirstLogin={isPasswordChangeForced}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// 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}>
|
|
<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' } }} >
|
|
Hola, {user.nombreCompleto}
|
|
</Typography>
|
|
)}
|
|
{isAuthenticated && (
|
|
<>
|
|
<IconButton onClick={handleOpenAlertasMenu} color="inherit">
|
|
<Badge badgeContent={numAlertas} color="error">
|
|
<NotificationsIcon />
|
|
</Badge>
|
|
</IconButton>
|
|
|
|
<Menu
|
|
id="alertas-menu"
|
|
anchorEl={anchorElAlertasMenu}
|
|
open={Boolean(anchorElAlertasMenu)}
|
|
onClose={() => setAnchorElAlertasMenu(null)}
|
|
>
|
|
<MenuItem disabled>
|
|
<ListItemText primary={`Tienes ${numAlertas} alertas pendientes.`} />
|
|
</MenuItem>
|
|
<Divider />
|
|
|
|
{gruposDeAlertas.map(([label, count]) => (
|
|
<MenuItem key={label} onClick={handleNavigateToAlertas}>
|
|
<ListItemIcon><Badge badgeContent={count} color="error" sx={{mr: 2}} /></ListItemIcon>
|
|
<ListItemText>{label}</ListItemText>
|
|
</MenuItem>
|
|
))}
|
|
|
|
{numAlertas > 0 && <Divider />}
|
|
|
|
<MenuItem onClick={handleNavigateToAlertas}>
|
|
<ListItemText sx={{textAlign: 'center'}}>Ver Todas las Alertas</ListItemText>
|
|
</MenuItem>
|
|
</Menu>
|
|
|
|
<IconButton
|
|
size="large"
|
|
aria-label="Cuenta del usuario"
|
|
aria-controls="menu-appbar"
|
|
aria-haspopup="true"
|
|
sx={{ padding: '15px' }}
|
|
onClick={handleOpenUserMenu}
|
|
color="inherit"
|
|
>
|
|
<AccountCircle sx={{ fontSize: 36 }} />
|
|
</IconButton>
|
|
<Menu
|
|
id="menu-appbar"
|
|
anchorEl={anchorElUserMenu}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
keepMounted
|
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
open={Boolean(anchorElUserMenu)}
|
|
onClose={handleCloseUserMenu}
|
|
sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }}
|
|
>
|
|
{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 && (
|
|
<MenuItem onClick={handleChangePasswordClick}>
|
|
<ListItemIcon><LockResetIcon fontSize="small" /></ListItemIcon>
|
|
<ListItemText>Cambiar Contraseña</ListItemText>
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem onClick={handleLogoutClick}>
|
|
<ListItemIcon><LogoutIcon fontSize="small" /></ListItemIcon>
|
|
<ListItemText>Cerrar Sesión</ListItemText>
|
|
</MenuItem>
|
|
</Menu>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</Toolbar>
|
|
{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 },
|
|
}
|
|
}}
|
|
>
|
|
{accessibleModules.map((module) => (
|
|
<Tab key={module.path} label={module.label} />
|
|
))}
|
|
</Tabs>
|
|
</Paper>
|
|
)}
|
|
</AppBar>
|
|
|
|
<Box
|
|
component="main"
|
|
sx={{
|
|
flexGrow: 1,
|
|
py: location.pathname.startsWith('/reportes') ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
|
|
px: location.pathname.startsWith('/reportes') ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
|
|
<Box component="footer" sx={{
|
|
p: 1, backgroundColor: 'grey.200', color: 'text.secondary',
|
|
textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}`
|
|
}}>
|
|
<Typography variant="caption">
|
|
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<ChangePasswordModal
|
|
open={showForcedPasswordChangeModal && !isPasswordChangeForced}
|
|
onClose={() => handleModalClose(false)}
|
|
isFirstLogin={false}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
export default MainLayout; |