Add PWA manifest, fix AI multi-coat pricing, and improve catalog lookup
- PWA: manifest.json + minimal service worker so iOS/Android persist camera permission after "Add to Home Screen"; theme-color and apple meta tags in layout - PWA icons: 192x192 and 512x512 from transparent PCL logo; updated pcl-logo.png - AI pricing: apply AdditionalCoatLaborPercent per extra coat on AI items, matching the calculated-item path (was ignoring extra coats entirely) - AI wizard: live price recalc when coats are added/removed; session-expiry errors now show a clear "refresh and sign in" message instead of raw HTTP status; smooth-scroll to follow-up/results sections on AI response - Catalog lookup: exclude SKUs already in company inventory from results; pass currentId on edit so own entry still appears; vendor-scoped search with cross-vendor fallback; result count shown in multi-match modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1029,12 +1029,13 @@ public class InventoryController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the platform-level PowderCatalogItems table by SKU or color name and returns
|
||||
/// up to 10 matches as JSON. Called by the inventory Create/Edit form before falling back
|
||||
/// to the AI Lookup, avoiding unnecessary API calls for known products.
|
||||
/// Searches the platform-level PowderCatalogItems table by SKU or color name.
|
||||
/// Excludes catalog entries already present in the company's inventory (by ManufacturerPartNumber).
|
||||
/// Pass currentId when editing an existing item so its own catalog entry is not filtered out.
|
||||
/// Called by the inventory Create/Edit form before falling back to AI Lookup.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> CatalogLookup(string? q, string? vendor)
|
||||
public async Task<IActionResult> CatalogLookup(string? q, string? vendor, int? currentId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q) || q.Length < 2)
|
||||
return Json(Array.Empty<object>());
|
||||
@@ -1042,18 +1043,47 @@ public class InventoryController : Controller
|
||||
var term = q.Trim().ToLower();
|
||||
var vendorTerm = vendor?.Trim().ToLower();
|
||||
|
||||
var matches = await _unitOfWork.PowderCatalog.FindAsync(p =>
|
||||
p.Sku.ToLower() == term ||
|
||||
p.ColorName.ToLower().Contains(term) ||
|
||||
p.Sku.ToLower().Contains(term));
|
||||
// Build a set of SKUs already in this company's inventory so we can exclude them.
|
||||
// When editing, the current item's own SKU is re-included so its catalog entry still appears.
|
||||
var existingItems = await _unitOfWork.InventoryItems.GetAllAsync();
|
||||
var existingSkus = existingItems
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.ManufacturerPartNumber) && i.Id != (currentId ?? 0))
|
||||
.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<PowderCatalogItem> 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));
|
||||
}
|
||||
|
||||
// When a vendor hint is provided, prefer records where VendorName matches,
|
||||
// then fall back to all results so the user still sees cross-vendor options.
|
||||
var results = matches
|
||||
.Where(p => !existingSkus.Contains(p.Sku.ToLower()))
|
||||
.OrderBy(p => p.Sku.ToLower() == term ? 0 : 1)
|
||||
.ThenBy(p => !string.IsNullOrEmpty(vendorTerm) && p.VendorName.ToLower().Contains(vendorTerm) ? 0 : 1)
|
||||
.ThenBy(p => p.ColorName)
|
||||
.Take(10)
|
||||
.Select(p => new
|
||||
{
|
||||
id = p.Id,
|
||||
@@ -1075,7 +1105,8 @@ public class InventoryController : Controller
|
||||
requiresClearCoat = p.RequiresClearCoat,
|
||||
coverageSqFtPerLb = p.CoverageSqFtPerLb,
|
||||
transferEfficiency = p.TransferEfficiency
|
||||
});
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Json(results);
|
||||
}
|
||||
|
||||
@@ -3393,7 +3393,6 @@ public class QuotesController : Controller
|
||||
/// Returns a tempId that the JS wizard tracks and submits as AiPhotoTempIds[] on form submit.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[EnableRateLimiting(AppConstants.RateLimitPolicies.Ai)]
|
||||
public async Task<IActionResult> UploadAiPhoto(IFormFile? file)
|
||||
{
|
||||
@@ -3431,7 +3430,6 @@ public class QuotesController : Controller
|
||||
/// so the model's estimates are calibrated to this company's pricing and material usage patterns.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[EnableRateLimiting(AppConstants.RateLimitPolicies.Ai)]
|
||||
public async Task<IActionResult> AiAnalyzeItem([FromBody] AiAnalyzeItemRequest request)
|
||||
{
|
||||
@@ -3604,6 +3602,11 @@ public class QuotesController : Controller
|
||||
var complexityCharge = subtotal * complexityPct;
|
||||
subtotal += complexityCharge;
|
||||
|
||||
// Additional coat charge — each coat beyond the first adds AdditionalCoatLaborPercent%,
|
||||
// matching the formula in PricingCalculationService.CalculateQuoteItemPriceAsync.
|
||||
if (request.CoatCount > 1)
|
||||
subtotal += subtotal * (request.CoatCount - 1) * (costs.AdditionalCoatLaborPercent / 100m);
|
||||
|
||||
if (subtotal < costs.ShopMinimumCharge && costs.ShopMinimumCharge > 0)
|
||||
subtotal = costs.ShopMinimumCharge;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user