Add platform powder catalog management UI with full CRUD and AI lookup

- PowderCatalogController: Create, Edit, ToggleDiscontinued actions; searchable/filterable/sortable Index with pagination; AiLookup and AiAugmentFromUrl endpoints backed by IInventoryAiLookupService
- New views: Create, Edit, _Form partial (with AI-assisted field population), overhauled Index grid with completeness quality badges and responsive mobile cards
- New ViewModels: PowderCatalogIndexViewModel, PowderCatalogFormViewModel, PowderCatalogListItemViewModel
- AI lookup improvements: SpecificGravity field added to InventoryAiLookupResult; ApplyPowderFallbacks derives CoverageSqFtPerLb from specific gravity when docs omit it; DefaultTransferEfficiency (65%) applied everywhere transfer efficiency is null
- powder-catalog-ai-lookup.js: client-side AI lookup and URL augment wiring for the catalog form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 00:27:44 -04:00
parent 713efbc2b6
commit 11a1b91be1
15 changed files with 8642 additions and 94 deletions
@@ -0,0 +1,184 @@
@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel
@{
var enableAiLookup = ViewData["EnableAiLookup"] as bool? == true;
}
@if (enableAiLookup)
{
<div class="card border-0 bg-light-subtle mb-4">
<div class="card-body p-3">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<h6 class="mb-0">
<i class="bi bi-stars me-2 text-primary"></i>AI Lookup
</h6>
<button type="button" class="btn btn-sm btn-primary" id="powder-ai-lookup-btn">
<i class="bi bi-search me-1"></i>Search Missing Info
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="powder-ai-url-btn">
<i class="bi bi-box-arrow-up-right me-1"></i>Use Product URL
</button>
</div>
<div class="small text-muted mb-2">
Search the web for missing specs, cure data, and SDS/TDS links. Existing values are left alone unless the field is blank.
</div>
<div id="ai-lookup-status" class="alert alert-info d-none py-2 small mb-0"></div>
</div>
</div>
}
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="VendorName" class="form-label fw-medium"></label>
<input asp-for="VendorName" class="form-control" id="field-vendorname" />
<span asp-validation-for="VendorName" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="Sku" class="form-label fw-medium"></label>
<input asp-for="Sku" class="form-control" id="field-sku" />
<span asp-validation-for="Sku" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="UnitPrice" class="form-label fw-medium"></label>
<input asp-for="UnitPrice" class="form-control" id="field-unitprice" />
<span asp-validation-for="UnitPrice" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="ColorName" class="form-label fw-medium"></label>
<input asp-for="ColorName" class="form-control" id="field-colorname" />
<span asp-validation-for="ColorName" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="Description" class="form-label fw-medium"></label>
<textarea asp-for="Description" class="form-control" id="field-description" rows="3"></textarea>
<span asp-validation-for="Description" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Finish" class="form-label fw-medium"></label>
<input asp-for="Finish" class="form-control" id="field-finish" placeholder="Gloss, Matte, Satin, Metallic..." />
<span asp-validation-for="Finish" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="ColorFamilies" class="form-label fw-medium"></label>
<input asp-for="ColorFamilies" class="form-control" id="field-colorfamilies" placeholder="Blue, Purple, Metallic" />
<span asp-validation-for="ColorFamilies" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="CureTemperatureF" class="form-label fw-medium"></label>
<input asp-for="CureTemperatureF" class="form-control" id="field-curetemp" />
<span asp-validation-for="CureTemperatureF" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="CureTimeMinutes" class="form-label fw-medium"></label>
<input asp-for="CureTimeMinutes" class="form-control" id="field-curetime" />
<span asp-validation-for="CureTimeMinutes" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="CoverageSqFtPerLb" class="form-label fw-medium"></label>
<input asp-for="CoverageSqFtPerLb" class="form-control" id="field-coverage" />
<span asp-validation-for="CoverageSqFtPerLb" class="text-danger small"></span>
</div>
<div class="col-md-3">
<label asp-for="TransferEfficiency" class="form-label fw-medium"></label>
<input asp-for="TransferEfficiency" class="form-control" id="field-transfer" />
<span asp-validation-for="TransferEfficiency" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="ProductUrl" class="form-label fw-medium"></label>
<div class="input-group">
<input asp-for="ProductUrl" class="form-control" id="field-producturl" />
<a id="field-producturl-link" href="@Model.ProductUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.ProductUrl) ? "d-none" : "")" title="Open Product URL">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<span asp-validation-for="ProductUrl" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="ImageUrl" class="form-label fw-medium"></label>
<input asp-for="ImageUrl" class="form-control" id="field-imageurl" />
<span asp-validation-for="ImageUrl" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="SdsUrl" class="form-label fw-medium"></label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS URL">
<i class="bi bi-file-earmark-pdf"></i>
</a>
</div>
<span asp-validation-for="SdsUrl" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="TdsUrl" class="form-label fw-medium"></label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS URL">
<i class="bi bi-file-earmark-text"></i>
</a>
</div>
<span asp-validation-for="TdsUrl" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="ApplicationGuideUrl" class="form-label fw-medium"></label>
<div class="input-group">
<input asp-for="ApplicationGuideUrl" class="form-control" id="field-applicationguideurl" />
<a id="field-applicationguideurl-link" href="@Model.ApplicationGuideUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.ApplicationGuideUrl) ? "d-none" : "")" title="Open Application Guide URL">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<span asp-validation-for="ApplicationGuideUrl" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="RequiresClearCoat" class="form-label fw-medium"></label>
<select asp-for="RequiresClearCoat" class="form-select" id="field-clearcoat">
<option value="">Unknown</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
<span asp-validation-for="RequiresClearCoat" class="text-danger small"></span>
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input asp-for="IsDiscontinued" class="form-check-input" type="checkbox" />
<label asp-for="IsDiscontinued" class="form-check-label"></label>
</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input asp-for="IsUserContributed" class="form-check-input" type="checkbox" />
<label asp-for="IsUserContributed" class="form-check-label"></label>
</div>
</div>
</div>
@if (Model.Id > 0)
{
<hr class="my-4" />
<div class="row g-3 small text-muted">
<div class="col-md-4">
<div class="fw-semibold text-body">Created</div>
<div>@Model.CreatedAt.ToString("MMM d, yyyy h:mm tt") UTC</div>
</div>
<div class="col-md-4">
<div class="fw-semibold text-body">Updated</div>
<div>@(Model.UpdatedAt?.ToString("MMM d, yyyy h:mm tt") ?? "Never")@(Model.UpdatedAt.HasValue ? " UTC" : string.Empty)</div>
</div>
<div class="col-md-4">
<div class="fw-semibold text-body">Last Synced</div>
<div>@(Model.LastSyncedAt?.ToString("MMM d, yyyy h:mm tt") ?? "Never")@(Model.LastSyncedAt.HasValue ? " UTC" : string.Empty)</div>
</div>
</div>
}