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:
@@ -590,53 +590,9 @@ public class PricingCalculationService : IPricingCalculationService
|
|||||||
{
|
{
|
||||||
QuoteItemPricingResult itemResult;
|
QuoteItemPricingResult itemResult;
|
||||||
|
|
||||||
// Catalog items - if they have coats, add coat costs to catalog base price
|
// All items (catalog and calculated) go through CalculateQuoteItemPriceAsync, which
|
||||||
if (item.CatalogItemId.HasValue)
|
// handles PowderCostOverride, prep cost inclusion, and all item type variants.
|
||||||
{
|
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResults.Add(itemResult);
|
itemResults.Add(itemResult);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,8 +104,11 @@ public class QuotePricingAssemblyService : IQuotePricingAssemblyService
|
|||||||
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value);
|
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value);
|
||||||
if (catalogItem != null)
|
if (catalogItem != null)
|
||||||
{
|
{
|
||||||
item.UnitPrice = catalogItem.DefaultPrice;
|
var unitPrice = itemDto.PowderCostOverride is > 0
|
||||||
item.TotalPrice = catalogItem.DefaultPrice * itemDto.Quantity;
|
? 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);
|
_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; }
|
if (!r.ok) { const d = await r.json(); errEl.textContent = d.error ?? 'Save failed.'; errEl.classList.remove('d-none'); return; }
|
||||||
modal.hide();
|
modal.hide();
|
||||||
await load();
|
await load();
|
||||||
|
costing.load();
|
||||||
showToast(id > 0 ? 'Time entry updated.' : 'Time logged.', 'success');
|
showToast(id > 0 ? 'Time entry updated.' : 'Time logged.', 'success');
|
||||||
} catch { errEl.textContent = 'Network error.'; errEl.classList.remove('d-none'); }
|
} catch { errEl.textContent = 'Network error.'; errEl.classList.remove('d-none'); }
|
||||||
}
|
}
|
||||||
@@ -2990,7 +2991,7 @@
|
|||||||
headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': tok },
|
headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': tok },
|
||||||
body: JSON.stringify({ id })
|
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) {
|
function esc(s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user