Diseño de un AuditoriaController con un patrón para añadir endpoints de historial para diferentes entidades.
Implementación de la lógica de servicio y repositorio para obtener datos de las tablas _H para:
Usuarios (gral_Usuarios_H)
Pagos de Distribuidores (cue_PagosDistribuidor_H)
Notas de Crédito/Débito (cue_CreditosDebitos_H)
Entradas/Salidas de Distribuidores (dist_EntradasSalidas_H)
Entradas/Salidas de Canillitas (dist_EntradasSalidasCanillas_H)
Novedades de Canillitas (dist_dtNovedadesCanillas_H)
Ajustes Manuales de Saldo (cue_SaldoAjustesHistorial)
Tipos de Pago (cue_dtTipopago_H)
Canillitas (Maestro) (dist_dtCanillas_H)
Distribuidores (Maestro) (dist_dtDistribuidores_H)
Empresas (Maestro) (dist_dtEmpresas_H)
DTOs específicos para cada tipo de historial, incluyendo NombreUsuarioModifico.
Frontend:
Servicio auditoriaService.ts con métodos para llamar a cada endpoint de historial.
Página AuditoriaGeneralPage.tsx con:
Selector de "Tipo de Entidad a Auditar".
Filtros comunes (Fechas, Usuario Modificador, Tipo de Modificación, ID Entidad).
Un DataGrid que muestra las columnas dinámicamente según el tipo de entidad seleccionada.
Lógica para cargar los datos correspondientes.
DTOs de historial en TypeScript.
Actualizaciones en AppRoutes.tsx y MainLayout.tsx para la nueva sección de Auditoría (restringida a SuperAdmin).
This commit is contained in:
2025-06-09 19:37:07 -03:00
parent 35e24ab7d2
commit 437b1e8864
98 changed files with 3683 additions and 325 deletions

View File

@@ -0,0 +1,170 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
} from '@mui/material';
import type { CambioParadaDto } from '../../../models/dtos/Distribucion/CambioParadaDto';
import type { CreateCambioParadaDto } from '../../../models/dtos/Distribucion/CreateCambioParadaDto';
import type { UpdateCambioParadaDto } from '../../../models/dtos/Distribucion/UpdateCambioParadaDto';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 3,
};
interface CambioParadaFormModalProps {
open: boolean;
onClose: () => void;
// onSubmit se usará para crear o para cerrar una parada.
// El ID del registro solo es relevante al cerrar.
onSubmit: (data: CreateCambioParadaDto | UpdateCambioParadaDto, idRegistroParada?: number) => Promise<void>;
idCanilla: number | null; // Necesario para crear una nueva parada para este canillita
nombreCanilla?: string; // Para mostrar en el título
paradaParaCerrar?: CambioParadaDto | null; // Si se está cerrando una parada existente
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const CambioParadaFormModal: React.FC<CambioParadaFormModalProps> = ({
open,
onClose,
onSubmit,
idCanilla,
nombreCanilla,
paradaParaCerrar, // Si este tiene valor, estamos en modo "Cerrar Vigencia"
errorMessage,
clearErrorMessage
}) => {
const [parada, setParada] = useState(''); // Solo para nueva parada
const [vigenciaD, setVigenciaD] = useState(''); // Solo para nueva parada
const [vigenciaHCierre, setVigenciaHCierre] = useState(''); // Solo para cerrar parada existente
const [loading, setLoading] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isModoCerrar = Boolean(paradaParaCerrar && paradaParaCerrar.idRegistro);
useEffect(() => {
if (open) {
clearErrorMessage();
setLocalErrors({});
if (isModoCerrar && paradaParaCerrar) {
setParada(paradaParaCerrar.parada); // Mostrar la parada que se está cerrando (readonly)
setVigenciaD(paradaParaCerrar.vigenciaD.split('T')[0]); // Mostrar VigenciaD (readonly)
setVigenciaHCierre(''); // Limpiar para nuevo input
} else { // Modo Crear Nueva Parada
setParada('');
setVigenciaD(new Date().toISOString().split('T')[0]); // Default a hoy
setVigenciaHCierre('');
}
}
}, [open, paradaParaCerrar, isModoCerrar, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (isModoCerrar) {
if (!vigenciaHCierre.trim()) errors.vigenciaHCierre = 'La Vigencia Hasta es obligatoria para cerrar.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(vigenciaHCierre)) errors.vigenciaHCierre = 'Formato de fecha inválido.';
else if (paradaParaCerrar && new Date(vigenciaHCierre) < new Date(paradaParaCerrar.vigenciaD.split('T')[0])) {
errors.vigenciaHCierre = 'Vigencia Hasta no puede ser anterior a la Vigencia Desde de esta parada.';
}
} else { // Modo Crear
if (!idCanilla) errors.general = "ID de Canillita no especificado."; // Error si no hay idCanilla
if (!parada.trim()) errors.parada = 'La dirección de parada es obligatoria.';
else if (parada.trim().length > 150) errors.parada = 'Máximo 150 caracteres.';
if (!vigenciaD.trim()) errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) errors.vigenciaD = 'Formato de fecha inválido.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
if (isModoCerrar && paradaParaCerrar) {
const dataToSubmit: UpdateCambioParadaDto = { vigenciaH: vigenciaHCierre };
await onSubmit(dataToSubmit, paradaParaCerrar.idRegistro);
} else if (idCanilla) { // Modo Crear
const dataToSubmit: CreateCambioParadaDto = { parada, vigenciaD };
// El idCanilla se pasará al servicio desde la página padre o ya está en el contexto del servicio
await onSubmit(dataToSubmit); // El onSubmit de la página se encargará de pasar idCanilla al servicio correcto
}
onClose();
} catch (error: any) {
console.error("Error en submit de CambioParadaFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isModoCerrar ? 'Cerrar Vigencia de Parada' : `Nueva Parada para ${nombreCanilla || 'Canillita'}`}
</Typography>
{isModoCerrar && paradaParaCerrar && (
<Box sx={{mb:2, p:1, backgroundColor: 'grey.100', borderRadius:1}}>
<Typography variant="body2">Parada Actual: <strong>{paradaParaCerrar.parada}</strong></Typography>
<Typography variant="body2">Vigente Desde: <strong>{new Date(paradaParaCerrar.vigenciaD + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone:'UTC'})}</strong></Typography>
</Box>
)}
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
{!isModoCerrar && (
<>
<TextField label="Nueva Dirección de Parada" value={parada} required
onChange={(e) => {setParada(e.target.value); handleInputChange('parada');}}
margin="normal" fullWidth multiline rows={2}
error={!!localErrors.parada} helperText={localErrors.parada || (parada ? `${150 - parada.length} caracteres restantes` : '')}
disabled={loading} autoFocus inputProps={{maxLength: 150}}
/>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="normal" fullWidth
error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
</>
)}
{isModoCerrar && (
<TextField label="Vigencia Hasta (Cierre)" type="date" value={vigenciaHCierre} required
onChange={(e) => {setVigenciaHCierre(e.target.value); handleInputChange('vigenciaHCierre');}}
margin="normal" fullWidth
error={!!localErrors.vigenciaHCierre} helperText={localErrors.vigenciaHCierre || ''}
disabled={loading} InputLabelProps={{ shrink: true }} autoFocus
/>
)}
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading}>
{loading ? <CircularProgress size={24} /> : (isModoCerrar ? 'Confirmar Cierre' : 'Agregar Parada')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default CambioParadaFormModal;

View File

@@ -12,7 +12,7 @@ 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
import { usePermissions } from '../hooks/usePermissions';
interface MainLayoutProps {
children: ReactNode;
@@ -27,6 +27,7 @@ const allAppModules = [
{ 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 }) => {
@@ -47,33 +48,41 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
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í)
if (!isAuthenticated) return [];
return allAppModules.filter(module => {
if (module.requiredPermission === null) return true; // Inicio siempre accesible
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]);
// --- 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);
// 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);
}
}
// --- FIN DE CAMBIO ---
}, [location.pathname, accessibleModules]); // << CAMBIO: dependencia a accessibleModules
}, [location.pathname, accessibleModules]);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUserMenu(event.currentTarget);
@@ -100,25 +109,20 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
if (isPasswordChangeForced) {
logout(); // Si es forzado y cancela/falla, desloguear
} else {
setShowForcedPasswordChangeModal(false); // Si no es forzado, solo cerrar modal
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 (
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<ChangePasswordModal
open={showForcedPasswordChangeModal}
@@ -151,8 +155,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
Sistema de Gestión - El Día
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* ... (Menú de usuario sin cambios) ... */}
{user && (
{user && (
<Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} >
Hola, {user.nombreCompleto}
</Typography>
@@ -168,7 +171,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
onClick={handleOpenUserMenu}
color="inherit"
>
<AccountCircle sx={{ fontSize: 36 }} />
<AccountCircle sx={{ fontSize: 36 }} />
</IconButton>
<Menu
id="menu-appbar"
@@ -202,7 +205,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
)}
</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
@@ -225,22 +227,20 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
}
}}
>
{/* 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) ... */
sx={{
flexGrow: 1,
py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
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'
}}
@@ -248,9 +248,9 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
{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}`
<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}`)}
@@ -265,5 +265,4 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Box>
);
};
export default MainLayout;

View File

@@ -0,0 +1,21 @@
export interface CanillaHistorialDto {
id_Canilla: number;
legajo?: number | null;
nomApe: string;
parada?: string | null;
id_Zona: number;
// nombreZona?: string;
accionista: boolean;
obs?: string | null;
empresa: number; // id de la empresa
// nombreEmpresa?: string;
baja: boolean;
fechaBaja?: string | null; // "yyyy-MM-ddTHH:mm:ss" o solo fecha
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string;
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,22 @@
export interface DistribuidorHistorialDto {
id_Distribuidor: number;
nombre: string;
contacto?: string | null;
nroDoc: string;
id_Zona?: number | null;
// nombreZona?: string;
calle?: string | null;
numero?: string | null;
piso?: string | null;
depto?: string | null;
telefono?: string | null;
email?: string | null;
localidad?: string | null;
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string;
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,12 @@
export interface EmpresaHistorialDto {
id_Empresa: number;
nombre: string;
detalle?: string | null;
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string;
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,21 @@
export interface EntradaSalidaCanillaHistorialDto {
id_Parte: number;
id_Publicacion: number;
// nombrePublicacion?: string;
id_Canilla: number;
// nombreCanilla?: string;
fecha: string; // Fecha original del movimiento
cantSalida: number;
cantEntrada: number;
id_Precio: number;
id_Recargo: number;
id_PorcMon: number;
observacion?: string | null;
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string; // Fecha de la modificación
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,22 @@
export interface EntradaSalidaDistHistorialDto {
id_Parte: number;
id_Publicacion: number;
// nombrePublicacion?: string;
id_Distribuidor: number;
// nombreDistribuidor?: string;
fecha: string; // Fecha original del movimiento
tipoMovimiento: string;
cantidad: number;
remito: number;
observacion?: string | null;
id_Precio: number;
id_Recargo: number;
id_Porcentaje: number;
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string; // Fecha de la modificación
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,20 @@
export interface NotaCreditoDebitoHistorialDto {
id_Nota: number;
destino: string;
id_Destino: number;
// nombreDestinatario?: string; // Opcional
referencia?: string | null;
tipo: string;
fecha: string; // Fecha original de la nota
monto: number;
observaciones?: string | null;
id_Empresa: number;
// nombreEmpresa?: string; // Opcional
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string; // Fecha de la modificación
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,14 @@
export interface NovedadCanillaHistorialDto {
id_Novedad: number;
id_Canilla: number;
// nombreCanilla?: string;
fecha: string; // Fecha original
detalle?: string | null;
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string; // Fecha de auditoría
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,19 @@
export interface PagoDistribuidorHistorialDto {
id_Pago: number; // ID del pago original
id_Distribuidor: number;
fecha: string; // Fecha del pago original (YYYY-MM-DDTHH:mm:ss)
tipoMovimiento: string;
recibo: number;
monto: number;
id_TipoPago: number;
detalle?: string | null;
id_Empresa: number;
// Campos de auditoría
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string; // Fecha de la modificación (YYYY-MM-DDTHH:mm:ss)
tipoMod: string;
id?: string; // Para el DataGrid, se generará en el frontend
}

View File

@@ -0,0 +1,19 @@
export interface SaldoAjusteHistorialDto {
idSaldoAjusteHist: number;
destino: string;
id_Destino: number;
// nombreDestinatario?: string;
id_Empresa: number;
// nombreEmpresa?: string;
montoAjuste: number;
saldoAnterior: number;
saldoNuevo: number;
justificacion: string;
fechaAjuste: string; // Es la FechaMod, pero la llamamos FechaAjuste para claridad
id_UsuarioAjuste: number; // Corresponde a Id_Usuario en el DTO de C#
nombreUsuarioModifico: string;
// tipoMod?: string; // Podrías añadirlo fijo como "Ajuste Manual" en el frontend si es necesario
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,12 @@
export interface TipoPagoHistorialDto {
id_TipoPago: number;
nombre: string; // Nombre del TipoPago en ese momento
detalle?: string | null; // Detalle en ese momento
id_Usuario: number;
nombreUsuarioModifico: string;
fechaMod: string;
tipoMod: string;
id?: string; // Para DataGrid
}

View File

@@ -0,0 +1,9 @@
export interface CambioParadaDto {
idRegistro: number;
idCanilla: number;
nombreCanilla: string;
parada: string;
vigenciaD: string; // "yyyy-MM-dd"
vigenciaH?: string | null; // "yyyy-MM-dd"
esActual: boolean; // Calculada en el backend o frontend
}

View File

@@ -0,0 +1,5 @@
export interface CreateCambioParadaDto {
// idCanilla se pasa por la ruta
parada: string;
vigenciaD: string; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,3 @@
export interface UpdateCambioParadaDto {
vigenciaH: string; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,471 @@
// src/pages/Auditoria/AuditoriaGeneralPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, Paper, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Tooltip
} from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import auditoriaService from '../../services/Auditoria/auditoriaService';
import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto';
import usuarioService from '../../services/Usuarios/usuarioService';
import { usePermissions } from '../../hooks/usePermissions';
//import axios from 'axios'; // Para el tipo de error de Axios
// Lista de tipos de entidad para el filtro
const TIPOS_ENTIDAD_AUDITABLES = [
{ value: "PagoDistribuidor", label: "Pagos de Distribuidores (cue_PagosDistribuidor_H)" },
{ value: "NotaCreditoDebito", label: "Notas C/D (cue_CreditosDebitos_H)" },
{ value: "EntradaSalidaDist", label: "E/S Distribuidores (dist_EntradasSalidas_H)" },
{ value: "EntradaSalidaCanilla", label: "E/S Canillitas (dist_EntradasSalidasCanillas_H)" },
{ value: "NovedadCanilla", label: "Novedades Canillitas (dist_dtNovedadesCanillas_H)" },
{ value: "SaldoAjuste", label: "Ajustes Manuales de Saldo (cue_SaldoAjustesHistorial)" },
{ value: "TipoPago", label: "Tipos de Pago (cue_dtTipopago_H)" },
{ value: "Canillita", label: "Canillitas (Maestro) (dist_dtCanillas_H)" },
{ value: "Distribuidor", label: "Distribuidores (Maestro) (dist_dtDistribuidores_H)" },
{ value: "Empresa", label: "Empresas (Maestro) (dist_dtEmpresas_H)" },
{ value: "Zona", label: "Zonas (Maestro) (dist_dtZonas_H)" },
{ value: "OtroDestino", label: "Otros Destinos (Maestro) (dist_dtOtrosDestinos_H)" },
{ value: "Publicacion", label: "Publicaciones (Maestro) (dist_dtPublicaciones_H)" },
{ value: "PubliSeccion", label: "Secciones de Publicación (dist_dtPubliSecciones_H)" },
{ value: "PrecioPublicacion", label: "Precios de Publicación (dist_Precios_H)" },
{ value: "RecargoZona", label: "Recargos por Zona (dist_RecargoZona_H)" },
{ value: "PorcPagoDistribuidor", label: "Porcentajes Pago Dist. (dist_PorcPago_H)" },
{ value: "PorcMonCanilla", label: "Porcentajes/Montos Canillita (dist_PorcMonPagoCanilla_H)" },
{ value: "ControlDevoluciones", label: "Control Devoluciones (dist_dtCtrlDevoluciones_H)" },
{ value: "TipoBobina", label: "Tipos de Bobina (bob_dtBobinas_H)" },
{ value: "EstadoBobina", label: "Estados de Bobina (bob_dtEstadosBobinas_H)" },
{ value: "PlantaImpresion", label: "Plantas de Impresión (bob_dtPlantas_H)" },
{ value: "StockBobina", label: "Stock de Bobinas (bob_StockBobinas_H)" },
{ value: "RegPublicacionesTirada", label: "Secciones de Tirada (bob_RegPublicaciones_H)" },
{ value: "RegTirada", label: "Registro de Tirada (bob_RegTiradas_H)" },
].sort((a, b) => a.label.localeCompare(b.label));
const TIPOS_MODIFICACION = [
"Creado", "Creada", "Actualizado", "Actualizada", "Modificado", "Modificada",
"Eliminado", "Eliminada", "Insertada", "Baja", "Alta", "Liquidada", "AjusteManualSaldo"
// Añadir más si es necesario (ej. "AjusteManualSaldo")
].sort();
const AuditoriaGeneralPage: React.FC = () => {
const [datosAuditoria, setDatosAuditoria] = useState<any[]>([]); // Tipo genérico para los datos de la tabla
const [columnasActuales, setColumnasActuales] = useState<GridColDef[]>([]);
const [loading, setLoading] = useState(false); // Un solo loading para la búsqueda
const [error, setError] = useState<string | null>(null); // Error general de la página o de la búsqueda
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdUsuarioMod, setFiltroIdUsuarioMod] = useState<number | string>('');
const [filtroTipoEntidad, setFiltroTipoEntidad] = useState<string>('');
const [filtroIdEntidad, setFiltroIdEntidad] = useState<string>('');
const [filtroTipoMod, setFiltroTipoMod] = useState<string>('');
const [usuariosDropdown, setUsuariosDropdown] = useState<UsuarioDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(25);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVerAuditoria = isSuperAdmin || tienePermiso("AU_GENERAL_VIEW"); // Define este permiso
const formatDate = (dateString?: string | null): string => {
if (!dateString) return '-';
const datePart = dateString.split('T')[0];
const parts = datePart.split('-');
if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; }
return datePart;
};
const currencyFormatter = (value?: number | null) => value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '-';
const numberFormatter = (value?: number | null) => value != null ? value.toLocaleString('es-AR') : '-';
useEffect(() => {
const fetchDropdowns = async () => {
if (!puedeVerAuditoria) return;
setLoadingDropdowns(true);
try {
const users = await usuarioService.getAllUsuarios();
setUsuariosDropdown(users);
} catch (e) {
console.error("Error cargando usuarios para filtro auditoría", e);
setError("Error al cargar usuarios para filtro.");
} finally {
setLoadingDropdowns(false);
}
};
fetchDropdowns();
}, [puedeVerAuditoria]);
const getCommonAuditColumns = (): GridColDef[] => [
{ field: 'fechaMod', headerName: 'Fecha Mod.', width: 170, type: 'dateTime', valueFormatter: (value) => formatDate(value as string) },
{ field: 'nombreUsuarioModifico', headerName: 'Modificado Por', width: 180, flex: 0.8 },
{ field: 'tipoMod', headerName: 'Acción', width: 120, flex: 0.5 },
// Se pueden añadir más columnas comunes aquí si aplican a TODOS los historiales
];
const cargarHistorial = useCallback(async () => {
if (!puedeVerAuditoria) {
setError("No tiene permiso para ver la auditoría."); setLoading(false); return;
}
if (!filtroTipoEntidad) {
setError("Debe seleccionar un 'Tipo de Entidad a Auditar' para buscar.");
setDatosAuditoria([]); setColumnasActuales(getCommonAuditColumns()); setLoading(false); return;
}
setLoading(true); setError(null);
try {
const commonParams = {
fechaDesde: filtroFechaDesde || undefined,
fechaHasta: filtroFechaHasta || undefined,
idUsuarioModificador: filtroIdUsuarioMod ? Number(filtroIdUsuarioMod) : undefined,
tipoModificacion: filtroTipoMod || undefined,
};
let rawData: any[] = [];
let cols: GridColDef[] = getCommonAuditColumns();
switch (filtroTipoEntidad) {
case "PagoDistribuidor":
const pagoHist = await auditoriaService.getHistorialPagosDistribuidor({
...commonParams,
idPagoAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = pagoHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Pago', headerName: 'ID Pago', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'id_Distribuidor', headerName: 'ID Dist.', width: 100, align: 'center', headerAlign: 'center' }, // Podrías añadir Nombre si el DTO lo trae
{ field: 'id_Empresa', headerName: 'ID Emp.', width: 100, align: 'center', headerAlign: 'center' }, // Podrías añadir Nombre
{ field: 'fecha', headerName: 'Fecha Pago', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{ field: 'recibo', headerName: 'Recibo', width: 100 },
{ field: 'tipoMovimiento', headerName: 'Tipo Mov.', width: 120 },
{ field: 'monto', headerName: 'Monto Pago', width: 130, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
{ field: 'id_TipoPago', headerName: 'ID Tipo Pago', width: 110, align: 'center', headerAlign: 'center' }, // Podrías añadir Nombre
{ field: 'detalle', headerName: 'Detalle Pago', flex: 1, minWidth: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
];
break;
case "NotaCreditoDebito":
const notaCDHist = await auditoriaService.getHistorialNotasCD({
...commonParams,
idNotaAfectada: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = notaCDHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Nota', headerName: 'ID Nota Orig.', width: 110, align: 'center', headerAlign: 'center' },
{ field: 'destino', headerName: 'Destino', width: 120 },
{ field: 'id_Destino', headerName: 'ID Dest.', width: 100, align: 'center', headerAlign: 'center' },
// NombreDestinatario y NombreEmpresa se podrían traer si el DTO de historial los incluyera
{ field: 'tipo', headerName: 'Tipo Nota', width: 100 },
{ field: 'fecha', headerName: 'Fecha Nota', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{ field: 'monto', headerName: 'Monto Nota', width: 130, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
{ field: 'referencia', headerName: 'Referencia', width: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
{ field: 'observaciones', headerName: 'Obs. Nota', flex: 1, minWidth: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
];
break;
case "EntradaSalidaDist":
const esDistHist = await auditoriaService.getHistorialEntradasSalidasDist({
...commonParams,
idParteAfectada: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
// Si añades más filtros al servicio, pásalos aquí
});
rawData = esDistHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Parte', headerName: 'ID Mov.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'id_Publicacion', headerName: 'ID Pub.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'id_Distribuidor', headerName: 'ID Dist.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{ field: 'tipoMovimiento', headerName: 'Tipo', width: 100 },
{ field: 'cantidad', headerName: 'Cantidad', width: 100, type: 'number', valueFormatter: (v) => numberFormatter(v as number) },
{ field: 'remito', headerName: 'Remito', width: 100, type: 'number' },
{ field: 'observacion', headerName: 'Obs. Mov.', flex: 1, minWidth: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
// Podrías mostrar Id_Precio, Id_Recargo, Id_Porcentaje si es útil
];
break;
case "EntradaSalidaCanilla":
const esCanillaHist = await auditoriaService.getHistorialEntradasSalidasCanilla({
...commonParams,
idParteAfectada: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = esCanillaHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Parte', headerName: 'ID Mov.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'id_Publicacion', headerName: 'ID Pub.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'id_Canilla', headerName: 'ID Can.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{ field: 'cantSalida', headerName: 'Salida', width: 90, type: 'number', valueFormatter: (v) => numberFormatter(v as number) },
{ field: 'cantEntrada', headerName: 'Entrada', width: 90, type: 'number', valueFormatter: (v) => numberFormatter(v as number) },
{ field: 'observacion', headerName: 'Obs. Mov.', flex: 1, minWidth: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
// Considera mostrar Id_Precio, Id_Recargo, Id_PorcMon si es relevante
];
break;
case "NovedadCanilla":
const novedadHist = await auditoriaService.getHistorialNovedadesCanilla({
...commonParams,
idNovedadAfectada: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
// También podrías pasar un filtro de idCanilla si lo añades a HistorialNovedadesCanillaParams y al backend
});
rawData = novedadHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Novedad', headerName: 'ID Novedad', width: 110, align: 'center', headerAlign: 'center' },
{ field: 'id_Canilla', headerName: 'ID Canillita', width: 110, align: 'center', headerAlign: 'center' },
// Aquí podrías querer mostrar el NombreCanilla, necesitarías un JOIN o una llamada extra en el servicio
{ field: 'fecha', headerName: 'Fecha Novedad', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{
field: 'detalle',
headerName: 'Detalle Novedad',
flex: 1,
minWidth: 250,
renderCell: (params) => (
<Tooltip title={params.value || ''} arrow placement="top">
<Typography noWrap variant="body2" sx={{ width: '100%' }}>{params.value || '-'}</Typography>
</Tooltip>
)
},
];
break;
case "SaldoAjuste":
const ajusteHist = await auditoriaService.getHistorialAjustesSaldo({
// Pasar los filtros comunes
fechaDesde: commonParams.fechaDesde,
fechaHasta: commonParams.fechaHasta,
idUsuarioModificador: commonParams.idUsuarioModificador,
// Filtros específicos para este historial (si el backend los usa para este endpoint en particular)
// Si no, estos se ignorarán o podrías omitirlos si el endpoint no los toma.
// El endpoint actual que definimos sí los toma.
destino: filtroIdEntidad ? (TIPOS_ENTIDAD_AUDITABLES.find(t => t.value === filtroTipoEntidad)?.label.includes("Dist") ? "Distribuidores" : "Canillas") : undefined, // Lógica para determinar Destino
idDestino: filtroIdEntidad ? Number(filtroIdEntidad) : undefined, // ID Entidad Afectada aquí sería idDestino
// idEmpresa: si tienes un filtro de empresa general, pásalo
});
rawData = ajusteHist;
cols = [
// Reutilizar 'fechaMod' como 'Fecha Ajuste' y 'nombreUsuarioModifico'
{ field: 'fechaAjuste', headerName: 'Fecha Ajuste', width: 170, type: 'dateTime', valueFormatter: (value) => formatDate(value as string) },
{ field: 'nombreUsuarioModifico', headerName: 'Ajustado Por', width: 180, flex: 0.8 },
{ field: 'destino', headerName: 'Tipo Dest.', width: 120 },
{ field: 'id_Destino', headerName: 'ID Dest.', width: 100, align: 'center', headerAlign: 'center' },
// Aquí podrías querer mostrar NombreDestinatario y NombreEmpresa,
// requeriría que SaldoAjusteHistorialDto los traiga (JOINs en backend)
{ field: 'id_Empresa', headerName: 'ID Emp.', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'montoAjuste', headerName: 'Monto Ajuste', width: 130, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
{ field: 'saldoAnterior', headerName: 'Saldo Ant.', width: 130, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
{ field: 'saldoNuevo', headerName: 'Saldo Nvo.', width: 130, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
{
field: 'justificacion',
headerName: 'Justificación',
flex: 1,
minWidth: 200,
renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2" sx={{ width: '100%' }}>{params.value || '-'}</Typography></Tooltip>)
},
];
break;
case "TipoPago":
const tipoPagoHist = await auditoriaService.getHistorialTiposPago({
...commonParams,
idTipoPagoAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = tipoPagoHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_TipoPago', headerName: 'ID Tipo Pago', width: 110, align: 'center', headerAlign: 'center' },
{ field: 'nombre', headerName: 'Nombre Tipo Pago', width: 200 },
{
field: 'detalle',
headerName: 'Detalle',
flex: 1,
minWidth: 250,
renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2" sx={{ width: '100%' }}>{params.value || '-'}</Typography></Tooltip>)
},
];
break;
case "Canillita": // Historial del maestro de Canillitas
const canMaestroHist = await auditoriaService.getHistorialCanillitasMaestro({
...commonParams,
idCanillaAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = canMaestroHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Canilla', headerName: 'ID Canillita', width: 110, align: 'center', headerAlign: 'center' },
{ field: 'nomApe', headerName: 'Nombre y Apellido', width: 200 },
{ field: 'legajo', headerName: 'Legajo', width: 100, type: 'number' },
{ field: 'parada', headerName: 'Parada', width: 180, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
{ field: 'id_Zona', headerName: 'ID Zona', width: 100, align: 'center', headerAlign: 'center' },
{ field: 'accionista', headerName: 'Accionista', width: 100, type: 'boolean' },
{ field: 'empresa', headerName: 'ID Empresa', width: 100, align: 'center', headerAlign: 'center' }, // ID de la empresa
{ field: 'baja', headerName: 'Baja', width: 80, type: 'boolean' },
{ field: 'fechaBaja', headerName: 'Fecha Baja', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
{ field: 'obs', headerName: 'Obs.', flex: 1, minWidth: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
];
break;
case "Distribuidor": // Historial del maestro de Distribuidores
const distMaestroHist = await auditoriaService.getHistorialDistribuidoresMaestro({
...commonParams,
idDistribuidorAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = distMaestroHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Distribuidor', headerName: 'ID Distribuidor', width: 110, align: 'center', headerAlign: 'center' },
{ field: 'nombre', headerName: 'Nombre', width: 200 },
{ field: 'nroDoc', headerName: 'Nro. Doc.', width: 120 },
{ field: 'contacto', headerName: 'Contacto', width: 150, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
{ field: 'id_Zona', headerName: 'ID Zona', width: 90, align: 'center', headerAlign: 'center' },
// Podrías añadir más campos como Calle, Localidad, etc. si son importantes para la auditoría visual
{ field: 'email', headerName: 'Email', width: 180, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
{ field: 'telefono', headerName: 'Teléfono', width: 130 },
];
break;
case "Empresa": // Historial del maestro de Empresas
const empMaestroHist = await auditoriaService.getHistorialEmpresasMaestro({
...commonParams,
idEmpresaAfectada: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
});
rawData = empMaestroHist;
cols = [
...getCommonAuditColumns(),
{ field: 'id_Empresa', headerName: 'ID Empresa', width: 110, align:'center', headerAlign:'center' },
{ field: 'nombre', headerName: 'Nombre Empresa', width: 250 },
{
field: 'detalle',
headerName: 'Detalle',
flex:1,
minWidth:250,
renderCell: (params) => ( <Tooltip title={params.value || ''}><Typography noWrap variant="body2" sx={{width:'100%'}}>{params.value || '-'}</Typography></Tooltip>)},
];
break;
default:
setError(`La vista de auditoría para '${filtroTipoEntidad}' aún no está implementada.`);
setDatosAuditoria([]);
setColumnasActuales(getCommonAuditColumns());
setLoading(false);
return;
}
// Asignar un ID único a cada fila para el DataGrid si no viene del DTO
setDatosAuditoria(rawData.map((item, index) => ({ ...item, id: item.id_NotaOriginal || item.id_Nota || item.idHist || `hist-${filtroTipoEntidad}-${index}-${Math.random()}` }))); // Asegurar ID único
setColumnasActuales(cols);
} catch (err: any) {
console.error(err);
setError(err.response?.data?.message || 'Error al cargar el historial.');
setDatosAuditoria([]);
setColumnasActuales(getCommonAuditColumns());
} finally {
setLoading(false);
}
}, [
puedeVerAuditoria, filtroTipoEntidad, filtroFechaDesde, filtroFechaHasta,
filtroIdUsuarioMod, filtroIdEntidad, filtroTipoMod
]);
const handleBuscar = () => {
setPage(0);
cargarHistorial();
}
if (!puedeVerAuditoria && !loadingDropdowns) { // Si ya terminaron de cargar los dropdowns y no tiene permiso
return <Box sx={{ p: 2 }}><Alert severity="error">No tiene permiso para acceder a la Auditoría General.</Alert></Box>;
}
if (loadingDropdowns && !usuariosDropdown.length) { // Spinner inicial para dropdowns
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
}
return (
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Auditoría General del Sistema</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
<TextField label="Fecha Mod. Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Mod. Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingDropdowns}>
<InputLabel>Modificado Por</InputLabel>
<Select value={filtroIdUsuarioMod} label="Modificado Por" onChange={(e) => setFiltroIdUsuarioMod(e.target.value)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{usuariosDropdown.map((u) => (<MenuItem key={u.id} value={u.id}>{u.nombre} {u.apellido} ({u.user})</MenuItem>))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} required error={!filtroTipoEntidad && !!error && error.includes("Debe seleccionar un 'Tipo de Entidad")}>
<InputLabel>Tipo de Entidad a Auditar</InputLabel>
<Select value={filtroTipoEntidad} label="Tipo de Entidad a Auditar"
onChange={(e) => {
setFiltroTipoEntidad(e.target.value);
setFiltroIdEntidad('');
setDatosAuditoria([]);
setColumnasActuales(getCommonAuditColumns());
if (error && error.includes("Debe seleccionar un 'Tipo de Entidad")) setError(null);
}}
>
<MenuItem value=""><em>Seleccione un tipo</em></MenuItem>
{TIPOS_ENTIDAD_AUDITABLES.map((t) => (<MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>))}
</Select>
{!filtroTipoEntidad && error && error.includes("Debe seleccionar un 'Tipo de Entidad") && <Typography color="error" variant="caption" sx={{ ml: 1.5, mt: 0.5 }}>{error}</Typography>}
</FormControl>
<TextField label="ID Entidad Afectada" type="number" size="small" value={filtroIdEntidad}
onChange={(e) => setFiltroIdEntidad(e.target.value)} sx={{ minWidth: 150 }}
InputProps={{ inputProps: { min: 1 } }}
disabled={!filtroTipoEntidad}
/>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Tipo Modificación</InputLabel>
<Select value={filtroTipoMod} label="Tipo Modificación" onChange={(e) => setFiltroTipoMod(e.target.value)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{TIPOS_MODIFICACION.map((t) => (<MenuItem key={t} value={t}>{t}</MenuItem>))}
</Select>
</FormControl>
<Button variant="contained" onClick={handleBuscar} disabled={loading || loadingDropdowns || !filtroTipoEntidad}>
{loading ? <CircularProgress size={24} color="inherit" /> : "Buscar"}
</Button>
</Box>
</Paper>
{/* Mostrar error general si no es un error de "seleccione tipo de entidad" */}
{error && !error.includes("Debe seleccionar un 'Tipo de Entidad") && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && filtroTipoEntidad && (
<Paper sx={{ height: 'calc(100vh - 360px)', width: '100%' }}> {/* Ajustar altura */}
<DataGrid
rows={datosAuditoria}
columns={columnasActuales}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
rowCount={datosAuditoria.length}
paginationModel={{ page, pageSize: rowsPerPage }}
onPaginationModelChange={(model) => {
setPage(model.page);
setRowsPerPage(model.pageSize);
}}
pageSizeOptions={[25, 50, 100]}
rowHeight={48}
sx={{
'& .MuiDataGrid-cell': { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
'& .MuiDataGrid-columnHeaderTitleContainer': { overflow: 'hidden' },
}}
getRowId={(row) => row.id} // Asegurar que getRowId use el 'id' que generamos
/>
</Paper>
)}
{!loading && !error && datosAuditoria.length === 0 && filtroTipoEntidad && (
<Typography sx={{ mt: 2, fontStyle: 'italic', textAlign: 'center' }}>
No se encontraron registros de auditoría para '{TIPOS_ENTIDAD_AUDITABLES.find(t => t.value === filtroTipoEntidad)?.label || filtroTipoEntidad}' con los filtros aplicados.
</Typography>
)}
{!loading && !error && !filtroTipoEntidad && (
<Typography sx={{ mt: 2, fontStyle: 'italic', textAlign: 'center' }}>
Seleccione un "Tipo de Entidad a Auditar" y presione "Buscar".
</Typography>
)}
</Box>
);
};
export default AuditoriaGeneralPage;

View File

@@ -7,6 +7,7 @@ import {
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
import HistoryIcon from '@mui/icons-material/History';
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
import EditIcon from '@mui/icons-material/Edit';
import EventNoteIcon from '@mui/icons-material/EventNote'; // << AÑADIR IMPORTACIÓN DEL ICONO
@@ -46,10 +47,8 @@ const GestionarCanillitasPage: React.FC = () => {
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
// Permisos para Novedades
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006"); // << DEFINIR PERMISO
// Para la opción "Ver Novedades", podemos usar el permiso de ver canillitas (CG001)
// O si solo se quiere mostrar si puede gestionarlas, usar puedeGestionarNovedades
const puedeGestionarParadas = isSuperAdmin || tienePermiso("CG007");
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006");
const puedeVerNovedadesCanilla = puedeVer || puedeGestionarNovedades; // << LÓGICA PARA MOSTRAR LA OPCIÓN
@@ -131,6 +130,12 @@ const GestionarCanillitasPage: React.FC = () => {
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const handleOpenParadas = (idCan: number) => {
navigate(`/distribucion/canillas/${idCan}/paradas`);
handleMenuClose();
};
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
@@ -181,7 +186,7 @@ const GestionarCanillitasPage: React.FC = () => {
{error && !apiErrorMessage && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también aquí
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también a
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
@@ -231,6 +236,12 @@ const GestionarCanillitasPage: React.FC = () => {
<ListItemText>Novedades</ListItemText>
</MenuItem>
)}
{puedeGestionarParadas && selectedCanillitaRow && (
<MenuItem onClick={() => handleOpenParadas(selectedCanillitaRow.idCanilla)}>
<ListItemIcon><HistoryIcon /></ListItemIcon> {/* Cambiar ícono si es necesario */}
<ListItemText>Gestionar Paradas</ListItemText>
</MenuItem>
)}
{puedeModificar && selectedCanillitaRow && ( // Asegurar que selectedCanillitaRow existe
<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
@@ -243,7 +254,7 @@ const GestionarCanillitasPage: React.FC = () => {
<ListItemText>{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
</MenuItem>
)}
{/* Mostrar "Sin acciones" si no hay ninguna acción permitida para la fila seleccionada */}
{selectedCanillitaRow && !puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla && (
<MenuItem disabled>Sin acciones</MenuItem>

View File

@@ -0,0 +1,218 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit'; // Para "Cerrar Vigencia"
import DeleteIcon from '@mui/icons-material/Delete';
import cambioParadaService from '../../services/Distribucion/cambioParadaService';
import canillaService from '../../services/Distribucion/canillaService';
import type { CambioParadaDto } from '../../models/dtos/Distribucion/CambioParadaDto';
import type { CreateCambioParadaDto } from '../../models/dtos/Distribucion/CreateCambioParadaDto';
import type { UpdateCambioParadaDto } from '../../models/dtos/Distribucion/UpdateCambioParadaDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import CambioParadaFormModal from '../../components/Modals/Distribucion/CambioParadaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarParadasCanillaPage: React.FC = () => {
const { idCanilla: idCanillaStr } = useParams<{ idCanilla: string }>();
const navigate = useNavigate();
const idCanilla = Number(idCanillaStr);
const [canillita, setCanillita] = useState<CanillaDto | null>(null);
const [paradas, setParadas] = useState<CambioParadaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [paradaParaCerrar, setParadaParaCerrar] = useState<CambioParadaDto | null>(null); // Para el modo "Cerrar" del modal
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedParadaRow, setSelectedParadaRow] = useState<CambioParadaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeGestionarParadas = isSuperAdmin || tienePermiso("CG007");
const puedeVerCanillitas = isSuperAdmin || tienePermiso("CG001");
const cargarDatos = useCallback(async () => {
if (isNaN(idCanilla)) {
setError("ID de Canillita inválido."); setLoading(false); return;
}
if (!puedeGestionarParadas && !puedeVerCanillitas) {
setError("No tiene permiso para acceder a esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [canData, paradasData] = await Promise.all([
puedeVerCanillitas ? canillaService.getCanillaById(idCanilla) : Promise.resolve(null),
(puedeGestionarParadas || puedeVerCanillitas) ? cambioParadaService.getParadasPorCanilla(idCanilla) : Promise.resolve([])
]);
if (canData) setCanillita(canData);
else if (puedeGestionarParadas || puedeVerCanillitas) setCanillita({ idCanilla, nomApe: `ID ${idCanilla}` } as CanillaDto);
setParadas(paradasData.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime()));
} catch (err: any) {
console.error(err);
setError(axios.isAxiosError(err) && err.response?.status === 404 ? `Canillita ID ${idCanilla} no encontrado.` : 'Error al cargar datos.');
} finally { setLoading(false); }
}, [idCanilla, puedeGestionarParadas, puedeVerCanillitas]);
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModalParaCrear = () => {
if (!puedeGestionarParadas) {
setApiErrorMessage("No tiene permiso para agregar paradas."); return;
}
setParadaParaCerrar(null); // Asegurar que es modo creación
setApiErrorMessage(null);
setModalOpen(true);
};
const handleOpenModalParaCerrar = (parada: CambioParadaDto) => {
if (!puedeGestionarParadas) {
setApiErrorMessage("No tiene permiso para modificar paradas."); return;
}
if (parada.vigenciaH) { // Ya está cerrada
setApiErrorMessage("Esta parada ya tiene una fecha de Vigencia Hasta."); return;
}
setParadaParaCerrar(parada);
setApiErrorMessage(null);
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setParadaParaCerrar(null);
};
const handleSubmitModal = async (data: CreateCambioParadaDto | UpdateCambioParadaDto, idRegistroParada?: number) => {
if (!puedeGestionarParadas || !idCanilla) return; // idCanilla es necesario para crear
setApiErrorMessage(null);
try {
if (isModoCerrar && idRegistroParada) { // Es UpdateCambioParadaDto (para cerrar)
await cambioParadaService.cerrarParada(idRegistroParada, data as UpdateCambioParadaDto);
} else { // Es CreateCambioParadaDto
await cambioParadaService.createParada(idCanilla, data as CreateCambioParadaDto);
}
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la parada.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idRegistro: number) => {
if (!puedeGestionarParadas) return;
if (window.confirm(`¿Seguro de eliminar este registro de parada (ID: ${idRegistro})? Esta acción no se puede deshacer.`)) {
setApiErrorMessage(null);
try {
await cambioParadaService.deleteParada(idRegistro);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el registro de parada.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: CambioParadaDto) => {
setAnchorEl(event.currentTarget); setSelectedParadaRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedParadaRow(null);
};
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone:'UTC'}) : '-';
const isModoCerrar = Boolean(paradaParaCerrar && paradaParaCerrar.idRegistro);
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
if (error) return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
if (!puedeGestionarParadas && !puedeVerCanillitas) return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/canillas`)} sx={{ mb: 2 }}>
Volver a Canillitas
</Button>
<Typography variant="h5" gutterBottom>
Historial de Paradas de: {canillita?.nomApe || `Canillita ID ${idCanilla}`}
</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionarParadas && (
<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenModalParaCrear} sx={{ mb: {xs: 2, sm:0} }}>
Registrar Nueva Parada
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Dirección de Parada</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
{puedeGestionarParadas && <TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{paradas.length === 0 ? (
<TableRow><TableCell colSpan={puedeGestionarParadas ? 5 : 4} align="center">No hay historial de paradas para este canillita.</TableCell></TableRow>
) : (
paradas.map((p) => (
<TableRow key={p.idRegistro} hover>
<TableCell>{p.parada}</TableCell>
<TableCell>{formatDate(p.vigenciaD)}</TableCell>
<TableCell>{formatDate(p.vigenciaH)}</TableCell>
<TableCell align="center">{p.esActual ? <Chip label="Activa" color="success" size="small" /> : <Chip label="Histórica" size="small" />}</TableCell>
{puedeGestionarParadas && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeGestionarParadas}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionarParadas && selectedParadaRow && selectedParadaRow.esActual && (
<MenuItem onClick={() => { handleOpenModalParaCerrar(selectedParadaRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Cerrar Vigencia</MenuItem>)}
{/* La eliminación de paradas históricas puede ser delicada, considerar si es necesaria */}
{puedeGestionarParadas && selectedParadaRow && selectedParadaRow.vigenciaH && ( /* Solo eliminar si está cerrada */
<MenuItem onClick={() => handleDelete(selectedParadaRow.idRegistro)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Registro</MenuItem>)}
</Menu>
{idCanilla &&
<CambioParadaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
idCanilla={idCanilla}
nombreCanilla={canillita?.nomApe}
paradaParaCerrar={paradaParaCerrar} // Para diferenciar modo crear vs cerrar
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarParadasCanillaPage;

View File

@@ -3,32 +3,30 @@ import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
const radiosSubModules = [
{ label: 'Ritmos', path: 'ritmos' },
{ label: 'Canciones', path: 'canciones' },
{ label: 'Generar Listas', path: 'generar-listas' },
{ label: 'Ritmos', path: 'ritmos' },
{ label: 'Canciones', path: 'canciones' },
];
const RadiosIndexPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
const [selectedSubTab, setSelectedSubTab] = useState<number>(0); // Inicializa en 0
useEffect(() => {
const currentBasePath = '/radios';
const subPath = location.pathname.startsWith(currentBasePath + '/')
? location.pathname.substring(currentBasePath.length + 1).split('/')[0]
: (location.pathname === currentBasePath ? radiosSubModules[0]?.path : undefined);
? location.pathname.substring(currentBasePath.length + 1).split('/')[0]
: undefined;
const activeTabIndex = radiosSubModules.findIndex(sm => sm.path === subPath);
if (activeTabIndex !== -1) {
if (location.pathname === currentBasePath) {
// Si está en /radios, redirige a la primera subruta
navigate(radiosSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
if (location.pathname === currentBasePath && radiosSubModules.length > 0) {
navigate(radiosSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false);
}
}
}, [location.pathname, navigate]);

View File

@@ -25,6 +25,7 @@ import GestionarSalidasOtrosDestinosPage from '../pages/Distribucion/GestionarSa
import GestionarEntradasSalidasDistPage from '../pages/Distribucion/GestionarEntradasSalidasDistPage';
import GestionarEntradasSalidasCanillaPage from '../pages/Distribucion/GestionarEntradasSalidasCanillaPage';
import GestionarControlDevolucionesPage from '../pages/Distribucion/GestionarControlDevolucionesPage';
import GestionarParadasCanillaPage from '../pages/Distribucion/GestionarParadasCanillaPage';
// Impresión
import ImpresionIndexPage from '../pages/Impresion/ImpresionIndexPage';
@@ -77,6 +78,7 @@ import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistM
// Auditorias
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage';
// --- ProtectedRoute y PublicRoute SIN CAMBIOS ---
@@ -144,6 +146,7 @@ const AppRoutes = () => {
<Route path="salidas-otros-destinos" element={<GestionarSalidasOtrosDestinosPage />} />
<Route path="canillas" element={<GestionarCanillitasPage />} />
<Route path="canillas/:idCanilla/novedades" element={<GestionarNovedadesCanillaPage />} />
<Route path="canillas/:idCanilla/paradas" element={<GestionarParadasCanillaPage />} />
<Route path="distribuidores" element={<GestionarDistribuidoresPage />} />
<Route path="otros-destinos" element={<GestionarOtrosDestinosPage />} />
<Route path="zonas" element={<GestionarZonasPage />} />
@@ -245,17 +248,21 @@ const AppRoutes = () => {
<Route path="auditoria-usuarios" element={<GestionarAuditoriaUsuariosPage />} />
</Route>
{/* Módulo de Auditoías (anidado) */}
<Route path="auditoria"
element={
<SectionProtectedRoute onlySuperAdmin={true} sectionName="Auditoría">
<Outlet />
</SectionProtectedRoute>
}
>
<Route index element={<Navigate to="general" replace />} />
<Route path="general" element={<AuditoriaGeneralPage />} />
</Route>
{/* Ruta catch-all DENTRO del layout protegido */}
<Route path="*" element={<Navigate to="/" replace />} />
</Route> {/* Cierre de la ruta padre "/" */}
{/* Podrías tener un catch-all global aquí si una ruta no coincide EN ABSOLUTO,
pero el path="*" dentro de la ruta "/" ya debería manejar la mayoría de los casos
después de un login exitoso.
Si un usuario no autenticado intenta una ruta inválida, ProtectedRoute lo manda a /login.
*/}
{/* <Route path="*" element={<Navigate to="/login" replace />} /> */}
</Routes>
</BrowserRouter>
);

View File

@@ -1,21 +1,26 @@
// src/routes/SectionProtectedRoute.tsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { usePermissions } from '../hooks/usePermissions';
import { Box, CircularProgress } from '@mui/material';
import { Alert, Box, CircularProgress } from '@mui/material';
interface SectionProtectedRouteProps {
requiredPermission: string;
requiredPermission?: string | null; // Hacerlo opcional
onlySuperAdmin?: boolean; // Nueva prop
sectionName: string;
children?: React.ReactNode;
}
const SectionProtectedRoute: React.FC<SectionProtectedRouteProps> = ({ requiredPermission, sectionName, children }) => {
const { isAuthenticated, isLoading: authIsLoading } = useAuth(); // isLoading de AuthContext
const SectionProtectedRoute: React.FC<SectionProtectedRouteProps> = ({
requiredPermission,
onlySuperAdmin = false, // Default a false
sectionName,
children
}) => {
const { isAuthenticated, isLoading: authIsLoading } = useAuth();
const { tienePermiso, isSuperAdmin, currentUser } = usePermissions();
if (authIsLoading) { // Esperar a que el AuthContext termine su carga inicial
if (authIsLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
<CircularProgress />
@@ -26,26 +31,37 @@ const SectionProtectedRoute: React.FC<SectionProtectedRouteProps> = ({ requiredP
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
// En este punto, si está autenticado, currentUser debería estar disponible.
// Si currentUser pudiera ser null aun estando autenticado (poco probable con tu AuthContext),
// se necesitaría un manejo adicional o un spinner aquí.
if (!currentUser) {
// Esto sería un estado inesperado si isAuthenticated es true.
// Podrías redirigir a login o mostrar un error genérico.
console.error("SectionProtectedRoute: Usuario autenticado pero currentUser es null.");
return <Navigate to="/login" replace />; // O un error más específico
return <Navigate to="/login" replace />;
}
let canAccessSection = false;
if (onlySuperAdmin) {
canAccessSection = isSuperAdmin;
} else if (requiredPermission) {
canAccessSection = isSuperAdmin || tienePermiso(requiredPermission);
} else {
// Si no es onlySuperAdmin y no hay requiredPermission, por defecto se permite si está autenticado
// Esto podría ser para secciones públicas post-login pero sin permiso específico.
// O podrías querer que siempre haya un requiredPermission o onlySuperAdmin.
// Por ahora, lo dejaremos pasar si no se especifica ninguno y no es onlySuperAdmin.
// Sin embargo, para los SSxxx, siempre habrá un requiredPermission.
// Este else es más un fallback teórico.
canAccessSection = true;
}
const canAccessSection = isSuperAdmin || tienePermiso(requiredPermission);
if (!canAccessSection) {
console.error('SectionProtectedRoute: Usuario autenticado pero sin acceso a sección ', sectionName);
return <Navigate to="/" replace />;
return (
<Box sx={{p: 3, display:'flex', justifyContent:'center', mt: 2}}>
<Alert severity="error" sx={{width: '100%', maxWidth: 'md'}}>
No tiene permiso para acceder a la sección de {sectionName}.
</Alert>
</Box>
);
}
// Si children se proporciona (como <SectionProtectedRoute><IndexPage/></SectionProtectedRoute>), renderiza children.
// Si no (como <Route element={<SectionProtectedRoute ... />} > <Route .../> </Route>), renderiza Outlet.
return children ? <>{children}</> : <Outlet />;
};

View File

@@ -0,0 +1,190 @@
import apiClient from '../apiClient';
import type { UsuarioHistorialDto } from '../../models/dtos/Usuarios/Auditoria/UsuarioHistorialDto';
import type { PagoDistribuidorHistorialDto } from '../../models/dtos/Auditoria/PagoDistribuidorHistorialDto';
import type { NotaCreditoDebitoHistorialDto } from '../../models/dtos/Auditoria/NotaCreditoDebitoHistorialDto';
import type { EntradaSalidaDistHistorialDto } from '../../models/dtos/Auditoria/EntradaSalidaDistHistorialDto';
import type { EntradaSalidaCanillaHistorialDto } from '../../models/dtos/Auditoria/EntradaSalidaCanillaHistorialDto';
import type { NovedadCanillaHistorialDto } from '../../models/dtos/Auditoria/NovedadCanillaHistorialDto';
import type { SaldoAjusteHistorialDto } from '../../models/dtos/Auditoria/SaldoAjusteHistorialDto';
import type { TipoPagoHistorialDto } from '../../models/dtos/Auditoria/TipoPagoHistorialDto';
import type { CanillaHistorialDto } from '../../models/dtos/Auditoria/CanillaHistorialDto';
import type { DistribuidorHistorialDto } from '../../models/dtos/Auditoria/DistribuidorHistorialDto';
import type { EmpresaHistorialDto } from '../../models/dtos/Auditoria/EmpresaHistorialDto';
interface HistorialParamsComunes {
fechaDesde?: string; // "yyyy-MM-dd"
fechaHasta?: string; // "yyyy-MM-dd"
idUsuarioModificador?: number; // Cambiado de idUsuarioModifico
tipoModificacion?: string; // Cambiado de tipoMod
}
interface HistorialEmpresasMaestroParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idEmpresaAfectada?: number;
}
interface HistorialDistribuidoresMaestroParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idDistribuidorAfectado?: number;
}
interface HistorialCanillitasMaestroParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idCanillaAfectado?: number;
}
interface HistorialTiposPagoParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idTipoPagoAfectado?: number;
}
interface HistorialAjustesSaldoParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
destino?: string;
idDestino?: number;
idEmpresa?: number;
}
interface HistorialNovedadesCanillaParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idNovedadAfectada?: number;
// idCanilla?: number; // Si quieres filtrar por canillita directamente aquí
}
interface HistorialEntradaSalidaDistParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idParteAfectada?: number; // ID del movimiento original
// Filtros adicionales si el backend los soporta para este historial específico
// idPublicacion?: number;
// idDistribuidor?: number;
}
interface HistorialNotasCDParams extends HistorialParamsComunes {
idNotaAfectada?: number; // ID de la nota original
// Podrías añadir más filtros específicos si fueran necesarios
}
interface HistorialUsuariosParams extends HistorialParamsComunes {
idUsuarioAfectado?: number;
}
interface HistorialPagosDistribuidorParams extends HistorialParamsComunes {
idPagoAfectado?: number; // ID del pago original
// Podría añadir filtros específicos para pagos si es necesario, como idDistribuidor, idEmpresa
}
interface HistorialEntradaSalidaCanillaParams extends HistorialParamsComunes { // << NUEVA INTERFAZ
idParteAfectada?: number;
// idPublicacion?: number; // Si quieres añadir más filtros específicos
// idCanilla?: number;
}
const getHistorialUsuarios = async (params: HistorialUsuariosParams): Promise<UsuarioHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador; // Renombrar para el backend si es diferente
const response = await apiClient.get<UsuarioHistorialDto[]>('/auditoria/usuarios', { params: queryParams });
return response.data;
};
const getHistorialPagosDistribuidor = async (params: HistorialPagosDistribuidorParams): Promise<PagoDistribuidorHistorialDto[]> => {
const queryParams: any = { ...params };
// Asegurar que los nombres de parámetros coincidan con el backend
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<PagoDistribuidorHistorialDto[]>('/auditoria/pagos-distribuidores', { params: queryParams });
return response.data;
};
const getHistorialNotasCD = async (params: HistorialNotasCDParams): Promise<NotaCreditoDebitoHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<NotaCreditoDebitoHistorialDto[]>('/auditoria/notas-credito-debito', { params: queryParams });
return response.data;
};
const getHistorialEntradasSalidasDist = async (params: HistorialEntradaSalidaDistParams): Promise<EntradaSalidaDistHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<EntradaSalidaDistHistorialDto[]>('/auditoria/entradas-salidas-dist', { params: queryParams });
return response.data;
};
const getHistorialEntradasSalidasCanilla = async (params: HistorialEntradaSalidaCanillaParams): Promise<EntradaSalidaCanillaHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<EntradaSalidaCanillaHistorialDto[]>('/auditoria/entradas-salidas-canilla', { params: queryParams });
return response.data;
};
const getHistorialNovedadesCanilla = async (params: HistorialNovedadesCanillaParams): Promise<NovedadCanillaHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<NovedadCanillaHistorialDto[]>('/auditoria/novedades-canilla', { params: queryParams });
return response.data;
};
const getHistorialAjustesSaldo = async (params: HistorialAjustesSaldoParams): Promise<SaldoAjusteHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
// Los otros filtros (destino, idDestino, idEmpresa) ya tienen los nombres correctos
const response = await apiClient.get<SaldoAjusteHistorialDto[]>('/auditoria/ajustes-saldo', { params: queryParams });
return response.data;
};
const getHistorialTiposPago = async (params: HistorialTiposPagoParams): Promise<TipoPagoHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<TipoPagoHistorialDto[]>('/auditoria/tipos-pago', { params: queryParams });
return response.data;
};
const getHistorialCanillitasMaestro = async (params: HistorialCanillitasMaestroParams): Promise<CanillaHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<CanillaHistorialDto[]>('/auditoria/canillitas-maestro', { params: queryParams });
return response.data;
};
const getHistorialDistribuidoresMaestro = async (params: HistorialDistribuidoresMaestroParams): Promise<DistribuidorHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<DistribuidorHistorialDto[]>('/auditoria/distribuidores-maestro', { params: queryParams });
return response.data;
};
const getHistorialEmpresasMaestro = async (params: HistorialEmpresasMaestroParams): Promise<EmpresaHistorialDto[]> => {
const queryParams: any = { ...params };
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
delete queryParams.idUsuarioModificador;
const response = await apiClient.get<EmpresaHistorialDto[]>('/auditoria/empresas-maestro', { params: queryParams });
return response.data;
};
const auditoriaService = {
getHistorialUsuarios,
getHistorialPagosDistribuidor,
getHistorialNotasCD,
getHistorialEntradasSalidasDist,
getHistorialEntradasSalidasCanilla,
getHistorialNovedadesCanilla,
getHistorialAjustesSaldo,
getHistorialTiposPago,
getHistorialCanillitasMaestro,
getHistorialDistribuidoresMaestro,
getHistorialEmpresasMaestro,
};
export default auditoriaService;

View File

@@ -0,0 +1,39 @@
import apiClient from '../apiClient';
import type { CambioParadaDto } from '../../models/dtos/Distribucion/CambioParadaDto';
import type { CreateCambioParadaDto } from '../../models/dtos/Distribucion/CreateCambioParadaDto';
import type { UpdateCambioParadaDto } from '../../models/dtos/Distribucion/UpdateCambioParadaDto';
// Obtiene todos los registros de cambio de parada para un canillita.
const getParadasPorCanilla = async (idCanilla: number): Promise<CambioParadaDto[]> => {
const response = await apiClient.get<CambioParadaDto[]>(`/canillas/${idCanilla}/paradas`);
return response.data;
};
// Crea un nuevo registro de cambio de parada (y cierra el anterior si aplica en backend).
const createParada = async (idCanilla: number, data: CreateCambioParadaDto): Promise<CambioParadaDto> => {
const response = await apiClient.post<CambioParadaDto>(`/canillas/${idCanilla}/paradas`, data);
return response.data;
};
// Cierra la vigencia de una parada específica (actualiza VigenciaH).
// Nota: La ruta podría ser /paradas/{idRegistroParada}/cerrar si el idRegistroParada es globalmente único.
// O /canillas/{idCanilla}/paradas/{idRegistroParada}/cerrar si necesitas el contexto del canillita.
// Asumiré la ruta que usaste en el controlador: /api/paradas/{idRegistroParada}/cerrar
const cerrarParada = async (idRegistroParada: number, data: UpdateCambioParadaDto): Promise<void> => {
await apiClient.put(`/paradas/${idRegistroParada}/cerrar`, data);
};
// Elimina un registro de cambio de parada (si se permite esta acción).
const deleteParada = async (idRegistroParada: number): Promise<void> => {
await apiClient.delete(`/paradas/${idRegistroParada}`);
};
const cambioParadaService = {
getParadasPorCanilla,
createParada,
cerrarParada,
deleteParada,
};
export default cambioParadaService;

View File

@@ -25,6 +25,34 @@ apiClient.interceptors.request.use(
}
);
apiClient.interceptors.response.use(
(response) => {
// Cualquier código de estado que este dentro del rango de 2xx causa la ejecución de esta función
return response;
},
(error) => {
// Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 401) {
// Token inválido o expirado
console.warn("Error 401: Token inválido o expirado. Deslogueando...");
// Limpiar localStorage y recargar la página.
// AuthContext se encargará de redirigir a /login al recargar porque no encontrará token.
localStorage.removeItem('authToken');
localStorage.removeItem('authUser'); // Asegurar limpiar también el usuario
// Forzar un hard refresh para que AuthContext se reinicialice y redirija
// Esto también limpiará cualquier estado de React.
// --- Mostrar mensaje antes de redirigir ---
alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión.");
window.location.href = '/login'; // Redirección más directa
}
}
// Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario
return Promise.reject(error);
}
);
// Puedes añadir interceptores de respuesta para manejar errores globales (ej: 401 Unauthorized)
export default apiClient;