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:
@@ -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';
|
||||
|
||||
@@ -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">—</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">—</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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user