diff --git a/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor b/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor index 4a39f48..61125ad 100644 --- a/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor +++ b/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor @@ -1,10 +1,8 @@ @page "/stock/expeditions/create" @using Blazored.Typeahead @using Domain.Dtos.Stock -@using Domain.Entities @using Services.Lookups @using Services.Stock.Expeditions - @using phronCare.UIBlazor.Pages.Stock.Shared @inject NavigationManager Navigation @@ -44,7 +42,7 @@
- +
@@ -118,26 +116,23 @@ - @for (int i = 0; i < 50; i++) - { - @foreach (var item in Details) - { - - @item.ProductId - @item.ProductName - @item.Quantity - @item.Batch - @item.Serial - @item.Expiration?.ToString("yyyy-MM-dd") - @item.LocationId - - - - - } - } + @foreach (var item in Details) + { + + @item.ProductId + @item.ProductName + @item.Quantity + @item.Batch + @item.Serial + @item.Expiration?.ToString("yyyy-MM-dd") + @item.LocationId + + + + + } @@ -164,11 +159,7 @@ private string DispatchInstruction = string.Empty; - private string ticketIdString - { - get => Model.TicketId?.ToString() ?? string.Empty; - set => Model.TicketId = Guid.TryParse(value, out var guid) ? guid : null; - } + private string ticketIdString = string.Empty; private async Task> SearchQuotes(string filter) { @@ -247,11 +238,29 @@ } } + /// Fusiona la lista de ítems seleccionados en el modal con la lista principal Details. + /// + /// Reglas principales: + /// - Se construye una clave de negocio (ProductId, LocationId, Batch, Expiration, Serial) para identificar ítems únicos. + /// - Si el ítem ya existe en Details: + /// • Si la cantidad recibida es > 0 → se actualiza la fila existente (SET exacto, no suma). + /// • Si la cantidad recibida es 0 y la fila tenía cantidad previa > 0 → se interpreta como + /// "snapshot sin intención de borrar" y se conserva la fila (no se elimina). + /// • Si la cantidad recibida es 0 y no había cantidad previa → se elimina la fila. + /// - Si el ítem no existe en Details y la cantidad recibida es > 0 → se agrega como nueva fila. + /// - Si el ítem no existe y la cantidad es 0 → no se hace nada. + /// - Si el ítem tiene número de serie → la cantidad se fuerza siempre a 1. + /// + /// Esta lógica evita el bug en el que, al reabrir el modal con un snapshot, los ítems volvían con + /// cantidad = 0 y eran eliminados indebidamente de la grilla principal. + /// private void MergeSelectionsByBusinessKey(List selected) { foreach (var s in selected) { - var exp = s.Expiration.HasValue ? DateOnly.FromDateTime(s.Expiration.Value) : (DateOnly?)null; + var exp = s.Expiration.HasValue + ? DateOnly.FromDateTime(s.Expiration.Value) + : (DateOnly?)null; var key = StockKeys.BuildBusinessKey( s.ProductId, @@ -265,9 +274,10 @@ StockKeys.BuildBusinessKey(d.ProductId, d.LocationId, d.Batch ?? string.Empty, d.Expiration, d.Serial ?? string.Empty) == key ); + // Normalización de quantity (no negativa) var newQty = s.Quantity < 0 ? 0 : s.Quantity; - // Serial ⇒ siempre 1 + // Serial ⇒ siempre 1 (ignora lo que venga del modal) if (!string.IsNullOrWhiteSpace(s.Serial)) newQty = 1; @@ -275,26 +285,26 @@ { if (newQty == 0) { + // ⚠️ Si ya había cantidad y el modal devolvió 0, + // lo tratamos como "snapshot sin intención de borrar" → no remover. if (existing.Quantity > 0) - { - // ⚠️ Caso snapshot con 0 → ignorar, mantener la fila continue; - } - // 0 explícito válido → borrar + // 0 explícito válido (no había cantidad previa): eliminar. Details.Remove(existing); + continue; } - else - { - // SET: exactamente lo que vino del modal - existing.Quantity = newQty; - existing.ProductName = s.ProductName; - existing.Batch = s.Batch; - existing.Serial = s.Serial; - existing.Expiration = exp; - existing.LocationId = s.LocationId; - existing.TraceabilityType = s.TraceabilityType; - } + + // SET exacto (no sumar): mantener coherencia con el modal + existing.Quantity = newQty; + + // Actualizaciones mínimas necesarias (evitar sobreescrituras innecesarias): + if (!string.IsNullOrWhiteSpace(s.ProductName)) existing.ProductName = s.ProductName; + if (s.Batch is not null) existing.Batch = s.Batch; + if (s.Serial is not null) existing.Serial = s.Serial; + existing.Expiration = exp; + existing.LocationId = s.LocationId; + existing.TraceabilityType = s.TraceabilityType; // UI only } else { @@ -307,23 +317,16 @@ Quantity = newQty, Batch = s.Batch, Expiration = exp, - TraceabilityType = s.TraceabilityType, + TraceabilityType = s.TraceabilityType, // UI only (no DB) Serial = s.Serial, LocationId = s.LocationId }); } + // si newQty == 0 y no existía, no hacemos nada } } } - private class ExtraInfoModel - { - public string? Professional { get; set; } - public string? Institution { get; set; } - public string? Patient { get; set; } - public DateTime? SurgeryDate { get; set; } - } - private List BuildSnapshotFromDetails() { return Details.Select(d => @@ -350,4 +353,12 @@ }; }).ToList(); } + + private class ExtraInfoModel + { + public string? Professional { get; set; } + public string? Institution { get; set; } + public string? Patient { get; set; } + public DateTime? SurgeryDate { get; set; } + } } diff --git a/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor b/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor deleted file mode 100644 index 7594a9e..0000000 --- a/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor +++ /dev/null @@ -1,123 +0,0 @@ -@using Blazored.Modal -@using Blazored.Modal.Services -@using Domain.Dtos.Stock -@inject IStockScanService StockScanService - -@inherits LayoutComponentBase - - - - - - - -@code { - [CascadingParameter] BlazoredModalInstance ModalInstance { get; set; } - - [Parameter] public int? LocationId { get; set; } - private string SearchAddress { get; set; } = string.Empty; - - private string ScanInput { get; set; } = string.Empty; - private string ErrorMessage { get; set; } = string.Empty; - - private List ScanResults { get; set; } = new(); - - private async Task HandleScan() - { - ErrorMessage = string.Empty; - ScanResults.Clear(); - - if (string.IsNullOrWhiteSpace(ScanInput)) - { - ErrorMessage = "Ingrese un código válido."; - return; - } - - if (LocationId is null) - { - ErrorMessage = "Falta el depósito para escanear correctamente."; - return; - } - - try - { - var result = await StockScanService.ParseAndMatchAsync(ScanInput, LocationId.Value); - - if (result is not null) - { - ScanResults.Add(result); - } - else - { - ErrorMessage = "No se encontró stock coincidente."; - } - } - catch (Exception ex) - { - ErrorMessage = $"Error: {ex.Message}"; - } - } - - private void Cancel() => ModalInstance.CancelAsync(); - - private void ConfirmSelection() - { - var selected = ScanResults.Where(r => r.Quantity > 0).ToList(); - ModalInstance.CloseAsync(ModalResult.Ok(selected)); - } -} diff --git a/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs index ba8849a..295956b 100644 --- a/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs +++ b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs @@ -6,7 +6,7 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions { public class ExpeditionService { - private readonly IJSRuntime _js; + private readonly IJSRuntime _js; //Todavia no se utiliza pero eventualmente para exportaciones seguramente./ private readonly HttpClient _http; public ExpeditionService(HttpClient http, IJSRuntime js) { @@ -29,6 +29,5 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions return null; } } - } } diff --git a/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs b/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs deleted file mode 100644 index 7f31248..0000000 --- a/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Domain.Dtos.Stock; - -public class MockStockScanService : IStockScanService -{ - public async Task ParseAndMatchAsync(string rawInput, int locationId) - { - // Simula lógica de parseo de código escaneado - await Task.Delay(100); // simula delay - - if (string.IsNullOrWhiteSpace(rawInput)) - return null; - - // Simulación: si empieza con 0108... devolvemos un producto de prueba - if (rawInput.StartsWith("0108")) - { - return new StockItemSelectionDto - { - StockItemId = 999, - ProductId = 88, - ProductName = "Tornillo 6x30 mm", - Batch = "LOTE-MOCK", - Expiration = DateTime.Today.AddMonths(10), - Quantity = 10, - LocationId = locationId - }; - } - - return null; - } -} diff --git a/phronCare.UIBlazor/Services/Stock/StockScanService.cs b/phronCare.UIBlazor/Services/Stock/StockScanService.cs index ebfcb48..023520f 100644 --- a/phronCare.UIBlazor/Services/Stock/StockScanService.cs +++ b/phronCare.UIBlazor/Services/Stock/StockScanService.cs @@ -38,7 +38,7 @@ public class StockScanService : IStockScanService Quantity = first.AvailableQty, LocationId = first.LocationId ?? locationId, TraceabilityType = first.TraceabilityType, - Serial = first.Serial // si lo devolvés en el DTO de scan + Serial = first.Serial ?? string.Empty }; } }