Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
		| @@ -8,14 +8,14 @@ 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'; | ||||
| import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; | ||||
| // import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Ya no es necesario cargar todos los canillitas aquí | ||||
| import publicacionService from '../../../services/Distribucion/publicacionService'; | ||||
| import canillaService from '../../../services/Distribucion/canillaService'; | ||||
| // import canillaService from '../../../services/Distribucion/canillaService'; // Ya no es necesario | ||||
| import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService'; | ||||
| import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto'; | ||||
| import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto'; | ||||
| import axios from 'axios'; | ||||
| import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; | ||||
|  | ||||
| const modalStyle = { | ||||
|   position: 'absolute' as 'absolute', | ||||
| @@ -34,14 +34,21 @@ const modalStyle = { | ||||
| interface EntradaSalidaCanillaFormModalProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   // El onSubmit de la página padre se usa solo para edición. La creación se maneja internamente. | ||||
|   onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise<void>; | ||||
|   initialData?: EntradaSalidaCanillaDto | null; | ||||
|   initialData?: EntradaSalidaCanillaDto | null; // Para edición | ||||
|   prefillData?: { // Para creación, prellenar desde la página padre | ||||
|     fecha?: string; // YYYY-MM-DD | ||||
|     idCanilla?: number | string; | ||||
|     nombreCanilla?: string; // << AÑADIR NOMBRE PARA MOSTRAR | ||||
|     idPublicacion?: number | string; // Para pre-seleccionar la primera publicación en la lista de items | ||||
|   } | null; | ||||
|   errorMessage?: string | null; | ||||
|   clearErrorMessage: () => void; | ||||
| } | ||||
|  | ||||
| interface FormRowItem { | ||||
|   id: string; | ||||
|   id: string; // ID temporal para el frontend | ||||
|   idPublicacion: number | string; | ||||
|   cantSalida: string; | ||||
|   cantEntrada: string; | ||||
| @@ -50,56 +57,62 @@ interface FormRowItem { | ||||
|  | ||||
| const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({ | ||||
|   open, | ||||
|   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 | ||||
|   onClose, | ||||
|   onSubmit: onSubmitEdit, // Renombrar para claridad, ya que solo se usa para editar | ||||
|   initialData, | ||||
|   prefillData, | ||||
|   errorMessage: parentErrorMessage, | ||||
|   clearErrorMessage | ||||
| }) => { | ||||
|   const [idCanilla, setIdCanilla] = useState<number | string>(''); | ||||
|   const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>(''); | ||||
|   // Estados para los campos que SÍ son editables o parte del formulario de items | ||||
|   const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>(''); // Solo para modo edición | ||||
|   const [editCantSalida, setEditCantSalida] = useState<string>('0'); | ||||
|   const [editCantEntrada, setEditCantEntrada] = useState<string>('0'); | ||||
|   const [editObservacion, setEditObservacion] = useState(''); | ||||
|   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); // Loading para submit | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs | ||||
|   const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items | ||||
|   const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|  | ||||
|   // Estados para datos de dropdowns | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); // Sigue siendo necesario para la lista de items | ||||
|    | ||||
|   // Estados de carga y error | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Solo para publicaciones | ||||
|   const [loadingItems, setLoadingItems] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|   const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null); | ||||
|  | ||||
|   const isEditing = Boolean(initialData); | ||||
|   const isEditing = Boolean(initialData && initialData.idParte); | ||||
|  | ||||
|   // Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true | ||||
|   // Datos que vienen prellenados y no son editables en el modal (Fecha y Canillita) | ||||
|   const displayFecha = isEditing ? (initialData?.fecha ? initialData.fecha.split('T')[0] : '') : (prefillData?.fecha || ''); | ||||
|   const displayIdCanilla = isEditing ? initialData?.idCanilla : prefillData?.idCanilla; | ||||
|   const displayNombreCanilla = isEditing ? initialData?.nomApeCanilla : prefillData?.nombreCanilla; | ||||
|  | ||||
|  | ||||
|   // Cargar publicaciones para el dropdown de items | ||||
|   useEffect(() => { | ||||
|     const fetchDropdownData = async () => { | ||||
|     const fetchPublicacionesDropdown = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
|       setLocalErrors(prev => ({ ...prev, dropdowns: null })); | ||||
|       try { | ||||
|         const [pubsData, canillitasData] = await Promise.all([ | ||||
|           publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|           canillaService.getAllCanillas(undefined, undefined, true) | ||||
|         ]); | ||||
|         // Usar getPublicacionesForDropdown si lo tienes, sino getAllPublicaciones | ||||
|         const pubsData = await publicacionService.getPublicacionesForDropdown(true); | ||||
|         setPublicaciones(pubsData); | ||||
|         setCanillitas(canillitasData); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar datos para dropdowns", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' })); | ||||
|         console.error("Error al cargar publicaciones para dropdown", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (open) { | ||||
|       fetchDropdownData(); | ||||
|       fetchPublicacionesDropdown(); | ||||
|     } | ||||
|   }, [open]); | ||||
|  | ||||
|  | ||||
|   // Efecto para inicializar el formulario cuando se abre o cambia initialData | ||||
|   // Inicializar formulario y/o pre-cargar items | ||||
|   useEffect(() => { | ||||
|     if (open) { | ||||
|       clearErrorMessage(); | ||||
| @@ -107,65 +120,90 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|       setLocalErrors({}); | ||||
|  | ||||
|       if (isEditing && initialData) { | ||||
|         setIdCanilla(initialData.idCanilla || ''); | ||||
|         setFecha(initialData.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]); | ||||
|         setEditIdPublicacion(initialData.idPublicacion || ''); | ||||
|         setEditCantSalida(initialData.cantSalida?.toString() || '0'); | ||||
|         setEditCantEntrada(initialData.cantEntrada?.toString() || '0'); | ||||
|         setEditObservacion(initialData.observacion || ''); | ||||
|         setItems([]); // En modo edición, no pre-cargamos items de la lista | ||||
|       } else { | ||||
|         // Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items | ||||
|         setIdCanilla(''); | ||||
|         setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto | ||||
|         // Los items se cargarán por el siguiente useEffect basado en la fecha | ||||
|         setItems([]); // No hay lista de items en modo edición de un solo movimiento | ||||
|       } else { // Modo Creación | ||||
|         // Limpiar campos de edición | ||||
|         setEditIdPublicacion(''); | ||||
|         setEditCantSalida('0'); | ||||
|         setEditCantEntrada('0'); | ||||
|         setEditObservacion(''); | ||||
|  | ||||
|         // Lógica para pre-cargar items basada en displayFecha y prefillData.idPublicacion | ||||
|         // (Esta lógica se mueve al siguiente useEffect que depende de displayFecha y publicaciones) | ||||
|         // Por ahora, solo aseguramos que `items` se resetee si es necesario. | ||||
|         const idPubPrefill = prefillData?.idPublicacion; | ||||
|         if (idPubPrefill && publicaciones.length > 0) { | ||||
|              // Si ya tenemos publicaciones, y un prefill de publicación, intentamos setearlo | ||||
|              const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay(); | ||||
|              setLoadingItems(true); | ||||
|              publicacionService.getPublicacionesPorDiaSemana(diaSemana) | ||||
|                  .then(pubsPorDefecto => { | ||||
|                      let itemsIniciales: FormRowItem[]; | ||||
|                      if (pubsPorDefecto.find(p => p.idPublicacion === Number(idPubPrefill))) { | ||||
|                          // Si la publicación prellenada está en las de por defecto, la usamos | ||||
|                          itemsIniciales = [{ id: Date.now().toString(), idPublicacion: Number(idPubPrefill), cantSalida: '0', cantEntrada: '0', observacion: '' }]; | ||||
|                      } else if (pubsPorDefecto.length > 0) { | ||||
|                          // Si no, pero hay otras por defecto, usamos la primera de ellas | ||||
|                          itemsIniciales = pubsPorDefecto.map(pub => ({ | ||||
|                              id: `${Date.now().toString()}-${pub.idPublicacion}`, | ||||
|                              idPublicacion: pub.idPublicacion, | ||||
|                              cantSalida: '0', cantEntrada: '0', observacion: '' | ||||
|                          })); | ||||
|                      } else { | ||||
|                          // Si no hay ninguna por defecto, y la prellenada no aplica, usamos la prellenada sola o vacía | ||||
|                          itemsIniciales = [{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }]; | ||||
|                      } | ||||
|                      setItems(itemsIniciales.length > 0 ? itemsIniciales : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|                  }) | ||||
|                  .catch(() => setItems([{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }])) // Fallback | ||||
|                  .finally(() => setLoadingItems(false)); | ||||
|         } else if (publicaciones.length === 0 && !loadingDropdowns) { // Si no hay prefill de pub o no hay pubs aún | ||||
|              setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, [open, initialData, isEditing, clearErrorMessage]); | ||||
|   }, [open, initialData, isEditing, prefillData, clearErrorMessage, publicaciones, loadingDropdowns, displayFecha]); // Añadir displayFecha | ||||
|  | ||||
|  | ||||
|   // Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO) | ||||
|   // y cuando las publicaciones están disponibles. | ||||
|   // Efecto para pre-cargar items por defecto cuando cambia la FECHA (displayFecha) en modo NUEVO | ||||
|   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 })); | ||||
|  | ||||
|     if (open && !isEditing && publicaciones.length > 0 && displayFecha) { | ||||
|       const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay(); | ||||
|       setLoadingItems(true); | ||||
|       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: '' }]); | ||||
|           } | ||||
|           const itemsPorDefecto = pubsPorDefecto.map(pub => ({ | ||||
|             id: `${Date.now().toString()}-${pub.idPublicacion}`, | ||||
|             idPublicacion: pub.idPublicacion, | ||||
|             cantSalida: '0', | ||||
|             cantEntrada: '0', | ||||
|             observacion: '' | ||||
|           })); | ||||
|           setItems(itemsPorDefecto.length > 0 ? itemsPorDefecto : [{ 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); | ||||
|           console.error("Error al cargar 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 | ||||
|   }, [open, isEditing, displayFecha, publicaciones]); // Dependencia de displayFecha y publicaciones | ||||
|  | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     // ... (lógica de validación sin cambios, pero 'idCanilla' y 'fecha' ya no son estados del modal) | ||||
|     const currentErrors: { [key: string]: string | null } = {}; | ||||
|     if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; | ||||
|     if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.'; | ||||
|     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|  | ||||
|     // Validar displayIdCanilla y displayFecha si es modo creación | ||||
|     if (!isEditing) { | ||||
|         if (!displayIdCanilla) currentErrors.idCanilla = 'El canillita es obligatorio (provisto por la página).'; | ||||
|         if (!displayFecha || !displayFecha.trim()) currentErrors.fecha = 'La fecha es obligatoria (provista por la página).'; | ||||
|         else if (!/^\d{4}-\d{2}-\d{2}$/.test(displayFecha)) currentErrors.fecha = 'Formato de fecha inválido.'; | ||||
|     } | ||||
|     // ... resto de la validación para items (modo creación) o campos edit (modo edición) ... | ||||
|     if (isEditing) { | ||||
|       const salidaNum = parseInt(editCantSalida, 10); | ||||
|       const entradaNum = parseInt(editCantEntrada, 10); | ||||
| @@ -177,14 +215,15 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|       } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||
|         currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; | ||||
|       } | ||||
|     } else { | ||||
|       if (!editIdPublicacion) { // En edición, la publicación es fija, pero debe existir | ||||
|         currentErrors.editIdPublicacion = 'Error: Publicación no especificada para edición.'; | ||||
|       } | ||||
|     } else { // Modo Creación (Bulk) | ||||
|       let hasValidItemWithQuantityOrPub = false; | ||||
|       const publicacionIdsEnLote = new Set<number>(); | ||||
|  | ||||
|       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); | ||||
| @@ -199,9 +238,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           const pubIdNum = Number(item.idPublicacion); | ||||
|           if (publicacionIdsEnLote.has(pubIdNum)) { | ||||
|             currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; | ||||
|           } else { | ||||
|             publicacionIdsEnLote.add(pubIdNum); | ||||
|           } | ||||
|           } else { publicacionIdsEnLote.add(pubIdNum); } | ||||
|  | ||||
|           if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||
|             currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; | ||||
|           } | ||||
| @@ -213,18 +251,11 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           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) { | ||||
|       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 (!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."; | ||||
|       } else if (items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0 || i.observacion.trim() !== '')) && !allItemsAreEmptyAndNoPubSelected) { | ||||
|         currentErrors.general = "Debe ingresar datos significativos (cantidades u observación) para al menos una publicación seleccionada."; | ||||
|       } | ||||
|     } | ||||
|     setLocalErrors(currentErrors); | ||||
| @@ -232,6 +263,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|   }; | ||||
|  | ||||
|   const handleInputChange = (fieldName: string) => { | ||||
|     // ... (sin cambios) | ||||
|     if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|     if (parentErrorMessage) clearErrorMessage(); | ||||
|     if (modalSpecificApiError) setModalSpecificApiError(null); | ||||
| @@ -252,16 +284,17 @@ 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) | ||||
|         await onSubmitEdit(dataToSubmitSingle, initialData.idParte); | ||||
|       } else { // Modo Creación | ||||
|         if (!displayIdCanilla || !displayFecha) { | ||||
|             setModalSpecificApiError("Faltan datos del canillita o la fecha para crear los movimientos."); | ||||
|             setLoading(false); | ||||
|             return; | ||||
|         } | ||||
|         const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items | ||||
|           .filter(item => | ||||
|             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() !== '') | ||||
|             ( (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), | ||||
| @@ -271,37 +304,30 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|           })); | ||||
|  | ||||
|         if (itemsToSubmit.length === 0) { | ||||
|           setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." })); | ||||
|           setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar." })); | ||||
|           setLoading(false); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const bulkData: CreateBulkEntradaSalidaCanillaDto = { | ||||
|           idCanilla: Number(idCanilla), | ||||
|           fecha, | ||||
|           idCanilla: Number(displayIdCanilla), // Usar el displayIdCanilla | ||||
|           fecha: displayFecha,               // Usar el displayFecha | ||||
|           items: itemsToSubmit, | ||||
|         }; | ||||
|         await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); | ||||
|         onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso | ||||
|       } | ||||
|       // onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito | ||||
|       onClose(); | ||||
|     } catch (error: any) { | ||||
|       console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); | ||||
|       if (axios.isAxiosError(error) && error.response) { | ||||
|         setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.'); | ||||
|       } else { | ||||
|         setModalSpecificApiError('Ocurrió un error inesperado.'); | ||||
|       } | ||||
|       // 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). | ||||
|       const message = axios.isAxiosError(error) && error.response?.data?.message  | ||||
|         ? error.response.data.message  | ||||
|         : 'Ocurrió un error inesperado al procesar la solicitud.'; | ||||
|       setModalSpecificApiError(message); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleAddRow = () => { | ||||
|   const handleAddRow = () => { /* ... (sin cambios) ... */  | ||||
|     if (items.length >= publicaciones.length) { | ||||
|       setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); | ||||
|       return; | ||||
| @@ -309,15 +335,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|     setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||
|     setLocalErrors(prev => ({ ...prev, general: null })); | ||||
|   }; | ||||
|  | ||||
|   const handleRemoveRow = (idToRemove: string) => { | ||||
|   const handleRemoveRow = (idToRemove: string) => { /* ... (sin cambios) ... */  | ||||
|     if (items.length <= 1 && !isEditing) return; | ||||
|     setItems(items.filter(item => item.id !== idToRemove)); | ||||
|   }; | ||||
|  | ||||
|   const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => { | ||||
|     setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX | ||||
|     if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item. | ||||
|   const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => { /* ... (sin cambios) ... */ | ||||
|     setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); | ||||
|     if (localErrors[`item_${id}_${field}`]) { | ||||
|       setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null })); | ||||
|     } | ||||
|     if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null })); | ||||
| @@ -325,200 +349,102 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | ||||
|     if (modalSpecificApiError) setModalSpecificApiError(null); | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   return ( | ||||
|     <Modal open={open} onClose={onClose}> | ||||
|       <Box sx={modalStyle}> | ||||
|         <Typography variant="h6" component="h2" gutterBottom> | ||||
|           {isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'} | ||||
|           {isEditing ? `Editar Movimiento (ID: ${initialData?.idParte})` : 'Registrar Nuevos Movimientos'} | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|           <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | ||||
|             <FormControl fullWidth margin="dense" error={!!localErrors.idCanilla} required> | ||||
|               <InputLabel id="canilla-esc-select-label">Canillita</InputLabel> | ||||
|               <Select labelId="canilla-esc-select-label" label="Canillita" value={idCanilla} | ||||
|                 onChange={(e) => { setIdCanilla(e.target.value as number); handleInputChange('idCanilla'); }} | ||||
|                 disabled={loading || loadingDropdowns || isEditing} | ||||
|               > | ||||
|                 <MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem> | ||||
|                 {canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))} | ||||
|               </Select> | ||||
|               {localErrors.idCanilla && <FormHelperText>{localErrors.idCanilla}</FormHelperText>} | ||||
|             </FormControl> | ||||
|           {/* --- MOSTRAR DATOS PRELLENADOS/FIJOS --- */} | ||||
|           <Paper variant="outlined" sx={{ p: 1.5, mb: 2, backgroundColor: 'grey.100' }}> | ||||
|             <Typography variant="body1" component="div" sx={{ display: 'flex', justifyContent: 'space-between', flexWrap:'wrap' }}> | ||||
|                 <Box><strong>{isEditing ? "Canillita:" : "Para Canillita:"}</strong> {displayNombreCanilla || 'N/A'}</Box> | ||||
|                 <Box><strong>{isEditing ? "Fecha Movimiento:" : "Para Fecha:"}</strong> {displayFecha ? new Date(displayFecha + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone: 'UTC'}) : 'N/A'}</Box> | ||||
|             </Typography> | ||||
|             {localErrors.idCanilla && <Typography color="error" variant="caption" display="block">{localErrors.idCanilla}</Typography>} | ||||
|             {localErrors.fecha && <Typography color="error" variant="caption" display="block">{localErrors.fecha}</Typography>} | ||||
|           </Paper> | ||||
|           {/* --- FIN DATOS PRELLENADOS --- */} | ||||
|  | ||||
|             <TextField label="Fecha Movimientos" type="date" value={fecha} required | ||||
|               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 && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado | ||||
|             /> | ||||
|           {/* El Select de Canillita y TextField de Fecha se eliminan de aquí si son fijos */} | ||||
|  | ||||
|             {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 }} | ||||
|                   /> | ||||
|                 </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 }} | ||||
|           {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, flexWrap: 'wrap' }}> | ||||
|                 <TextField label="Cant. Salida" type="number" value={editCantSalida} | ||||
|                   onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }} | ||||
|                   margin="dense" error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} | ||||
|                   disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }} | ||||
|                 /> | ||||
|               </Paper> | ||||
|             )} | ||||
|                 <TextField label="Cant. Entrada" type="number" value={editCantEntrada} | ||||
|                   onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }} | ||||
|                   margin="dense" error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} | ||||
|                   disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }} | ||||
|                 /> | ||||
|               </Box> | ||||
|               <TextField label="Observación (Movimiento)" value={editObservacion} // Label cambiado | ||||
|                 onChange={(e) => setEditObservacion(e.target.value)} | ||||
|                 margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }} | ||||
|               /> | ||||
|             </Paper> | ||||
|           )} | ||||
|  | ||||
|             {!isEditing && ( | ||||
|               <Box> | ||||
|                 <Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography> | ||||
|                 {/* 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`]} | ||||
|                         > | ||||
|                           <InputLabel | ||||
|                             required={ | ||||
|                               parseInt(itemRow.cantSalida) > 0 || | ||||
|                               parseInt(itemRow.cantEntrada) > 0 || | ||||
|                               itemRow.observacion.trim() !== '' | ||||
|                             } | ||||
|                           > | ||||
|           {!isEditing && ( | ||||
|             <Box> | ||||
|               <Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography> | ||||
|               {loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>} | ||||
|               {!loadingItems && items.map((itemRow, index) => ( | ||||
|                 // ... (Renderizado de la fila de items sin cambios significativos, | ||||
|                 //      solo asegúrate que el Select de Publicación use `publicaciones` y no `canillitas`) | ||||
|                 <Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, }}> | ||||
|                     <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, }}> | ||||
|                       <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', flexGrow: 1, }}> | ||||
|                         <FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }} 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} | ||||
|                             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 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 }} > | ||||
|                             <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> | ||||
|                           )} | ||||
|                           {localErrors[`item_${itemRow.id}_idPublicacion`] && ( <FormHelperText> {localErrors[`item_${itemRow.id}_idPublicacion`]} </FormHelperText> )} | ||||
|                         </FormControl> | ||||
|  | ||||
|                         <TextField | ||||
|                           label="Llevados" | ||||
|                           type="number" | ||||
|                           size="small" | ||||
|                           value={itemRow.cantSalida} | ||||
|                         <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, | ||||
|                           }} | ||||
|                           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} | ||||
|                         <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, | ||||
|                           }} | ||||
|                           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} | ||||
|                         <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 | ||||
|                           }} | ||||
|                         > | ||||
|                         <IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" aria-label="Quitar fila" sx={{ alignSelf: 'center', }} > | ||||
|                           <DeleteIcon fontSize="medium" /> | ||||
|                         </IconButton> | ||||
|                       )} | ||||
|                     </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 | ||||
|                 </Button> | ||||
|               </Box> | ||||
|             )} | ||||
|           </Box> | ||||
|               ))} | ||||
|               {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 | ||||
|               </Button> | ||||
|             </Box> | ||||
|           )} | ||||
|  | ||||
|           {parentErrorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{parentErrorMessage}</Alert>} | ||||
|           {modalSpecificApiError && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{modalSpecificApiError}</Alert>} | ||||
|   | ||||
| @@ -0,0 +1,165 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert | ||||
| } from '@mui/material'; | ||||
| import type { NovedadCanillaDto } from '../../../models/dtos/Distribucion/NovedadCanillaDto'; | ||||
| import type { CreateNovedadCanillaDto } from '../../../models/dtos/Distribucion/CreateNovedadCanillaDto'; | ||||
| import type { UpdateNovedadCanillaDto } from '../../../models/dtos/Distribucion/UpdateNovedadCanillaDto'; | ||||
|  | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '90%', sm: 500 }, | ||||
|     bgcolor: 'background.paper', | ||||
|     border: '2px solid #000', | ||||
|     boxShadow: 24, | ||||
|     p: 4, | ||||
|     maxHeight: '90vh', | ||||
|     overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| interface NovedadCanillaFormModalProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   onSubmit: (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => Promise<void>; | ||||
|   // Props para pasar datos necesarios: | ||||
|   idCanilla: number | null; // Necesario para crear una nueva novedad | ||||
|   nombreCanilla?: string;   // Para mostrar en el título | ||||
|   initialData?: NovedadCanillaDto | null; // Para editar | ||||
|   errorMessage?: string | null; | ||||
|   clearErrorMessage: () => void; | ||||
| } | ||||
|  | ||||
| const NovedadCanillaFormModal: React.FC<NovedadCanillaFormModalProps> = ({ | ||||
|   open, | ||||
|   onClose, | ||||
|   onSubmit, | ||||
|   idCanilla, | ||||
|   nombreCanilla, | ||||
|   initialData, | ||||
|   errorMessage, | ||||
|   clearErrorMessage | ||||
| }) => { | ||||
|   const [fecha, setFecha] = useState<string>(''); | ||||
|   const [detalle, setDetalle] = useState(''); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   const isEditing = Boolean(initialData && initialData.idNovedad); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (open) { | ||||
|       setFecha(initialData?.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]); | ||||
|       setDetalle(initialData?.detalle || ''); | ||||
|       setLocalErrors({}); | ||||
|       clearErrorMessage(); | ||||
|     } | ||||
|   }, [open, initialData, clearErrorMessage]); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.'; | ||||
|     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||
|      | ||||
|     if (!detalle.trim()) errors.detalle = 'El detalle es obligatorio.'; | ||||
|     else if (detalle.trim().length > 250) errors.detalle = 'El detalle no puede exceder los 250 caracteres.'; | ||||
|  | ||||
|     setLocalErrors(errors); | ||||
|     return Object.keys(errors).length === 0; | ||||
|   }; | ||||
|  | ||||
|   const handleInputChange = (fieldName: 'fecha' | 'detalle') => { | ||||
|     if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); | ||||
|     if (errorMessage) clearErrorMessage(); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { | ||||
|     event.preventDefault(); | ||||
|     clearErrorMessage(); | ||||
|     if (!validate()) return; | ||||
|  | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       if (isEditing && initialData) { | ||||
|         const dataToSubmit: UpdateNovedadCanillaDto = { detalle }; | ||||
|         await onSubmit(dataToSubmit, initialData.idNovedad); | ||||
|       } else if (idCanilla) { // Asegurarse que idCanilla esté disponible para creación | ||||
|         const dataToSubmit: CreateNovedadCanillaDto = { | ||||
|             idCanilla: idCanilla, // Tomado de props | ||||
|             fecha, | ||||
|             detalle, | ||||
|         }; | ||||
|         await onSubmit(dataToSubmit); | ||||
|       } else { | ||||
|         // Esto no debería pasar si la lógica de la página que llama al modal es correcta | ||||
|         setLocalErrors(prev => ({...prev, general: "No se proporcionó ID de Canillita para crear la novedad."})) | ||||
|         setLoading(false); | ||||
|         return; | ||||
|       } | ||||
|       onClose(); | ||||
|     } catch (error: any) { | ||||
|       // El error de API es manejado por la página padre a través de 'errorMessage' | ||||
|       console.error("Error en submit de NovedadCanillaFormModal:", error); | ||||
|     } finally { | ||||
|        setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Modal open={open} onClose={onClose}> | ||||
|       <Box sx={modalStyle}> | ||||
|         <Typography variant="h6" component="h2" gutterBottom> | ||||
|           {isEditing ? 'Editar Novedad' : `Agregar Novedad para ${nombreCanilla || 'Canillita'}`} | ||||
|         </Typography> | ||||
|         <Typography variant="body2" color="text.secondary" gutterBottom> | ||||
|           {isEditing && initialData ? `Editando Novedad ID: ${initialData.idNovedad}` : `Canillita ID: ${idCanilla || 'N/A'}`} | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|             <TextField | ||||
|                 label="Fecha Novedad" | ||||
|                 type="date" | ||||
|                 value={fecha} | ||||
|                 required | ||||
|                 onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}} | ||||
|                 margin="normal" | ||||
|                 fullWidth | ||||
|                 error={!!localErrors.fecha} | ||||
|                 helperText={localErrors.fecha || ''} | ||||
|                 disabled={loading || isEditing} // Fecha no se edita | ||||
|                 InputLabelProps={{ shrink: true }} | ||||
|                 autoFocus={!isEditing} | ||||
|             /> | ||||
|             <TextField | ||||
|                 label="Detalle Novedad" | ||||
|                 value={detalle} | ||||
|                 required | ||||
|                 onChange={(e) => {setDetalle(e.target.value); handleInputChange('detalle');}} | ||||
|                 margin="normal" | ||||
|                 fullWidth | ||||
|                 multiline | ||||
|                 rows={4} | ||||
|                 error={!!localErrors.detalle} | ||||
|                 helperText={localErrors.detalle || (detalle ? `${250 - detalle.length} caracteres restantes` : '')} | ||||
|                 disabled={loading} | ||||
|                 inputProps={{ maxLength: 250 }} | ||||
|             /> | ||||
|  | ||||
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|           {localErrors.general && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.general}</Alert>} | ||||
|  | ||||
|  | ||||
|           <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|             <Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button> | ||||
|             <Button type="submit" variant="contained" disabled={loading}> | ||||
|               {loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Novedad')} | ||||
|             </Button> | ||||
|           </Box> | ||||
|         </Box> | ||||
|       </Box> | ||||
|     </Modal> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default NovedadCanillaFormModal; | ||||
		Reference in New Issue
	
	Block a user