Backend:
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:
@@ -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;
|
||||
@@ -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;
|
||||
21
Frontend/src/models/dtos/Auditoria/CanillaHistorialDto.ts
Normal file
21
Frontend/src/models/dtos/Auditoria/CanillaHistorialDto.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
12
Frontend/src/models/dtos/Auditoria/EmpresaHistorialDto.ts
Normal file
12
Frontend/src/models/dtos/Auditoria/EmpresaHistorialDto.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
12
Frontend/src/models/dtos/Auditoria/TipoPagoHistorialDto.ts
Normal file
12
Frontend/src/models/dtos/Auditoria/TipoPagoHistorialDto.ts
Normal 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
|
||||
}
|
||||
9
Frontend/src/models/dtos/Distribucion/CambioParadaDto.ts
Normal file
9
Frontend/src/models/dtos/Distribucion/CambioParadaDto.ts
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface CreateCambioParadaDto {
|
||||
// idCanilla se pasa por la ruta
|
||||
parada: string;
|
||||
vigenciaD: string; // "yyyy-MM-dd"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UpdateCambioParadaDto {
|
||||
vigenciaH: string; // "yyyy-MM-dd"
|
||||
}
|
||||
471
Frontend/src/pages/Auditoria/AuditoriaGeneralPage.tsx
Normal file
471
Frontend/src/pages/Auditoria/AuditoriaGeneralPage.tsx
Normal 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;
|
||||
@@ -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 acá
|
||||
<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>
|
||||
|
||||
218
Frontend/src/pages/Distribucion/GestionarParadasCanillaPage.tsx
Normal file
218
Frontend/src/pages/Distribucion/GestionarParadasCanillaPage.tsx
Normal 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;
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
|
||||
190
Frontend/src/services/Auditoria/auditoriaService.ts
Normal file
190
Frontend/src/services/Auditoria/auditoriaService.ts
Normal 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;
|
||||
39
Frontend/src/services/Distribucion/cambioParadaService.ts
Normal file
39
Frontend/src/services/Distribucion/cambioParadaService.ts
Normal 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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user