feat: Implementar ingreso de bobinas por lote
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m13s

Se introduce una nueva funcionalidad para el ingreso masivo de bobinas a partir de un único remito. Esto agiliza significativamente la carga de datos y reduce errores al evitar la repetición de la planta, número y fecha de remito.

La implementación incluye:
- Un modal maestro-detalle de dos pasos que primero verifica el remito y luego permite la carga de las bobinas.
- Lógica de autocompletado de fecha y feedback al usuario si el remito ya existe.
- Un nuevo endpoint en el backend para procesar el lote de forma transaccional.
This commit is contained in:
2025-11-20 09:50:54 -03:00
parent 29109cff13
commit bc19e184aa
14 changed files with 914 additions and 77 deletions

View File

@@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react';
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
import type { UpdateFechaRemitoLoteDto } from '../../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%', left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 450 },
bgcolor: 'background.paper',
border: '2px solid #000', boxShadow: 24, p: 4
};
interface Props {
open: boolean;
onClose: () => void;
onSubmit: (data: UpdateFechaRemitoLoteDto) => Promise<void>;
bobinaContexto: StockBobinaDto | null;
errorMessage: string | null;
clearErrorMessage: () => void;
}
const StockBobinaFechaRemitoModal: React.FC<Props> = ({ open, onClose, onSubmit, bobinaContexto, errorMessage, clearErrorMessage }) => {
const [nuevaFecha, setNuevaFecha] = useState('');
const [loading, setLoading] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
useEffect(() => {
if (open && bobinaContexto) {
setNuevaFecha(bobinaContexto.fechaRemito.split('T')[0]); // Iniciar con la fecha actual
setLocalError(null);
clearErrorMessage();
}
}, [open, bobinaContexto, clearErrorMessage]);
if (!bobinaContexto) return null;
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!nuevaFecha) {
setLocalError('Debe seleccionar una nueva fecha.');
return;
}
setLoading(true);
try {
const data: UpdateFechaRemitoLoteDto = {
idPlanta: bobinaContexto.idPlanta,
remito: bobinaContexto.remito,
fechaRemitoActual: bobinaContexto.fechaRemito.split('T')[0],
nuevaFechaRemito: nuevaFecha
};
await onSubmit(data);
onClose();
} catch (err) {
// El error de la API es manejado por el prop `errorMessage`
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2">Corregir Fecha de Remito</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Esto cambiará la fecha para <strong>todas</strong> las bobinas del remito <strong>{bobinaContexto.remito}</strong> en la planta <strong>{bobinaContexto.nombrePlanta}</strong>.
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<TextField label="Fecha Actual" value={new Date(bobinaContexto.fechaRemito).toLocaleDateString('es-AR', { timeZone: 'UTC' })} disabled fullWidth margin="normal" />
<TextField label="Nueva Fecha de Remito" type="date" value={nuevaFecha}
onChange={e => { setNuevaFecha(e.target.value); setLocalError(null); }}
required fullWidth margin="normal" InputLabelProps={{ shrink: true }}
error={!!localError} helperText={localError} autoFocus
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading}>
{loading ? <CircularProgress size={24} /> : 'Guardar Nueva Fecha'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaFechaRemitoModal;

View File

@@ -0,0 +1,374 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Stepper, Step, StepLabel,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Divider,
InputAdornment, Tooltip
} from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import EditIcon from '@mui/icons-material/Edit';
import CloseIcon from '@mui/icons-material/Close';
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
import type { CreateStockBobinaLoteDto } from '../../../models/dtos/Impresion/CreateStockBobinaLoteDto';
import type { BobinaLoteDetalleDto } from '../../../models/dtos/Impresion/BobinaLoteDetalleDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
import stockBobinaService from '../../../services/Impresion/stockBobinaService';
import plantaService from '../../../services/Impresion/plantaService';
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%', left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '900px' },
bgcolor: 'background.paper',
border: '2px solid #000', boxShadow: 24, p: 3,
maxHeight: '90vh', overflowY: 'auto', display: 'flex', flexDirection: 'column'
};
interface NuevaBobinaState extends BobinaLoteDetalleDto {
idTemporal: string;
}
interface StockBobinaLoteFormModalProps {
open: boolean;
onClose: (refrescar: boolean) => void;
}
const steps = ['Datos del Remito', 'Ingreso de Bobinas'];
const StockBobinaLoteFormModal: React.FC<StockBobinaLoteFormModalProps> = ({ open, onClose }) => {
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
// Step 1 State
const [idPlanta, setIdPlanta] = useState<number | ''>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(new Date().toISOString().split('T')[0]);
const [headerErrors, setHeaderErrors] = useState<{ [key: string]: string }>({});
const [isVerifying, setIsVerifying] = useState(false);
const [remitoStatusMessage, setRemitoStatusMessage] = useState<string | null>(null);
const [remitoStatusSeverity, setRemitoStatusSeverity] = useState<'success' | 'info'>('info');
const [isDateAutocompleted, setIsDateAutocompleted] = useState(false);
// Step 2 State
const [bobinasExistentes, setBobinasExistentes] = useState<StockBobinaDto[]>([]);
const [nuevasBobinas, setNuevasBobinas] = useState<NuevaBobinaState[]>([]);
const [detalleErrors, setDetalleErrors] = useState<{ [key: string]: string }>({});
// Dropdowns data
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(true);
const tableContainerRef = useRef<HTMLDivElement>(null);
const resetState = useCallback(() => {
setActiveStep(0); setLoading(false); setApiError(null);
setIdPlanta(''); setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]);
setHeaderErrors({}); setBobinasExistentes([]); setNuevasBobinas([]); setDetalleErrors({});
setRemitoStatusMessage(null);
setIsDateAutocompleted(false);
}, []);
useEffect(() => {
const fetchDropdowns = async () => {
setLoadingDropdowns(true);
try {
const [plantasData, tiposData] = await Promise.all([
plantaService.getAllPlantas(),
tipoBobinaService.getAllTiposBobina()
]);
setPlantas(plantasData);
setTiposBobina(tiposData);
} catch (error) {
setApiError("Error al cargar datos necesarios (plantas, tipos).");
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdowns();
} else {
resetState();
}
}, [open, resetState]);
useEffect(() => {
const verificarRemitoParaAutocompletar = async () => {
setRemitoStatusMessage(null);
if (remito.trim() && idPlanta) {
setIsVerifying(true);
try {
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito.trim());
if (existentes.length > 0) {
setFechaRemito(existentes[0].fechaRemito.split('T')[0]);
setRemitoStatusMessage("Remito existente. Se autocompletó la fecha.");
setRemitoStatusSeverity('info');
setIsDateAutocompleted(true);
} else {
setRemitoStatusMessage("Este es un remito nuevo.");
setRemitoStatusSeverity('success');
setIsDateAutocompleted(false);
}
} catch (error) {
console.error("Fallo la verificación automática de remito: ", error);
setRemitoStatusMessage("No se pudo verificar el remito.");
setRemitoStatusSeverity('info');
} finally {
setIsVerifying(false);
}
}
};
const handler = setTimeout(() => {
verificarRemitoParaAutocompletar();
}, 500);
return () => {
clearTimeout(handler);
};
}, [idPlanta, remito]);
const handleClose = () => onClose(false);
const handleNext = async () => {
const errors: { [key: string]: string } = {};
if (!remito.trim()) errors.remito = "El número de remito es obligatorio.";
if (!idPlanta) errors.idPlanta = "Seleccione una planta.";
if (!fechaRemito) errors.fechaRemito = "La fecha es obligatoria.";
if (Object.keys(errors).length > 0) {
setHeaderErrors(errors);
return;
}
setLoading(true); setApiError(null);
try {
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito, fechaRemito);
setBobinasExistentes(existentes);
setActiveStep(1);
} catch (error: any) {
const message = error.response?.data?.message || "Error al verificar el remito.";
setApiError(message);
} finally {
setLoading(false);
}
};
const handleBack = () => setActiveStep(0);
const handleAddBobina = () => {
setNuevasBobinas(prev => [...prev, {
idTemporal: crypto.randomUUID(), idTipoBobina: 0, nroBobina: '', peso: 0
}]);
setTimeout(() => {
tableContainerRef.current?.scrollTo({ top: tableContainerRef.current.scrollHeight, behavior: 'smooth' });
}, 100);
};
const handleRemoveBobina = (idTemporal: string) => {
setNuevasBobinas(prev => prev.filter(b => b.idTemporal !== idTemporal));
};
const handleBobinaChange = (idTemporal: string, field: keyof NuevaBobinaState, value: any) => {
setNuevasBobinas(prev => prev.map(b => b.idTemporal === idTemporal ? { ...b, [field]: value } : b));
};
const handleSubmit = async () => {
const errors: { [key: string]: string } = {};
if (nuevasBobinas.length === 0) {
setApiError("Debe agregar al menos una nueva bobina para guardar.");
return;
}
const todosNrosBobina = new Set(bobinasExistentes.map(b => b.nroBobina));
nuevasBobinas.forEach(b => {
if (!b.idTipoBobina) errors[b.idTemporal + '_tipo'] = "Requerido";
if (!b.nroBobina.trim()) errors[b.idTemporal + '_nro'] = "Requerido";
if ((b.peso || 0) <= 0) errors[b.idTemporal + '_peso'] = "Inválido";
if (todosNrosBobina.has(b.nroBobina.trim())) errors[b.idTemporal + '_nro'] = "Duplicado";
todosNrosBobina.add(b.nroBobina.trim());
});
if (Object.keys(errors).length > 0) {
setDetalleErrors(errors);
return;
}
setLoading(true); setApiError(null);
try {
const lote: CreateStockBobinaLoteDto = {
idPlanta: Number(idPlanta),
remito: remito.trim(),
fechaRemito,
bobinas: nuevasBobinas.map(({ idTipoBobina, nroBobina, peso }) => ({
idTipoBobina: Number(idTipoBobina), nroBobina: nroBobina.trim(), peso: Number(peso)
}))
};
await stockBobinaService.ingresarLoteBobinas(lote);
onClose(true);
} catch (error: any) {
const message = error.response?.data?.message || "Error al guardar el lote de bobinas.";
setApiError(message);
} finally {
setLoading(false);
}
};
const renderStepContent = (step: number) => {
switch (step) {
case 0:
return (
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="h6">Datos de Cabecera</Typography>
<TextField label="Número de Remito" value={remito}
onChange={e => {
setRemito(e.target.value);
setIsDateAutocompleted(false);
}}
required error={!!headerErrors.remito} helperText={headerErrors.remito} disabled={loading} autoFocus
/>
<FormControl fullWidth error={!!headerErrors.idPlanta}>
<InputLabel id="planta-label" required>Planta de Destino</InputLabel>
<Select labelId="planta-label" value={idPlanta} label="Planta de Destino"
onChange={e => {
setIdPlanta(e.target.value as number);
setIsDateAutocompleted(false);
}}
disabled={loading || loadingDropdowns}
endAdornment={isVerifying && (<InputAdornment position="end" sx={{ mr: 2 }}><CircularProgress size={20} /></InputAdornment>)}
>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
{headerErrors.idPlanta && <Typography color="error" variant="caption">{headerErrors.idPlanta}</Typography>}
</FormControl>
<TextField
label="Fecha de Remito"
type="date"
value={fechaRemito}
onChange={e => setFechaRemito(e.target.value)}
InputLabelProps={{ shrink: true }}
required
error={!!headerErrors.fechaRemito}
helperText={headerErrors.fechaRemito}
disabled={loading || isDateAutocompleted}
InputProps={{
endAdornment: (
isDateAutocompleted && (
<InputAdornment position="end">
<Tooltip title="Editar fecha">
<IconButton onClick={() => setIsDateAutocompleted(false)} edge="end">
<EditIcon />
</IconButton>
</Tooltip>
</InputAdornment>
)
)
}}
/>
<Box sx={{ minHeight: 48, mt: 1 }}>
{remitoStatusMessage && !isVerifying && (
<Alert severity={remitoStatusSeverity} icon={false} variant="outlined">
{remitoStatusMessage}
</Alert>
)}
</Box>
</Box>
);
case 1:
return (
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{bobinasExistentes.length > 0 && (
<>
<Typography variant="subtitle1" gutterBottom>Bobinas ya ingresadas para este remito:</Typography>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: '150px', mb: 2 }}>
<Table size="small" stickyHeader>
<TableHead><TableRow><TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell align="right">Peso (Kg)</TableCell></TableRow></TableHead>
<TableBody>
{bobinasExistentes.map(b => (
<TableRow key={b.idBobina}><TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell><TableCell align="right">{b.peso}</TableCell></TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
)}
<Typography variant="h6">Nuevas Bobinas a Ingresar</Typography>
<TableContainer component={Paper} variant="outlined" sx={{ flexGrow: 1, my: 1, minHeight: '150px' }} ref={tableContainerRef}>
<Table size="small" stickyHeader>
<TableHead><TableRow><TableCell sx={{ minWidth: 200 }}>Tipo Bobina</TableCell><TableCell>Nro. Bobina</TableCell><TableCell>Peso (Kg)</TableCell><TableCell></TableCell></TableRow></TableHead>
<TableBody>
{nuevasBobinas.map(bobina => (
<TableRow key={bobina.idTemporal}>
<TableCell><FormControl fullWidth size="small" error={!!detalleErrors[bobina.idTemporal + '_tipo']}><Select value={bobina.idTipoBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'idTipoBobina', e.target.value)} disabled={loadingDropdowns}><MenuItem value={0} disabled>Seleccione</MenuItem>{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}</Select></FormControl></TableCell>
<TableCell><TextField fullWidth size="small" value={bobina.nroBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'nroBobina', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_nro']} helperText={detalleErrors[bobina.idTemporal + '_nro']} /></TableCell>
<TableCell><TextField fullWidth size="small" type="number" value={bobina.peso || ''} onChange={e => handleBobinaChange(bobina.idTemporal, 'peso', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_peso']} helperText={detalleErrors[bobina.idTemporal + '_peso']} /></TableCell>
<TableCell><IconButton size="small" color="error" onClick={() => handleRemoveBobina(bobina.idTemporal)}><DeleteOutlineIcon /></IconButton></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddBobina} sx={{ mt: 1 }}>Agregar Bobina</Button>
</Box>
);
default:
return null;
}
};
return (
<Modal open={open} onClose={handleClose}>
<Box sx={modalStyle}>
<IconButton
aria-label="close"
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: (theme) => theme.palette.grey[500],
}}
>
<CloseIcon />
</IconButton>
<Typography variant="h5" component="h2" gutterBottom>Ingreso de Bobinas por Lote</Typography>
<Stepper activeStep={activeStep} sx={{ mb: 2 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box sx={{ flexGrow: 1, overflowY: 'auto' }}>
{loadingDropdowns && activeStep === 0 ? <Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}><CircularProgress /></Box> : renderStepContent(activeStep)}
</Box>
{apiError && <Alert severity="error" sx={{ mt: 2, flexShrink: 0 }}>{apiError}</Alert>}
<Divider sx={{ my: 2, flexShrink: 0 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between', pt: 1, flexShrink: 0 }}>
<Button color="inherit" disabled={activeStep === 0 || loading} onClick={handleBack}>
Atrás
</Button>
<Box>
{activeStep === 0 && <Button onClick={handleNext} variant="contained" disabled={loading || loadingDropdowns}>{loading ? <CircularProgress size={24} /> : 'Verificar y Continuar'}</Button>}
{activeStep === 1 && <Button onClick={handleSubmit} variant="contained" color="success" disabled={loading}>{loading ? <CircularProgress size={24} /> : 'Guardar Lote'}</Button>}
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaLoteFormModal;

View File

@@ -0,0 +1,5 @@
export interface BobinaLoteDetalleDto {
idTipoBobina: number;
nroBobina: string;
peso: number;
}

View File

@@ -0,0 +1,8 @@
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
export interface CreateStockBobinaLoteDto {
idPlanta: number;
remito: string;
fechaRemito: string; // "yyyy-MM-dd"
bobinas: BobinaLoteDetalleDto[];
}

View File

@@ -0,0 +1,6 @@
export interface UpdateFechaRemitoLoteDto {
idPlanta: number;
remito: string;
fechaRemitoActual: string; // "yyyy-MM-dd"
nuevaFechaRemito: string; // "yyyy-MM-dd"
}

View File

@@ -11,6 +11,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import EditCalendarIcon from '@mui/icons-material/EditCalendar';
import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
@@ -24,10 +25,13 @@ import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/Cambiar
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
import StockBobinaFechaRemitoModal from '../../components/Modals/Impresion/StockBobinaFechaRemitoModal';
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
import StockBobinaLoteFormModal from '../../components/Modals/Impresion/StockBobinaLoteFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
@@ -38,7 +42,7 @@ const ID_ESTADO_DANADA = 3;
const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]);
const [loading, setLoading] = useState(false); // No carga al inicio
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
@@ -48,7 +52,7 @@ const GestionarStockBobinasPage: React.FC = () => {
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState('');
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false); // <-- NUEVO
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false);
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
@@ -62,9 +66,8 @@ const GestionarStockBobinasPage: React.FC = () => {
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
// Estado para la bobina seleccionada en un modal o menú
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
const [loteModalOpen, setLoteModalOpen] = useState(false);
const [fechaRemitoModalOpen, setFechaRemitoModalOpen] = useState(false);
// Estados para la paginación y el menú de acciones
const [page, setPage] = useState(0);
@@ -84,7 +87,6 @@ const GestionarStockBobinasPage: React.FC = () => {
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
@@ -138,7 +140,7 @@ const GestionarStockBobinasPage: React.FC = () => {
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
const handleBuscarClick = () => {
setPage(0); // Resetear la paginación al buscar
setPage(0);
cargarStock();
};
@@ -151,83 +153,119 @@ const GestionarStockBobinasPage: React.FC = () => {
setFiltroFechaHabilitado(false);
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
setStock([]); // Limpiar los resultados actuales
setStock([]);
setError(null);
};
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
//const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.ingresarBobina(data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenEditModal = (bobina: StockBobinaDto | null) => {
if (!bobina) return;
setSelectedBobina(bobina);
setApiErrorMessage(null);
setEditModalOpen(true);
};
const handleCloseEditModal = () => {
setEditModalOpen(false);
setSelectedBobina(null);
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
const handleLoteModalClose = (refrescar: boolean) => {
setLoteModalOpen(false);
if (refrescar) {
cargarStock();
}
};
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto | null) => {
if (!bobina) return;
setSelectedBobina(bobina);
setApiErrorMessage(null);
setCambioEstadoModalOpen(true);
};
const handleCloseCambioEstadoModal = () => setCambioEstadoModalOpen(false);
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
};
const handleDeleteBobina = async (bobina: StockBobinaDto | null) => {
if (!bobina) return;
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
const handleDeleteBobina = () => {
if (!selectedBobinaForRowMenu) return;
if (selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DISPONIBLE && selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DANADA) {
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
handleMenuClose();
return;
}
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${bobina.idBobina})?`)) {
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${selectedBobinaForRowMenu.idBobina})?`)) {
setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
stockBobinaService.deleteIngresoBobina(selectedBobinaForRowMenu.idBobina)
.then(() => cargarStock())
.catch((err: any) => {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(msg);
});
}
handleMenuClose();
};
const handleSubmitFechaRemitoModal = async (data: UpdateFechaRemitoLoteDto) => {
setApiErrorMessage(null);
try {
await stockBobinaService.actualizarFechaRemitoLote(data);
cargarStock(); // Recargar la grilla para ver el cambio
} catch (err: any) {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar la fecha del remito.';
setApiErrorMessage(msg);
throw err;
}
};
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget);
setSelectedBobinaForRowMenu(bobina);
lastOpenedMenuButtonRef.current = event.currentTarget;
};
// 1. handleMenuClose ahora solo cierra el menú. No limpia el estado de la bobina seleccionada.
const handleMenuClose = () => {
setAnchorEl(null);
};
// 2. Handlers para abrir modales. Abren el modal y cierran el menú.
const handleOpenEditModal = () => {
setEditModalOpen(true);
handleMenuClose();
};
const handleOpenCambioEstadoModal = () => {
setCambioEstadoModalOpen(true);
handleMenuClose();
};
const handleOpenFechaRemitoModal = () => {
setFechaRemitoModalOpen(true);
handleMenuClose();
};
// 3. Handlers para cerrar modales. Cierran el modal y AHORA limpian el estado de la bobina seleccionada.
const handleCloseEditModal = () => {
setEditModalOpen(false);
setSelectedBobinaForRowMenu(null);
};
const handleCloseCambioEstadoModal = () => {
setCambioEstadoModalOpen(false);
setSelectedBobinaForRowMenu(null);
};
const handleCloseFechaRemitoModal = () => {
setFechaRemitoModalOpen(false);
setSelectedBobinaForRowMenu(null);
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
}
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => {
if (!dateString) return '-';
const date = new Date(dateString);
@@ -285,18 +323,34 @@ const GestionarStockBobinasPage: React.FC = () => {
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: 2,
mb: 2,
justifyContent: 'flex-end'
mt: 2
}}
>
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>Buscar</Button>
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>Limpiar Filtros</Button>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>
Buscar
</Button>
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>
Limpiar Filtros
</Button>
</Box>
{puedeIngresar && (
<Box sx={{ display: 'flex', gap: 2 }}>
{/*
<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal}>
Ingreso Individual
</Button>
*/}
<Button variant="contained" color="secondary" startIcon={<AddIcon />} onClick={() => setLoteModalOpen(true)}>
Ingreso por Remito (Lote)
</Button>
</Box>
)}
</Box>
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ ml: 'auto' }}>Ingresar Bobina</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
@@ -333,7 +387,7 @@ const GestionarStockBobinasPage: React.FC = () => {
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, b)}
disabled={
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
!(puedeModificarDatos) && // Simplificado, ya que todas las opciones requieren este permiso
!(puedeCambiarEstado) &&
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
}
@@ -353,49 +407,56 @@ const GestionarStockBobinasPage: React.FC = () => {
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{selectedBobinaForRowMenu && puedeModificarDatos && (
<MenuItem onClick={handleOpenFechaRemitoModal}>
<EditCalendarIcon fontSize="small" sx={{ mr: 1 }} /> Corregir Fecha Remito
</MenuItem>
)}
{selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
<MenuItem onClick={handleOpenEditModal}>
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos Bobina
</MenuItem>
)}
{selectedBobinaForRowMenu && puedeCambiarEstado && (
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
<MenuItem onClick={handleOpenCambioEstadoModal}>
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
</MenuItem>
)}
{selectedBobinaForRowMenu && puedeEliminar &&
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobinaForRowMenu)}>
<MenuItem onClick={handleDeleteBobina}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
</MenuItem>
)}
{selectedBobinaForRowMenu &&
!((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) &&
!(puedeCambiarEstado) &&
!(((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)) &&
<MenuItem disabled>Sin acciones disponibles</MenuItem>
}
</Menu>
{/* Modales sin cambios */}
{/* Modales */}
<StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
{editModalOpen && selectedBobina &&
<StockBobinaEditFormModal
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
{cambioEstadoModalOpen && selectedBobina &&
<StockBobinaCambioEstadoModal
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
<StockBobinaEditFormModal
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
<StockBobinaCambioEstadoModal
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
<StockBobinaLoteFormModal
open={loteModalOpen}
onClose={handleLoteModalClose}
/>
<StockBobinaFechaRemitoModal
open={fechaRemitoModalOpen}
onClose={handleCloseFechaRemitoModal}
onSubmit={handleSubmitFechaRemitoModal}
bobinaContexto={selectedBobinaForRowMenu}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};

View File

@@ -3,6 +3,8 @@ import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { CreateStockBobinaLoteDto } from '../../models/dtos/Impresion/CreateStockBobinaLoteDto';
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
interface GetAllStockBobinasParams {
idTipoBobina?: number | null;
@@ -50,6 +52,23 @@ const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
await apiClient.delete(`/stockbobinas/${idBobina}`);
};
const verificarRemitoExistente = async (idPlanta: number, remito: string, fechaRemito?: string | null): Promise<StockBobinaDto[]> => {
const params: { idPlanta: number; remito: string; fechaRemito?: string } = { idPlanta, remito };
if (fechaRemito) {
params.fechaRemito = fechaRemito;
}
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas/verificar-remito', { params });
return response.data;
};
const ingresarLoteBobinas = async (data: CreateStockBobinaLoteDto): Promise<void> => {
await apiClient.post('/stockbobinas/lote', data);
};
const actualizarFechaRemitoLote = async (data: UpdateFechaRemitoLoteDto): Promise<void> => {
await apiClient.put('/stockbobinas/actualizar-fecha-remito', data);
};
const stockBobinaService = {
getAllStockBobinas,
getStockBobinaById,
@@ -57,6 +76,9 @@ const stockBobinaService = {
updateDatosBobinaDisponible,
cambiarEstadoBobina,
deleteIngresoBobina,
verificarRemitoExistente,
ingresarLoteBobinas,
actualizarFechaRemitoLote,
};
export default stockBobinaService;