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 = ({ open, onClose }) => { const [activeStep, setActiveStep] = useState(0); const [loading, setLoading] = useState(false); const [apiError, setApiError] = useState(null); // Step 1 State const [idPlanta, setIdPlanta] = useState(''); 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(null); const [remitoStatusSeverity, setRemitoStatusSeverity] = useState<'success' | 'info'>('info'); const [isDateAutocompleted, setIsDateAutocompleted] = useState(false); // Step 2 State const [bobinasExistentes, setBobinasExistentes] = useState([]); const [nuevasBobinas, setNuevasBobinas] = useState([]); const [detalleErrors, setDetalleErrors] = useState<{ [key: string]: string }>({}); // Dropdowns data const [plantas, setPlantas] = useState([]); const [tiposBobina, setTiposBobina] = useState([]); const [loadingDropdowns, setLoadingDropdowns] = useState(true); const tableContainerRef = useRef(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 ( Datos de Cabecera { setRemito(e.target.value); setIsDateAutocompleted(false); }} required error={!!headerErrors.remito} helperText={headerErrors.remito} disabled={loading} autoFocus /> Planta de Destino {headerErrors.idPlanta && {headerErrors.idPlanta}} setFechaRemito(e.target.value)} InputLabelProps={{ shrink: true }} required error={!!headerErrors.fechaRemito} helperText={headerErrors.fechaRemito} disabled={loading || isDateAutocompleted} InputProps={{ endAdornment: ( isDateAutocompleted && ( setIsDateAutocompleted(false)} edge="end"> ) ) }} /> {remitoStatusMessage && !isVerifying && ( {remitoStatusMessage} )} ); case 1: return ( {bobinasExistentes.length > 0 && ( <> Bobinas ya ingresadas para este remito: Nro. BobinaTipoPeso (Kg) {bobinasExistentes.map(b => ( {b.nroBobina}{b.nombreTipoBobina}{b.peso} ))}
)} Nuevas Bobinas a Ingresar Tipo BobinaNro. BobinaPeso (Kg) {nuevasBobinas.map(bobina => ( handleBobinaChange(bobina.idTemporal, 'nroBobina', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_nro']} helperText={detalleErrors[bobina.idTemporal + '_nro']} /> handleBobinaChange(bobina.idTemporal, 'peso', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_peso']} helperText={detalleErrors[bobina.idTemporal + '_peso']} /> handleRemoveBobina(bobina.idTemporal)}> ))}
); default: return null; } }; return ( theme.palette.grey[500], }} > Ingreso de Bobinas por Lote {steps.map((label) => ( {label} ))} {loadingDropdowns && activeStep === 0 ? : renderStepContent(activeStep)} {apiError && {apiError}} {activeStep === 0 && } {activeStep === 1 && } ); }; export default StockBobinaLoteFormModal;