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
@@ -230,21 +230,21 @@
<tbody>
@foreach (var item in Model.InvoiceItems)
{
<tr>
<tr data-item-id="@item.Id">
<td>
<div class="fw-semibold">@item.Description</div>
<span class="fw-semibold" data-inline-field="description" data-raw-value="@item.Description">@item.Description</span>
@if (!string.IsNullOrWhiteSpace(item.ColorName))
{
<small class="text-muted">@item.ColorName</small>
<small class="text-muted d-block">@item.ColorName</small>
}
@if (!string.IsNullOrWhiteSpace(item.Notes))
{
<small class="text-muted d-block">@item.Notes</small>
}
</td>
<td class="text-center">@item.Quantity.ToString("G")</td>
<td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</td>
<td class="text-center"><span data-inline-field="quantity" data-raw-value="@item.Quantity">@item.Quantity.ToString("G")</span></td>
<td class="text-end"><span data-inline-field="unitPrice" data-raw-value="@item.UnitPrice">@item.UnitPrice.ToString("C")</span></td>
<td class="text-end fw-semibold" data-line-total>@item.TotalPrice.ToString("C")</td>
</tr>
}
</tbody>
@@ -257,7 +257,7 @@
<div class="col-md-4">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted">Subtotal</span>
<span>@Model.SubTotal.ToString("C")</span>
<span id="inv-subtotal">@Model.SubTotal.ToString("C")</span>
</div>
@if (Model.DiscountAmount > 0)
{
@@ -276,12 +276,12 @@
<span class="badge bg-warning-subtle text-warning-emphasis ms-1 small fw-normal">@Model.SalesTaxAccountName</span>
}
</span>
<span>@Model.TaxAmount.ToString("C")</span>
<span id="inv-tax">@Model.TaxAmount.ToString("C")</span>
</div>
}
<div class="d-flex justify-content-between fw-bold border-top pt-2 mt-1 fs-5">
<span>Total</span>
<span>@Model.Total.ToString("C")</span>
<span id="inv-total">@Model.Total.ToString("C")</span>
</div>
@if (Model.AmountPaid > 0)
{
@@ -291,7 +291,7 @@
</div>
<div class="d-flex justify-content-between fw-bold border-top pt-2 mt-1">
<span>Balance Due</span>
<span class="@(Model.BalanceDue > 0 ? "text-danger" : "text-success")">@Model.BalanceDue.ToString("C")</span>
<span id="inv-balance" class="@(Model.BalanceDue > 0 ? "text-danger" : "text-success")">@Model.BalanceDue.ToString("C")</span>
</div>
}
</div>
@@ -1442,6 +1442,19 @@
}
@section Scripts {
<script src="~/js/inline-item-edit.js"></script>
<script>
window.inlineItemEdit = {
patchUrl: '@Url.Action("PatchItem", "Invoices")',
canEdit: @Json.Serialize(canEdit),
totals: {
subtotal: '#inv-subtotal',
tax: '#inv-tax',
total: '#inv-total',
balance: '#inv-balance'
}
};
</script>
<script>
function submitSendInvoice(sendEmail, sendSms) {
document.getElementById('sendInvoiceSendEmail').value = sendEmail ? 'true' : 'false';