Files
GestionIntegralWeb/Frontend/src/layouts/MainLayout.tsx

269 lines
12 KiB
TypeScript
Raw Normal View History

// 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,
Button
} 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 { useAuth } from '../contexts/AuthContext';
feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación Backend API: - Canillitas (`dist_dtCanillas`): - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador). - Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`. - Auditoría en `dist_dtCanillas_H`. - Validación de legajo único y lógica de empresa vs accionista. - Distribuidores (`dist_dtDistribuidores`): - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador). - Auditoría en `dist_dtDistribuidores_H`. - Creación de saldos iniciales para el nuevo distribuidor en todas las empresas. - Verificación de NroDoc único y Nombre opcionalmente único. - Precios de Publicación (`dist_Precios`): - Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador). - Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`. - Lógica de negocio para cerrar período de precio anterior al crear uno nuevo. - Lógica de negocio para reabrir período de precio anterior al eliminar el último. - Auditoría en `dist_Precios_H`. - Auditoría en Eliminación de Publicaciones: - Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación. - Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica). - Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada. - Correcciones de Nulabilidad: - Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables. Frontend React: - Canillitas: - `canillaService.ts`. - `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista. - `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja). - Distribuidores: - `distribuidorService.ts`. - `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona. - `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar). - Precios de Publicación: - `precioService.ts`. - `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día). - `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica. - Layout: - Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios. - Navegación: - Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
2025-05-20 12:38:55 -03:00
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;
}
// 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 ya está disponible aquí
logout,
isAuthenticated,
isPasswordChangeForced,
showForcedPasswordChangeModal,
setShowForcedPasswordChangeModal,
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);
// --- 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(() => {
// --- 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 === '/') {
// 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);
}
// --- FIN DE CAMBIO ---
}, [location.pathname, accessibleModules]); // << CAMBIO: dependencia a accessibleModules
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUserMenu(event.currentTarget);
};
const handleCloseUserMenu = () => {
setAnchorElUserMenu(null);
};
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) => {
// --- INICIO DE CAMBIO: Navegar usando accessibleModules ---
if (accessibleModules[newValue]) {
setSelectedTab(newValue);
navigate(accessibleModules[newValue].path);
}
// --- FIN DE CAMBIO ---
};
const isReportesModule = location.pathname.startsWith('/reportes');
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
// ... (sin cambios)
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' }}>
{/* ... (Menú de usuario sin cambios) ... */}
{user && (
<Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} >
Hola, {user.nombreCompleto}
</Typography>
)}
{isAuthenticated && (
<>
<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>
{/* --- 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={{ /* ... (estilos sin cambios) ... */
flexGrow: 1,
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'
}}
>
{children}
</Box>
<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">
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;