Fix price override ignored for catalog items; fix time entry delete UI

- QuotePricingAssemblyService: catalog no-coat items now respect PowderCostOverride instead of always using DefaultPrice
- PricingCalculationService: removed duplicate catalog fast-path in CalculateQuoteTotalsAsync that bypassed CalculateQuoteItemPriceAsync (and thus ignored PowderCostOverride); all items now go through the single authoritative path
- Jobs/Details.cshtml: timeTracking.del() and timeTracking.save() now call costing.load() after success so the time entry row and costing breakdown stay in sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 20:38:49 -04:00
parent f95397204c
commit 4ca90f561e
3 changed files with 10 additions and 50 deletions
@@ -590,53 +590,9 @@ public class PricingCalculationService : IPricingCalculationService
{
QuoteItemPricingResult itemResult;
// Catalog items - if they have coats, add coat costs to catalog base price
if (item.CatalogItemId.HasValue)
{
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(item.CatalogItemId.Value);
if (catalogItem != null)
{
// If the catalog item has coats, calculate using CalculateQuoteItemPriceAsync
// (which already includes the catalog base price + coat costs)
if (item.Coats != null && item.Coats.Any())
{
// CalculateQuoteItemPriceAsync already adds catalog base price to coat costs
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
}
else
{
// No coats - use simple catalog default price
var catalogItemTotal = catalogItem.DefaultPrice * item.Quantity;
itemResult = new QuoteItemPricingResult
{
MaterialCost = 0,
LaborCost = 0,
EquipmentCost = 0,
ItemSubtotal = catalogItemTotal,
UnitPrice = catalogItem.DefaultPrice,
TotalPrice = catalogItemTotal
};
}
}
else
{
// Catalog item not found, create zero result
itemResult = new QuoteItemPricingResult
{
MaterialCost = 0,
LaborCost = 0,
EquipmentCost = 0,
ItemSubtotal = 0,
UnitPrice = 0,
TotalPrice = 0
};
}
}
else
{
// Calculated items use the full pricing calculation
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
}
// All items (catalog and calculated) go through CalculateQuoteItemPriceAsync, which
// handles PowderCostOverride, prep cost inclusion, and all item type variants.
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
itemResults.Add(itemResult);
}
@@ -104,8 +104,11 @@ public class QuotePricingAssemblyService : IQuotePricingAssemblyService
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value);
if (catalogItem != null)
{
item.UnitPrice = catalogItem.DefaultPrice;
item.TotalPrice = catalogItem.DefaultPrice * itemDto.Quantity;
var unitPrice = itemDto.PowderCostOverride is > 0
? itemDto.PowderCostOverride.Value
: catalogItem.DefaultPrice;
item.UnitPrice = unitPrice;
item.TotalPrice = unitPrice * itemDto.Quantity;
_logger.LogInformation("Catalog item no coats: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice);
}
@@ -2978,6 +2978,7 @@
if (!r.ok) { const d = await r.json(); errEl.textContent = d.error ?? 'Save failed.'; errEl.classList.remove('d-none'); return; }
modal.hide();
await load();
costing.load();
showToast(id > 0 ? 'Time entry updated.' : 'Time logged.', 'success');
} catch { errEl.textContent = 'Network error.'; errEl.classList.remove('d-none'); }
}
@@ -2990,7 +2991,7 @@
headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': tok },
body: JSON.stringify({ id })
});
if (r.ok) { await load(); showToast('Time entry deleted.', 'secondary'); }
if (r.ok) { await load(); costing.load(); showToast('Time entry deleted.', 'secondary'); }
}
function esc(s) {