Miscellaneous UI and pricing updates from prior sessions

- PricingCalculationService: powder coverage and specific gravity math fixes
- Dashboard/Index: minor widget updates
- Jobs/Details, Jobs/Intake: shop floor and intake view improvements
- Quotes/Details: detail view updates
- GiftCertificates/Details: detail view update
- job-photos.js: photo gallery improvements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:27:37 -04:00
parent 74414c6c71
commit 2e73cfab54
7 changed files with 226 additions and 38 deletions
@@ -422,12 +422,14 @@ public class PricingCalculationService : IPricingCalculationService
else
{
// Non-catalog: derive base from first coat's material + labor + equipment + markup
decimal coatLaborCost = 0m; // coat-only labor, used for coating booth (not prep/sandblast)
if (item.Coats != null && item.Coats.Count > 0)
{
var firstCoatResult = await CalculateCoatPriceAsync(
item.Coats[0], item.SurfaceAreaSqFt, item.Quantity, 0, item.EstimatedMinutes, companyId);
totalMaterialCost = firstCoatResult.CoatMaterialCost;
totalLaborCost = firstCoatResult.CoatLaborCost;
coatLaborCost = firstCoatResult.CoatLaborCost;
totalLaborCost = coatLaborCost;
}
// Prep service labor (done once per item batch)
@@ -443,9 +445,10 @@ public class PricingCalculationService : IPricingCalculationService
// Consumables surcharge (5% of material)
totalMaterialCost += totalMaterialCost * ConsumablesSurchargePercent;
// Equipment cost: coating booth only (oven cost moved to quote-level batch calculation)
var totalLaborHours = totalLaborCost / costs.StandardLaborRate;
totalEquipmentCost = totalLaborHours * costs.CoatingBoothCostPerHour;
// Equipment cost: coating booth only — use coat labor hours, not prep/sandblast hours
// (sandblasting happens in a blast cabinet, not the powder coating booth)
var coatLaborHours = costs.StandardLaborRate > 0 ? coatLaborCost / costs.StandardLaborRate : 0m;
totalEquipmentCost = coatLaborHours * costs.CoatingBoothCostPerHour;
// Apply pricing mode: markup on material only, or target margin on total cost
if (costs.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost)
@@ -675,22 +678,24 @@ public class PricingCalculationService : IPricingCalculationService
var effectiveBatches = Math.Max(1, ovenBatches);
var fullOvenBatchCost = effectiveBatches * (effectiveCycleMinutes / 60m) * effectiveOvenRate;
// Scale oven cost by the fraction of total surface area coming from non-AI items.
// Use item count as a fallback when surface areas are all zero.
var totalSqFt = items.Sum(i => i.SurfaceAreaSqFt * i.Quantity);
var aiSqFt = items.Where(i => i.IsAiItem).Sum(i => i.SurfaceAreaSqFt * i.Quantity);
var nonAiSqFt = totalSqFt - aiSqFt;
// Only items with coating layers go in the oven — sandblast/prep-only items (zero coats) don't.
// Of those coating items, AI items already have oven cost baked into their AI price.
var coatingItems = items.Where(i => i.Coats != null && i.Coats.Any()).ToList();
var nonAiCoatItems = coatingItems.Where(i => !i.IsAiItem).ToList();
decimal nonAiFraction;
if (totalSqFt > 0)
if (!coatingItems.Any())
{
nonAiFraction = nonAiSqFt / totalSqFt;
nonAiFraction = 0m; // No coated items — no oven charge
}
else
{
var totalCount = items.Count;
var aiCount = items.Count(i => i.IsAiItem);
nonAiFraction = totalCount > 0 ? (decimal)(totalCount - aiCount) / totalCount : 1m;
var totalCoatSqFt = coatingItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity);
var nonAiCoatSqFt = nonAiCoatItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity);
if (totalCoatSqFt > 0)
nonAiFraction = nonAiCoatSqFt / totalCoatSqFt;
else
nonAiFraction = coatingItems.Count > 0 ? (decimal)nonAiCoatItems.Count / coatingItems.Count : 1m;
}
var ovenBatchCost = fullOvenBatchCost * nonAiFraction;