@page "/stock/expeditions/create" @using Blazored.Typeahead @using Domain.Dtos.Stock @using Services.Lookups @using Services.Stock.Expeditions @using phronCare.UIBlazor.Pages.Stock.Shared @inject NavigationManager Navigation @inject ExpeditionService expeditionService @inject ISalesLookupService lookUpService @inject IToastService toastService @inject IModalService Modal

Nueva Expedición

@item.Nombre @item.Nombre
@if (!string.IsNullOrWhiteSpace(DispatchInstruction)) {
Instrucciones desde presupuesto:
@DispatchInstruction
}
Productos a Expedir
@if (Details.Any()) {
@foreach (var item in Details) { }
Producto Nombre Cant. Lote Serial Vencimiento Ubicación
@item.ProductId @item.ProductName @item.Quantity @item.Batch @item.Serial @item.Expiration?.ToString("yyyy-MM-dd") @item.LocationId
} else {

No hay productos agregados.

}
@code { private ELSExpeditionHeader Model = new(); private ExtraInfoModel ExtraInfo = new(); private ELookUpItem? SelectedQuote; private List Details = new(); private List ProductSetItems = new(); private string DispatchInstruction = string.Empty; private string ticketIdString = string.Empty; private async Task> SearchQuotes(string filter) { return await lookUpService.SearchApprovedQuotesAsync(filter); } private async Task OnQuoteSelected(ELookUpItem? selected) { if (selected is null || string.IsNullOrWhiteSpace(selected.Nombre)) { SelectedQuote = null; ExtraInfo = new(); // Limpiar datos cargados DispatchInstruction = ""; return; } SelectedQuote = selected; var quoteNumber = selected.Nombre.Split(" - ")[0]; var quote = await expeditionService.GetQuoteByNumberAsync(quoteNumber); if (quote is null) { toastService.ShowError("No se pudo cargar el presupuesto."); return; } ExtraInfo.Professional = quote.ProfessionalName; ExtraInfo.Institution = quote.InstitutionName; ExtraInfo.Patient = quote.PatientName; ExtraInfo.SurgeryDate = quote.EstimatedDate; DispatchInstruction = quote.Observations ?? ""; } private void AddProduct() { // TODO: abrir modal de producto individual } private void AddSet() { // TODO: abrir modal de set } private void ScanProduct() { // TODO: activar input de escáner } private void RemoveItem(ELSExpeditionDetail item) { Details.Remove(item); } private async Task HandleValidSubmit() { // TODO: Lógica de guardado de la expedición completa } private async Task OpenStockItemSelectorModal() { var parameters = new ModalParameters(); parameters.Add(nameof(StockItemSelectorModal.SetItems), ProductSetItems); parameters.Add(nameof(StockItemSelectorModal.Snapshot), BuildSnapshotFromDetails()); // ← clave var options = new ModalOptions { Size = ModalSize.Large, HideHeader = true }; var modal = Modal.Show("", parameters, options); var result = await modal.Result; if (!result.Cancelled && result.Data is List selectedItems) { MergeSelectionsByBusinessKey(selectedItems); // ← usar merge (ver abajo) StateHasChanged(); toastService.ShowSuccess($"{selectedItems.Count} item(s) agregados/actualizados."); } } /// 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 key = StockKeys.BuildBusinessKey( s.ProductId, s.LocationId, s.Batch ?? string.Empty, exp, s.Serial ?? string.Empty ); var existing = Details.FirstOrDefault(d => 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 (ignora lo que venga del modal) if (!string.IsNullOrWhiteSpace(s.Serial)) newQty = 1; if (existing is not null) { 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) continue; // 0 explícito válido (no había cantidad previa): eliminar. Details.Remove(existing); continue; } // 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 { if (newQty > 0) { Details.Add(new ELSExpeditionDetail { ProductId = s.ProductId, ProductName = s.ProductName, Quantity = newQty, Batch = s.Batch, Expiration = exp, TraceabilityType = s.TraceabilityType, // UI only (no DB) Serial = s.Serial, LocationId = s.LocationId }); } // si newQty == 0 y no existía, no hacemos nada } } } private List BuildSnapshotFromDetails() { return Details.Select(d => { var key = StockKeys.BuildBusinessKey( d.ProductId, d.LocationId, d.Batch ?? string.Empty, d.Expiration, d.Serial ?? string.Empty ); return new StockSnapshotItem { ProductId = d.ProductId, ProductName = d.ProductName, LocationId = d.LocationId, Batch = d.Batch ?? string.Empty, Expiration = d.Expiration, Serial = d.Serial ?? string.Empty, TraceabilityType = d.TraceabilityType, // UI only (no DB) Quantity = d.Quantity, BusinessKey = key }; }).ToList(); } private class ExtraInfoModel { public string? Professional { get; set; } public string? Institution { get; set; } public string? Patient { get; set; } public DateTime? SurgeryDate { get; set; } } }