Fix inline edit not updating pricing breakdown on Job Details

Jobs/PatchItem now returns the full breakdown (itemsSubtotal,
subtotalBeforeDiscount, subtotalAfterDiscount, taxAmount) so all rows
in the pricing card update live without a page refresh.

Added data-pb attributes to the matching spans in the pricing panel.
Updated window.inlineItemEdit.totals config for jobs to map each
response key to its DOM selector.

updateTotals in inline-item-edit.js is now fully generic — cfg.totals
keys must match server response property names directly, eliminating
the old hardcoded tax/taxAmount and balance/balanceDue mismatches.
Updated Quote and Invoice configs accordingly (tax→taxAmount,
balance→balanceDue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 22:58:26 -04:00
parent 30c644a8ec
commit eb13283e76
5 changed files with 31 additions and 21 deletions
@@ -4264,9 +4264,18 @@ public class JobsController : Controller
await _unitOfWork.Jobs.UpdateAsync(job); await _unitOfWork.Jobs.UpdateAsync(job);
await _unitOfWork.CompleteAsync(); await _unitOfWork.CompleteAsync();
// Deserialize again after possible re-serialization to get final values
QuotePricingBreakdownDto? pbFinal = null;
if (!string.IsNullOrEmpty(job.PricingBreakdownJson))
pbFinal = JsonSerializer.Deserialize<QuotePricingBreakdownDto>(job.PricingBreakdownJson);
return Json(new { return Json(new {
lineTotal = item.TotalPrice, lineTotal = item.TotalPrice,
finalPrice = job.FinalPrice finalPrice = job.FinalPrice,
itemsSubtotal = pbFinal?.ItemsSubtotal,
subtotalBeforeDiscount = pbFinal?.SubtotalBeforeDiscount,
subtotalAfterDiscount = pbFinal?.SubtotalAfterDiscount,
taxAmount = pbFinal?.TaxAmount
}); });
} }
} }
@@ -1458,9 +1458,9 @@
canEdit: @Json.Serialize(canEdit), canEdit: @Json.Serialize(canEdit),
totals: { totals: {
subtotal: '#inv-subtotal', subtotal: '#inv-subtotal',
tax: '#inv-tax', taxAmount: '#inv-tax',
total: '#inv-total', total: '#inv-total',
balance: '#inv-balance' balanceDue: '#inv-balance'
} }
}; };
</script> </script>
@@ -1611,7 +1611,7 @@
} }
<div class="d-flex justify-content-between small fw-semibold border-top pt-1 mt-1"> <div class="d-flex justify-content-between small fw-semibold border-top pt-1 mt-1">
<span>Items subtotal</span> <span>Items subtotal</span>
<span>@jobPb.ItemsSubtotal.ToString("C")</span> <span data-pb="itemsSubtotal">@jobPb.ItemsSubtotal.ToString("C")</span>
</div> </div>
</div> </div>
@@ -1660,7 +1660,7 @@
</div> </div>
<div class="d-flex justify-content-between small mb-1"> <div class="d-flex justify-content-between small mb-1">
<span class="text-muted">Subtotal</span> <span class="text-muted">Subtotal</span>
<span>@jobPb.SubtotalBeforeDiscount.ToString("C")</span> <span data-pb="subtotalBeforeDiscount">@jobPb.SubtotalBeforeDiscount.ToString("C")</span>
</div> </div>
@if (jobPb.DiscountAmount > 0) @if (jobPb.DiscountAmount > 0)
{ {
@@ -1670,7 +1670,7 @@
</div> </div>
<div class="d-flex justify-content-between small mb-1"> <div class="d-flex justify-content-between small mb-1">
<span class="text-muted">After discount</span> <span class="text-muted">After discount</span>
<span>@jobPb.SubtotalAfterDiscount.ToString("C")</span> <span data-pb="subtotalAfterDiscount">@jobPb.SubtotalAfterDiscount.ToString("C")</span>
</div> </div>
} }
@if (jobPb.RushFee > 0) @if (jobPb.RushFee > 0)
@@ -1684,7 +1684,7 @@
{ {
<div class="d-flex justify-content-between small mb-1"> <div class="d-flex justify-content-between small mb-1">
<span class="text-muted">Tax (@jobPb.TaxPercent.ToString("G29")%)</span> <span class="text-muted">Tax (@jobPb.TaxPercent.ToString("G29")%)</span>
<span>@jobPb.TaxAmount.ToString("C")</span> <span data-pb="taxAmount">@jobPb.TaxAmount.ToString("C")</span>
</div> </div>
} }
<div class="d-flex justify-content-between fw-bold border-top pt-2 mt-1"> <div class="d-flex justify-content-between fw-bold border-top pt-2 mt-1">
@@ -2422,6 +2422,10 @@
patchUrl: '@Url.Action("PatchItem", "Jobs")', patchUrl: '@Url.Action("PatchItem", "Jobs")',
canEdit: true, canEdit: true,
totals: { totals: {
itemsSubtotal: '[data-pb="itemsSubtotal"]',
subtotalBeforeDiscount: '[data-pb="subtotalBeforeDiscount"]',
subtotalAfterDiscount: '[data-pb="subtotalAfterDiscount"]',
taxAmount: '[data-pb="taxAmount"]',
finalPrice: '.job-final-price-display' finalPrice: '.job-final-price-display'
} }
}; };
@@ -2269,7 +2269,7 @@
canEdit: true, canEdit: true,
totals: { totals: {
subtotal: '#quote-subtotal', subtotal: '#quote-subtotal',
tax: '#quote-tax', taxAmount: '#quote-tax',
total: '#quote-total' total: '#quote-total'
} }
}; };
@@ -24,17 +24,14 @@
setTimeout(() => el.remove(), 4000); setTimeout(() => el.remove(), 4000);
} }
// cfg.totals maps response-property-name → CSS selector.
// Each key must exactly match a property returned by the server's PatchItem action.
function updateTotals(cfg, data) { function updateTotals(cfg, data) {
const t = cfg.totals || {}; const t = cfg.totals || {};
[ Object.entries(t).forEach(([key, selector]) => {
[t.subtotal, data.subtotal], const val = data[key];
[t.tax, data.taxAmount], if (selector && val !== undefined && val !== null) {
[t.total, data.total], document.querySelectorAll(selector).forEach(el => { el.textContent = fmt(val); });
[t.finalPrice, data.finalPrice],
[t.balance, data.balanceDue],
].forEach(([sel, val]) => {
if (sel && val !== undefined && val !== null) {
document.querySelectorAll(sel).forEach(el => { el.textContent = fmt(val); });
} }
}); });
} }