diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs index 02212c4..6df07a6 100644 --- a/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs @@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion; using GestionIntegral.Api.Services.Impresion; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -127,7 +128,7 @@ namespace GestionIntegral.Api.Controllers.Impresion if (!ModelState.IsValid) return BadRequest(ModelState); // Validaciones de DTO (Required, Range, etc.) var userId = GetCurrentUserId(); if (userId == null) return Unauthorized(); - + // La validación de que IdPublicacion/IdSeccion son requeridos para estado "En Uso" // ahora está más robusta en el servicio. Se puede quitar del controlador // si se prefiere que el servicio sea la única fuente de verdad para esa lógica. @@ -172,5 +173,72 @@ namespace GestionIntegral.Api.Controllers.Impresion } return NoContent(); } + + // GET: api/stockbobinas/verificar-remito + [HttpGet("verificar-remito")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + // CAMBIO: Hacer fechaRemito opcional (nullable) + public async Task VerificarRemito([FromQuery, BindRequired] int idPlanta, [FromQuery, BindRequired] string remito, [FromQuery] DateTime? fechaRemito) + { + if (!TienePermiso(PermisoIngresarBobina)) return Forbid(); + + try + { + // Pasamos el parámetro nullable al servicio + var bobinasExistentes = await _stockBobinaService.VerificarRemitoExistenteAsync(idPlanta, remito, fechaRemito); + return Ok(bobinasExistentes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al verificar remito {Remito} para planta {IdPlanta}", remito, idPlanta); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al verificar el remito."); + } + } + + [HttpPut("actualizar-fecha-remito")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task ActualizarFechaRemitoLote([FromBody] UpdateFechaRemitoLoteDto dto) + { + // Reutilizamos el permiso de modificar datos, ya que es una corrección. + if (!TienePermiso(PermisoModificarDatos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _stockBobinaService.ActualizarFechaRemitoLoteAsync(dto, userId.Value); + + if (!exito) + { + return BadRequest(new { message = error }); + } + + return NoContent(); + } + + // POST: api/stockbobinas/lote + [HttpPost("lote")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task IngresarLoteBobinas([FromBody] CreateStockBobinaLoteDto loteDto) + { + if (!TienePermiso(PermisoIngresarBobina)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _stockBobinaService.IngresarBobinaLoteAsync(loteDto, userId.Value); + + if (!exito) + { + return BadRequest(new { message = error }); + } + + return NoContent(); // 204 es una buena respuesta para un lote procesado exitosamente sin devolver contenido. + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs new file mode 100644 index 0000000..c7c1ff7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs @@ -0,0 +1,11 @@ +// Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class BobinaLoteDetalleDto + { + public int IdTipoBobina { get; set; } + public string NroBobina { get; set; } = string.Empty; + public int Peso { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs new file mode 100644 index 0000000..9a95f94 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs @@ -0,0 +1,23 @@ +// Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs + +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreateStockBobinaLoteDto + { + [Required] + public int IdPlanta { get; set; } + + [Required] + [StringLength(15)] + public string Remito { get; set; } = string.Empty; + + [Required] + public DateTime FechaRemito { get; set; } + + [Required] + [MinLength(1, ErrorMessage = "Debe ingresar al menos una bobina.")] + public List Bobinas { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs new file mode 100644 index 0000000..3f98583 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs @@ -0,0 +1,21 @@ +// Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs + +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class UpdateFechaRemitoLoteDto + { + [Required] + public int IdPlanta { get; set; } + + [Required] + public required string Remito { get; set; } + + [Required] + public DateTime FechaRemitoActual { get; set; } // Para seguridad, nos aseguramos de cambiar el lote correcto + + [Required] + public DateTime NuevaFechaRemito { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 87684e2..064ad81 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -187,7 +187,7 @@ builder.Services.AddCors(options => policy => { policy.WithOrigins( - "http://localhost:5175", // Para desarrollo local + "http://localhost:5173", // Para desarrollo local "https://gestion.eldiaservicios.com" // Para producción ) .AllowAnyHeader() diff --git a/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs index 9a5fc4a..33f8dd8 100644 --- a/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs +++ b/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs @@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro); + Task> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito); + Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs index e899939..ff685e7 100644 --- a/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs +++ b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs @@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion } if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null) return (false, "Tipo de bobina inválido."); - if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null) - return (false, "Planta inválida."); + //if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null) + // return (false, "Planta inválida."); bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina; bobinaExistente.NroBobina = updateDto.NroBobina; bobinaExistente.Peso = updateDto.Peso; - bobinaExistente.IdPlanta = updateDto.IdPlanta; - bobinaExistente.Remito = updateDto.Remito; - bobinaExistente.FechaRemito = updateDto.FechaRemito.Date; + //bobinaExistente.IdPlanta = updateDto.IdPlanta; + //bobinaExistente.Remito = updateDto.Remito; + //bobinaExistente.FechaRemito = updateDto.FechaRemito.Date; // FechaEstado se mantiene ya que el estado no cambia aquí var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados"); @@ -383,5 +383,151 @@ namespace GestionIntegral.Api.Services.Impresion TipoMod = h.Historial.TipoMod }).ToList(); } + + public async Task> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito) + { + // Si la fecha tiene valor, filtramos por ese día exacto. Si no, busca en cualquier fecha. + DateTime? fechaDesde = fechaRemito?.Date; + DateTime? fechaHasta = fechaRemito?.Date; + + var bobinas = await _stockBobinaRepository.GetAllAsync(null, null, idPlanta, null, remito, fechaDesde, fechaHasta); + + var dtos = new List(); + foreach (var bobina in bobinas) + { + dtos.Add(await MapToDto(bobina)); + } + return dtos; + } + + public async Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario) + { + // 1. Buscar todas las bobinas que coinciden con el lote a modificar. + var bobinasAActualizar = await _stockBobinaRepository.GetAllAsync( + idTipoBobina: null, + nroBobinaFilter: null, + idPlanta: dto.IdPlanta, + idEstadoBobina: null, + remitoFilter: dto.Remito, + fechaDesde: dto.FechaRemitoActual.Date, + fechaHasta: dto.FechaRemitoActual.Date + ); + + if (!bobinasAActualizar.Any()) + { + return (false, "No se encontraron bobinas para el remito, planta y fecha especificados. Es posible que ya hayan sido modificados."); + } + + // 2. Iniciar una transacción para asegurar que todas las actualizaciones se completen o ninguna. + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + // 3. Iterar sobre cada bobina y actualizarla. + foreach (var bobina in bobinasAActualizar) + { + // Modificamos solo la fecha del remito. + bobina.FechaRemito = dto.NuevaFechaRemito.Date; + + // Reutilizamos el método UpdateAsync que ya maneja la lógica de historial. + // Le pasamos un mensaje específico para el historial. + await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, "Fecha Remito Corregida"); + } + + // 4. Si todo salió bien, confirmar la transacción. + transaction.Commit(); + _logger.LogInformation( + "{Count} bobinas del remito {Remito} (Planta ID {IdPlanta}) actualizadas a nueva fecha {NuevaFecha} por Usuario ID {IdUsuario}.", + bobinasAActualizar.Count(), dto.Remito, dto.IdPlanta, dto.NuevaFechaRemito.Date, idUsuario + ); + return (true, null); + } + catch (Exception ex) + { + // 5. Si algo falla, revertir todos los cambios. + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error transaccional al actualizar fecha de remito {Remito}.", dto.Remito); + return (false, $"Error interno al actualizar el lote: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario) + { + // --- FASE 1: VALIDACIÓN PREVIA (FUERA DE LA TRANSACCIÓN) --- + + // Validación de la cabecera + if (await _plantaRepository.GetByIdAsync(loteDto.IdPlanta) == null) + return (false, "La planta especificada no es válida."); + + // Validación de cada bobina del lote + foreach (var bobinaDetalle in loteDto.Bobinas) + { + if (await _tipoBobinaRepository.GetByIdAsync(bobinaDetalle.IdTipoBobina) == null) + { + return (false, $"El tipo de bobina con ID {bobinaDetalle.IdTipoBobina} no es válido."); + } + + // Esta es la lectura que causaba el bloqueo. Ahora se hace ANTES de la transacción. + if (await _stockBobinaRepository.GetByNroBobinaAsync(bobinaDetalle.NroBobina) != null) + { + return (false, $"El número de bobina '{bobinaDetalle.NroBobina}' ya existe en el sistema."); + } + } + + // Validación de números de bobina duplicados dentro del mismo lote + var nrosBobinaEnLote = loteDto.Bobinas.Select(b => b.NroBobina.Trim()).ToList(); + if (nrosBobinaEnLote.Count != nrosBobinaEnLote.Distinct().Count()) + { + var duplicado = nrosBobinaEnLote.GroupBy(n => n).Where(g => g.Count() > 1).First().Key; + return (false, $"El número de bobina '{duplicado}' está duplicado en el lote que intenta ingresar."); + } + + + // --- FASE 2: ESCRITURA TRANSACCIONAL --- + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + // Ahora este bucle solo contiene operaciones de escritura. No habrá bloqueos. + foreach (var bobinaDetalle in loteDto.Bobinas) + { + var nuevaBobina = new StockBobina + { + IdTipoBobina = bobinaDetalle.IdTipoBobina, + NroBobina = bobinaDetalle.NroBobina, + Peso = bobinaDetalle.Peso, + IdPlanta = loteDto.IdPlanta, + Remito = loteDto.Remito, + FechaRemito = loteDto.FechaRemito.Date, + IdEstadoBobina = 1, // 1 = Disponible + FechaEstado = loteDto.FechaRemito.Date, + IdPublicacion = null, + IdSeccion = null, + Obs = null + }; + + var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction); + if (bobinaCreada == null) + { + throw new DataException($"No se pudo crear el registro para la bobina '{nuevaBobina.NroBobina}'."); + } + } + + transaction.Commit(); + _logger.LogInformation("Lote de {Count} bobinas para remito {Remito} ingresado por Usuario ID {UserId}.", loteDto.Bobinas.Count, loteDto.Remito, idUsuario); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error al ingresar lote de bobinas para remito {Remito}", loteDto.Remito); + return (false, $"Error interno al procesar el lote: {ex.Message}"); + } + } } } \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaFechaRemitoModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaFechaRemitoModal.tsx new file mode 100644 index 0000000..26f9652 --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/StockBobinaFechaRemitoModal.tsx @@ -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; + bobinaContexto: StockBobinaDto | null; + errorMessage: string | null; + clearErrorMessage: () => void; +} + +const StockBobinaFechaRemitoModal: React.FC = ({ open, onClose, onSubmit, bobinaContexto, errorMessage, clearErrorMessage }) => { + const [nuevaFecha, setNuevaFecha] = useState(''); + const [loading, setLoading] = useState(false); + const [localError, setLocalError] = useState(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) => { + 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 ( + + + Corregir Fecha de Remito + + Esto cambiará la fecha para todas las bobinas del remito {bobinaContexto.remito} en la planta {bobinaContexto.nombrePlanta}. + + + + { setNuevaFecha(e.target.value); setLocalError(null); }} + required fullWidth margin="normal" InputLabelProps={{ shrink: true }} + error={!!localError} helperText={localError} autoFocus + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default StockBobinaFechaRemitoModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaLoteFormModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaLoteFormModal.tsx new file mode 100644 index 0000000..61b113e --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/StockBobinaLoteFormModal.tsx @@ -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 = ({ 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; \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/BobinaLoteDetalleDto.ts b/Frontend/src/models/dtos/Impresion/BobinaLoteDetalleDto.ts new file mode 100644 index 0000000..94276a7 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/BobinaLoteDetalleDto.ts @@ -0,0 +1,5 @@ +export interface BobinaLoteDetalleDto { + idTipoBobina: number; + nroBobina: string; + peso: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreateStockBobinaLoteDto.ts b/Frontend/src/models/dtos/Impresion/CreateStockBobinaLoteDto.ts new file mode 100644 index 0000000..9c3ad32 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreateStockBobinaLoteDto.ts @@ -0,0 +1,8 @@ +import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto'; + +export interface CreateStockBobinaLoteDto { + idPlanta: number; + remito: string; + fechaRemito: string; // "yyyy-MM-dd" + bobinas: BobinaLoteDetalleDto[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/UpdateFechaRemitoLoteDto.ts b/Frontend/src/models/dtos/Impresion/UpdateFechaRemitoLoteDto.ts new file mode 100644 index 0000000..3f7222c --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/UpdateFechaRemitoLoteDto.ts @@ -0,0 +1,6 @@ +export interface UpdateFechaRemitoLoteDto { + idPlanta: number; + remito: string; + fechaRemitoActual: string; // "yyyy-MM-dd" + nuevaFechaRemito: string; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx index effa1a7..2536862 100644 --- a/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx +++ b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx @@ -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([]); - const [loading, setLoading] = useState(false); // No carga al inicio + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [apiErrorMessage, setApiErrorMessage] = useState(null); @@ -48,7 +52,7 @@ const GestionarStockBobinasPage: React.FC = () => { const [filtroPlanta, setFiltroPlanta] = useState(''); const [filtroEstadoBobina, setFiltroEstadoBobina] = useState(''); const [filtroRemito, setFiltroRemito] = useState(''); - const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState(false); // <-- NUEVO + const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState(false); const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); const [filtroFechaHasta, setFiltroFechaHasta] = useState(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(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, 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) => { 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 = () => { - - + + + + + {puedeIngresar && ( + + {/* + + */} + + + )} - {puedeIngresar && ()} - {loading && } @@ -333,7 +387,7 @@ const GestionarStockBobinasPage: React.FC = () => { 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 = () => { )} + {selectedBobinaForRowMenu && puedeModificarDatos && ( + + Corregir Fecha Remito + + )} {selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && ( - { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}> - Editar Datos + + Editar Datos Bobina )} {selectedBobinaForRowMenu && puedeCambiarEstado && ( - { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}> + Cambiar Estado )} {selectedBobinaForRowMenu && puedeEliminar && (selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && ( - handleDeleteBobina(selectedBobinaForRowMenu)}> + Eliminar Ingreso )} - {selectedBobinaForRowMenu && - !((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) && - !(puedeCambiarEstado) && - !(((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)) && - Sin acciones disponibles - } - {/* Modales sin cambios */} + {/* Modales */} setApiErrorMessage(null)} /> - {editModalOpen && selectedBobina && - setApiErrorMessage(null)} - /> - } - {cambioEstadoModalOpen && selectedBobina && - setApiErrorMessage(null)} - /> - } + setApiErrorMessage(null)} + /> + setApiErrorMessage(null)} + /> + + setApiErrorMessage(null)} + /> ); }; diff --git a/Frontend/src/services/Impresion/stockBobinaService.ts b/Frontend/src/services/Impresion/stockBobinaService.ts index e424261..a02e4ea 100644 --- a/Frontend/src/services/Impresion/stockBobinaService.ts +++ b/Frontend/src/services/Impresion/stockBobinaService.ts @@ -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 => { await apiClient.delete(`/stockbobinas/${idBobina}`); }; +const verificarRemitoExistente = async (idPlanta: number, remito: string, fechaRemito?: string | null): Promise => { + const params: { idPlanta: number; remito: string; fechaRemito?: string } = { idPlanta, remito }; + if (fechaRemito) { + params.fechaRemito = fechaRemito; + } + const response = await apiClient.get('/stockbobinas/verificar-remito', { params }); + return response.data; +}; + +const ingresarLoteBobinas = async (data: CreateStockBobinaLoteDto): Promise => { + await apiClient.post('/stockbobinas/lote', data); +}; + +const actualizarFechaRemitoLote = async (data: UpdateFechaRemitoLoteDto): Promise => { + 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; \ No newline at end of file