Add quote-changed banner with re-sync to job details

When a source quote is edited after a job was created from it, the job
details page now shows a warning banner with the date of the change and
a link to the quote. Two actions are offered:

- Re-sync from Quote: replaces all job items, coats, prep services, and
  pricing from the current quote. Only available while the job is still
  in a pre-production status (Pending, Quoted, Approved); hidden once
  shop work has started (InPreparation or beyond).
- Dismiss: acknowledges the change without altering the job, clearing
  the banner by advancing the stored snapshot timestamp.

Implemented via Job.QuoteSnapshotUpdatedAt (new nullable column), set at
quote→job conversion time. The banner fires when quote.UpdatedAt exceeds
this baseline. Migration: AddJobQuoteSnapshotUpdatedAt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 18:02:46 -04:00
parent ac3e4452b2
commit 9221fcc783
6 changed files with 9648 additions and 1 deletions
@@ -4,6 +4,7 @@
ViewData["Title"] = $"Job {Model.JobNumber}";
ViewData["PageIcon"] = "bi-briefcase";
var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel;
bool quoteUpdated = ViewBag.QuoteUpdatedAfterConversion == true;
}
<div class="row justify-content-center">
@@ -39,6 +40,49 @@
</div>
</div>
@if (quoteUpdated)
{
bool canResync = ViewBag.CanResyncFromQuote == true;
<div class="alert alert-warning alert-permanent mb-4">
<div class="d-flex align-items-start gap-3">
<i class="bi bi-exclamation-triangle-fill fs-5 mt-1 flex-shrink-0"></i>
<div class="flex-grow-1">
<div class="fw-semibold mb-1">Source quote was updated on @(((DateTime)ViewBag.QuoteUpdatedAt).ToString("MMM d, yyyy"))</div>
<div class="mb-3">
<a asp-controller="Quotes" asp-action="Details" asp-route-id="@ViewBag.SourceQuoteId" class="alert-link">@ViewBag.SourceQuoteNumber</a>
was edited after this job was created.
@if (canResync)
{
<span>Re-sync to replace job items and pricing with the latest quote, or dismiss if you've already handled it manually.</span>
}
else
{
<span>Shop work has started — review the quote and apply any changes manually.</span>
}
</div>
<div class="d-flex gap-2 flex-wrap">
@if (canResync)
{
<form asp-action="ResyncFromQuote" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-warning fw-semibold"
onclick="return confirm('This will replace all job items and pricing with the current quote. Continue?')">
<i class="bi bi-arrow-repeat me-1"></i>Re-sync from Quote
</button>
</form>
}
<form asp-action="DismissQuoteChangedBanner" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-check me-1"></i>Dismiss
</button>
</form>
</div>
</div>
</div>
</div>
}
@if (guidedActivationCallout?.Show == true)
{
<div class="alert alert-primary alert-permanent border-0 shadow-sm mb-4">