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

154 lines
6.5 KiB
TypeScript
Raw Normal View History

import React, { type ReactNode, useState, useEffect } from 'react';
import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material';
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'; // Para manejar la navegación y la ruta actual
interface MainLayoutProps {
children: ReactNode; // Esto será el <Outlet /> que renderiza las páginas del módulo
}
// Definir los módulos y sus rutas base
const modules = [
{ label: 'Inicio', path: '/' },
{ label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc.
{ label: 'Contables', path: '/contables' },
{ label: 'Impresión', path: '/impresion' },
{ label: 'Reportes', path: '/reportes' },
{ label: 'Radios', path: '/radios' },
{ label: 'Usuarios', path: '/usuarios' },
];
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const {
user,
logout,
showForcedPasswordChangeModal,
isPasswordChangeForced,
passwordChangeCompleted,
setShowForcedPasswordChangeModal,
isAuthenticated
} = useAuth();
const navigate = useNavigate();
const location = useLocation(); // Para obtener la ruta actual
// Estado para el tab seleccionado
const [selectedTab, setSelectedTab] = useState<number | false>(false);
// Efecto para sincronizar el tab seleccionado con la ruta actual
useEffect(() => {
const currentModulePath = modules.findIndex(module =>
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
);
if (currentModulePath !== -1) {
setSelectedTab(currentModulePath);
} else if (location.pathname === '/') {
setSelectedTab(0); // Seleccionar "Inicio" si es la raíz
} else {
setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal)
}
}, [location.pathname]);
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
// ... (lógica de handleModalClose existente) ...
if (passwordChangedSuccessfully) {
passwordChangeCompleted();
} else {
if (isPasswordChangeForced) {
logout();
} else {
setShowForcedPasswordChangeModal(false);
}
}
};
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedTab(newValue);
navigate(modules[newValue].path); // Navegar a la ruta base del módulo
};
// Si el modal de cambio de clave forzado está activo, no mostramos la navegación principal aún.
// El modal se superpone.
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<ChangePasswordModal
open={showForcedPasswordChangeModal}
onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced}
/>
{/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */}
</Box>
);
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Sistema de Gestión - El Día
</Typography>
{user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>}
{isAuthenticated && !isPasswordChangeForced && (
<Button
color="inherit"
onClick={() => setShowForcedPasswordChangeModal(true)} // Ahora abre el modal
>
Cambiar Contraseña
</Button>
)}
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
</Toolbar>
{/* Navegación Principal por Módulos */}
<Paper square elevation={0} > {/* Usamos Paper para un fondo consistente para los Tabs */}
<Tabs
value={selectedTab}
onChange={handleTabChange}
indicatorColor="secondary" // O "primary"
textColor="inherit" // O "primary" / "secondary"
variant="scrollable" // Permite scroll si hay muchos tabs
scrollButtons="auto" // Muestra botones de scroll si es necesario
aria-label="módulos principales"
sx={{ backgroundColor: 'primary.main', color: 'white' }} // Color de fondo para los tabs
>
{modules.map((module) => (
<Tab key={module.path} label={module.label} />
))}
</Tabs>
</Paper>
</AppBar>
{/* Contenido del Módulo (renderizado por <Outlet /> en AppRoutes) */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3, // Padding
// overflowY: 'auto' // Si el contenido del módulo es muy largo
}}
>
{children}
</Box>
<Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}>
<Typography variant="body2">
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Admin' : `Perfil ID ${user?.userId}`} | Versión: {/* TODO: Obtener versión */}
</Typography>
</Box>
{/* Modal para cambio de clave opcional (no forzado) */}
{/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false,
se mostrará aquí también. */}
<ChangePasswordModal
open={showForcedPasswordChangeModal}
onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced} // Esto controla el comportamiento del modal
/>
</Box>
);
};
export default MainLayout;