Hide churned trial accounts from company/health screens by default

- Companies list and Company Health now hide Expired/Canceled accounts
  whose subscription ended 14+ days ago; show/hide toggle via banner
- KPI cards on Company Health exclude churned tenants when hidden
- showChurned param threads through sort, pagination, search, and filter forms
- Powder catalog: fix missing UnitPrice on user-contributed entries;
  add back-sync to fill catalog gaps on existing matches; wire
  AiAugmentFromUrl and manual inventory Create into catalog contribute path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 13:59:12 -04:00
parent a73f14fa7f
commit 2ad6df1195
7 changed files with 164 additions and 15 deletions
@@ -304,6 +304,32 @@ public class InventoryController : Controller
await _unitOfWork.SaveChangesAsync();
}
// Contribute/sync to the platform powder catalog if we have enough identity data.
// Runs silently — a failure here never blocks the inventory save.
if (!string.IsNullOrWhiteSpace(dto.Manufacturer) && !string.IsNullOrWhiteSpace(dto.ManufacturerPartNumber))
{
var catalogResult = new InventoryAiLookupResult
{
Manufacturer = dto.Manufacturer,
ManufacturerPartNumber = dto.ManufacturerPartNumber,
ColorName = dto.ColorName ?? item.Name,
Finish = dto.Finish,
CureTemperatureF = dto.CureTemperatureF,
CureTimeMinutes = dto.CureTimeMinutes,
ColorFamilies = dto.ColorFamilies,
RequiresClearCoat = dto.RequiresClearCoat ? true : (bool?)null,
CoverageSqFtPerLb = dto.CoverageSqFtPerLb,
SpecificGravity = dto.SpecificGravity,
TransferEfficiency = dto.TransferEfficiency,
UnitCostPerLb = dto.UnitCost > 0 ? dto.UnitCost : null,
SpecPageUrl = dto.SpecPageUrl,
ImageUrl = dto.ImageUrl,
SdsUrl = dto.SdsUrl,
TdsUrl = dto.TdsUrl,
};
await EnrichFromCatalogAsync(catalogResult, autoContribute: true);
}
TempData["Success"] = "Inventory item created successfully.";
return RedirectToAction(nameof(Details), new { id = item.Id });
}
@@ -704,6 +730,8 @@ public class InventoryController : Controller
return Json(new { success = false, errorMessage = "No product URL provided." });
var result = await _aiLookupService.LookupByUrlAsync(productUrl, colorName);
if (result.Success)
await EnrichFromCatalogAsync(result, autoContribute: true);
return Json(result);
}
@@ -750,6 +778,39 @@ public class InventoryController : Controller
result.SdsUrl ??= match.SdsUrl;
result.TdsUrl ??= match.TdsUrl;
if (match.UnitPrice > 0) result.UnitCostPerLb ??= match.UnitPrice;
// Back-sync: fill NULL catalog fields from the incoming result so the catalog
// gets richer over time without overwriting anything already stored.
bool catalogDirty = false;
if (match.Finish == null && !string.IsNullOrWhiteSpace(result.Finish)) { match.Finish = result.Finish; catalogDirty = true; }
if (match.CureTemperatureF == null && result.CureTemperatureF != null) { match.CureTemperatureF = result.CureTemperatureF; catalogDirty = true; }
if (match.CureTimeMinutes == null && result.CureTimeMinutes != null) { match.CureTimeMinutes = result.CureTimeMinutes; catalogDirty = true; }
if (match.ColorFamilies == null && !string.IsNullOrWhiteSpace(result.ColorFamilies)){ match.ColorFamilies = result.ColorFamilies; catalogDirty = true; }
if (match.RequiresClearCoat == null && result.RequiresClearCoat != null) { match.RequiresClearCoat = result.RequiresClearCoat; catalogDirty = true; }
if (match.CoverageSqFtPerLb == null && result.CoverageSqFtPerLb != null) { match.CoverageSqFtPerLb = result.CoverageSqFtPerLb; catalogDirty = true; }
if (match.SpecificGravity == null && result.SpecificGravity != null) { match.SpecificGravity = result.SpecificGravity; catalogDirty = true; }
if (match.TransferEfficiency == null && result.TransferEfficiency != null) { match.TransferEfficiency = result.TransferEfficiency; catalogDirty = true; }
if (string.IsNullOrWhiteSpace(match.ImageUrl) && !string.IsNullOrWhiteSpace(result.ImageUrl)) { match.ImageUrl = result.ImageUrl; catalogDirty = true; }
if (string.IsNullOrWhiteSpace(match.ProductUrl) && !string.IsNullOrWhiteSpace(result.SpecPageUrl)){ match.ProductUrl = result.SpecPageUrl; catalogDirty = true; }
if (string.IsNullOrWhiteSpace(match.SdsUrl) && !string.IsNullOrWhiteSpace(result.SdsUrl)) { match.SdsUrl = result.SdsUrl; catalogDirty = true; }
if (string.IsNullOrWhiteSpace(match.TdsUrl) && !string.IsNullOrWhiteSpace(result.TdsUrl)) { match.TdsUrl = result.TdsUrl; catalogDirty = true; }
if (match.UnitPrice == 0 && (result.UnitCostPerLb ?? 0) > 0) { match.UnitPrice = result.UnitCostPerLb!.Value; catalogDirty = true; }
if (catalogDirty)
{
match.UpdatedAt = DateTime.UtcNow;
try
{
await _unitOfWork.PowderCatalog.UpdateAsync(match);
await _unitOfWork.CompleteAsync();
_logger.LogInformation("Back-synced catalog gaps for {VendorName} {Sku}", match.VendorName, match.Sku);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to back-sync catalog entry {Id}", match.Id);
}
}
return (true, false);
}
@@ -767,6 +828,7 @@ public class InventoryController : Controller
VendorName = manufacturer,
Sku = sku,
ColorName = colorName,
UnitPrice = result.UnitCostPerLb ?? 0m,
CureTemperatureF = result.CureTemperatureF,
CureTimeMinutes = result.CureTimeMinutes,
Finish = result.Finish,