@using Blazored.Modal @using Blazored.Modal.Services @using Domain.Dtos.Stock @using Microsoft.AspNetCore.Components.Web @using System.Globalization @inject IToastService toastService @inject IStockScanService stockScanService @inject IModalService Modal @inherits LayoutComponentBase @code { [CascadingParameter] BlazoredModalInstance ModalInstance { get; set; } = default!; [Parameter] public int? ProductId { get; set; } [Parameter] public int? LocationId { get; set; } [Parameter] public List? SetItems { get; set; } [Parameter] public List Snapshot { get; set; } = new(); // Mapa: BusinessKey -> Cantidad ya en la expedición private Dictionary _alreadyInExpedition = new(); private bool HasLastScan { get; set; } //private readonly List SelectedItems = new(); private List StockList = new(); private string InputCode { get; set; } = string.Empty; private ElementReference scanInput; protected override void OnParametersSet() { HasLastScan = false; _alreadyInExpedition = Snapshot .GroupBy(x => x.BusinessKey) .ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity)); // Precargar la tabla del modal con lo que ya estaba en la expedición StockList = Snapshot.Select(s => new StockDisplayRow { ProductId = s.ProductId, StockItemId = s.StockitemId, ProductName = s.ProductName ?? "", Batch = s.Batch, Serial = s.Serial, Expiration = s.Expiration?.ToDateTime(TimeOnly.MinValue), LocationId = s.LocationId, TraceabilityType = s.TraceabilityType, BusinessKey = s.BusinessKey, Selected = s.Quantity, // cantidad actual en la expedición Available = 0 // opcional: si querés recalcular, traer de BD }).ToList(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) await scanInput.FocusAsync(); } private async Task OnKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter") await HandleScan(); } private async Task HandleKeyDown(KeyboardEventArgs e) { if (e.Key is "Enter" or "NumpadEnter") { await HandleScan(); await scanInput.FocusAsync(); } } private async Task HandleScan() { try { var raw = InputCode?.Trim(); if (string.IsNullOrWhiteSpace(raw)) return; // Parsear y resolver candidato desde backend var s = await stockScanService.ParseAndMatchAsync(raw, LocationId ?? 1); // limpiar input y devolver foco InputCode = string.Empty; //wait Refocus(); if (s is null) { toastService.ShowWarning("No se encontró un artículo para ese código."); return; } HasLastScan = true; // Mapear a fila del modal var row = new StockDisplayRow { StockItemId = s.StockItemId, ProductId = s.ProductId, ProductName = s.ProductName ?? string.Empty, Batch = s.Batch ?? string.Empty, Serial = s.Serial ?? string.Empty, Expiration = s.Expiration, LocationId = s.LocationId, Available = s.Quantity, TraceabilityType = s.TraceabilityType, Selected = 0 }; // Clave de negocio + contexto del snapshot row.BusinessKey = BuildKeyFromRow(row); var alreadyInExpedition = _alreadyInExpedition.TryGetValue(row.BusinessKey, out var q) ? q : 0m; var selectedInModal = StockList .Where(x => x.BusinessKey == row.BusinessKey) .Sum(x => x.Selected); var effectiveAvailable = row.Available - selectedInModal; // Reglas por trazabilidad // SERIAL*: nunca duplicar ni incrementar if (!string.IsNullOrWhiteSpace(row.Serial)) { // buscar si ya existe la misma BusinessKey en la tabla del modal var existingSerial = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey); // 🔄 REFRESH siempre que tengamos la fila (si existe), antes de cualquier return if (existingSerial is not null) { existingSerial.Available = row.Available ; // actualizar disponible con el del re-escaneo } var existsInModal = existingSerial is not null; //var existsInModal = StockList.Any(x => x.BusinessKey == row.BusinessKey); if (alreadyInExpedition > 0 || existsInModal) { toastService.ShowWarning("Este serial ya está agregado."); await Refocus(); return; } if (effectiveAvailable <= 0) { toastService.ShowWarning("No hay disponible para este serial."); await Refocus(); return; } row.Selected = 1; // serial siempre = 1 StockList.Insert(0, row); await Refocus(); return; } // BATCH / NONE: sumar sin pasar el disponible efectivo if (effectiveAvailable <= 0) { var existingSerial = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey); // 🔄 REFRESH siempre que tengamos la fila (si existe), antes de cualquier return if (existingSerial is not null) { existingSerial.Available = row.Available; // actualizar disponible con el del re-escaneo } toastService.ShowWarning("No hay más disponible para esta combinación."); await Refocus(); return; } var existingBN = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey); if (existingBN is not null) { existingBN.Available = s.Quantity; // suma +1 cap al disponible efectivo var add = 1m; var maxAdd = Math.Min(add, effectiveAvailable); if (maxAdd <= 0) { toastService.ShowWarning("Se alcanzó el máximo disponible."); await Refocus(); return; } existingBN.Selected += maxAdd; await Refocus(); return; } // Nueva fila (otra ubicación/lote/etc.) row.Selected = 1; StockList.Insert(0, row); await Refocus(); } catch (Exception ex) { toastService.ShowError($"Error al procesar el escaneo: {ex.Message}"); await Refocus(); } } private async Task Refocus() { await Task.Yield(); // asegura que el DOM está listo await scanInput.FocusAsync(); // deja el cursor listo para el próximo escaneo } private async Task Confirm() { var selected = StockList .Where(x => x.Selected > 0) .Select(x => new StockItemSelectionDto { StockItemId = x.StockItemId, ProductId = x.ProductId, ProductName = x.ProductName, Batch = x.Batch, Serial= x.Serial, Expiration = x.Expiration, TraceabilityType = x.TraceabilityType, LocationId = x.LocationId, Quantity = !string.IsNullOrWhiteSpace(x.Serial) ? 1m : decimal.Round(Math.Min(x.Selected, x.Available), 2, MidpointRounding.AwayFromZero) }) .ToList(); if (!selected.Any()) { toastService.ShowWarning("No se seleccionó ningún producto."); return; } await ModalInstance.CloseAsync(ModalResult.Ok(selected)); } private Task Cancel() => ModalInstance.CancelAsync(); public class StockDisplayRow { public int StockItemId { get; set; } public int ProductId { get; set; } public string ProductName { get; set; } = string.Empty; public string Batch { get; set; } = string.Empty; public string Serial { get; set; } = string.Empty; public DateTime? Expiration { get; set; } public decimal Available { get; set; } public decimal Selected { get; set; } public int LocationId { get; set; } public int TraceabilityType { get; set; } /// Clave de fusión (no se persiste). public string BusinessKey { get; set; } = string.Empty; } private static string BuildKeyFromRow(StockDisplayRow r) { return Domain.Dtos.Stock.StockKeys.BuildBusinessKey( r.ProductId, r.LocationId, r.Batch, r.Expiration.HasValue ? DateOnly.FromDateTime(r.Expiration.Value) : (DateOnly?)null, r.Serial ); } }