Link inventory to powder catalog and flag discontinued items

Phase 5 (part): the inventory tie-in.

- Set InventoryItem.PowderCatalogItemId on the catalog-sourced create paths:
  directly in CreateIncomingFromCatalog, and via a new FindCatalogMatchAsync
  (Manufacturer + ManufacturerPartNumber) helper in Create.
- Inventory Details loads the linked catalog row (falling back to an identity
  match for items created before linking) and shows a "Discontinued by
  manufacturer — cannot reorder" badge + banner when it's discontinued.
  Deliberately distinct from the shop's own Active/Inactive status: existing
  stock can still be used and quoted, it just can't be reordered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:18:15 -04:00
parent a07f6aa1a8
commit 4506c1f641
2 changed files with 56 additions and 0 deletions
@@ -240,6 +240,17 @@ public class InventoryController : Controller
var useMetric = await _tenantContext.UseMetricSystemAsync(); var useMetric = await _tenantContext.UseMetricSystemAsync();
ViewBag.CoverageUnit = _measurementService.GetCoverageUnitLabel(useMetric); ViewBag.CoverageUnit = _measurementService.GetCoverageUnitLabel(useMetric);
// Manufacturer-level catalog status: prefer the linked catalog row, fall back to an
// identity match for items added before they were linked. Drives the "discontinued by
// manufacturer — cannot reorder" warning. This is distinct from the shop's own
// IsActive/DiscontinuedDate (whether the shop still stocks it).
var catalogItem = item.PowderCatalogItemId.HasValue
? await _unitOfWork.PowderCatalog.GetByIdAsync(item.PowderCatalogItemId.Value)
: await FindCatalogMatchAsync(item.Manufacturer, item.ManufacturerPartNumber);
ViewBag.CatalogDiscontinued = catalogItem?.IsDiscontinued ?? false;
ViewBag.CatalogVendorName = catalogItem?.VendorName;
ViewBag.CatalogProductUrl = catalogItem?.ProductUrl;
return View(itemDto); return View(itemDto);
} }
catch (Exception ex) catch (Exception ex)
@@ -302,6 +313,12 @@ public class InventoryController : Controller
item.Category = category.DisplayName; item.Category = category.DisplayName;
} }
// Link to the platform catalog row when this item's identity matches one, so the detail
// screen can show manufacturer-level status (discontinued / cannot reorder).
var catalogMatch = await FindCatalogMatchAsync(item.Manufacturer, item.ManufacturerPartNumber);
if (catalogMatch != null)
item.PowderCatalogItemId = catalogMatch.Id;
await _unitOfWork.InventoryItems.AddAsync(item); await _unitOfWork.InventoryItems.AddAsync(item);
await _unitOfWork.SaveChangesAsync(); await _unitOfWork.SaveChangesAsync();
@@ -763,6 +780,24 @@ public class InventoryController : Controller
/// Returns (wasInCatalog, addedToCatalog) so callers can surface UI badges. /// Returns (wasInCatalog, addedToCatalog) so callers can surface UI badges.
/// Mutates <paramref name="result"/> in place. /// Mutates <paramref name="result"/> in place.
/// </summary> /// </summary>
/// <summary>
/// Finds the platform powder catalog row matching an inventory item's identity
/// (Manufacturer + ManufacturerPartNumber), or null. Used to set
/// <see cref="InventoryItem.PowderCatalogItemId"/> and to surface manufacturer-level status
/// (e.g. discontinued / cannot reorder) on the detail screen.
/// </summary>
private async Task<PowderCatalogItem?> FindCatalogMatchAsync(string? manufacturer, string? sku)
{
if (string.IsNullOrWhiteSpace(manufacturer) || string.IsNullOrWhiteSpace(sku))
return null;
var skuLower = sku.Trim().ToLower();
var mfrLower = manufacturer.Trim().ToLower();
var hits = await _unitOfWork.PowderCatalog.FindAsync(p =>
p.Sku.ToLower() == skuLower && p.VendorName.ToLower().Contains(mfrLower));
return hits.FirstOrDefault();
}
private async Task<(bool wasInCatalog, bool addedToCatalog)> EnrichFromCatalogAsync( private async Task<(bool wasInCatalog, bool addedToCatalog)> EnrichFromCatalogAsync(
InventoryAiLookupResult result, bool autoContribute) InventoryAiLookupResult result, bool autoContribute)
{ {
@@ -1257,6 +1292,7 @@ public class InventoryController : Controller
ColorName = catalogItem.ColorName, ColorName = catalogItem.ColorName,
Manufacturer = catalogItem.VendorName, Manufacturer = catalogItem.VendorName,
ManufacturerPartNumber= catalogItem.Sku, ManufacturerPartNumber= catalogItem.Sku,
PowderCatalogItemId = catalogItem.Id,
Finish = catalogItem.Finish, Finish = catalogItem.Finish,
ColorFamilies = catalogItem.ColorFamilies, ColorFamilies = catalogItem.ColorFamilies,
RequiresClearCoat = catalogItem.RequiresClearCoat ?? false, RequiresClearCoat = catalogItem.RequiresClearCoat ?? false,
@@ -69,6 +69,12 @@
{ {
<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>Inactive</span> <span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>Inactive</span>
} }
@if ((bool?)ViewBag.CatalogDiscontinued == true)
{
<span class="badge bg-warning text-dark" title="Discontinued by the manufacturer — cannot reorder">
<i class="bi bi-slash-circle me-1"></i>Discontinued by manufacturer
</span>
}
</div> </div>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@@ -103,6 +109,20 @@
<div><strong>Status:</strong> This item is inactive</div> <div><strong>Status:</strong> This item is inactive</div>
</div> </div>
} }
@if ((bool?)ViewBag.CatalogDiscontinued == true)
{
<div class="alert alert-warning alert-permanent d-flex align-items-center mb-3">
<i class="bi bi-slash-circle me-2"></i>
<div>
<strong>Discontinued by @(ViewBag.CatalogVendorName ?? "manufacturer"):</strong>
this powder has been discontinued and cannot be reordered. Existing stock can still be used and quoted.
@if (!string.IsNullOrEmpty(ViewBag.CatalogProductUrl as string))
{
<a href="@ViewBag.CatalogProductUrl" target="_blank" rel="noopener" class="alert-link ms-1">View product page</a>
}
</div>
</div>
}
<div class="row g-4"> <div class="row g-4">
<!-- Left Column --> <!-- Left Column -->