diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index 95b220d..be95457 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -391,6 +391,100 @@ public async Task>> BarcodeDuplicat return result; } + public record QuickSearchItem( + Guid Id, string Name, string? Article, string? DefaultBarcode, + decimal? ReferencePrice, decimal? StockQty); + + /// Лёгкий поиск для inline-добавления строк в документы (приёмка, + /// продажа). Ранжирует точное совпадение штрихкода → точное артикула → + /// префикс артикула → префикс имени → имя contains. Возвращает остаток + /// по storeId если передан, иначе сумму по всем складам организации. + [HttpGet("quick-search")] + public async Task>> QuickSearch( + [FromQuery] string? search, + [FromQuery] Guid? storeId, + [FromQuery] int limit = 20, + CancellationToken ct = default) + { + var s = (search ?? "").Trim(); + if (s.Length == 0) return Array.Empty(); + var sLower = s.ToLower(); + if (limit <= 0 || limit > 50) limit = 20; + + var q = _db.Products.AsNoTracking().Where(p => + p.Name.ToLower().Contains(sLower) || + (p.Article != null && p.Article.ToLower().Contains(sLower)) || + p.Barcodes.Any(b => b.Code.Contains(s))); + + // Ранжирование выводим в память: SQL'ом аккуратно сортировать по + // нескольким булевым приоритетам сложно, а лимит 20–50 строк + // делает накладные расходы пренебрежимыми. + var raw = await q.Select(p => new + { + p.Id, p.Name, p.Article, p.ReferencePrice, + Barcodes = p.Barcodes.Select(b => new { b.Code, b.IsPrimary }).ToList(), + StockQty = storeId == null + ? (decimal?)_db.Stocks.Where(st => st.ProductId == p.Id).Sum(st => (decimal?)st.Quantity) + : _db.Stocks.Where(st => st.ProductId == p.Id && st.StoreId == storeId) + .Select(st => (decimal?)st.Quantity).FirstOrDefault(), + }).Take(limit * 4).ToListAsync(ct); + + int Rank(string name, string? article, IEnumerable codes) + { + if (codes.Any(c => c.Equals(s, StringComparison.OrdinalIgnoreCase))) return 0; + if (article != null && article.Equals(s, StringComparison.OrdinalIgnoreCase)) return 1; + if (article != null && article.StartsWith(s, StringComparison.OrdinalIgnoreCase)) return 2; + if (name.StartsWith(s, StringComparison.OrdinalIgnoreCase)) return 3; + return 4; + } + + var items = raw + .Select(r => new + { + Item = new QuickSearchItem( + r.Id, r.Name, r.Article, + r.Barcodes.OrderByDescending(b => b.IsPrimary).Select(b => b.Code).FirstOrDefault(), + r.ReferencePrice, r.StockQty), + Rank = Rank(r.Name, r.Article, r.Barcodes.Select(b => b.Code)), + }) + .OrderBy(x => x.Rank).ThenBy(x => x.Item.Name) + .Take(limit) + .Select(x => x.Item) + .ToList(); + return items; + } + + public record ByBarcodeResult(IReadOnlyList Items); + + /// Точный поиск по штрихкоду (для сканера). 0 → 404, 1 → объект, + /// несколько → { items: [...] } чтобы UI показал диалог выбора. + [HttpGet("by-barcode/{value}")] + public async Task> ByBarcode( + string value, [FromQuery] Guid? storeId, CancellationToken ct) + { + var v = (value ?? "").Trim(); + if (v.Length == 0) return NotFound(); + var matches = await _db.Products.AsNoTracking() + .Where(p => p.Barcodes.Any(b => b.Code == v)) + .Select(p => new + { + p.Id, p.Name, p.Article, p.ReferencePrice, + Barcodes = p.Barcodes.Select(b => new { b.Code, b.IsPrimary }).ToList(), + StockQty = storeId == null + ? (decimal?)_db.Stocks.Where(st => st.ProductId == p.Id).Sum(st => (decimal?)st.Quantity) + : _db.Stocks.Where(st => st.ProductId == p.Id && st.StoreId == storeId) + .Select(st => (decimal?)st.Quantity).FirstOrDefault(), + }) + .ToListAsync(ct); + if (matches.Count == 0) return NotFound(); + var items = matches.Select(r => new QuickSearchItem( + r.Id, r.Name, r.Article, + r.Barcodes.OrderByDescending(b => b.IsPrimary).Select(b => b.Code).FirstOrDefault() ?? v, + r.ReferencePrice, r.StockQty)).ToList(); + if (items.Count == 1) return items[0]; + return new ByBarcodeResult(items); + } + private IQueryable QueryIncludes() => _db.Products .Include(p => p.UnitOfMeasure) .Include(p => p.ProductGroup)