diff --git a/src/PowderCoating.Application/Services/PricingCalculationService.cs b/src/PowderCoating.Application/Services/PricingCalculationService.cs index 226aaa0..8c8e2a1 100644 --- a/src/PowderCoating.Application/Services/PricingCalculationService.cs +++ b/src/PowderCoating.Application/Services/PricingCalculationService.cs @@ -252,11 +252,27 @@ public class PricingCalculationService : IPricingCalculationService }; } - // AI items use ManualUnitPrice directly (set to either the AI estimate or the user's price override). - // The AI already factored in all costs — skip the pricing engine entirely. + // AI items use ManualUnitPrice as the single-coat base price. + // Apply the same additional-coat charge as the calculated-item path so that + // adding a 2nd or 3rd coat in step 3 increases the price by AdditionalCoatLaborPercent% + // per coat — matching what catalog/calculated items charge. if (item.IsAiItem && item.ManualUnitPrice.HasValue) { var aiUnitPrice = item.ManualUnitPrice.Value; + + int additionalAiCoats = 0; + if (item.Coats != null) + { + for (int i = 1; i < item.Coats.Count; i++) + { + if (!item.Coats[i].NoExtraLayerCharge) + additionalAiCoats++; + } + } + if (additionalAiCoats > 0) + aiUnitPrice = Math.Round( + aiUnitPrice * (1m + additionalAiCoats * (costs.AdditionalCoatLaborPercent / 100m)), 2); + var aiTotal = aiUnitPrice * item.Quantity; return new QuoteItemPricingResult { diff --git a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs index 8f8086e..fb79bf0 100644 --- a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs +++ b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs @@ -540,6 +540,16 @@ Respond with the JSON object only."; var complexityCharge = subtotalBeforeComplexity * complexityPct; var subtotal = subtotalBeforeComplexity + complexityCharge; + // Additional coat charge — each coat beyond the first adds AdditionalCoatLaborPercent % of + // the subtotal, matching the formula in PricingCalculationService.CalculateQuoteItemPriceAsync. + // The coat count here comes from the wizard's step-2 field (aiCoatCount), so the preview + // reflects whatever multi-coat configuration the user specified before clicking Analyze. + if (request.CoatCount > 1) + { + var additionalCoatCharge = subtotal * (request.CoatCount - 1) * (costs.AdditionalCoatLaborPercent / 100m); + subtotal += additionalCoatCharge; + } + var markupAmount = (materialCost + consumablesSurcharge) * (costs.GeneralMarkupPercentage / 100m); // Apply shop minimum diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index 69f8de2..3756c98 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -1029,12 +1029,13 @@ public class InventoryController : Controller } /// - /// 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. /// [HttpGet] - public async Task CatalogLookup(string? q, string? vendor) + public async Task CatalogLookup(string? q, string? vendor, int? currentId = null) { if (string.IsNullOrWhiteSpace(q) || q.Length < 2) return Json(Array.Empty()); @@ -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 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); } diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index 35a7f95..ded5df7 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -3393,7 +3393,6 @@ public class QuotesController : Controller /// Returns a tempId that the JS wizard tracks and submits as AiPhotoTempIds[] on form submit. /// [HttpPost] - [ValidateAntiForgeryToken] [EnableRateLimiting(AppConstants.RateLimitPolicies.Ai)] public async Task 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. /// [HttpPost] - [ValidateAntiForgeryToken] [EnableRateLimiting(AppConstants.RateLimitPolicies.Ai)] public async Task 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; diff --git a/src/PowderCoating.Web/Views/Inventory/Create.cshtml b/src/PowderCoating.Web/Views/Inventory/Create.cshtml index 3952995..7ccfe82 100644 --- a/src/PowderCoating.Web/Views/Inventory/Create.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Create.cshtml @@ -77,7 +77,7 @@ @if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false)) { - } diff --git a/src/PowderCoating.Web/Views/Inventory/Edit.cshtml b/src/PowderCoating.Web/Views/Inventory/Edit.cshtml index 77ac821..842b135 100644 --- a/src/PowderCoating.Web/Views/Inventory/Edit.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Edit.cshtml @@ -74,12 +74,12 @@
Product Details - @if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false)) { - } diff --git a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml index 00cf96f..d68c92e 100644 --- a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml +++ b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml @@ -101,6 +101,12 @@ @ViewData["Title"] - Powder Coating Logix + + + + + + @@ -2192,5 +2198,10 @@ }, true); })(); + diff --git a/src/PowderCoating.Web/wwwroot/images/pcl-logo-white-cloud.png b/src/PowderCoating.Web/wwwroot/images/pcl-logo-white-cloud.png new file mode 100644 index 0000000..d9855f6 Binary files /dev/null and b/src/PowderCoating.Web/wwwroot/images/pcl-logo-white-cloud.png differ diff --git a/src/PowderCoating.Web/wwwroot/images/pcl-logo.png b/src/PowderCoating.Web/wwwroot/images/pcl-logo.png index d9855f6..a20c31f 100644 Binary files a/src/PowderCoating.Web/wwwroot/images/pcl-logo.png and b/src/PowderCoating.Web/wwwroot/images/pcl-logo.png differ diff --git a/src/PowderCoating.Web/wwwroot/images/pcl_icon_192.png b/src/PowderCoating.Web/wwwroot/images/pcl_icon_192.png new file mode 100644 index 0000000..18ea942 Binary files /dev/null and b/src/PowderCoating.Web/wwwroot/images/pcl_icon_192.png differ diff --git a/src/PowderCoating.Web/wwwroot/images/pwa-icon-192.png b/src/PowderCoating.Web/wwwroot/images/pwa-icon-192.png new file mode 100644 index 0000000..48bd8ea Binary files /dev/null and b/src/PowderCoating.Web/wwwroot/images/pwa-icon-192.png differ diff --git a/src/PowderCoating.Web/wwwroot/images/pwa-icon-512.png b/src/PowderCoating.Web/wwwroot/images/pwa-icon-512.png new file mode 100644 index 0000000..a20c31f Binary files /dev/null and b/src/PowderCoating.Web/wwwroot/images/pwa-icon-512.png differ diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js index 55e76a7..b3234f3 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js @@ -54,6 +54,8 @@ const params = new URLSearchParams(); if (searchTerm) params.set('q', searchTerm); if (manufacturer) params.set('vendor', manufacturer); + const currentId = smartBtn.dataset.currentId; + if (currentId) params.set('currentId', currentId); const resp = await fetch(`${LOOKUP_URL}?${params}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); @@ -358,7 +360,7 @@