using Domain.Dtos.Stock; // StockItemScanResultDto using Domain.Generics; // PagedResult using Microsoft.EntityFrameworkCore; using Models.Helpers; // ToPagedResultAsync using Models.Interfaces; // IPhLSMStockItemRepository using Models.Models; // PhronCareOperationsHubContext namespace Models.Repositories.Stock { public class PhLSMStockItemRepository(PhronCareOperationsHubContext context) : IPhLSMStockItemRepository { private readonly PhronCareOperationsHubContext _context = context; public async Task> SearchStockItemsAsync( string? codeOrText, string? batch, int? locationId, int? productType, int? traceabilityType, bool? plusProcess, int page, int take) { // Base: stock disponible (>0) + joins necesarios var baseQuery = from si in _context.PhLsmStockItems.AsNoTracking() join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc from loc in _loc.DefaultIfEmpty() where si.Quantity > 0 select new { si, p, loc }; // ---- Filtros (case-insensitive donde aplica) ---- if (!string.IsNullOrWhiteSpace(codeOrText)) { var t = codeOrText.Trim().ToLower(); baseQuery = baseQuery.Where(x => (!string.IsNullOrEmpty(x.p.FactoryCode) && x.p.FactoryCode.ToLower().Contains(t)) || (!string.IsNullOrEmpty(x.p.ExternalCode) && x.p.ExternalCode.ToLower().Contains(t)) || (!string.IsNullOrEmpty(x.p.Name) && x.p.Name.ToLower().Contains(t)) || (!string.IsNullOrEmpty(x.p.Descripcion) && x.p.Descripcion.ToLower().Contains(t)) || (!string.IsNullOrEmpty(x.si.Batch) && x.si.Batch.ToLower().Contains(t)) ); } if (!string.IsNullOrWhiteSpace(batch)) { var b = batch.Trim().ToLower(); baseQuery = baseQuery.Where(x => x.si.Batch != null && x.si.Batch.ToLower().Contains(b)); } if (locationId.HasValue) baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value); if (productType.HasValue) baseQuery = baseQuery.Where(x => x.p.ProductType == productType.Value); if (traceabilityType.HasValue) baseQuery = baseQuery.Where(x => x.p.TraceabilityType == traceabilityType.Value); if (plusProcess.HasValue) baseQuery = baseQuery.Where(x => x.p.PlusProcess == plusProcess.Value); // Orden lógico baseQuery = baseQuery .OrderBy(x => x.si.Expiration) .ThenBy(x => x.p.Name); // Proyección final a DTO (IQueryable) var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto { StockItemId = x.si.Id, ProductId = x.p.Id, FactoryCode = x.p.FactoryCode ?? string.Empty, ExternalCode = x.p.ExternalCode, ProductName = x.p.Name ?? string.Empty, Description = x.p.Descripcion, LocationId = x.si.LocationId, LocationName = x.loc != null ? x.loc.Descripcion : null, Batch = x.si.Batch, Expiration = x.si.Expiration, TraceabilityType = x.p.TraceabilityType, AvailableQty = x.si.Quantity, PlusProcess = x.p.PlusProcess }); page = page <= 0 ? 1 : page; take = take <= 0 ? 20 : take; // Paginado con tu helper (manteniendo el mismo patrón que ProductRepository) var paged = await dtoQuery.ToPagedResultAsync(page, take); return new PagedResult { Items = paged.Items, TotalItems = paged.TotalItems, Page = paged.Page, PageSize = paged.PageSize }; } // -------- Búsqueda PARSEADA (GS1/DataMatrix) ---------- public async Task> SearchStockItemsParsedAsync( string? gtin, string? batch, DateOnly? expiration, string? serial, int? locationId, int page, int take) { // 0) Sin ningún dato parseado -> vacío (evitar traer todo) if (string.IsNullOrWhiteSpace(gtin) && string.IsNullOrWhiteSpace(batch) && !expiration.HasValue && string.IsNullOrWhiteSpace(serial)) { return new PagedResult { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; } // 1) Resolver productos por GTIN/Factory/Regulatory si vino GTIN var productIds = new List(); if (!string.IsNullOrWhiteSpace(gtin)) { var g = gtin.Trim(); productIds = await _context.PhLsmProducts.AsNoTracking() .Where(p => p.ExternalCode == g || p.FactoryCode == g || p.RegulatoryCode == g) .Select(p => p.Id).ToListAsync(); if (productIds.Count == 0) { return new PagedResult { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; } } // 2) Base query (stock disponible) var baseQuery = from si in _context.PhLsmStockItems.AsNoTracking() join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc from loc in _loc.DefaultIfEmpty() where si.Quantity > 0 select new { si, p, loc }; if (productIds.Count > 0) baseQuery = baseQuery.Where(x => productIds.Contains(x.p.Id)); if (locationId.HasValue) baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.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 { StockItemId = x.si.Id, ProductId = x.p.Id, FactoryCode = x.p.FactoryCode ?? string.Empty, ExternalCode = x.p.ExternalCode, ProductName = x.p.Name ?? string.Empty, Description = x.p.Descripcion, LocationId = x.si.LocationId, LocationName = x.loc != null ? x.loc.Descripcion : null, Batch = x.si.Batch, Expiration = x.si.Expiration, Serial = x.si.Serial, TraceabilityType = x.p.TraceabilityType, AvailableQty = x.si.Quantity, PlusProcess = x.p.PlusProcess }); page = page <= 0 ? 1 : page; take = take <= 0 ? 20 : take; var paged = await dtoQuery.ToPagedResultAsync(page, take); return new PagedResult { Items = paged.Items, TotalItems = paged.TotalItems, Page = paged.Page, PageSize = paged.PageSize }; } } }