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:
@@ -4216,9 +4216,69 @@ public class JobsController : Controller
|
||||
return Json(new { success = false, message = "An error occurred. Please try again." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inline-edits description, quantity, and unit price on a single job line item.
|
||||
/// Adjusts FinalPrice and the stored PricingBreakdownJson snapshot by the price delta.
|
||||
/// Returns updated totals so the page can reflect the change without a reload.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> PatchItem([FromBody] PatchJobItemRequest request)
|
||||
{
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var item = await _unitOfWork.JobItems.GetByIdAsync(request.ItemId);
|
||||
if (item == null) return NotFound();
|
||||
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(item.JobId);
|
||||
if (job == null || job.CompanyId != currentUser.CompanyId) return NotFound();
|
||||
|
||||
var oldTotal = item.TotalPrice;
|
||||
item.Description = request.Description.Trim();
|
||||
item.Quantity = request.Quantity;
|
||||
item.UnitPrice = request.UnitPrice;
|
||||
item.TotalPrice = Math.Round(request.Quantity * request.UnitPrice, 2);
|
||||
await _unitOfWork.JobItems.UpdateAsync(item);
|
||||
|
||||
var delta = item.TotalPrice - oldTotal;
|
||||
job.FinalPrice = Math.Round(job.FinalPrice + delta, 2);
|
||||
|
||||
// Keep the stored pricing snapshot in sync so the breakdown panel stays consistent
|
||||
if (!string.IsNullOrEmpty(job.PricingBreakdownJson))
|
||||
{
|
||||
var pb = JsonSerializer.Deserialize<QuotePricingBreakdownDto>(job.PricingBreakdownJson);
|
||||
if (pb != null)
|
||||
{
|
||||
pb.ItemsSubtotal += delta;
|
||||
pb.SubtotalBeforeDiscount += delta;
|
||||
pb.SubtotalAfterDiscount = pb.SubtotalBeforeDiscount - pb.DiscountAmount;
|
||||
pb.TaxAmount = Math.Round(pb.SubtotalAfterDiscount * pb.TaxPercent / 100m, 2);
|
||||
pb.Total = Math.Round(pb.SubtotalAfterDiscount + pb.RushFee + pb.TaxAmount, 2);
|
||||
job.FinalPrice = pb.Total;
|
||||
job.PricingBreakdownJson = JsonSerializer.Serialize(pb);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.Jobs.UpdateAsync(job);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
return Json(new {
|
||||
lineTotal = item.TotalPrice,
|
||||
finalPrice = job.FinalPrice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteTimeEntryRequest { public int Id { get; set; } }
|
||||
public class PatchJobItemRequest
|
||||
{
|
||||
public int ItemId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
}
|
||||
public class LogMaterialRequest
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user