Add platform powder catalog, catalog-first lookup, and label scanner

- Platform PowderCatalogItem table (IPlainRepository, no tenant filter) with
  full spec fields: cure temp/time, finish, color families, clear coat flag,
  coverage sq ft/lb, transfer efficiency, IsUserContributed
- Two EF migrations: AddPowderCatalogItem + AddPowderCatalogSpecFields
- PowderCatalogController (SuperAdminOnly): import from Prismatic JSON scrape,
  Lookup AJAX endpoint (catalog-first, ranked by SKU exact match), stats view
  with Tenant Contributed card
- Unified smart Lookup button on inventory Create/Edit: catalog hit fills all
  fields via catalogSnapshot pattern; AI augments cure/finish data from product
  URL if subscription enabled; catalog miss falls through to AI lookup
- In-browser label scanner (_LabelScanModal): getUserMedia live camera feed,
  jsQR auto-detects QR codes in rAF loop; "Scan Label Text" fallback sends
  captured frame to Claude vision via /Inventory/ScanLabel
- ScanLabel endpoint handles both QR URL path (LookupByUrlAsync) and vision
  path (ScanLabelAsync); auto-inserts unrecognized products as
  IsUserContributed=true; returns wasInCatalog/addedToCatalog flags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 16:36:25 -04:00
parent 90f333c8f3
commit 1fc79b77fe
25 changed files with 21279 additions and 23 deletions
@@ -72,17 +72,20 @@
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0 d-flex align-items-center">
<i class="bi bi-palette me-2 text-primary"></i>Product Details
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? true))
<button type="button" class="btn btn-sm btn-primary ms-2" id="smart-lookup-btn">
<i class="bi bi-search me-1"></i>Lookup
</button>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="ai-lookup-btn">
<i class="bi bi-stars me-1"></i>AI Lookup
<button type="button" class="btn btn-sm btn-outline-secondary ms-1" id="scan-label-btn" title="Scan a powder bag label with your camera">
<i class="bi bi-qr-code-scan me-1"></i>Scan Label
</button>
}
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Product Details"
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. AI Lookup can auto-fill these fields from a manufacturer name or part number. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 6070%). Both values are used to calculate Powder Needed on quotes and jobs.">
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields — it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 6070%). Both values are used to calculate Powder Needed on quotes and jobs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -123,6 +126,28 @@
</div>
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://…" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
<i class="bi bi-file-earmark-pdf"></i>
</a>
</div>
<span asp-validation-for="SdsUrl" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://…" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
<i class="bi bi-file-earmark-text"></i>
</a>
</div>
<span asp-validation-for="TdsUrl" class="text-danger"></span>
</div>
<input asp-for="ImageUrl" type="hidden" id="field-imageurl" />
<div class="col-12" id="wrap-imagepreview" style="display:@(Model.ImageUrl != null ? "" : "none");">
<label class="form-label text-muted small">Product Image (from AI Lookup)</label>
@@ -374,8 +399,18 @@
</div>
</div>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<partial name="_LabelScanModal" />
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>const inventoryFormIsCreate = true;</script>
<partial name="_InventoryColorFamilyScripts" />
<script src="~/js/inventory-catalog-lookup.js"></script>
@if ((bool)(ViewBag.AiInventoryAssistEnabled ?? false))
{
<script src="~/js/inventory-label-scan.js"></script>
}
}