diff --git a/Domain/Dtos/Stock/StockItemSelectionDto.cs b/Domain/Dtos/Stock/StockItemSelectionDto.cs index 1b92b5a..3491e94 100644 --- a/Domain/Dtos/Stock/StockItemSelectionDto.cs +++ b/Domain/Dtos/Stock/StockItemSelectionDto.cs @@ -30,7 +30,7 @@ /// /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. /// - public string? Serial { get; set; } + public string Serial { get; set; } = string.Empty ; /// /// Fecha de vencimiento (si aplica) diff --git a/Models/Repositories/Stock/PhLSMStockItemRepository.cs b/Models/Repositories/Stock/PhLSMStockItemRepository.cs index bf82ac1..5dbf8f5 100644 --- a/Models/Repositories/Stock/PhLSMStockItemRepository.cs +++ b/Models/Repositories/Stock/PhLSMStockItemRepository.cs @@ -109,7 +109,7 @@ namespace Models.Repositories.Stock int page, int take) { - // 0) Si no hay NINGÚN dato parseado, no traigas todo + // 0) Sin ningún dato parseado -> vacío (evitar traer todo) if (string.IsNullOrWhiteSpace(gtin) && string.IsNullOrWhiteSpace(batch) && !expiration.HasValue && @@ -128,7 +128,6 @@ namespace Models.Repositories.Stock .Where(p => p.ExternalCode == g || p.FactoryCode == g || p.RegulatoryCode == g) .Select(p => p.Id).ToListAsync(); - // Si se pidió GTIN y no matchea ningún producto, devolvé vacío if (productIds.Count == 0) { return new PagedResult @@ -151,34 +150,150 @@ namespace Models.Repositories.Stock if (locationId.HasValue) baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value); - // 3) Reglas por tipo de trazabilidad (sin fuzzy) - baseQuery = baseQuery.Where(x => - // None: solo producto + ubicación - (x.p.TraceabilityType == 1) - // BatchOnly: requiere batch exacto - || (x.p.TraceabilityType == 2 - && !string.IsNullOrWhiteSpace(batch) - && x.si.Batch == batch!.Trim()) - // Batch+Exp: requiere batch + expiration exactos - || (x.p.TraceabilityType == 3 - && !string.IsNullOrWhiteSpace(batch) && expiration.HasValue - && x.si.Batch == batch!.Trim() - && x.si.Expiration.HasValue && x.si.Expiration.Value == expiration.Value) - // SerialUnit: requiere serial exacto (ignora expiration) - || (x.p.TraceabilityType == 4 - && !string.IsNullOrWhiteSpace(serial) - && x.si.Serial != null && x.si.Serial == serial!.Trim()) - // Serial+Exp: requiere serial + expiration exactos - || (x.p.TraceabilityType == 5 - && !string.IsNullOrWhiteSpace(serial) && expiration.HasValue - && x.si.Serial != null && x.si.Serial == serial!.Trim() - && x.si.Expiration.HasValue && x.si.Expiration.Value == expiration.Value) - ); + // 3) Reglas por tipo de trazabilidad — PRIORIDAD: tipo REAL del producto si hay productIds + if (productIds.Count > 0) + { + var traces = await _context.PhLsmProducts.AsNoTracking() + .Where(p => productIds.Contains(p.Id)) + .Select(p => p.TraceabilityType) + .Distinct() + .ToListAsync(); + + // Nota: normalmente habrá un solo tipo; si hubiera mezcla, estas ramas las contemplan. + + if (traces.Contains(4) || traces.Contains(5)) + { + // T4/T5: requieren serial. T5 además requiere expiration exacta. + if (string.IsNullOrWhiteSpace(serial)) + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + + var s = serial.Trim(); + + if (traces.Contains(5) && expiration.HasValue) + { + var e = expiration.Value; + baseQuery = baseQuery.Where(x => + (x.p.TraceabilityType == 5 && + x.si.Serial != null && x.si.Serial == s && + x.si.Expiration.HasValue && x.si.Expiration.Value == e) + || + // si el conjunto tuviera también productos T4, permitimos T4 por serial + (x.p.TraceabilityType == 4 && + x.si.Serial != null && x.si.Serial == s) + ); + } + else + { + baseQuery = baseQuery.Where(x => + x.p.TraceabilityType == 4 && + x.si.Serial != null && x.si.Serial == s + ); + } + } + else if (traces.Contains(3)) + { + // T3: exige batch + expiration exactos + if (string.IsNullOrWhiteSpace(batch) || !expiration.HasValue) + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + + var b = batch.Trim(); + var e = expiration.Value; + + baseQuery = baseQuery.Where(x => + x.p.TraceabilityType == 3 && + x.si.Batch == b && + x.si.Expiration.HasValue && x.si.Expiration.Value == e + ); + } + else if (traces.Contains(2)) + { + // T2: exige batch exacto (ignorar serial/expiration si vinieran) + if (string.IsNullOrWhiteSpace(batch)) + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + + var b = batch.Trim(); + + baseQuery = baseQuery.Where(x => + x.p.TraceabilityType == 2 && + x.si.Batch == b + ); + } + else if (traces.Contains(1)) + { + // T1: sin traza (solo producto + ubicación) + baseQuery = baseQuery.Where(x => x.p.TraceabilityType == 1); + } + else + { + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + } + } + else + { + // Fallback: sin productIds -> decidir por presencia de datos (tu heurística original) + if (!string.IsNullOrWhiteSpace(serial)) + { + var s = serial.Trim(); + + if (expiration.HasValue) + { + var e = expiration.Value; + baseQuery = baseQuery.Where(x => + (x.p.TraceabilityType == 5 && + x.si.Serial != null && x.si.Serial == s && + x.si.Expiration.HasValue && x.si.Expiration.Value == e) + || + (x.p.TraceabilityType == 4 && + x.si.Serial != null && x.si.Serial == s) + ); + } + else + { + baseQuery = baseQuery.Where(x => + x.p.TraceabilityType == 4 && + x.si.Serial != null && x.si.Serial == s + ); + } + } + else if (!string.IsNullOrWhiteSpace(batch)) + { + var b = batch.Trim(); + + if (expiration.HasValue) + { + var e = expiration.Value; + baseQuery = baseQuery.Where(x => + (x.p.TraceabilityType == 3 && + x.si.Batch == b && + x.si.Expiration.HasValue && x.si.Expiration.Value == e) + || + (x.p.TraceabilityType == 2 && + x.si.Batch == b) + ); + } + else + { + baseQuery = baseQuery.Where(x => + x.p.TraceabilityType == 2 && + x.si.Batch == b + ); + } + } + else + { + // Solo aceptar T1 cuando no hay otra evidencia + baseQuery = baseQuery.Where(x => x.p.TraceabilityType == 1); + } + } // 4) Orden y proyección baseQuery = baseQuery.OrderBy(x => x.si.Expiration).ThenBy(x => x.p.Name); - var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto + var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto { StockItemId = x.si.Id, ProductId = x.p.Id, @@ -189,8 +304,8 @@ namespace Models.Repositories.Stock LocationId = x.si.LocationId, LocationName = x.loc != null ? x.loc.Descripcion : null, Batch = x.si.Batch, - Expiration = x.si.Expiration, // DateOnly? - Serial = x.si.Serial, // si aplica + Expiration = x.si.Expiration, + Serial = x.si.Serial, TraceabilityType = x.p.TraceabilityType, AvailableQty = x.si.Quantity, PlusProcess = x.p.PlusProcess diff --git a/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor b/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor index 4ea29d9..2a85038 100644 --- a/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor +++ b/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor @@ -49,6 +49,7 @@ Producto Lote + Serial Vencimiento Disponible Cantidad a usar @@ -60,6 +61,7 @@ @item.ProductName @item.Batch + @item.Serial @item.Expiration?.ToShortDateString() @item.Available @@ -145,6 +147,7 @@ ProductId = matchedItem.ProductId, ProductName = matchedItem.ProductName, Batch = matchedItem.Batch, + Serial = matchedItem.Serial, Expiration = matchedItem.Expiration, Available = matchedItem.Quantity, Selected = 1, @@ -181,6 +184,7 @@ ProductId = x.ProductId, ProductName = x.ProductName, Batch = x.Batch, + Serial= x.Serial, Expiration = x.Expiration, Quantity = x.Selected, LocationId = x.LocationId @@ -213,6 +217,7 @@ 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; }