Files
GestionIntegralWeb/Frontend/src/components/Modals/Impresion/StockBobinaLoteFormModal.tsx

374 lines
16 KiB
TypeScript
Raw Normal View History

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;