diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index fe160cb..107f72e 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -1112,61 +1112,50 @@ public class InventoryController : Controller .Select(i => i.ManufacturerPartNumber!.Trim().ToLower()) .ToHashSet(); - // When a vendor is specified, search vendor-scoped first. Only widen to all vendors - // if the scoped search returns nothing — prevents a cross-vendor color match from - // being returned as the only result when the user clearly intended a specific manufacturer. - IEnumerable matches; - if (!string.IsNullOrEmpty(vendorTerm)) - { - matches = await _unitOfWork.PowderCatalog.FindAsync(p => - p.VendorName.ToLower().Contains(vendorTerm) && ( - p.Sku.ToLower() == term || - p.ColorName.ToLower().Contains(term) || - p.Sku.ToLower().Contains(term))); - - // Fall back to all vendors only when the scoped search finds nothing - if (!matches.Any()) - { - matches = await _unitOfWork.PowderCatalog.FindAsync(p => - p.Sku.ToLower() == term || - p.ColorName.ToLower().Contains(term) || - p.Sku.ToLower().Contains(term)); - } - } - else - { - matches = await _unitOfWork.PowderCatalog.FindAsync(p => - p.Sku.ToLower() == term || - p.ColorName.ToLower().Contains(term) || - p.Sku.ToLower().Contains(term)); - } + // Single query — all partial color/SKU matches across all vendors. + // Results are ranked: exact vendor + exact color (isExact=true) sorts first and + // triggers auto-fill in the JS. Everything else goes to the picker modal. + // This means a user who typed "Columbia Coatings" + "Lime Green" gets auto-fill + // only when that exact product is in the catalog; otherwise they see a ranked modal + // with same-vendor results at the top and a "Not Listed — Search Online" escape hatch. + var matches = await _unitOfWork.PowderCatalog.FindAsync(p => + p.ColorName.ToLower().Contains(term) || + p.Sku.ToLower() == term || + p.Sku.ToLower().Contains(term)); var results = matches .Where(p => !existingSkus.Contains(p.Sku.ToLower())) - .OrderBy(p => p.Sku.ToLower() == term ? 0 : 1) - .ThenBy(p => p.ColorName) - .Select(p => new + .Select(p => { - id = p.Id, - vendorName = p.VendorName, - sku = p.Sku, - colorName = p.ColorName, - description = p.Description, - unitPrice = p.UnitPrice, - imageUrl = p.ImageUrl, - sdsUrl = p.SdsUrl, - tdsUrl = p.TdsUrl, - applicationGuideUrl = p.ApplicationGuideUrl, - productUrl = p.ProductUrl, - isDiscontinued = p.IsDiscontinued, - cureTemperatureF = p.CureTemperatureF, - cureTimeMinutes = p.CureTimeMinutes, - finish = p.Finish, - colorFamilies = p.ColorFamilies, - requiresClearCoat = p.RequiresClearCoat, - coverageSqFtPerLb = p.CoverageSqFtPerLb, - specificGravity = p.SpecificGravity, - transferEfficiency = GetEffectiveTransferEfficiency(p.TransferEfficiency) + var vendorMatch = string.IsNullOrEmpty(vendorTerm) || p.VendorName.ToLower().Contains(vendorTerm); + var colorExact = p.ColorName.ToLower() == term; + return (p, isExact: vendorMatch && colorExact, vendorMatch, colorExact); + }) + .OrderBy(x => x.isExact ? 0 : x.vendorMatch ? 1 : x.colorExact ? 2 : 3) + .ThenBy(x => x.p.ColorName) + .Select(x => new + { + id = x.p.Id, + vendorName = x.p.VendorName, + sku = x.p.Sku, + colorName = x.p.ColorName, + description = x.p.Description, + unitPrice = x.p.UnitPrice, + imageUrl = x.p.ImageUrl, + sdsUrl = x.p.SdsUrl, + tdsUrl = x.p.TdsUrl, + applicationGuideUrl = x.p.ApplicationGuideUrl, + productUrl = x.p.ProductUrl, + isDiscontinued = x.p.IsDiscontinued, + isExact = x.isExact, + cureTemperatureF = x.p.CureTemperatureF, + cureTimeMinutes = x.p.CureTimeMinutes, + finish = x.p.Finish, + colorFamilies = x.p.ColorFamilies, + requiresClearCoat = x.p.RequiresClearCoat, + coverageSqFtPerLb = x.p.CoverageSqFtPerLb, + specificGravity = x.p.SpecificGravity, + transferEfficiency = GetEffectiveTransferEfficiency(x.p.TransferEfficiency) }) .ToList(); diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js index 6c845d3..7748a31 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js @@ -62,23 +62,25 @@ const items = await resp.json(); if (items.length === 0) { - // No catalog match — fall back to AI if available - hideStatus(); - if (typeof window._runInventoryAiLookup === 'function') { - showStatus('info', 'Not in catalog — searching with AI…'); - await window._runInventoryAiLookup(); - } else { - showStatus('warning', 'No match found in the catalog. Enter details manually or enable AI Lookup.'); - } + // Nothing in catalog — go straight to AI + await runAiOrWarn(); return; } - if (items.length === 1) { + // Single exact match (vendor + color name both match precisely) — auto-fill + if (items.length === 1 && items[0].isExact) { await fillFields(items[0]); return; } - // Multiple matches — let the user pick via modal + // Exact match exists but so do other results — auto-fill the exact one + const exactMatches = items.filter(i => i.isExact); + if (exactMatches.length === 1) { + await fillFields(exactMatches[0]); + return; + } + + // No exact match (or ambiguous) — show picker modal with "Not Listed" escape hatch hideStatus(); showPickerModal(items); @@ -89,6 +91,18 @@ } }); + // ── AI fallback helper ─────────────────────────────────────────────────── + + async function runAiOrWarn() { + hideStatus(); + if (typeof window._runInventoryAiLookup === 'function') { + showStatus('info', 'Not in catalog — searching online with AI…'); + await window._runInventoryAiLookup(); + } else { + showStatus('warning', 'No match found in the catalog. Enter details manually or enable AI Lookup.'); + } + } + // ── Fill fields from a catalog result ──────────────────────────────────── async function fillFields(item) { @@ -368,6 +382,12 @@ + `; @@ -383,6 +403,11 @@ }); }); + document.getElementById('catalogPickerNotListed').addEventListener('click', function () { + bsModal.hide(); + runAiOrWarn(); + }); + bsModal.show(); }