Finalización de Reportes y arreglos varios de controles y comportamientos...

This commit is contained in:
2025-06-03 13:45:20 -03:00
parent 99532b03f1
commit 062cc05fd0
67 changed files with 4523 additions and 993 deletions

View File

@@ -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}}

View File

@@ -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

View File

@@ -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;