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

@@ -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;