Finalización de Reportes y arreglos varios de controles y comportamientos...
This commit is contained in:
		| @@ -162,7 +162,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> | ||||
|                     margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} | ||||
|                     disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} | ||||
|                 /> | ||||
|                 <TextField label="Entrada (Devolución Total)" type="number" value={entrada} required | ||||
|                 <TextField label="Entrada (Por Remito)" type="number" value={entrada} required | ||||
|                     onChange={(e) => {setEntrada(e.target.value); handleInputChange('entrada');}} | ||||
|                     margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''} | ||||
|                     disabled={loading} inputProps={{min:0}} | ||||
| @@ -172,7 +172,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> | ||||
|                     margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''} | ||||
|                     disabled={loading} inputProps={{min:0}} | ||||
|                 /> | ||||
|                  <TextField label="Sin Cargo" type="number" value={sinCargo} required | ||||
|                  <TextField label="Ejemplares Sin Cargo" type="number" value={sinCargo} required | ||||
|                     onChange={(e) => {setSinCargo(e.target.value); handleInputChange('sinCargo');}} | ||||
|                     margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''} | ||||
|                     disabled={loading} inputProps={{min:0}} | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| // src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText | ||||
|   Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|   FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||||
|  | ||||
| import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; | ||||
| import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; | ||||
| import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | ||||
| @@ -19,17 +18,17 @@ import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribuc | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '95%', sm: '80%', md: '750px' }, | ||||
|     bgcolor: 'background.paper', | ||||
|     border: '2px solid #000', | ||||
|     boxShadow: 24, | ||||
|     p: 2.5, | ||||
|     maxHeight: '90vh', | ||||
|     overflowY: 'auto' | ||||
|   position: 'absolute' as 'absolute', | ||||
|   top: '50%', | ||||
|   left: '50%', | ||||
|   transform: 'translate(-50%, -50%)', | ||||
|   width: { xs: '95%', sm: '80%', md: '750px' }, | ||||
|   bgcolor: 'background.paper', | ||||
|   border: '2px solid #000', | ||||
|   boxShadow: 24, | ||||
|   p: 2.5, | ||||
|   maxHeight: '90vh', | ||||
|   overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| interface EntradaSalidaCanillaFormModalProps { | ||||
| @@ -51,8 +50,8 @@ interface FormRowItem { | ||||
|  | ||||
| const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({ | ||||
|   open, | ||||
|   onClose, | ||||
|   onSubmit, | ||||
|   onClose, // Este onClose es el que se pasa desde GestionarEntradasSalidasCanillaPage | ||||
|   onSubmit, // Este onSubmit es el que se pasa para la lógica de EDICIÓN | ||||
|   initialData, | ||||
|   errorMessage: parentErrorMessage, | ||||
|   clearErrorMessage | ||||
| @@ -63,16 +62,18 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|   const [editCantSalida, setEditCantSalida] = useState<string>('0'); | ||||
|   const [editCantEntrada, setEditCantEntrada] = useState<string>('0'); | ||||
|   const [editObservacion, setEditObservacion] = useState(''); | ||||
|   const [items, setItems] = useState<FormRowItem[]>([]); | ||||
|   const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); // Iniciar con una fila | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [loading, setLoading] = useState(false); // Loading para submit | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs | ||||
|   const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|   const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null); | ||||
|  | ||||
|   const isEditing = Boolean(initialData); | ||||
|  | ||||
|   // Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true | ||||
|   useEffect(() => { | ||||
|     const fetchDropdownData = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
| @@ -94,6 +95,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|  | ||||
|     if (open) { | ||||
|       fetchDropdownData(); | ||||
|     } | ||||
|   }, [open]); | ||||
|  | ||||
|  | ||||
|   // Efecto para inicializar el formulario cuando se abre o cambia initialData | ||||
|   useEffect(() => { | ||||
|     if (open) { | ||||
|       clearErrorMessage(); | ||||
|       setModalSpecificApiError(null); | ||||
|       setLocalErrors({}); | ||||
| @@ -105,19 +113,53 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|         setEditCantSalida(initialData.cantSalida?.toString() || '0'); | ||||
|         setEditCantEntrada(initialData.cantEntrada?.toString() || '0'); | ||||
|         setEditObservacion(initialData.observacion || ''); | ||||
|         setItems([]); | ||||
|         setItems([]); // En modo edición, no pre-cargamos items de la lista | ||||
|       } else { | ||||
|         setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|         // Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items | ||||
|         setIdCanilla(''); | ||||
|         setFecha(new Date().toISOString().split('T')[0]); | ||||
|         setEditCantSalida('0'); | ||||
|         setEditCantEntrada('0'); | ||||
|         setEditObservacion(''); | ||||
|         setEditIdPublicacion(''); | ||||
|         setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto | ||||
|         // Los items se cargarán por el siguiente useEffect basado en la fecha | ||||
|       } | ||||
|     } | ||||
|   }, [open, initialData, isEditing, clearErrorMessage]); | ||||
|  | ||||
|  | ||||
|   // Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO) | ||||
|   // y cuando las publicaciones están disponibles. | ||||
|   useEffect(() => { | ||||
|     if (open && !isEditing && publicaciones.length > 0 && fecha) { // Asegurarse que 'fecha' tiene un valor | ||||
|       const diaSemana = new Date(fecha + 'T00:00:00Z').getUTCDay(); // Usar UTC para getDay consistente | ||||
|       setLoadingItems(true); // Indicador de carga para los items | ||||
|       setLocalErrors(prev => ({ ...prev, general: null })); | ||||
|  | ||||
|       publicacionService.getPublicacionesPorDiaSemana(diaSemana) | ||||
|         .then(pubsPorDefecto => { | ||||
|           if (pubsPorDefecto.length > 0) { | ||||
|             const itemsPorDefecto = pubsPorDefecto.map(pub => ({ | ||||
|               id: `${Date.now().toString()}-${pub.idPublicacion}`, | ||||
|               idPublicacion: pub.idPublicacion, | ||||
|               cantSalida: '0', | ||||
|               cantEntrada: '0', | ||||
|               observacion: '' | ||||
|             })); | ||||
|             setItems(itemsPorDefecto); | ||||
|           } else { | ||||
|             // Si no hay configuraciones para el día, iniciar con una fila vacía | ||||
|             setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|           } | ||||
|         }) | ||||
|         .catch(err => { | ||||
|           console.error("Error al cargar/recargar publicaciones por defecto para el día:", err); | ||||
|           setLocalErrors(prev => ({ ...prev, general: 'Error al pre-cargar publicaciones del día.' })); | ||||
|           setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|         }) | ||||
|         .finally(() => setLoadingItems(false)); | ||||
|     } else if (open && !isEditing && publicaciones.length === 0 && !loadingDropdowns) { | ||||
|       // Si las publicaciones aún no se cargaron pero los dropdowns terminaron de cargar, iniciar con 1 item vacío. | ||||
|       setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|     } | ||||
|   }, [open, isEditing, fecha, publicaciones, loadingDropdowns]); // Dependencias clave | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const currentErrors: { [key: string]: string | null } = {}; | ||||
|     if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; | ||||
| @@ -125,65 +167,65 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|  | ||||
|     if (isEditing) { | ||||
|         const salidaNum = parseInt(editCantSalida, 10); | ||||
|         const entradaNum = parseInt(editCantEntrada, 10); | ||||
|         if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||
|             currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.'; | ||||
|         } | ||||
|         if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||
|             currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.'; | ||||
|         } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||
|             currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; | ||||
|         } | ||||
|       const salidaNum = parseInt(editCantSalida, 10); | ||||
|       const entradaNum = parseInt(editCantEntrada, 10); | ||||
|       if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||
|         currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.'; | ||||
|       } | ||||
|       if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||
|         currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.'; | ||||
|       } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||
|         currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; | ||||
|       } | ||||
|     } else { | ||||
|         let hasValidItemWithQuantityOrPub = false; | ||||
|         const publicacionIdsEnLote = new Set<number>(); | ||||
|       let hasValidItemWithQuantityOrPub = false; | ||||
|       const publicacionIdsEnLote = new Set<number>(); | ||||
|  | ||||
|         if (items.length === 0) { | ||||
|             currentErrors.general = "Debe agregar al menos una publicación."; | ||||
|       if (items.length === 0) { | ||||
|         currentErrors.general = "Debe agregar al menos una publicación."; | ||||
|       } | ||||
|  | ||||
|       items.forEach((item, index) => { | ||||
|         const salidaNum = parseInt(item.cantSalida, 10); | ||||
|         const entradaNum = parseInt(item.cantEntrada, 10); | ||||
|         const hasQuantity = !isNaN(salidaNum) && salidaNum >= 0 && !isNaN(entradaNum) && entradaNum >= 0 && (salidaNum > 0 || entradaNum > 0); | ||||
|         const hasObservation = item.observacion.trim() !== ''; | ||||
|  | ||||
|         if (item.idPublicacion === '') { | ||||
|           if (hasQuantity || hasObservation) { | ||||
|             currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`; | ||||
|           } | ||||
|         } else { | ||||
|           const pubIdNum = Number(item.idPublicacion); | ||||
|           if (publicacionIdsEnLote.has(pubIdNum)) { | ||||
|             currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; | ||||
|           } else { | ||||
|             publicacionIdsEnLote.add(pubIdNum); | ||||
|           } | ||||
|           if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||
|             currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; | ||||
|           } | ||||
|           if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||
|             currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`; | ||||
|           } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||
|             currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`; | ||||
|           } | ||||
|           if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true; | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|         items.forEach((item, index) => { | ||||
|             const salidaNum = parseInt(item.cantSalida, 10); | ||||
|             const entradaNum = parseInt(item.cantEntrada, 10); | ||||
|             const hasQuantity = !isNaN(salidaNum) && salidaNum >=0 && !isNaN(entradaNum) && entradaNum >=0 && (salidaNum > 0 || entradaNum > 0); | ||||
|             const hasObservation = item.observacion.trim() !== ''; | ||||
|       const allItemsAreEmptyAndNoPubSelected = items.every( | ||||
|         itm => itm.idPublicacion === '' && | ||||
|           (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && | ||||
|           (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && | ||||
|           itm.observacion.trim() === '' | ||||
|       ); | ||||
|  | ||||
|             if (item.idPublicacion === '') { | ||||
|                 if (hasQuantity || hasObservation) { | ||||
|                     currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`; | ||||
|                 } | ||||
|             } else { | ||||
|                 const pubIdNum = Number(item.idPublicacion); | ||||
|                 if (publicacionIdsEnLote.has(pubIdNum)) { | ||||
|                     currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; | ||||
|                 } else { | ||||
|                     publicacionIdsEnLote.add(pubIdNum); | ||||
|                 } | ||||
|                  if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||
|                     currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; | ||||
|                 } | ||||
|                 if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||
|                     currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`; | ||||
|                 } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||
|                     currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`; | ||||
|                 } | ||||
|                 if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true; | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         const allItemsAreEmptyAndNoPubSelected = items.every( | ||||
|           itm => itm.idPublicacion === '' &&  | ||||
|                  (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && | ||||
|                  (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && | ||||
|                  itm.observacion.trim() === '' | ||||
|         ); | ||||
|  | ||||
|         if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { | ||||
|              currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; | ||||
|         } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida,10) > 0 || parseInt(i.cantEntrada,10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { | ||||
|             currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; | ||||
|         } | ||||
|       if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { | ||||
|         currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; | ||||
|       } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { | ||||
|         currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; | ||||
|       } | ||||
|     } | ||||
|     setLocalErrors(currentErrors); | ||||
|     return Object.keys(currentErrors).length === 0; | ||||
| @@ -200,7 +242,6 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|     clearErrorMessage(); | ||||
|     setModalSpecificApiError(null); | ||||
|     if (!validate()) return; | ||||
|  | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       if (isEditing && initialData) { | ||||
| @@ -211,12 +252,16 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           cantEntrada: entradaNum, | ||||
|           observacion: editObservacion.trim() || undefined, | ||||
|         }; | ||||
|         // Aquí se llama al onSubmit que viene de la página padre (GestionarEntradasSalidasCanillaPage) | ||||
|         // para la lógica de actualización. | ||||
|         await onSubmit(dataToSubmitSingle, initialData.idParte); | ||||
|         onClose(); // Cerrar el modal DESPUÉS de un submit de edición exitoso | ||||
|       } else { | ||||
|         // Lógica de creación BULK (se maneja internamente en el modal) | ||||
|         const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items | ||||
|           .filter(item => | ||||
|             item.idPublicacion !== '' && | ||||
|             ( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida,10) > 0 || parseInt(item.cantEntrada,10) > 0 ) || item.observacion.trim() !== '') | ||||
|             item.idPublicacion && Number(item.idPublicacion) > 0 && | ||||
|             ((parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '') | ||||
|           ) | ||||
|           .map(item => ({ | ||||
|             idPublicacion: Number(item.idPublicacion), | ||||
| @@ -226,7 +271,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           })); | ||||
|  | ||||
|         if (itemsToSubmit.length === 0) { | ||||
|           setLocalErrors(prev => ({...prev, general: "No hay movimientos válidos para registrar. Asegúrese de seleccionar una publicación y/o ingresar cantidades."})); | ||||
|           setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." })); | ||||
|           setLoading(false); | ||||
|           return; | ||||
|         } | ||||
| @@ -237,8 +282,9 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           items: itemsToSubmit, | ||||
|         }; | ||||
|         await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); | ||||
|         onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso | ||||
|       } | ||||
|       onClose(); | ||||
|       // onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito | ||||
|     } catch (error: any) { | ||||
|       console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); | ||||
|       if (axios.isAxiosError(error) && error.response) { | ||||
| @@ -246,7 +292,10 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|       } else { | ||||
|         setModalSpecificApiError('Ocurrió un error inesperado.'); | ||||
|       } | ||||
|       if (isEditing) throw error; | ||||
|       // NO llamar a onClose() aquí si hubo un error, para que el modal permanezca abierto | ||||
|       // y muestre el modalSpecificApiError. | ||||
|       // Si la edición (que usa el 'onSubmit' del padre) lanza un error, ese error se propagará | ||||
|       // al padre y el padre decidirá si el modal se cierra o no (actualmente no lo cierra). | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
| @@ -254,8 +303,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|  | ||||
|   const handleAddRow = () => { | ||||
|     if (items.length >= publicaciones.length) { | ||||
|         setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); | ||||
|         return; | ||||
|       setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); | ||||
|       return; | ||||
|     } | ||||
|     setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|     setLocalErrors(prev => ({ ...prev, general: null })); | ||||
| @@ -300,27 +349,27 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|               onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }} | ||||
|               margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} | ||||
|               disabled={loading || isEditing} InputLabelProps={{ shrink: true }} | ||||
|               autoFocus={!isEditing} | ||||
|               autoFocus={!isEditing && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado | ||||
|             /> | ||||
|  | ||||
|             {isEditing && initialData && ( | ||||
|               <Paper elevation={1} sx={{ p: 1.5, mt: 1 }}> | ||||
|                  <Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p=>p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography> | ||||
|                  <Box sx={{ display: 'flex', gap: 2, mt:0.5}}> | ||||
|                     <TextField label="Cant. Salida" type="number" value={editCantSalida} | ||||
|                         onChange={(e) => {setEditCantSalida(e.target.value); handleInputChange('editCantSalida');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} | ||||
|                         disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} | ||||
|                     /> | ||||
|                     <TextField label="Cant. Entrada" type="number" value={editCantEntrada} | ||||
|                         onChange={(e) => {setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} | ||||
|                         disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} | ||||
|                     /> | ||||
|                 <Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, mt: 0.5 }}> | ||||
|                   <TextField label="Cant. Salida" type="number" value={editCantSalida} | ||||
|                     onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }} | ||||
|                     margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} | ||||
|                     disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} | ||||
|                   /> | ||||
|                   <TextField label="Cant. Entrada" type="number" value={editCantEntrada} | ||||
|                     onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }} | ||||
|                     margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} | ||||
|                     disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} | ||||
|                   /> | ||||
|                 </Box> | ||||
|                 <TextField label="Observación (General)" value={editObservacion} | ||||
|                     onChange={(e) => setEditObservacion(e.target.value)} | ||||
|                     margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}} | ||||
|                   onChange={(e) => setEditObservacion(e.target.value)} | ||||
|                   margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }} | ||||
|                 /> | ||||
|               </Paper> | ||||
|             )} | ||||
| @@ -328,47 +377,141 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|             {!isEditing && ( | ||||
|               <Box> | ||||
|                 <Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography> | ||||
|                 {items.map((itemRow, index) => ( // item renombrado a itemRow | ||||
|                   <Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, position: 'relative' }}> | ||||
|                      {items.length > 1 && ( | ||||
|                         <IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" size="small" | ||||
|                             sx={{ position: 'absolute', top: 4, right: 4, zIndex:1 }} | ||||
|                             aria-label="Quitar fila" | ||||
|                 {/* Indicador de carga para los items */} | ||||
|                 {loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>} | ||||
|                 {!loadingItems && items.map((itemRow, index) => ( | ||||
|                   <Paper | ||||
|                     key={itemRow.id} | ||||
|                     elevation={1} | ||||
|                     sx={{ | ||||
|                       p: 1.5, | ||||
|                       mb: 1, | ||||
|                     }} | ||||
|                   > | ||||
|                     {/* Nivel 1: contenedor “padre” sin wrap */} | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         display: 'flex', | ||||
|                         alignItems: 'center', // centra ícono + campos | ||||
|                         gap: 1, | ||||
|                         // NOTA: aquí NO ponemos flexWrap, por defecto es 'nowrap' | ||||
|                       }} | ||||
|                     > | ||||
|                       {/* Nivel 2: contenedor que agrupa solo los campos y sí puede hacer wrap */} | ||||
|                       <Box | ||||
|                         sx={{ | ||||
|                           display: 'flex', | ||||
|                           alignItems: 'center', | ||||
|                           gap: 1, | ||||
|                           flexWrap: 'wrap',        // los campos sí hacen wrap si no caben | ||||
|                           flexGrow: 1,             // ocupa todo el espacio disponible antes del ícono | ||||
|                         }} | ||||
|                       > | ||||
|                         <FormControl | ||||
|                           sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }} | ||||
|                           size="small" | ||||
|                           error={!!localErrors[`item_${itemRow.id}_idPublicacion`]} | ||||
|                         > | ||||
|                             <DeleteIcon fontSize="inherit" /> | ||||
|                           <InputLabel | ||||
|                             required={ | ||||
|                               parseInt(itemRow.cantSalida) > 0 || | ||||
|                               parseInt(itemRow.cantEntrada) > 0 || | ||||
|                               itemRow.observacion.trim() !== '' | ||||
|                             } | ||||
|                           > | ||||
|                             Pub. {index + 1} | ||||
|                           </InputLabel> | ||||
|                           <Select | ||||
|                             value={itemRow.idPublicacion} | ||||
|                             label={`Publicación ${index + 1}`} | ||||
|                             onChange={(e) => | ||||
|                               handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number) | ||||
|                             } | ||||
|                             disabled={loading || loadingDropdowns} | ||||
|                             sx={{ minWidth: 0 }} // permite que shrink si hace falta | ||||
|                           > | ||||
|                             <MenuItem value="" disabled> | ||||
|                               <em>Seleccione</em> | ||||
|                             </MenuItem> | ||||
|                             {publicaciones.map((p) => ( | ||||
|                               <MenuItem key={p.idPublicacion} value={p.idPublicacion}> | ||||
|                                 {p.nombre} | ||||
|                               </MenuItem> | ||||
|                             ))} | ||||
|                           </Select> | ||||
|                           {localErrors[`item_${itemRow.id}_idPublicacion`] && ( | ||||
|                             <FormHelperText> | ||||
|                               {localErrors[`item_${itemRow.id}_idPublicacion`]} | ||||
|                             </FormHelperText> | ||||
|                           )} | ||||
|                         </FormControl> | ||||
|  | ||||
|                         <TextField | ||||
|                           label="Llevados" | ||||
|                           type="number" | ||||
|                           size="small" | ||||
|                           value={itemRow.cantSalida} | ||||
|                           onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)} | ||||
|                           error={!!localErrors[`item_${itemRow.id}_cantSalida`]} | ||||
|                           helperText={localErrors[`item_${itemRow.id}_cantSalida`]} | ||||
|                           inputProps={{ min: 0 }} | ||||
|                           sx={{ | ||||
|                             flexBasis: 'calc(15% - 8px)', | ||||
|                             minWidth: '80px', | ||||
|                             minHeight: 0, | ||||
|                           }} | ||||
|                         /> | ||||
|  | ||||
|                         <TextField | ||||
|                           label="Devueltos" | ||||
|                           type="number" | ||||
|                           size="small" | ||||
|                           value={itemRow.cantEntrada} | ||||
|                           onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} | ||||
|                           error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} | ||||
|                           helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} | ||||
|                           inputProps={{ min: 0 }} | ||||
|                           sx={{ | ||||
|                             flexBasis: 'calc(15% - 8px)', | ||||
|                             minWidth: '80px', | ||||
|                             minHeight: 0, | ||||
|                           }} | ||||
|                         /> | ||||
|  | ||||
|                         <TextField | ||||
|                           label="Obs." | ||||
|                           value={itemRow.observacion} | ||||
|                           onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)} | ||||
|                           size="small" | ||||
|                           sx={{ | ||||
|                             flexGrow: 1, | ||||
|                             flexBasis: 'calc(25% - 8px)', | ||||
|                             minWidth: '120px', | ||||
|                             minHeight: 0, | ||||
|                           }} | ||||
|                           multiline | ||||
|                           maxRows={1} | ||||
|                         /> | ||||
|                       </Box> | ||||
|  | ||||
|                       {/* Ícono de eliminar: siempre en la misma línea */} | ||||
|                       {items.length > 1 && ( | ||||
|                         <IconButton | ||||
|                           onClick={() => handleRemoveRow(itemRow.id)} | ||||
|                           color="error" | ||||
|                           aria-label="Quitar fila" | ||||
|                           sx={{ | ||||
|                             alignSelf: 'center', // mantén centrado verticalmente | ||||
|                             // No necesita flexShrink, porque el padre no hace wrap | ||||
|                           }} | ||||
|                         > | ||||
|                           <DeleteIcon fontSize="medium" /> | ||||
|                         </IconButton> | ||||
|                     )} | ||||
|                     <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}> | ||||
|                       <FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow:1 }} size="small" error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}> | ||||
|                         <InputLabel required={parseInt(itemRow.cantSalida) > 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== ''}>Pub. {index + 1}</InputLabel> | ||||
|                         <Select value={itemRow.idPublicacion} label={`Publicación ${index + 1}`} | ||||
|                           onChange={(e) => handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)} | ||||
|                           disabled={loading || loadingDropdowns} | ||||
|                         > | ||||
|                           <MenuItem value="" disabled><em>Seleccione</em></MenuItem> | ||||
|                           {publicaciones.map((p) => ( | ||||
|                             <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem> | ||||
|                           ))} | ||||
|                         </Select> | ||||
|                         {localErrors[`item_${itemRow.id}_idPublicacion`] && <FormHelperText>{localErrors[`item_${itemRow.id}_idPublicacion`]}</FormHelperText>} | ||||
|                       </FormControl> | ||||
|                       <TextField label="Llevados" type="number" size="small" value={itemRow.cantSalida} | ||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)} | ||||
|                         error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]} | ||||
|                         inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} | ||||
|                       /> | ||||
|                       <TextField label="Devueltos" type="number" size="small" value={itemRow.cantEntrada} | ||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} | ||||
|                         error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} | ||||
|                         inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} | ||||
|                       /> | ||||
|                        <TextField label="Obs." value={itemRow.observacion} | ||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)} | ||||
|                         size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px' }} multiline maxRows={1} | ||||
|                       /> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|                 ))} | ||||
|  | ||||
|                 {localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>} | ||||
|                 <Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}> | ||||
|                   Agregar Publicación | ||||
|   | ||||
| @@ -0,0 +1,141 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, Button, CircularProgress, Alert, FormGroup, FormControlLabel, Checkbox, Paper | ||||
| } from '@mui/material'; | ||||
| import publicacionService from '../../../services/Distribucion/publicacionService'; | ||||
| import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | ||||
| //import type { PublicacionDiaSemanaDto } from '../../../models/dtos/Distribucion/PublicacionDiaSemanaDto'; | ||||
| import type { UpdatePublicacionDiasSemanaRequestDto } from '../../../models/dtos/Distribucion/UpdatePublicacionDiasSemanaRequestDto'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '90%', sm: '70%', md: '500px' }, | ||||
|     bgcolor: 'background.paper', | ||||
|     border: '2px solid #000', | ||||
|     boxShadow: 24, | ||||
|     p: 3, | ||||
| }; | ||||
|  | ||||
| const diasSemanaNombres = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; | ||||
|  | ||||
| interface PublicacionDiasSemanaModalProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   publicacion: PublicacionDto | null; | ||||
|   onConfigSaved: () => void; // Para recargar la lista de publicaciones si es necesario | ||||
| } | ||||
|  | ||||
| const PublicacionDiasSemanaModal: React.FC<PublicacionDiasSemanaModalProps> = ({ | ||||
|   open, | ||||
|   onClose, | ||||
|   publicacion, | ||||
|   onConfigSaved | ||||
| }) => { | ||||
|   const [selectedDays, setSelectedDays] = useState<Set<number>>(new Set()); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (open && publicacion) { | ||||
|       setLoading(true); | ||||
|       setError(null); | ||||
|       publicacionService.getConfiguracionDiasPublicacion(publicacion.idPublicacion) | ||||
|         .then(configs => { | ||||
|           const activeDays = new Set(configs.filter(c => c.activo).map(c => c.diaSemana)); | ||||
|           setSelectedDays(activeDays); | ||||
|         }) | ||||
|         .catch(err => { | ||||
|           console.error("Error al cargar configuración de días:", err); | ||||
|           setError("Error al cargar la configuración actual de días."); | ||||
|         }) | ||||
|         .finally(() => setLoading(false)); | ||||
|     } else { | ||||
|       setSelectedDays(new Set()); // Resetear al cerrar o si no hay publicación | ||||
|     } | ||||
|   }, [open, publicacion]); | ||||
|  | ||||
|   const handleCheckboxChange = (dayIndex: number) => { | ||||
|     setSelectedDays(prev => { | ||||
|       const newSelection = new Set(prev); | ||||
|       if (newSelection.has(dayIndex)) { | ||||
|         newSelection.delete(dayIndex); | ||||
|       } else { | ||||
|         newSelection.add(dayIndex); | ||||
|       } | ||||
|       return newSelection; | ||||
|     }); | ||||
|     setError(null); // Limpiar error al cambiar | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit = async () => { | ||||
|     if (!publicacion) return; | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     const requestDto: UpdatePublicacionDiasSemanaRequestDto = { | ||||
|       diasActivos: Array.from(selectedDays) | ||||
|     }; | ||||
|     try { | ||||
|       await publicacionService.updateConfiguracionDiasPublicacion(publicacion.idPublicacion, requestDto); | ||||
|       onConfigSaved(); // Notificar al padre que se guardó | ||||
|       onClose(); | ||||
|     } catch (err: any) { | ||||
|       console.error("Error al guardar configuración de días:", err); | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message  | ||||
|         ? err.response.data.message  | ||||
|         : 'Error al guardar la configuración.'; | ||||
|       setError(message); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (!publicacion) return null; | ||||
|  | ||||
|   return ( | ||||
|     <Modal open={open} onClose={onClose}> | ||||
|       <Box sx={modalStyle}> | ||||
|         <Typography variant="h6" gutterBottom> | ||||
|           Configurar Días de Salida para: {publicacion.nombre} | ||||
|         </Typography> | ||||
|         <Typography variant="body2" color="text.secondary" sx={{mb:2}}> | ||||
|             Marque los días en que esta publicación debe aparecer por defecto en los movimientos de canillitas. | ||||
|         </Typography> | ||||
|          | ||||
|         {loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress /></Box>} | ||||
|         {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|  | ||||
|         {!loading && ( | ||||
|             <Paper variant="outlined" sx={{p:2}}> | ||||
|                 <FormGroup> | ||||
|                 {diasSemanaNombres.map((nombreDia, index) => ( | ||||
|                     <FormControlLabel | ||||
|                     key={index} | ||||
|                     control={ | ||||
|                         <Checkbox | ||||
|                         checked={selectedDays.has(index)} | ||||
|                         onChange={() => handleCheckboxChange(index)} | ||||
|                         /> | ||||
|                     } | ||||
|                     label={nombreDia} | ||||
|                     /> | ||||
|                 ))} | ||||
|                 </FormGroup> | ||||
|             </Paper> | ||||
|         )} | ||||
|  | ||||
|         <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|           <Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button> | ||||
|           <Button onClick={handleSubmit} variant="contained" disabled={loading}> | ||||
|             {loading ? <CircularProgress size={24} /> : 'Guardar Configuración'} | ||||
|           </Button> | ||||
|         </Box> | ||||
|       </Box> | ||||
|     </Modal> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default PublicacionDiasSemanaModal; | ||||
		Reference in New Issue
	
	Block a user