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
+28 -17
View File
@@ -341,9 +341,9 @@
@foreach (var item in catalogItems)
{
var catIdx = allItems.IndexOf(item);
<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>
@if (item.Coats != null && item.Coats.Any())
{
<br />
@@ -399,9 +399,9 @@
<br /><small class="text-muted"><i class="bi bi-sticky me-1"></i>@item.Notes</small>
}
</td>
<td class="text-center">@item.Quantity</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</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>
<td class="text-center">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="openWizard(@catIdx)" title="Edit"><i class="bi bi-pencil"></i></button>
@@ -440,6 +440,7 @@
{
var custIdx = allItems.IndexOf(item);
// Use stored PowderToOrder per coat; fall back to calculating from efficiency data
// Note: row has data-item-id for inline editing
decimal totalPowderNeeded = 0;
if (item.Coats != null && item.Coats.Any(c => c.PowderToOrder > 0))
{
@@ -458,9 +459,9 @@
{
totalPowderNeeded = (item.SurfaceAreaSqFt * item.Quantity) / (30m * 0.65m);
}
<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>
@if (item.RequiresSandblasting || item.RequiresMasking)
{
<br />
@@ -528,7 +529,7 @@
<br /><small class="text-muted"><i class="bi bi-sticky me-1"></i>@item.Notes</small>
}
</td>
<td class="text-center">@item.Quantity</td>
<td class="text-center"><span data-inline-field="quantity" data-raw-value="@item.Quantity">@item.Quantity</span></td>
<td class="text-center">
@if (item.SurfaceAreaSqFt > 0)
{
@@ -553,8 +554,8 @@
}
else { <span class="text-muted">&mdash;</span> }
</td>
<td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</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>
<td class="text-center">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="openWizard(@custIdx)" title="Edit"><i class="bi bi-pencil"></i></button>
@@ -590,15 +591,15 @@
@foreach (var item in laborItems)
{
var labIdx = allItems.IndexOf(item);
<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>
@if (!string.IsNullOrEmpty(item.Notes))
{
<br /><small class="text-muted"><i class="bi bi-sticky me-1"></i>@item.Notes</small>
}
</td>
<td class="text-center">@item.Quantity</td>
<td class="text-center"><span data-inline-field="quantity" data-raw-value="@item.Quantity">@item.Quantity</span></td>
<td class="text-center">
@if (item.EstimatedMinutes > 0)
{
@@ -606,8 +607,8 @@
}
else { <span class="text-muted">&mdash;</span> }
</td>
<td class="text-end">@item.UnitPrice.ToString("C")</td>
<td class="text-end fw-semibold">@item.TotalPrice.ToString("C")</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>
<td class="text-center">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="openWizard(@labIdx)" title="Edit"><i class="bi bi-pencil"></i></button>
@@ -1688,7 +1689,7 @@
}
<div class="d-flex justify-content-between fw-bold border-top pt-2 mt-1">
<span>Total</span>
<span>@jobPb.Total.ToString("C")</span>
<span class="job-final-price-display">@jobPb.Total.ToString("C")</span>
</div>
@{
var jobTotalDirectCost = jobPb.MaterialCosts + jobPb.LaborCosts + jobPb.EquipmentCosts + jobPb.OvenBatchCost + jobPb.FacilityOverheadCost + jobPb.ShopSuppliesAmount;
@@ -1717,7 +1718,7 @@
}
<div>
<label class="text-muted small mb-1">Final Price</label>
<h3 class="mb-0 text-primary">@Model.FinalPrice.ToString("C")</h3>
<h3 class="mb-0 text-primary job-final-price-display">@Model.FinalPrice.ToString("C")</h3>
</div>
}
</div>
@@ -2415,6 +2416,16 @@
<link rel="stylesheet" href="~/css/job-photos.css" />
<script src="~/js/job-photos.js" asp-append-version="true"></script>
<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", "Jobs")',
canEdit: true,
totals: {
finalPrice: '.job-final-price-display'
}
};
</script>
<script>
// -- Inline date editing ----------------------------------------------
const jobId = @Model.Id;