Inline item editing on details pages; fix Stripe receipt_email

Allow description, quantity, and price to be edited inline on Quote,
Job, and Invoice details pages without re-opening the wizard. Coating
and prep service rows remain read-only by design. Invoice editing is
gated to Draft/Sent/Overdue statuses; totals update live in the DOM.

Remove receipt_email from Stripe PaymentIntent creation so customers
can use any email they choose at checkout — Stripe validates format
and sends the receipt to whatever the customer enters in the Payment
Element, eliminating the risk of a stored email mismatch blocking a
payment from processing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 11:49:04 -04:00
parent 8452ea3fcd
commit 7fa385aeb8
12 changed files with 418 additions and 52 deletions
@@ -276,14 +276,14 @@
<tbody>
@foreach (var item in catalogItems)
{
<tr>
<tr data-item-id="@item.Id">
<td>
@{
var displayDescription = item.Description == "Product Item" || string.IsNullOrWhiteSpace(item.Description)
? (item.CatalogItemName ?? "Catalog Item")
: item.Description;
}
<strong>@displayDescription</strong>
<span data-inline-field="description" data-raw-value="@displayDescription"><strong>@displayDescription</strong></span>
@if (item.CatalogItemId.HasValue &&
item.Description != "Product Item" &&
!string.IsNullOrWhiteSpace(item.Description))
@@ -354,9 +354,9 @@
<small class="text-muted"><i class="bi bi-sticky"></i> <strong>Notes:</strong> @item.Notes</small>
}
</td>
<td>@item.Quantity</td>
<td>@item.UnitPrice.ToString("C")</td>
<td><strong>@item.TotalPrice.ToString("C")</strong></td>
<td><span data-inline-field="quantity" data-raw-value="@item.Quantity">@item.Quantity</span></td>
<td><span data-inline-field="unitPrice" data-raw-value="@item.UnitPrice">@item.UnitPrice.ToString("C")</span></td>
<td data-line-total><strong>@item.TotalPrice.ToString("C")</strong></td>
</tr>
}
</tbody>
@@ -394,9 +394,9 @@
var totalSqFt = item.SurfaceAreaSqFt * item.Quantity;
var powderOnPart = totalSqFt / coverageRate;
var totalPowderNeeded = powderOnPart / transferEff;
<tr>
<tr data-item-id="@item.Id">
<td>
<strong>@item.Description</strong>
<span data-inline-field="description" data-raw-value="@item.Description"><strong>@item.Description</strong></span>
@* Display coating layers *@
@if (item.Coats != null && item.Coats.Any())
@@ -474,7 +474,7 @@
<small class="text-muted"><i class="bi bi-sticky"></i> <strong>Notes:</strong> @item.Notes</small>
}
</td>
<td>@item.Quantity</td>
<td><span data-inline-field="quantity" data-raw-value="@item.Quantity">@item.Quantity</span></td>
<td>
@item.SurfaceAreaSqFt.ToString("F2") @ViewBag.AreaUnit
<br /><small class="text-muted">per item</small>
@@ -487,8 +487,8 @@
<strong class="text-success">@totalPowderNeeded.ToString("F2") lbs</strong>
<br /><small class="text-muted">total batch</small>
</td>
<td>@item.UnitPrice.ToString("C")</td>
<td><strong>@item.TotalPrice.ToString("C")</strong></td>
<td><span data-inline-field="unitPrice" data-raw-value="@item.UnitPrice">@item.UnitPrice.ToString("C")</span></td>
<td data-line-total><strong>@item.TotalPrice.ToString("C")</strong></td>
</tr>
}
</tbody>
@@ -1023,7 +1023,7 @@
<div class="d-flex justify-content-between mb-2">
<span>Subtotal:</span>
<strong>@Model.PricingBreakdown.SubtotalBeforeDiscount.ToString("C")</strong>
<strong id="quote-subtotal">@Model.PricingBreakdown.SubtotalBeforeDiscount.ToString("C")</strong>
</div>
@if (Model.PricingBreakdown.DiscountAmount > 0)
@@ -1075,7 +1075,7 @@
{
<div class="d-flex justify-content-between mb-2">
<span>Tax (@Model.PricingBreakdown.TaxPercent.ToString("G29")%):</span>
<strong>@Model.PricingBreakdown.TaxAmount.ToString("C")</strong>
<strong id="quote-tax">@Model.PricingBreakdown.TaxAmount.ToString("C")</strong>
</div>
}
@@ -1083,7 +1083,7 @@
<div class="d-flex justify-content-between mb-0">
<h5>Total:</h5>
<h5 class="text-primary"><strong>@Model.Total.ToString("C")</strong></h5>
<h5 class="text-primary"><strong id="quote-total">@Model.Total.ToString("C")</strong></h5>
</div>
@if (Model.PricingBreakdown.DiscountAmount > 0)
@@ -2262,6 +2262,18 @@
@section Scripts {
<script src="~/js/customer-change.js" asp-append-version="true"></script>
<script src="~/js/inline-item-edit.js" asp-append-version="true"></script>
<script>
window.inlineItemEdit = {
patchUrl: '@Url.Action("PatchItem", "Quotes")',
canEdit: true,
totals: {
subtotal: '#quote-subtotal',
tax: '#quote-tax',
total: '#quote-total'
}
};
</script>
<script>
function sendQuoteToAdHocEmail(quoteId) {
const email = (document.getElementById('quoteAdHocEmailInput').value ?? '').trim();