Fix powder catalog lookup: exact match auto-fills, partials show picker modal
- CatalogLookup now returns all partial color name matches ranked by specificity (exact vendor+color first, same-vendor partial, cross-vendor) with isExact flag so JS can decide to auto-fill vs show modal - Removed cross-vendor fallback that was silently overwriting manufacturer field with wrong brand when vendor-scoped search found nothing - Picker modal now includes "Not listed — search online" option that triggers AI lookup as an escape hatch from the catalog results Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1112,61 +1112,50 @@ public class InventoryController : Controller
|
|||||||
.Select(i => i.ManufacturerPartNumber!.Trim().ToLower())
|
.Select(i => i.ManufacturerPartNumber!.Trim().ToLower())
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
// When a vendor is specified, search vendor-scoped first. Only widen to all vendors
|
// Single query — all partial color/SKU matches across all vendors.
|
||||||
// if the scoped search returns nothing — prevents a cross-vendor color match from
|
// Results are ranked: exact vendor + exact color (isExact=true) sorts first and
|
||||||
// being returned as the only result when the user clearly intended a specific manufacturer.
|
// triggers auto-fill in the JS. Everything else goes to the picker modal.
|
||||||
IEnumerable<PowderCatalogItem> matches;
|
// This means a user who typed "Columbia Coatings" + "Lime Green" gets auto-fill
|
||||||
if (!string.IsNullOrEmpty(vendorTerm))
|
// 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.
|
||||||
matches = await _unitOfWork.PowderCatalog.FindAsync(p =>
|
var matches = await _unitOfWork.PowderCatalog.FindAsync(p =>
|
||||||
p.VendorName.ToLower().Contains(vendorTerm) && (
|
|
||||||
p.Sku.ToLower() == term ||
|
|
||||||
p.ColorName.ToLower().Contains(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.Sku.ToLower() == term ||
|
||||||
p.ColorName.ToLower().Contains(term) ||
|
|
||||||
p.Sku.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));
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = matches
|
var results = matches
|
||||||
.Where(p => !existingSkus.Contains(p.Sku.ToLower()))
|
.Where(p => !existingSkus.Contains(p.Sku.ToLower()))
|
||||||
.OrderBy(p => p.Sku.ToLower() == term ? 0 : 1)
|
.Select(p =>
|
||||||
.ThenBy(p => p.ColorName)
|
|
||||||
.Select(p => new
|
|
||||||
{
|
{
|
||||||
id = p.Id,
|
var vendorMatch = string.IsNullOrEmpty(vendorTerm) || p.VendorName.ToLower().Contains(vendorTerm);
|
||||||
vendorName = p.VendorName,
|
var colorExact = p.ColorName.ToLower() == term;
|
||||||
sku = p.Sku,
|
return (p, isExact: vendorMatch && colorExact, vendorMatch, colorExact);
|
||||||
colorName = p.ColorName,
|
})
|
||||||
description = p.Description,
|
.OrderBy(x => x.isExact ? 0 : x.vendorMatch ? 1 : x.colorExact ? 2 : 3)
|
||||||
unitPrice = p.UnitPrice,
|
.ThenBy(x => x.p.ColorName)
|
||||||
imageUrl = p.ImageUrl,
|
.Select(x => new
|
||||||
sdsUrl = p.SdsUrl,
|
{
|
||||||
tdsUrl = p.TdsUrl,
|
id = x.p.Id,
|
||||||
applicationGuideUrl = p.ApplicationGuideUrl,
|
vendorName = x.p.VendorName,
|
||||||
productUrl = p.ProductUrl,
|
sku = x.p.Sku,
|
||||||
isDiscontinued = p.IsDiscontinued,
|
colorName = x.p.ColorName,
|
||||||
cureTemperatureF = p.CureTemperatureF,
|
description = x.p.Description,
|
||||||
cureTimeMinutes = p.CureTimeMinutes,
|
unitPrice = x.p.UnitPrice,
|
||||||
finish = p.Finish,
|
imageUrl = x.p.ImageUrl,
|
||||||
colorFamilies = p.ColorFamilies,
|
sdsUrl = x.p.SdsUrl,
|
||||||
requiresClearCoat = p.RequiresClearCoat,
|
tdsUrl = x.p.TdsUrl,
|
||||||
coverageSqFtPerLb = p.CoverageSqFtPerLb,
|
applicationGuideUrl = x.p.ApplicationGuideUrl,
|
||||||
specificGravity = p.SpecificGravity,
|
productUrl = x.p.ProductUrl,
|
||||||
transferEfficiency = GetEffectiveTransferEfficiency(p.TransferEfficiency)
|
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();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -62,23 +62,25 @@
|
|||||||
const items = await resp.json();
|
const items = await resp.json();
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
// No catalog match — fall back to AI if available
|
// Nothing in catalog — go straight to AI
|
||||||
hideStatus();
|
await runAiOrWarn();
|
||||||
if (typeof window._runInventoryAiLookup === 'function') {
|
|
||||||
showStatus('info', '<span class="spinner-border spinner-border-sm me-1"></span>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.');
|
|
||||||
}
|
|
||||||
return;
|
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]);
|
await fillFields(items[0]);
|
||||||
return;
|
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();
|
hideStatus();
|
||||||
showPickerModal(items);
|
showPickerModal(items);
|
||||||
|
|
||||||
@@ -89,6 +91,18 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── AI fallback helper ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runAiOrWarn() {
|
||||||
|
hideStatus();
|
||||||
|
if (typeof window._runInventoryAiLookup === 'function') {
|
||||||
|
showStatus('info', '<span class="spinner-border spinner-border-sm me-1"></span>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 ────────────────────────────────────
|
// ── Fill fields from a catalog result ────────────────────────────────────
|
||||||
|
|
||||||
async function fillFields(item) {
|
async function fillFields(item) {
|
||||||
@@ -368,6 +382,12 @@
|
|||||||
<div class="modal-body p-0">
|
<div class="modal-body p-0">
|
||||||
<div class="list-group list-group-flush">${rows}</div>
|
<div class="list-group list-group-flush">${rows}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer py-2 justify-content-start">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="catalogPickerNotListed">
|
||||||
|
<i class="bi bi-search me-1"></i>Not listed — search online
|
||||||
|
</button>
|
||||||
|
<span class="text-muted small ms-2">Uses AI to look up the exact product</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -383,6 +403,11 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('catalogPickerNotListed').addEventListener('click', function () {
|
||||||
|
bsModal.hide();
|
||||||
|
runAiOrWarn();
|
||||||
|
});
|
||||||
|
|
||||||
bsModal.show();
|
bsModal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user