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) Si no hay NINGÚN dato parseado, no traigas 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(); // Si se pidió GTIN y no matchea ningún producto, devolvé vacío 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 (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) ); // 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, // DateOnly? Serial = x.si.Serial, // si aplica 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 }; } } }