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,3 +1,4 @@ | ||||
| // src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
| @@ -6,13 +7,14 @@ import { | ||||
| import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; | ||||
| import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto'; | ||||
| import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto'; | ||||
| import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; // Asumiendo que tienes este DTO | ||||
| import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; // Para cargar estados | ||||
| import publicacionService from '../../../services/Distribucion/publicacionService'; // Para cargar publicaciones | ||||
| import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; // Para cargar secciones | ||||
| // --- CAMBIO: Importar PublicacionDropdownDto --- | ||||
| import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; | ||||
| import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; | ||||
| import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; | ||||
| import publicacionService from '../../../services/Distribucion/publicacionService'; | ||||
| import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; | ||||
|  | ||||
| const modalStyle = { /* ... (mismo estilo) ... */ | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
| @@ -26,16 +28,15 @@ const modalStyle = { /* ... (mismo estilo) ... */ | ||||
|     overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| // IDs de estados conocidos (ajusta según tu BD) | ||||
| const ID_ESTADO_EN_USO = 2; | ||||
| const ID_ESTADO_DISPONIBLE = 1; | ||||
| const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente | ||||
| const ID_ESTADO_DANADA = 3; | ||||
| // const ID_ESTADO_DISPONIBLE = 1; // No se cambia a Disponible desde este modal | ||||
|  | ||||
| interface StockBobinaCambioEstadoModalProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>; | ||||
|   bobinaActual: StockBobinaDto | null; // La bobina cuyo estado se va a cambiar | ||||
|   bobinaActual: StockBobinaDto | null; | ||||
|   errorMessage?: string | null; | ||||
|   clearErrorMessage: () => void; | ||||
| } | ||||
| @@ -55,36 +56,47 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|   const [fechaCambioEstado, setFechaCambioEstado] = useState(''); | ||||
|  | ||||
|   const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]); | ||||
|   const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDto[]>([]); | ||||
|   // --- CAMBIO: Usar PublicacionDropdownDto para el estado --- | ||||
|   const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]); | ||||
|   const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]); | ||||
|  | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   useEffect(() => { | ||||
|    useEffect(() => { | ||||
|     const fetchDropdownData = async () => { | ||||
|         if (!bobinaActual) return; | ||||
|         setLoadingDropdowns(true); | ||||
|         try { | ||||
|             const estadosData = await estadoBobinaService.getAllEstadosBobina(); | ||||
|             // Filtrar estados: no se puede volver a "Disponible" o al mismo estado actual desde aquí. | ||||
|             // Y si está "Dañada", no se puede cambiar. | ||||
|             let estadosFiltrados = estadosData.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina && e.idEstadoBobina !== 1); | ||||
|             if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { // Si ya está dañada, no hay más cambios | ||||
|                 estadosFiltrados = []; | ||||
|             } else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { // Si está en uso, solo puede pasar a Dañada | ||||
|                  estadosFiltrados = estadosData.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA); | ||||
|             } | ||||
|             const todosLosEstados = await estadoBobinaService.getAllEstadosBobina(); | ||||
|             let estadosFiltrados: EstadoBobinaDto[]; | ||||
|  | ||||
|             if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { | ||||
|                 estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE); | ||||
|             } else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { | ||||
|                 estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA); | ||||
|             } else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) { | ||||
|                 // --- CAMBIO: Usar ID_ESTADO_EN_USO --- | ||||
|                 estadosFiltrados = todosLosEstados.filter( | ||||
|                     e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA | ||||
|                 ); | ||||
|             } else { | ||||
|                 estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina); | ||||
|             } | ||||
|  | ||||
|             setEstadosDisponibles(estadosFiltrados); | ||||
|  | ||||
|             if (estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO)) { // Solo cargar publicaciones si "En Uso" es una opción | ||||
|                 const publicacionesData = await publicacionService.getAllPublicaciones(undefined, undefined, true); // Solo habilitadas | ||||
|             const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO); | ||||
|  | ||||
|             if (sePuedePonerEnUso) { | ||||
|                 // --- CAMBIO: La data es PublicacionDropdownDto[] --- | ||||
|                 const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true); | ||||
|                 setPublicacionesDisponibles(publicacionesData); | ||||
|             } else { | ||||
|                 setPublicacionesDisponibles([]); | ||||
|                 setIdPublicacion(''); | ||||
|                 setIdSeccion(''); | ||||
|             } | ||||
|  | ||||
|         } catch (error) { | ||||
| @@ -98,42 +110,67 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|     if (open && bobinaActual) { | ||||
|         fetchDropdownData(); | ||||
|         setNuevoEstadoId(''); | ||||
|         setIdPublicacion(''); | ||||
|         setIdSeccion(''); | ||||
|         setObs(bobinaActual.obs || ''); // Pre-cargar obs existente | ||||
|         setFechaCambioEstado(new Date().toISOString().split('T')[0]); // Default a hoy | ||||
|         // Pre-cargar basado en si la bobina actual está "En Uso" | ||||
|         if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { | ||||
|             setIdPublicacion(bobinaActual.idPublicacion?.toString() || ''); | ||||
|             // Solo pre-cargar sección si la publicación también estaba pre-cargada | ||||
|             if (bobinaActual.idPublicacion) { | ||||
|                 setIdSeccion(bobinaActual.idSeccion?.toString() || ''); | ||||
|             } else { | ||||
|                 setIdSeccion(''); | ||||
|             } | ||||
|         } else { | ||||
|             setIdPublicacion(''); | ||||
|             setIdSeccion(''); | ||||
|         } | ||||
|         setObs(bobinaActual.obs || ''); | ||||
|         setFechaCambioEstado(new Date().toISOString().split('T')[0]); | ||||
|         setLocalErrors({}); | ||||
|         clearErrorMessage(); | ||||
|     } | ||||
|   }, [open, bobinaActual, clearErrorMessage]); | ||||
|  | ||||
|  | ||||
|   // Cargar secciones cuando cambia la publicación seleccionada y el estado es "En Uso" | ||||
|   useEffect(() => { | ||||
|     const fetchSecciones = async () => { | ||||
|         if (nuevoEstadoId === ID_ESTADO_EN_USO && idPublicacion) { | ||||
|             setLoadingDropdowns(true); // Podrías tener un loader específico para secciones | ||||
|         if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) { | ||||
|             setLoadingDropdowns(true); | ||||
|             try { | ||||
|                 const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); // Solo activas | ||||
|                 const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); | ||||
|                 setSeccionesDisponibles(data); | ||||
|             } catch (error) { | ||||
|                 console.error("Error al cargar secciones:", error); | ||||
|                 setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'})); | ||||
|                 setSeccionesDisponibles([]); | ||||
|             } finally { | ||||
|                 setLoadingDropdowns(false); | ||||
|             } | ||||
|         } else { | ||||
|             setSeccionesDisponibles([]); // Limpiar secciones si no aplica | ||||
|             setSeccionesDisponibles([]); | ||||
|             // No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace. | ||||
|         } | ||||
|     }; | ||||
|     fetchSecciones(); | ||||
|     if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion | ||||
|         fetchSecciones(); | ||||
|     } else { | ||||
|         setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones | ||||
|     } | ||||
|   }, [nuevoEstadoId, idPublicacion]); | ||||
|  | ||||
|  | ||||
|   // Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso" | ||||
|   useEffect(() => { | ||||
|       if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) { | ||||
|           setIdPublicacion(''); | ||||
|           setIdSeccion(''); | ||||
|       } | ||||
|   }, [nuevoEstadoId]); | ||||
|  | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.'; | ||||
|     if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha de cambio es obligatoria.'; | ||||
|     if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha es obligatoria.'; | ||||
|     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.'; | ||||
|  | ||||
|     if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { | ||||
| @@ -147,11 +184,9 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|   const handleInputChange = (fieldName: string) => { | ||||
|     if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|     if (errorMessage) clearErrorMessage(); | ||||
|     if (fieldName === 'nuevoEstadoId') { // Si cambia el estado, resetear pub/secc | ||||
|         setIdPublicacion(''); | ||||
|         setIdSeccion(''); | ||||
|     } | ||||
|      if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion | ||||
|     // La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId | ||||
|     // y el de sección a un useEffect de idPublicacion | ||||
|     if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion | ||||
|         setIdSeccion(''); | ||||
|     } | ||||
|   }; | ||||
| @@ -163,11 +198,12 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|  | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO; | ||||
|       const dataToSubmit: CambiarEstadoBobinaDto = { | ||||
|           nuevoEstadoId: Number(nuevoEstadoId), | ||||
|           idPublicacion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idPublicacion) : null, | ||||
|           idSeccion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idSeccion) : null, | ||||
|           obs: obs || undefined, | ||||
|           idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null, | ||||
|           idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null, | ||||
|           obs: obs.trim() || null, | ||||
|           fechaCambioEstado, | ||||
|       }; | ||||
|       await onSubmit(bobinaActual.idBobina, dataToSubmit); | ||||
| @@ -192,10 +228,16 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|             <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | ||||
|                 <FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId}> | ||||
|                     <InputLabel id="nuevo-estado-select-label" required>Nuevo Estado</InputLabel> | ||||
|                     <Select labelId="nuevo-estado-select-label" label="Nuevo Estado" value={nuevoEstadoId} | ||||
|                         onChange={(e) => {setNuevoEstadoId(e.target.value as number); handleInputChange('nuevoEstadoId');}} | ||||
|                 <FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required> | ||||
|                     <InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel> | ||||
|                     <Select  | ||||
|                         labelId="nuevo-estado-select-label"  | ||||
|                         label="Nuevo Estado"  | ||||
|                         value={nuevoEstadoId} | ||||
|                         onChange={(e) => { | ||||
|                             setNuevoEstadoId(e.target.value as number | string);  | ||||
|                             handleInputChange('nuevoEstadoId'); | ||||
|                         }} | ||||
|                         disabled={loading || loadingDropdowns || estadosDisponibles.length === 0} | ||||
|                     > | ||||
|                         <MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem> | ||||
| @@ -206,24 +248,24 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|  | ||||
|                 {Number(nuevoEstadoId) === ID_ESTADO_EN_USO && ( | ||||
|                     <> | ||||
|                         <FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion}> | ||||
|                             <InputLabel id="publicacion-estado-select-label" required>Publicación</InputLabel> | ||||
|                         <FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required> | ||||
|                             <InputLabel id="publicacion-estado-select-label">Publicación</InputLabel> | ||||
|                             <Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion} | ||||
|                                 onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}} | ||||
|                                 disabled={loading || loadingDropdowns} | ||||
|                                 disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0} | ||||
|                             > | ||||
|                                 <MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem> | ||||
|                                 {publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))} | ||||
|                             </Select> | ||||
|                              {localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>} | ||||
|                         </FormControl> | ||||
|                         <FormControl fullWidth margin="dense" error={!!localErrors.idSeccion}> | ||||
|                             <InputLabel id="seccion-estado-select-label" required>Sección</InputLabel> | ||||
|                         <FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required> | ||||
|                             <InputLabel id="seccion-estado-select-label">Sección</InputLabel> | ||||
|                             <Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion} | ||||
|                                 onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}} | ||||
|                                 disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0} | ||||
|                             > | ||||
|                                 <MenuItem value="" disabled><em>{idPublicacion ? 'Seleccione sección' : 'Seleccione publicación primero'}</em></MenuItem> | ||||
|                                 <MenuItem value="" disabled><em>{idPublicacion ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : 'Seleccione publicación primero'}</em></MenuItem> | ||||
|                                 {seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))} | ||||
|                             </Select> | ||||
|                              {localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>} | ||||
| @@ -248,7 +290,8 @@ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> | ||||
|  | ||||
|           <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 || loadingDropdowns || estadosDisponibles.length === 0}> | ||||
|             <Button type="submit" variant="contained"  | ||||
|                 disabled={loading || loadingDropdowns || (estadosDisponibles.length === 0 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) }> | ||||
|               {loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'} | ||||
|             </Button> | ||||
|           </Box> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem | ||||
|   Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|   FormControl, InputLabel, Select, MenuItem | ||||
| } from '@mui/material'; | ||||
| import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; | ||||
| import type { UpdateStockBobinaDto } from '../../../models/dtos/Impresion/UpdateStockBobinaDto'; | ||||
| @@ -11,17 +11,17 @@ import tipoBobinaService from '../../../services/Impresion/tipoBobinaService'; | ||||
| import plantaService from '../../../services/Impresion/plantaService'; | ||||
|  | ||||
| const modalStyle = { /* ... (mismo estilo que StockBobinaIngresoFormModal) ... */ | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '90%', sm: 550 }, | ||||
|     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: '90%', sm: 550 }, | ||||
|   bgcolor: 'background.paper', | ||||
|   border: '2px solid #000', | ||||
|   boxShadow: 24, | ||||
|   p: 4, | ||||
|   maxHeight: '90vh', | ||||
|   overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| interface StockBobinaEditFormModalProps { | ||||
| @@ -56,34 +56,43 @@ const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({ | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchDropdownData = async () => { | ||||
|         setLoadingDropdowns(true); | ||||
|         try { | ||||
|             const [tiposData, plantasData] = await Promise.all([ | ||||
|                 tipoBobinaService.getAllTiposBobina(), | ||||
|                 plantaService.getAllPlantas() | ||||
|             ]); | ||||
|             setTiposBobina(tiposData); | ||||
|             setPlantas(plantasData); | ||||
|         } catch (error) { | ||||
|             console.error("Error al cargar datos para dropdowns (StockBobina Edit)", error); | ||||
|             setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'})); | ||||
|         } finally { | ||||
|             setLoadingDropdowns(false); | ||||
|         } | ||||
|       setLoadingDropdowns(true); // Ya está arriba, pero por claridad | ||||
|       try { | ||||
|         // --- CAMBIO: Usar los servicios que devuelven DTOs de dropdown si los tienes --- | ||||
|         const [tiposData, plantasData] = await Promise.all([ | ||||
|           tipoBobinaService.getAllTiposBobina(), // Asume que existe y devuelve TipoBobinaDropdownDto[] | ||||
|           plantaService.getAllPlantas()        // Asume que existe y devuelve PlantaDropdownDto[] | ||||
|         ]); | ||||
|         setTiposBobina(tiposData); | ||||
|         setPlantas(plantasData); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar datos para dropdowns (StockBobina Edit)", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar tipos/plantas.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (open && initialData) { | ||||
|         fetchDropdownData(); | ||||
|         setIdTipoBobina(initialData.idTipoBobina || ''); | ||||
|         setNroBobina(initialData.nroBobina || ''); | ||||
|         setPeso(initialData.peso?.toString() || ''); | ||||
|         setIdPlanta(initialData.idPlanta || ''); | ||||
|         setRemito(initialData.remito || ''); | ||||
|         setFechaRemito(initialData.fechaRemito || ''); // Asume yyyy-MM-dd del DTO | ||||
|         setLocalErrors({}); | ||||
|         clearErrorMessage(); | ||||
|     if (open) { // Solo fetchear si el modal está abierto | ||||
|       fetchDropdownData(); | ||||
|     } | ||||
|   }, [open, initialData, clearErrorMessage]); | ||||
|   }, [open]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (open && initialData && !loadingDropdowns) { // <<--- ESPERAR A QUE loadingDropdowns SEA FALSE | ||||
|       setIdTipoBobina(initialData.idTipoBobina || ''); | ||||
|       setNroBobina(initialData.nroBobina || ''); | ||||
|       setPeso(initialData.peso?.toString() || ''); | ||||
|       setIdPlanta(initialData.idPlanta || ''); | ||||
|       setRemito(initialData.remito || ''); | ||||
|       setFechaRemito(initialData.fechaRemito ? initialData.fechaRemito.split('T')[0] : ''); // Formatear si es necesario | ||||
|       setLocalErrors({}); | ||||
|       clearErrorMessage(); | ||||
|     } else if (open && !initialData) { // Si se abre para crear (aunque este modal es de edición) | ||||
|       // Resetear o manejar como prefieras | ||||
|       setIdTipoBobina(''); setNroBobina(''); setPeso(''); setIdPlanta(''); | ||||
|       setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]); | ||||
|     } | ||||
|   }, [open, initialData, loadingDropdowns, clearErrorMessage]); // Añadir loadingDropdowns | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
| @@ -111,12 +120,12 @@ const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({ | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       const dataToSubmit: UpdateStockBobinaDto = { | ||||
|           idTipoBobina: Number(idTipoBobina), | ||||
|           nroBobina, | ||||
|           peso: parseInt(peso, 10), | ||||
|           idPlanta: Number(idPlanta), | ||||
|           remito, | ||||
|           fechaRemito, | ||||
|         idTipoBobina: Number(idTipoBobina), | ||||
|         nroBobina, | ||||
|         peso: parseInt(peso, 10), | ||||
|         idPlanta: Number(idPlanta), | ||||
|         remito, | ||||
|         fechaRemito, | ||||
|       }; | ||||
|       await onSubmit(initialData.idBobina, dataToSubmit); | ||||
|       onClose(); | ||||
| @@ -124,80 +133,91 @@ const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({ | ||||
|       console.error("Error en submit de StockBobinaEditFormModal:", error); | ||||
|       // El error de API lo maneja la página que llama a este modal | ||||
|     } finally { | ||||
|        setLoading(false); | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (!initialData) return null; // No renderizar si no hay datos iniciales (aunque open lo controla) | ||||
|   if (!initialData && open) { // Si se abre sin initialData (no debería pasar para un modal de EDICIÓN) | ||||
|     return <Modal open={open} onClose={onClose}><Box sx={modalStyle}><Alert severity="error">Error: No se proporcionaron datos iniciales para editar.</Alert></Box></Modal>; | ||||
|   } | ||||
|   if (!open) return null; // No renderizar nada si no está abierto | ||||
|  | ||||
|   return ( | ||||
|     <Modal open={open} onClose={onClose}> | ||||
|       <Box sx={modalStyle}> | ||||
|         <Typography variant="h6" component="h2" gutterBottom> | ||||
|             Editar Datos de Bobina (ID: {initialData.idBobina}) | ||||
|           Editar Datos de Bobina (ID: {initialData?.idBobina || 'N/A'}) {/* Usar initialData?.idBobina */} | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|              <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                     <FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}> | ||||
|                         <InputLabel id="edit-tipo-bobina-select-label" required>Tipo Bobina</InputLabel> | ||||
|                         <Select labelId="edit-tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina} | ||||
|                             onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}} | ||||
|                             disabled={loading || loadingDropdowns} | ||||
|                         > | ||||
|                             <MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem> | ||||
|                             {tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))} | ||||
|                         </Select> | ||||
|                         {localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>} | ||||
|                     </FormControl> | ||||
|                     <TextField label="Nro. Bobina" value={nroBobina} required | ||||
|                         onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''} | ||||
|                         disabled={loading} sx={{flex:1, minWidth: '200px'}} autoFocus | ||||
|                     /> | ||||
|                 </Box> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                     <TextField label="Peso (Kg)" type="number" value={peso} required | ||||
|                         onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''} | ||||
|                         disabled={loading} sx={{flex:1, minWidth: '150px'}} | ||||
|                     /> | ||||
|                     <FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}> | ||||
|                         <InputLabel id="edit-planta-select-label" required>Planta Destino</InputLabel> | ||||
|                         <Select labelId="edit-planta-select-label" label="Planta Destino" value={idPlanta} | ||||
|                             onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}} | ||||
|                             disabled={loading || loadingDropdowns} | ||||
|                         > | ||||
|                             <MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem> | ||||
|                             {plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))} | ||||
|                         </Select> | ||||
|                         {localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>} | ||||
|                     </FormControl> | ||||
|                 </Box> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                     <TextField label="Nro. Remito" value={remito} required | ||||
|                         onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} | ||||
|                         disabled={loading} sx={{flex:1, minWidth: '200px'}} | ||||
|                     /> | ||||
|                     <TextField label="Fecha Remito" type="date" value={fechaRemito} required | ||||
|                         onChange={(e) => {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''} | ||||
|                         disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}} | ||||
|                     /> | ||||
|                 </Box> | ||||
|         {loadingDropdowns ? <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box> : ( | ||||
|           <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|             <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | ||||
|               <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                 <FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{ flex: 1, minWidth: '200px' }}> | ||||
|                   <InputLabel id="edit-tipo-bobina-select-label" required>Tipo Bobina</InputLabel> | ||||
|                   <Select | ||||
|                     labelId="edit-tipo-bobina-select-label" | ||||
|                     label="Tipo Bobina" | ||||
|                     value={idTipoBobina} // MUI Select maneja bien number/string si los values de MenuItem son consistentes | ||||
|                     onChange={(e) => { setIdTipoBobina(e.target.value as number | string); handleInputChange('idTipoBobina'); }} | ||||
|                     disabled={loading || loadingDropdowns} | ||||
|                   > | ||||
|                     <MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem> | ||||
|                     {tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))} | ||||
|                   </Select> | ||||
|                   {localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>} | ||||
|                 </FormControl> | ||||
|                 <TextField label="Nro. Bobina" value={nroBobina} required | ||||
|                   onChange={(e) => { setNroBobina(e.target.value); handleInputChange('nroBobina'); }} | ||||
|                   margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''} | ||||
|                   disabled={loading} sx={{ flex: 1, minWidth: '200px' }} autoFocus | ||||
|                 /> | ||||
|               </Box> | ||||
|               <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                 <TextField label="Peso (Kg)" type="number" value={peso} required | ||||
|                   onChange={(e) => { setPeso(e.target.value); handleInputChange('peso'); }} | ||||
|                   margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''} | ||||
|                   disabled={loading} sx={{ flex: 1, minWidth: '150px' }} | ||||
|                 /> | ||||
|                 <FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{ flex: 1, minWidth: '200px' }}> | ||||
|                   <InputLabel id="edit-planta-select-label" required>Planta Destino</InputLabel> | ||||
|                   <Select | ||||
|                     labelId="edit-planta-select-label" | ||||
|                     label="Planta Destino" | ||||
|                     value={idPlanta} | ||||
|                     onChange={(e) => { setIdPlanta(e.target.value as number | string); handleInputChange('idPlanta'); }} | ||||
|                     disabled={loading || loadingDropdowns} | ||||
|                   > | ||||
|                     <MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem> | ||||
|                     {plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))} | ||||
|                   </Select> | ||||
|                   {localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>} | ||||
|                 </FormControl> | ||||
|               </Box> | ||||
|               <Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}> | ||||
|                 <TextField label="Nro. Remito" value={remito} required | ||||
|                   onChange={(e) => { setRemito(e.target.value); handleInputChange('remito'); }} | ||||
|                   margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} | ||||
|                   disabled={loading} sx={{ flex: 1, minWidth: '200px' }} | ||||
|                 /> | ||||
|                 <TextField label="Fecha Remito" type="date" value={fechaRemito} required | ||||
|                   onChange={(e) => { setFechaRemito(e.target.value); handleInputChange('fechaRemito'); }} | ||||
|                   margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''} | ||||
|                   disabled={loading} InputLabelProps={{ shrink: true }} sx={{ flex: 1, minWidth: '200px' }} | ||||
|                 /> | ||||
|               </Box> | ||||
|             </Box> | ||||
|  | ||||
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|           {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|             {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|             {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</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 || loadingDropdowns}> | ||||
|               {loading ? <CircularProgress size={24} /> : 'Guardar Cambios'} | ||||
|             </Button> | ||||
|             <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 || loadingDropdowns}> | ||||
|                 {loading ? <CircularProgress size={24} /> : 'Guardar Cambios'} | ||||
|               </Button> | ||||
|             </Box> | ||||
|           </Box> | ||||
|         </Box> | ||||
|         )} | ||||
|       </Box> | ||||
|     </Modal> | ||||
|   ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user