1. Funcionalidad Principal: Auditoría General
Se creó una nueva sección de "Auditoría" en la aplicación, diseñada para ser accedida por SuperAdmins. Se implementó una página AuditoriaGeneralPage.tsx que actúa como un visor centralizado para el historial de cambios de múltiples entidades del sistema. 2. Backend: Nuevo Controlador (AuditoriaController.cs): Centraliza los endpoints para obtener datos de las tablas de historial (_H). Servicios y Repositorios Extendidos: Se añadieron métodos GetHistorialAsync y ObtenerHistorialAsync a las capas de repositorio y servicio para cada una de las siguientes entidades, permitiendo consultar sus tablas _H con filtros: 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) Tipos de Pago (cue_dtTipopago_H) Canillitas (Maestro) (dist_dtCanillas_H) Distribuidores (Maestro) (dist_dtDistribuidores_H) Empresas (Maestro) (dist_dtEmpresas_H) Zonas (Maestro) (dist_dtZonas_H) Otros Destinos (Maestro) (dist_dtOtrosDestinos_H) Publicaciones (Maestro) (dist_dtPublicaciones_H) Secciones de Publicación (dist_dtPubliSecciones_H) Precios de Publicación (dist_Precios_H) Recargos por Zona (dist_RecargoZona_H) Porcentajes Pago Distribuidores (dist_PorcPago_H) Porcentajes/Montos Canillita (dist_PorcMonPagoCanilla_H) Control de Devoluciones (dist_dtCtrlDevoluciones_H) Tipos de Bobina (bob_dtBobinas_H) Estados de Bobina (bob_dtEstadosBobinas_H) Plantas de Impresión (bob_dtPlantas_H) Stock de Bobinas (bob_StockBobinas_H) Tiradas (Registro Principal) (bob_RegTiradas_H) Secciones de Tirada (bob_RegPublicaciones_H) Cambios de Parada de Canillitas (dist_CambiosParadasCanillas_H) Ajustes Manuales de Saldo (cue_SaldoAjustesHistorial) DTOs de Historial: Se crearon DTOs específicos para cada tabla de historial (ej. UsuarioHistorialDto, PagoDistribuidorHistorialDto, etc.) para transferir los datos al frontend, incluyendo el nombre del usuario que realizó la modificación. Corrección de Lógica de Saldos: Se revisó y corrigió la lógica de afectación de saldos en los servicios PagoDistribuidorService y NotaCreditoDebitoService para asegurar que los débitos y créditos se apliquen correctamente. 3. Frontend: Nuevo Servicio (auditoriaService.ts): Contiene métodos para llamar a cada uno de los nuevos endpoints de auditoría del backend. Nueva Página (AuditoriaGeneralPage.tsx): Permite al SuperAdmin seleccionar el "Tipo de Entidad" a auditar desde un dropdown. Ofrece filtros comunes (rango de fechas, usuario modificador, tipo de acción) y filtros específicos que aparecen dinámicamente según la entidad seleccionada. Utiliza un DataGrid de Material-UI para mostrar el historial, con columnas que se adaptan dinámicamente al tipo de entidad consultada. Nuevos DTOs en TypeScript: Se crearon las interfaces correspondientes a los DTOs de historial del backend. Gestión de Permisos: La sección de Auditoría en MainLayout.tsx y su ruta en AppRoutes.tsx están protegidas para ser visibles y accesibles solo por SuperAdmins. Se añadió un permiso de ejemplo AU_GENERAL_VIEW para ser usado si se decide extender el acceso en el futuro. Corrección de Errores Menores: Se solucionó el problema del "parpadeo" del selector de fecha en GestionarNovedadesCanillaPage al adoptar un patrón de carga de datos más controlado, similar a otras páginas funcionales.
This commit is contained in:
		| @@ -1,25 +1,25 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     InputAdornment | ||||
|     // Quitar Grid si no se usa | ||||
|   Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|   InputAdornment | ||||
|   // Quitar Grid si no se usa | ||||
| } from '@mui/material'; | ||||
| import type { PrecioDto } from '../../../models/dtos/Distribucion/PrecioDto'; | ||||
| import type { CreatePrecioDto } from '../../../models/dtos/Distribucion/CreatePrecioDto'; | ||||
| import type { UpdatePrecioDto } from '../../../models/dtos/Distribucion/UpdatePrecioDto'; | ||||
|  | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '95%', sm: '80%', md: '700px' }, | ||||
|     bgcolor: 'background.paper', | ||||
|     border: '2px solid #000', | ||||
|     boxShadow: 24, | ||||
|     p: 4, | ||||
|     maxHeight: '90vh', | ||||
|     overflowY: 'auto' | ||||
|   position: 'absolute' as 'absolute', | ||||
|   top: '50%', | ||||
|   left: '50%', | ||||
|   transform: 'translate(-50%, -50%)', | ||||
|   width: { xs: '95%', sm: '80%', md: '700px' }, | ||||
|   bgcolor: 'background.paper', | ||||
|   border: '2px solid #000', | ||||
|   boxShadow: 24, | ||||
|   p: 4, | ||||
|   maxHeight: '90vh', | ||||
|   overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| const diasSemana = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"] as const; | ||||
| @@ -55,48 +55,50 @@ const PrecioFormModal: React.FC<PrecioFormModalProps> = ({ | ||||
|   const isEditing = Boolean(initialData); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (open) { | ||||
|         setVigenciaD(initialData?.vigenciaD || ''); | ||||
|         setVigenciaH(initialData?.vigenciaH || ''); | ||||
|         const initialPrecios: Record<DiaSemana, string> = { Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: '' }; | ||||
|         if (initialData) { | ||||
|             diasSemana.forEach(dia => { | ||||
|                 const key = dia.toLowerCase() as keyof Omit<PrecioDto, 'idPrecio' | 'idPublicacion' | 'vigenciaD' | 'vigenciaH'>; | ||||
|                 initialPrecios[dia] = initialData[key]?.toString() || ''; | ||||
|             }); | ||||
|         } | ||||
|         setPreciosDia(initialPrecios); | ||||
|         setLocalErrors({}); | ||||
|         clearErrorMessage(); | ||||
|   // Este efecto se ejecuta cuando el modal se abre o cuando los datos iniciales cambian | ||||
|   if (open) { | ||||
|     setVigenciaD(initialData?.vigenciaD || ''); | ||||
|     setVigenciaH(initialData?.vigenciaH || ''); | ||||
|     const initialPrecios: Record<DiaSemana, string> = { Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: '' }; | ||||
|     if (initialData) { | ||||
|       diasSemana.forEach(dia => { | ||||
|         const key = dia.toLowerCase() as keyof Omit<PrecioDto, 'idPrecio' | 'idPublicacion' | 'vigenciaD' | 'vigenciaH'>; | ||||
|         initialPrecios[dia] = initialData[key]?.toString() || ''; | ||||
|       }); | ||||
|     } | ||||
|   }, [open, initialData, clearErrorMessage]); | ||||
|     setPreciosDia(initialPrecios); | ||||
|     setLocalErrors({}); | ||||
|     // NO llamar a clearErrorMessage aquí automáticamente al abrir si ya hay un error de un intento previo. | ||||
|     // Solo limpiar si es una "nueva" apertura (ej, initialData cambió o se abrió desde cerrado) | ||||
|   } | ||||
| }, [open, initialData]); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!isEditing && !vigenciaD.trim()) { | ||||
|         errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; | ||||
|       errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; | ||||
|     } else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) { | ||||
|         errors.vigenciaD = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|       errors.vigenciaD = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|     } | ||||
|  | ||||
|     if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) { | ||||
|         errors.vigenciaH = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|       errors.vigenciaH = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|     } else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) { | ||||
|         errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; | ||||
|       errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; | ||||
|     } | ||||
|  | ||||
|     let alMenosUnPrecio = false; | ||||
|     diasSemana.forEach(dia => { | ||||
|         const valor = preciosDia[dia]; | ||||
|         if (valor.trim()) { | ||||
|             alMenosUnPrecio = true; | ||||
|             if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) { | ||||
|                 errors[dia.toLowerCase()] = `Precio de ${dia} inválido.`; | ||||
|             } | ||||
|       const valor = preciosDia[dia]; | ||||
|       if (valor.trim()) { | ||||
|         alMenosUnPrecio = true; | ||||
|         if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) { | ||||
|           errors[dia.toLowerCase()] = `Precio de ${dia} inválido.`; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     if (!isEditing && !alMenosUnPrecio) { | ||||
|         errors.dias = 'Debe ingresar al menos un precio para un día de la semana.'; | ||||
|       errors.dias = 'Debe ingresar al menos un precio para un día de la semana.'; | ||||
|     } | ||||
|  | ||||
|     setLocalErrors(errors); | ||||
| @@ -106,53 +108,59 @@ const PrecioFormModal: React.FC<PrecioFormModalProps> = ({ | ||||
|   const handlePrecioDiaChange = (dia: DiaSemana, value: string) => { | ||||
|     setPreciosDia(prev => ({ ...prev, [dia]: value })); | ||||
|     if (localErrors[dia.toLowerCase()] || localErrors.dias) { | ||||
|         setLocalErrors(prev => ({ ...prev, [dia.toLowerCase()]: null, dias: null })); | ||||
|       setLocalErrors(prev => ({ ...prev, [dia.toLowerCase()]: null, dias: null })); | ||||
|     } | ||||
|     if (errorMessage) clearErrorMessage(); | ||||
|   }; | ||||
|   const handleDateChange = (setter: React.Dispatch<React.SetStateAction<string>>, fieldName: string) => (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setter(e.target.value); | ||||
|     if (localErrors[fieldName]) { | ||||
|         setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|       setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|     } | ||||
|     if (errorMessage) clearErrorMessage(); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { | ||||
|     event.preventDefault(); | ||||
|     clearErrorMessage(); | ||||
|     clearErrorMessage(); // Limpiar al inicio de un nuevo intento de submit | ||||
|     if (!validate()) return; | ||||
|  | ||||
|     setLoading(true); | ||||
|     let success = false; // Bandera para controlar el cierre | ||||
|     try { | ||||
|       const preciosNumericos: Partial<Record<keyof Omit<PrecioDto, 'idPrecio'|'idPublicacion'|'vigenciaD'|'vigenciaH'>, number | null>> = {}; | ||||
|         diasSemana.forEach(dia => { | ||||
|             const valor = preciosDia[dia].trim(); | ||||
|             // Convertir el nombre del día a la clave correcta del DTO (ej. "Lunes" -> "lunes") | ||||
|             const key = dia.toLowerCase() as keyof typeof preciosNumericos; | ||||
|             preciosNumericos[key] = valor ? parseFloat(valor) : null; | ||||
|         }); | ||||
|  | ||||
|       diasSemana.forEach(dia => { | ||||
|         const valor = preciosDia[dia].trim(); | ||||
|         const key = dia.toLowerCase() as keyof typeof preciosNumericos; | ||||
|         preciosNumericos[key] = valor ? parseFloat(valor) : null; | ||||
|       }); | ||||
|  | ||||
|       if (isEditing && initialData) { | ||||
|         const dataToSubmit: UpdatePrecioDto = { | ||||
|             vigenciaH: vigenciaH.trim() ? vigenciaH : null, | ||||
|             ...preciosNumericos // Spread de los precios de los días | ||||
|           vigenciaH: vigenciaH.trim() ? vigenciaH.split('T')[0] : null, | ||||
|           ...preciosNumericos | ||||
|         }; | ||||
|         await onSubmit(dataToSubmit, initialData.idPrecio); | ||||
|       } else { | ||||
|         const dataToSubmit: CreatePrecioDto = { | ||||
|             idPublicacion, | ||||
|             vigenciaD, | ||||
|             ...preciosNumericos | ||||
|           idPublicacion, | ||||
|           vigenciaD, | ||||
|           ...preciosNumericos | ||||
|         }; | ||||
|         await onSubmit(dataToSubmit); | ||||
|       } | ||||
|       onClose(); | ||||
|       success = true; // Marcar como exitoso si no hubo excepciones | ||||
|     } catch (error: any) { | ||||
|       console.error("Error en submit de PrecioFormModal:", error); | ||||
|       // El error ya fue seteado en el padre (GestionarPreciosPublicacionPage) | ||||
|       // y se pasa como prop 'errorMessage' a este modal. | ||||
|       // No necesitamos setearlo aquí de nuevo. | ||||
|       console.error("Error propagado al submit de PrecioFormModal:", error); | ||||
|       success = false; | ||||
|     } finally { | ||||
|        setLoading(false); | ||||
|       setLoading(false); | ||||
|       if (success) { | ||||
|         onClose(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -163,51 +171,52 @@ const PrecioFormModal: React.FC<PrecioFormModalProps> = ({ | ||||
|           {isEditing ? 'Editar Período de Precio' : 'Agregar Nuevo Período de Precio'} | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|             {/* Sección de Vigencias */} | ||||
|             <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||
|                 <TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing} | ||||
|                     onChange={handleDateChange(setVigenciaD, 'vigenciaD')} margin="dense" | ||||
|                     error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''} | ||||
|                     disabled={loading || isEditing} | ||||
|                     InputLabelProps={{ shrink: true }} | ||||
|                     sx={{ flex: 1, minWidth: isEditing ? 'calc(50% - 8px)' : '100%' }} | ||||
|                     autoFocus={!isEditing} | ||||
|                 /> | ||||
|                 {isEditing && ( | ||||
|                     <TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH} | ||||
|                         onChange={handleDateChange(setVigenciaH, 'vigenciaH')} margin="dense" | ||||
|                         error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''} | ||||
|                         disabled={loading} | ||||
|                         InputLabelProps={{ shrink: true }} | ||||
|                         sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} | ||||
|                     /> | ||||
|                 )} | ||||
|             </Box> | ||||
|           {/* Sección de Vigencias */} | ||||
|           <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||
|             <TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing} | ||||
|               onChange={handleDateChange(setVigenciaD, 'vigenciaD')} margin="dense" | ||||
|               error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''} | ||||
|               disabled={loading || isEditing} | ||||
|               InputLabelProps={{ shrink: true }} | ||||
|               sx={{ flex: 1, minWidth: isEditing ? 'calc(50% - 8px)' : '100%' }} | ||||
|               autoFocus={!isEditing} | ||||
|             /> | ||||
|             {isEditing && ( | ||||
|               <TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH} | ||||
|                 onChange={handleDateChange(setVigenciaH, 'vigenciaH')} margin="dense" | ||||
|                 error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''} | ||||
|                 disabled={loading} | ||||
|                 InputLabelProps={{ shrink: true }} | ||||
|                 sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} | ||||
|               /> | ||||
|             )} | ||||
|           </Box> | ||||
|  | ||||
|             {/* Sección de Precios por Día con Flexbox */} | ||||
|             <Typography variant="subtitle1" sx={{mt: 2, mb: 1}}>Precios por Día:</Typography> | ||||
|             <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> | ||||
|                 {diasSemana.map(dia => ( | ||||
|                     <Box key={dia} sx={{ flexBasis: { xs: 'calc(50% - 8px)', sm: 'calc(33.33% - 11px)', md: 'calc(25% - 12px)' }, minWidth: '120px' }}> | ||||
|                          {/* El ajuste de -Xpx en flexBasis es aproximado para compensar el gap.  | ||||
|           {/* Sección de Precios por Día con Flexbox */} | ||||
|           <Typography variant="subtitle1" sx={{ mt: 2, mb: 1 }}>Precios por Día:</Typography> | ||||
|           <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> | ||||
|             {diasSemana.map(dia => ( | ||||
|               <Box key={dia} sx={{ flexBasis: { xs: 'calc(50% - 8px)', sm: 'calc(33.33% - 11px)', md: 'calc(25% - 12px)' }, minWidth: '120px' }}> | ||||
|                 {/* El ajuste de -Xpx en flexBasis es aproximado para compensar el gap.  | ||||
|                              Para 3 columnas (33.33%): gap es 16px, se distribuye entre 2 espacios -> 16/2 * 2/3 = ~11px | ||||
|                              Para 4 columnas (25%): gap es 16px, se distribuye entre 3 espacios -> 16/3 * 3/4 = 12px | ||||
|                          */} | ||||
|                         <TextField label={dia} type="number" | ||||
|                             value={preciosDia[dia]} | ||||
|                             onChange={(e) => handlePrecioDiaChange(dia, e.target.value)} | ||||
|                             margin="dense" fullWidth | ||||
|                             error={!!localErrors[dia.toLowerCase()]} helperText={localErrors[dia.toLowerCase()] || ''} | ||||
|                             disabled={loading} | ||||
|                             InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} | ||||
|                             inputProps={{ step: "0.01", lang:"es-AR" }} | ||||
|                         /> | ||||
|                     </Box> | ||||
|                 ))} | ||||
|             </Box> | ||||
|             {localErrors.dias && <Alert severity="error" sx={{width: '100%', mt:1}}>{localErrors.dias}</Alert>} | ||||
|                 <TextField label={dia} type="number" | ||||
|                   value={preciosDia[dia]} | ||||
|                   onChange={(e) => handlePrecioDiaChange(dia, e.target.value)} | ||||
|                   margin="dense" fullWidth | ||||
|                   error={!!localErrors[dia.toLowerCase()]} helperText={localErrors[dia.toLowerCase()] || ''} | ||||
|                   disabled={loading} | ||||
|                   InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} | ||||
|                   inputProps={{ step: "0.01", lang: "es-AR" }} | ||||
|                 /> | ||||
|               </Box> | ||||
|             ))} | ||||
|           </Box> | ||||
|           {localErrors.dias && <Alert severity="error" sx={{ width: '100%', mt: 1 }}>{localErrors.dias}</Alert>} | ||||
|  | ||||
|  | ||||
|           {/* Mostrar error de la API si existe (pasado desde el padre) */} | ||||
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|  | ||||
|           <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // src/components/Modals/Distribucion/RecargoZonaFormModal.tsx | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
| @@ -6,10 +7,10 @@ import { | ||||
| import type { RecargoZonaDto } from '../../../models/dtos/Distribucion/RecargoZonaDto'; | ||||
| import type { CreateRecargoZonaDto } from '../../../models/dtos/Distribucion/CreateRecargoZonaDto'; | ||||
| import type { UpdateRecargoZonaDto } from '../../../models/dtos/Distribucion/UpdateRecargoZonaDto'; | ||||
| import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas | ||||
| import zonaService from '../../../services/Distribucion/zonaService'; // Para cargar zonas | ||||
| import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; | ||||
| import zonaService from '../../../services/Distribucion/zonaService'; | ||||
|  | ||||
| const modalStyle = { /* ... (mismo estilo) ... */ | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
| @@ -28,7 +29,7 @@ interface RecargoZonaFormModalProps { | ||||
|   onClose: () => void; | ||||
|   onSubmit: (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => Promise<void>; | ||||
|   idPublicacion: number; | ||||
|   initialData?: RecargoZonaDto | null; // Para editar | ||||
|   initialData?: RecargoZonaDto | null; | ||||
|   errorMessage?: string | null; | ||||
|   clearErrorMessage: () => void; | ||||
| } | ||||
| @@ -39,12 +40,12 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|   onSubmit, | ||||
|   idPublicacion, | ||||
|   initialData, | ||||
|   errorMessage, | ||||
|   errorMessage, // Esta prop viene del padre (GestionarRecargosPublicacionPage) | ||||
|   clearErrorMessage | ||||
| }) => { | ||||
|   const [idZona, setIdZona] = useState<number | string>(''); | ||||
|   const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd" | ||||
|   const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd" | ||||
|   const [vigenciaD, setVigenciaD] = useState(''); | ||||
|   const [vigenciaH, setVigenciaH] = useState(''); | ||||
|   const [valor, setValor] = useState<string>(''); | ||||
|  | ||||
|   const [zonas, setZonas] = useState<ZonaDto[]>([]); | ||||
| @@ -58,7 +59,7 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|     const fetchZonas = async () => { | ||||
|         setLoadingZonas(true); | ||||
|         try { | ||||
|             const data = await zonaService.getAllZonas(); // Asume que devuelve zonas activas | ||||
|             const data = await zonaService.getAllZonas(); | ||||
|             setZonas(data); | ||||
|         } catch (error) { | ||||
|             console.error("Error al cargar zonas", error); | ||||
| @@ -75,11 +76,13 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|         setVigenciaH(initialData?.vigenciaH || ''); | ||||
|         setValor(initialData?.valor?.toString() || ''); | ||||
|         setLocalErrors({}); | ||||
|         clearErrorMessage(); | ||||
|         // No limpiar errorMessage aquí si queremos que persista un error de submit anterior | ||||
|         // clearErrorMessage(); // Se limpia al abrir desde la página o al inicio de un nuevo submit | ||||
|     } | ||||
|   }, [open, initialData, clearErrorMessage]); | ||||
|   }, [open, initialData]); // Quitar clearErrorMessage de aquí para que no limpie el error del padre en cada re-render por 'open' | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     // ... (lógica de validación sin cambios) | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!idZona) errors.idZona = 'Debe seleccionar una zona.'; | ||||
|     if (!isEditing && !vigenciaD.trim()) { | ||||
| @@ -102,38 +105,46 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|  | ||||
|   const handleInputChange = (fieldName: string) => { | ||||
|     if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|     if (errorMessage) clearErrorMessage(); | ||||
|     if (errorMessage) clearErrorMessage(); // Limpiar error del padre si el usuario modifica un campo | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { | ||||
|     event.preventDefault(); | ||||
|     clearErrorMessage(); | ||||
|     clearErrorMessage(); // Limpiar errores del padre antes de un nuevo intento | ||||
|     if (!validate()) return; | ||||
|  | ||||
|     setLoading(true); | ||||
|     let success = false; // Bandera para controlar el cierre del modal | ||||
|     try { | ||||
|       const valorNum = parseFloat(valor); | ||||
|  | ||||
|       if (isEditing && initialData) { | ||||
|         const dataToSubmit: UpdateRecargoZonaDto = { | ||||
|             valor: valorNum, | ||||
|             vigenciaH: vigenciaH.trim() ? vigenciaH : null, | ||||
|             vigenciaH: vigenciaH.trim() ? vigenciaH.split('T')[0] : null, // Enviar solo fecha o null | ||||
|         }; | ||||
|         await onSubmit(dataToSubmit, initialData.idRecargo); | ||||
|       } else { | ||||
|         const dataToSubmit: CreateRecargoZonaDto = { | ||||
|             idPublicacion, | ||||
|             idZona: Number(idZona), | ||||
|             vigenciaD, | ||||
|             vigenciaD: vigenciaD.split('T')[0], // Enviar solo fecha | ||||
|             valor: valorNum, | ||||
|         }; | ||||
|         await onSubmit(dataToSubmit); | ||||
|       } | ||||
|       onClose(); | ||||
|       success = true; // Marcar como exitoso si no hubo excepciones | ||||
|     } catch (error: any) { | ||||
|       console.error("Error en submit de RecargoZonaFormModal:", error); | ||||
|       // El error de la API ya fue seteado en la página padre (GestionarRecargosPublicacionPage) | ||||
|       // y se pasa a este modal a través de la prop 'errorMessage'. | ||||
|       // No necesitamos hacer setApiErrorMessage aquí. | ||||
|       console.error("Error propagado al submit de RecargoZonaFormModal:", error); | ||||
|       success = false; // Marcar como no exitoso | ||||
|     } finally { | ||||
|        setLoading(false); | ||||
|        if (success) { // Solo cerrar si la operación fue exitosa | ||||
|          onClose(); | ||||
|        } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -148,7 +159,7 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|                 <InputLabel id="zona-recargo-select-label" required>Zona</InputLabel> | ||||
|                 <Select labelId="zona-recargo-select-label" label="Zona" value={idZona} | ||||
|                     onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}} | ||||
|                     disabled={loading || loadingZonas || isEditing} // Zona no se edita | ||||
|                     disabled={loading || loadingZonas || isEditing} | ||||
|                 > | ||||
|                     <MenuItem value="" disabled><em>Seleccione una zona</em></MenuItem> | ||||
|                     {zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))} | ||||
| @@ -175,6 +186,7 @@ const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({ | ||||
|                 inputProps={{ step: "0.01", lang:"es-AR" }} | ||||
|             /> | ||||
|  | ||||
|           {/* Este Alert mostrará el error de la API que viene de la página padre */} | ||||
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|           {localErrors.zonas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.zonas}</Alert>} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user