Ya perdí el hilo de los cambios pero ahi van.

This commit is contained in:
2025-05-23 15:47:39 -03:00
parent e7e185a9cb
commit 3c1fe15b1f
141 changed files with 9764 additions and 190 deletions

View File

@@ -0,0 +1,201 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { ControlDevolucionesDto } from '../../../models/dtos/Distribucion/ControlDevolucionesDto';
import type { CreateControlDevolucionesDto } from '../../../models/dtos/Distribucion/CreateControlDevolucionesDto';
import type { UpdateControlDevolucionesDto } from '../../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; // DTO de Empresa
import empresaService from '../../../services//Distribucion/empresaService'; // Servicio de Empresa
const modalStyle = { /* ... (mismo estilo) ... */
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 ControlDevolucionesFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => Promise<void>;
initialData?: ControlDevolucionesDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [entrada, setEntrada] = useState<string>('');
const [sobrantes, setSobrantes] = useState<string>('');
const [detalle, setDetalle] = useState('');
const [sinCargo, setSinCargo] = useState<string>('0'); // Default 0
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchEmpresas = async () => {
setLoadingEmpresas(true);
try {
const data = await empresaService.getAllEmpresas();
setEmpresas(data);
} catch (error) {
console.error("Error al cargar empresas", error);
setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'}));
} finally {
setLoadingEmpresas(false);
}
};
if (open) {
fetchEmpresas();
setIdEmpresa(initialData?.idEmpresa || '');
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
setEntrada(initialData?.entrada?.toString() || '0');
setSobrantes(initialData?.sobrantes?.toString() || '0');
setDetalle(initialData?.detalle || '');
setSinCargo(initialData?.sinCargo?.toString() || '0');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.';
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.';
const entradaNum = parseInt(entrada, 10);
const sobrantesNum = parseInt(sobrantes, 10);
const sinCargoNum = parseInt(sinCargo, 10);
if (entrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) errors.entrada = 'Entrada debe ser un número >= 0.';
if (sobrantes.trim() === '' || isNaN(sobrantesNum) || sobrantesNum < 0) errors.sobrantes = 'Sobrantes debe ser un número >= 0.';
if (sinCargo.trim() === '' || isNaN(sinCargoNum) || sinCargoNum < 0) errors.sinCargo = 'Sin Cargo debe ser un número >= 0.';
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
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 {
const commonData = {
entrada: parseInt(entrada, 10),
sobrantes: parseInt(sobrantes, 10),
detalle: detalle || undefined,
sinCargo: parseInt(sinCargo, 10),
};
if (isEditing && initialData) {
const dataToSubmit: UpdateControlDevolucionesDto = { ...commonData };
await onSubmit(dataToSubmit, initialData.idControl);
} else {
const dataToSubmit: CreateControlDevolucionesDto = {
...commonData,
idEmpresa: Number(idEmpresa),
fecha,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de ControlDevolucionesFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Control de Devoluciones' : 'Registrar Control de Devoluciones'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required>
<InputLabel id="empresa-cd-select-label">Empresa</InputLabel>
<Select labelId="empresa-cd-select-label" label="Empresa" value={idEmpresa}
onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
disabled={loading || loadingEmpresas || isEditing}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
</Select>
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
</FormControl>
<TextField label="Fecha" 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}
/>
<TextField label="Entrada (Devolución Total)" 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}}
/>
<TextField label="Sobrantes" type="number" value={sobrantes} required
onChange={(e) => {setSobrantes(e.target.value); handleInputChange('sobrantes');}}
margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''}
disabled={loading} inputProps={{min:0}}
/>
<TextField label="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}}
/>
<TextField label="Detalle (Opcional)" value={detalle}
onChange={(e) => setDetalle(e.target.value)}
margin="dense" fullWidth multiline rows={2} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.empresas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.empresas}</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 || loadingEmpresas}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Control')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default ControlDevolucionesFormModal;

View File

@@ -0,0 +1,396 @@
// 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
} 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';
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto';
import publicacionService from '../../../services/Distribucion/publicacionService';
import canillaService from '../../../services/Distribucion/canillaService';
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';
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'
};
interface EntradaSalidaCanillaFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise<void>;
initialData?: EntradaSalidaCanillaDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
interface FormRowItem {
id: string;
idPublicacion: number | string;
cantSalida: string;
cantEntrada: string;
observacion: string;
}
const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
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>('');
const [editCantSalida, setEditCantSalida] = useState<string>('0');
const [editCantEntrada, setEditCantEntrada] = useState<string>('0');
const [editObservacion, setEditObservacion] = useState('');
const [items, setItems] = useState<FormRowItem[]>([]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null);
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchDropdownData = 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)
]);
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).' }));
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdownData();
clearErrorMessage();
setModalSpecificApiError(null);
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([]);
} else {
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
setIdCanilla('');
setFecha(new Date().toISOString().split('T')[0]);
setEditCantSalida('0');
setEditCantEntrada('0');
setEditObservacion('');
setEditIdPublicacion('');
}
}
}, [open, initialData, isEditing, clearErrorMessage]);
const validate = (): boolean => {
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).';
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.';
}
} else {
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);
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;
}
});
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.";
}
}
setLocalErrors(currentErrors);
return Object.keys(currentErrors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (parentErrorMessage) clearErrorMessage();
if (modalSpecificApiError) setModalSpecificApiError(null);
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
setModalSpecificApiError(null);
if (!validate()) return;
setLoading(true);
try {
if (isEditing && initialData) {
const salidaNum = parseInt(editCantSalida, 10);
const entradaNum = parseInt(editCantEntrada, 10);
const dataToSubmitSingle: UpdateEntradaSalidaCanillaDto = {
cantSalida: salidaNum,
cantEntrada: entradaNum,
observacion: editObservacion.trim() || undefined,
};
await onSubmit(dataToSubmitSingle, initialData.idParte);
} else {
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() !== '')
)
.map(item => ({
idPublicacion: Number(item.idPublicacion),
cantSalida: parseInt(item.cantSalida, 10) || 0,
cantEntrada: parseInt(item.cantEntrada, 10) || 0,
observacion: item.observacion.trim() || undefined,
}));
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."}));
setLoading(false);
return;
}
const bulkData: CreateBulkEntradaSalidaCanillaDto = {
idCanilla: Number(idCanilla),
fecha,
items: itemsToSubmit,
};
await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData);
}
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.');
}
if (isEditing) throw error;
} finally {
setLoading(false);
}
};
const handleAddRow = () => {
if (items.length >= publicaciones.length) {
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 }));
};
const handleRemoveRow = (idToRemove: string) => {
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.
setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null }));
}
if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null }));
if (parentErrorMessage) clearErrorMessage();
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'}
</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>
<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}
/>
{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}}
/>
</Paper>
)}
{!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"
>
<DeleteIcon fontSize="inherit" />
</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
</Button>
</Box>
)}
</Box>
{parentErrorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{parentErrorMessage}</Alert>}
{modalSpecificApiError && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{modalSpecificApiError}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Movimientos')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default EntradaSalidaCanillaFormModal;

View File

@@ -29,7 +29,7 @@ interface EntradaSalidaDistFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise<void>;
initialData?: EntradaSalidaDistDto | null; // Para editar
initialData?: EntradaSalidaDistDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
@@ -63,8 +63,8 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
setLoadingDropdowns(true);
try {
const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
distribuidorService.getAllDistribuidores()
publicacionService.getAllPublicaciones(undefined, undefined, true),
distribuidorService.getAllDistribuidores() // Asume que este servicio existe y funciona
]);
setPublicaciones(pubsData);
setDistribuidores(distData);
@@ -100,7 +100,7 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
errors.cantidad = 'La cantidad debe ser un número positivo.';
}
if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) {
if (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0) { // Remito obligatorio en creación y edición
errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.';
}
setLocalErrors(errors);
@@ -123,6 +123,7 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
const dataToSubmit: UpdateEntradaSalidaDistDto = {
cantidad: parseInt(cantidad, 10),
observacion: observacion || undefined,
// Remito no se edita según el DTO de Update
};
await onSubmit(dataToSubmit, initialData.idParte);
} else {
@@ -184,15 +185,15 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
/>
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
<Typography component="legend" variant="body2" sx={{mb:0.5, fontSize:'0.8rem'}}>Tipo Movimiento</Typography>
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} >
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida (a Distribuidor)" disabled={loading || isEditing}/>
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada (de Distribuidor)" disabled={loading || isEditing}/>
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida" disabled={loading || isEditing}/>
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada" disabled={loading || isEditing}/>
</RadioGroup>
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
</FormControl>
<TextField label="Nro. Remito" type="number" value={remito} required={!isEditing}
<TextField label="Nro. Remito" type="number" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading || isEditing} inputProps={{min:1}}