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:
@@ -27,6 +27,9 @@ namespace PowderCoating.Infrastructure.Services;
|
||||
/// </summary>
|
||||
public class InventoryAiLookupService : IInventoryAiLookupService
|
||||
{
|
||||
private const decimal DefaultTransferEfficiency = 65m;
|
||||
private const decimal TheoreticalCoverageAtOneMilFactor = 192.3m;
|
||||
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<InventoryAiLookupService> _logger;
|
||||
@@ -47,6 +50,7 @@ Respond ONLY with a valid JSON object — no markdown, no explanation:
|
||||
""cureTimeMinutes"": number or null,
|
||||
""colorFamilies"": ""comma-separated list from: Red,Orange,Yellow,Green,Blue,Purple,Pink,Brown,Black,White,Gray,Silver,Gold,Bronze,Copper,Clear — or null if unknown"",
|
||||
""requiresClearCoat"": true or false or null,
|
||||
""specificGravity"": number or null,
|
||||
""coverageSqFtPerLb"": number or null,
|
||||
""transferEfficiency"": number or null,
|
||||
""unitCostPerLb"": number or null,
|
||||
@@ -250,6 +254,7 @@ Rules:
|
||||
result.CureTimeMinutes = GetInt(parsed, "cureTimeMinutes");
|
||||
result.ColorFamilies = GetString(parsed, "colorFamilies");
|
||||
result.RequiresClearCoat = GetBool(parsed, "requiresClearCoat");
|
||||
result.SpecificGravity = GetDecimal(parsed, "specificGravity");
|
||||
result.CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb");
|
||||
result.TransferEfficiency = GetDecimal(parsed, "transferEfficiency");
|
||||
result.UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb");
|
||||
@@ -260,6 +265,7 @@ Rules:
|
||||
result.ImageUrl = pageImageUrl;
|
||||
result.Reasoning = GetString(parsed, "reasoning");
|
||||
|
||||
ApplyPowderFallbacks(result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -366,7 +372,7 @@ Rules:
|
||||
}
|
||||
|
||||
var parsed = JsonSerializer.Deserialize<JsonElement>(rawText);
|
||||
return new InventoryAiLookupResult
|
||||
var result = new InventoryAiLookupResult
|
||||
{
|
||||
Success = true,
|
||||
Manufacturer = GetString(parsed, "manufacturer"),
|
||||
@@ -378,11 +384,15 @@ Rules:
|
||||
CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"),
|
||||
ColorFamilies = GetString(parsed, "colorFamilies"),
|
||||
RequiresClearCoat = GetBool(parsed, "requiresClearCoat"),
|
||||
SpecificGravity = GetDecimal(parsed, "specificGravity"),
|
||||
CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb"),
|
||||
TransferEfficiency = GetDecimal(parsed, "transferEfficiency"),
|
||||
VendorName = GetString(parsed, "vendorName"),
|
||||
Reasoning = GetString(parsed, "reasoning"),
|
||||
};
|
||||
|
||||
ApplyPowderFallbacks(result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -447,7 +457,7 @@ Rules:
|
||||
}
|
||||
|
||||
var parsed = JsonSerializer.Deserialize<JsonElement>(rawText);
|
||||
return new InventoryAiLookupResult
|
||||
var result = new InventoryAiLookupResult
|
||||
{
|
||||
Success = true,
|
||||
Manufacturer = GetString(parsed, "manufacturer"),
|
||||
@@ -460,6 +470,7 @@ Rules:
|
||||
CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"),
|
||||
ColorFamilies = GetString(parsed, "colorFamilies"),
|
||||
RequiresClearCoat = GetBool(parsed, "requiresClearCoat"),
|
||||
SpecificGravity = GetDecimal(parsed, "specificGravity"),
|
||||
CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb"),
|
||||
TransferEfficiency = GetDecimal(parsed, "transferEfficiency"),
|
||||
UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb"),
|
||||
@@ -470,6 +481,9 @@ Rules:
|
||||
ImageUrl = pageImageUrl,
|
||||
Reasoning = GetString(parsed, "reasoning"),
|
||||
};
|
||||
|
||||
ApplyPowderFallbacks(result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1226,4 +1240,15 @@ Rules:
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ApplyPowderFallbacks(InventoryAiLookupResult result)
|
||||
{
|
||||
result.TransferEfficiency ??= DefaultTransferEfficiency;
|
||||
|
||||
if (!result.CoverageSqFtPerLb.HasValue && result.SpecificGravity is > 0)
|
||||
{
|
||||
var calculatedCoverage = TheoreticalCoverageAtOneMilFactor / result.SpecificGravity.Value;
|
||||
result.CoverageSqFtPerLb = Math.Round(calculatedCoverage, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user