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