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; | ||||
		Reference in New Issue
	
	Block a user