feat: Implementar ingreso de bobinas por lote
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m13s
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:
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion;
|
|||||||
using GestionIntegral.Api.Services.Impresion;
|
using GestionIntegral.Api.Services.Impresion;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -172,5 +173,72 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
}
|
}
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/stockbobinas/verificar-remito
|
||||||
|
[HttpGet("verificar-remito")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
// CAMBIO: Hacer fechaRemito opcional (nullable)
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BobinaLoteDetalleDto> Bobinas { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,7 +187,7 @@ builder.Services.AddCors(options =>
|
|||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins(
|
policy.WithOrigins(
|
||||||
"http://localhost:5175", // Para desarrollo local
|
"http://localhost:5173", // Para desarrollo local
|
||||||
"https://gestion.eldiaservicios.com" // Para producción
|
"https://gestion.eldiaservicios.com" // Para producción
|
||||||
)
|
)
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
|
|||||||
@@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||||
int? idUsuarioModifico, string? tipoModificacion,
|
int? idUsuarioModifico, string? tipoModificacion,
|
||||||
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
||||||
|
Task<IEnumerable<StockBobinaDto>> 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
}
|
}
|
||||||
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
||||||
return (false, "Tipo de bobina inválido.");
|
return (false, "Tipo de bobina inválido.");
|
||||||
if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
//if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
||||||
return (false, "Planta inválida.");
|
// return (false, "Planta inválida.");
|
||||||
|
|
||||||
|
|
||||||
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
||||||
bobinaExistente.NroBobina = updateDto.NroBobina;
|
bobinaExistente.NroBobina = updateDto.NroBobina;
|
||||||
bobinaExistente.Peso = updateDto.Peso;
|
bobinaExistente.Peso = updateDto.Peso;
|
||||||
bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
//bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
||||||
bobinaExistente.Remito = updateDto.Remito;
|
//bobinaExistente.Remito = updateDto.Remito;
|
||||||
bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
//bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
||||||
// FechaEstado se mantiene ya que el estado no cambia aquí
|
// FechaEstado se mantiene ya que el estado no cambia aquí
|
||||||
|
|
||||||
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
||||||
@@ -383,5 +383,151 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
TipoMod = h.Historial.TipoMod
|
TipoMod = h.Historial.TipoMod
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<StockBobinaDto>> 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<StockBobinaDto>();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface BobinaLoteDetalleDto {
|
||||||
|
idTipoBobina: number;
|
||||||
|
nroBobina: string;
|
||||||
|
peso: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
|
||||||
|
|
||||||
|
export interface CreateStockBobinaLoteDto {
|
||||||
|
idPlanta: number;
|
||||||
|
remito: string;
|
||||||
|
fechaRemito: string; // "yyyy-MM-dd"
|
||||||
|
bobinas: BobinaLoteDetalleDto[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface UpdateFechaRemitoLoteDto {
|
||||||
|
idPlanta: number;
|
||||||
|
remito: string;
|
||||||
|
fechaRemitoActual: string; // "yyyy-MM-dd"
|
||||||
|
nuevaFechaRemito: string; // "yyyy-MM-dd"
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
import EditCalendarIcon from '@mui/icons-material/EditCalendar';
|
||||||
|
|
||||||
import stockBobinaService from '../../services/Impresion/stockBobinaService';
|
import stockBobinaService from '../../services/Impresion/stockBobinaService';
|
||||||
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
|
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 { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
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 StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
||||||
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
||||||
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
|
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
|
||||||
|
import StockBobinaLoteFormModal from '../../components/Modals/Impresion/StockBobinaLoteFormModal';
|
||||||
|
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -38,7 +42,7 @@ const ID_ESTADO_DANADA = 3;
|
|||||||
|
|
||||||
const GestionarStockBobinasPage: React.FC = () => {
|
const GestionarStockBobinasPage: React.FC = () => {
|
||||||
const [stock, setStock] = useState<StockBobinaDto[]>([]);
|
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 [error, setError] = useState<string | null>(null);
|
||||||
const [apiErrorMessage, setApiErrorMessage] = 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 [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
|
||||||
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
|
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
|
||||||
const [filtroRemito, setFiltroRemito] = useState('');
|
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 [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||||
const [filtroFechaHasta, setFiltroFechaHasta] = 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 [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
|
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
|
||||||
|
const [loteModalOpen, setLoteModalOpen] = useState(false);
|
||||||
// Estado para la bobina seleccionada en un modal o menú
|
const [fechaRemitoModalOpen, setFechaRemitoModalOpen] = useState(false);
|
||||||
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
|
|
||||||
|
|
||||||
// Estados para la paginación y el menú de acciones
|
// Estados para la paginación y el menú de acciones
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
@@ -84,7 +87,6 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
const fetchFiltersDropdownData = useCallback(async () => {
|
const fetchFiltersDropdownData = useCallback(async () => {
|
||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
|
|
||||||
const [tiposData, plantasData, estadosData] = await Promise.all([
|
const [tiposData, plantasData, estadosData] = await Promise.all([
|
||||||
tipoBobinaService.getAllTiposBobina(),
|
tipoBobinaService.getAllTiposBobina(),
|
||||||
plantaService.getAllPlantas(),
|
plantaService.getAllPlantas(),
|
||||||
@@ -138,7 +140,7 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
|
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
|
||||||
|
|
||||||
const handleBuscarClick = () => {
|
const handleBuscarClick = () => {
|
||||||
setPage(0); // Resetear la paginación al buscar
|
setPage(0);
|
||||||
cargarStock();
|
cargarStock();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,11 +153,11 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
setFiltroFechaHabilitado(false);
|
setFiltroFechaHabilitado(false);
|
||||||
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
|
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
|
||||||
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
|
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
|
||||||
setStock([]); // Limpiar los resultados actuales
|
setStock([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
|
//const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
|
||||||
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
|
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
|
||||||
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
|
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
@@ -163,71 +165,107 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
|
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) => {
|
const handleLoteModalClose = (refrescar: boolean) => {
|
||||||
if (!bobina) return;
|
setLoteModalOpen(false);
|
||||||
setSelectedBobina(bobina);
|
if (refrescar) {
|
||||||
setApiErrorMessage(null);
|
cargarStock();
|
||||||
setEditModalOpen(true);
|
|
||||||
};
|
|
||||||
const handleCloseEditModal = () => {
|
|
||||||
setEditModalOpen(false);
|
|
||||||
setSelectedBobina(null);
|
|
||||||
if (lastOpenedMenuButtonRef.current) {
|
|
||||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
|
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
|
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; }
|
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) => {
|
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
|
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; }
|
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) => {
|
const handleDeleteBobina = () => {
|
||||||
if (!bobina) return;
|
if (!selectedBobinaForRowMenu) return;
|
||||||
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
|
|
||||||
|
if (selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DISPONIBLE && selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DANADA) {
|
||||||
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
|
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
return;
|
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);
|
setApiErrorMessage(null);
|
||||||
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); }
|
stockBobinaService.deleteIngresoBobina(selectedBobinaForRowMenu.idBobina)
|
||||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
.then(() => cargarStock())
|
||||||
|
.catch((err: any) => {
|
||||||
|
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||||
|
setApiErrorMessage(msg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
handleMenuClose();
|
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) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
setSelectedBobinaForRowMenu(bobina);
|
setSelectedBobinaForRowMenu(bobina);
|
||||||
lastOpenedMenuButtonRef.current = event.currentTarget;
|
lastOpenedMenuButtonRef.current = event.currentTarget;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1. handleMenuClose ahora solo cierra el menú. No limpia el estado de la bobina seleccionada.
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null);
|
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);
|
setSelectedBobinaForRowMenu(null);
|
||||||
if (lastOpenedMenuButtonRef.current) {
|
|
||||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||||
|
|
||||||
const formatDate = (dateString?: string | null) => {
|
const formatDate = (dateString?: string | null) => {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return '-';
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
@@ -285,18 +323,34 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 2,
|
mt: 2
|
||||||
justifyContent: 'flex-end'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>Buscar</Button>
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>Limpiar Filtros</Button>
|
<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>
|
</Box>
|
||||||
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ ml: 'auto' }}>Ingresar Bobina</Button>)}
|
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||||
@@ -333,7 +387,7 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton onClick={(e) => handleMenuOpen(e, b)}
|
<IconButton onClick={(e) => handleMenuOpen(e, b)}
|
||||||
disabled={
|
disabled={
|
||||||
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
|
!(puedeModificarDatos) && // Simplificado, ya que todas las opciones requieren este permiso
|
||||||
!(puedeCambiarEstado) &&
|
!(puedeCambiarEstado) &&
|
||||||
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
|
!((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}>
|
<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 && (
|
{selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
|
||||||
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
<MenuItem onClick={handleOpenEditModal}>
|
||||||
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
|
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos Bobina
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{selectedBobinaForRowMenu && puedeCambiarEstado && (
|
{selectedBobinaForRowMenu && puedeCambiarEstado && (
|
||||||
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
<MenuItem onClick={handleOpenCambioEstadoModal}>
|
||||||
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
|
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{selectedBobinaForRowMenu && puedeEliminar &&
|
{selectedBobinaForRowMenu && puedeEliminar &&
|
||||||
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
|
(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
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
|
||||||
</MenuItem>
|
</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>
|
</Menu>
|
||||||
|
|
||||||
{/* Modales sin cambios */}
|
{/* Modales */}
|
||||||
<StockBobinaIngresoFormModal
|
<StockBobinaIngresoFormModal
|
||||||
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
|
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
|
||||||
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
|
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
|
||||||
/>
|
/>
|
||||||
{editModalOpen && selectedBobina &&
|
|
||||||
<StockBobinaEditFormModal
|
<StockBobinaEditFormModal
|
||||||
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
||||||
initialData={selectedBobina} errorMessage={apiErrorMessage}
|
initialData={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
|
||||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
{cambioEstadoModalOpen && selectedBobina &&
|
|
||||||
<StockBobinaCambioEstadoModal
|
<StockBobinaCambioEstadoModal
|
||||||
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
||||||
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
|
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)}
|
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'
|
|||||||
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
|
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
|
||||||
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
||||||
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
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 {
|
interface GetAllStockBobinasParams {
|
||||||
idTipoBobina?: number | null;
|
idTipoBobina?: number | null;
|
||||||
@@ -50,6 +52,23 @@ const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
|
|||||||
await apiClient.delete(`/stockbobinas/${idBobina}`);
|
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 = {
|
const stockBobinaService = {
|
||||||
getAllStockBobinas,
|
getAllStockBobinas,
|
||||||
getStockBobinaById,
|
getStockBobinaById,
|
||||||
@@ -57,6 +76,9 @@ const stockBobinaService = {
|
|||||||
updateDatosBobinaDisponible,
|
updateDatosBobinaDisponible,
|
||||||
cambiarEstadoBobina,
|
cambiarEstadoBobina,
|
||||||
deleteIngresoBobina,
|
deleteIngresoBobina,
|
||||||
|
verificarRemitoExistente,
|
||||||
|
ingresarLoteBobinas,
|
||||||
|
actualizarFechaRemitoLote,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default stockBobinaService;
|
export default stockBobinaService;
|
||||||
Reference in New Issue
Block a user