Fix NoExtraLayerCharge persistence, appointment reminders, coat notes display, scroll restoration, and invoice Send dead-button
- Appointment reminders: add AppointmentReminderBackgroundService (60s poll), ReminderSentAt dedup stamp, NotifyAppointmentReminderAsync sends both customer email and creator staff email; AppointmentReminderStaff notification type + default template added; DateTime.Now used instead of UtcNow to match locally-stored ScheduledStartTime; ToLocalTime() double-conversion removed - NoExtraLayerCharge not persisted: flag existed on CreateQuoteItemCoatDto and was used by pricing engine but never written to JobItemCoat/QuoteItemCoat entities — every edit reset it to false and re-applied the extra layer charge; added column to both entities (migration AddNoExtraLayerChargeToCoats), both read DTOs, all 3 JobItemAssemblyService overloads, JobItemCoatSeed inner class, and existingItemsData JSON in all 5 wizard views; fixed JS template path that hard-coded noExtraLayerCharge: false - Coat notes not visible: notes were rendered in desktop job details but missing from the wizard item card summary and the mobile card view; both fixed - Scroll position lost on item save: sessionStorage save/restore added to item-wizard.js owner form submit handler; path-keyed so cross-page navigation does not restore stale position; requestAnimationFrame used for reliable mobile scroll restoration - Invoice Send dead button: #sendChannelModal was gated inside @if (isDraft) but the button targeting it fires for Sent/Overdue invoices too when customer has both email and SMS; modal moved outside the Draft guard - InitialCreate migration added for fresh database installs; Baseline migration guarded with IF OBJECT_ID check so it no-ops on fresh DBs; Razor scoping bug fixed in Customers/Index.cshtml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -185,12 +185,13 @@
|
||||
<div class="mobile-card-view">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
var isMobileCustomerListFiltered = !string.IsNullOrEmpty(ViewBag.SearchTerm as string);
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No customers found</h5>
|
||||
<p class="text-muted mb-4">@(isCustomerListFiltered ? "No customers match your search." : "Get started by adding your first customer.")</p>
|
||||
<p class="text-muted mb-4">@(isMobileCustomerListFiltered ? "No customers match your search." : "Get started by adding your first customer.")</p>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>@(isCustomerListFiltered ? "Add Customer" : "Add Your First Customer")
|
||||
<i class="bi bi-plus-circle me-2"></i>@(isMobileCustomerListFiltered ? "Add Customer" : "Add Your First Customer")
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -869,42 +869,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showSendModal)
|
||||
{
|
||||
<!-- Send Channel Choice Modal (shown when customer has both email + SMS) -->
|
||||
<div class="modal fade" id="sendChannelModal" tabindex="-1" aria-labelledby="sendChannelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title" id="sendChannelModalLabel">
|
||||
<i class="bi bi-send text-primary me-2"></i>Send Invoice
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p class="mb-3">How would you like to send <strong>@Model.InvoiceNumber</strong> to <strong>@Model.CustomerName</strong>?</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(true, false)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-envelope me-2"></i>Email only
|
||||
<small class="d-block text-muted ms-4">PDF attached · @Model.CustomerEmail</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(false, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-phone me-2"></i>SMS only
|
||||
<small class="d-block text-muted ms-4">View link · @smsPhone</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary text-start" onclick="submitSendInvoice(true, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-send me-2"></i>Both Email & SMS
|
||||
<small class="d-block text-muted ms-4">PDF via email + view link via SMS</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
}
|
||||
|
||||
@if (showSendModal)
|
||||
{
|
||||
<!-- Send Channel Choice Modal (shown when customer has both email + SMS available).
|
||||
Lives outside the isDraft block so it also renders for Sent/Overdue invoices
|
||||
where the customer's email was added after an SMS-only initial send. -->
|
||||
<div class="modal fade" id="sendChannelModal" tabindex="-1" aria-labelledby="sendChannelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title" id="sendChannelModalLabel">
|
||||
<i class="bi bi-send text-primary me-2"></i>Send Invoice
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p class="mb-3">How would you like to send <strong>@Model.InvoiceNumber</strong> to <strong>@Model.CustomerName</strong>?</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(true, false)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-envelope me-2"></i>Email only
|
||||
<small class="d-block text-muted ms-4">PDF attached · @Model.CustomerEmail</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(false, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-phone me-2"></i>SMS only
|
||||
<small class="d-block text-muted ms-4">View link · @smsPhone</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary text-start" onclick="submitSendInvoice(true, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-send me-2"></i>Both Email & SMS
|
||||
<small class="d-block text-muted ms-4">PDF via email + view link via SMS</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canPay)
|
||||
|
||||
@@ -370,7 +370,8 @@
|
||||
transferEfficiency = c.TransferEfficiency,
|
||||
powderCostPerLb = c.PowderCostPerLb,
|
||||
powderToOrder = c.PowderToOrder,
|
||||
notes = c.Notes
|
||||
notes = c.Notes,
|
||||
noExtraLayerCharge = c.NoExtraLayerCharge
|
||||
}),
|
||||
prepServices = item.PrepServices.Select(ps => new {
|
||||
prepServiceId = ps.PrepServiceId,
|
||||
|
||||
@@ -653,7 +653,10 @@
|
||||
<span class="mobile-card-value">
|
||||
@foreach (var coat in item.Coats.OrderBy(c => c.Sequence))
|
||||
{
|
||||
<small class="d-block">@coat.CoatName@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> – @coat.ColorName</text> }</small>
|
||||
<small class="d-block">
|
||||
@coat.CoatName@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> – @coat.ColorName</text> }
|
||||
@if (!string.IsNullOrEmpty(coat.Notes)) { <text><br /><span class="fst-italic text-muted ms-2">@coat.Notes</span></text> }
|
||||
</small>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +357,8 @@
|
||||
transferEfficiency = c.TransferEfficiency,
|
||||
powderCostPerLb = c.PowderCostPerLb,
|
||||
powderToOrder = c.PowderToOrder,
|
||||
notes = c.Notes
|
||||
notes = c.Notes,
|
||||
noExtraLayerCharge = c.NoExtraLayerCharge
|
||||
}),
|
||||
prepServices = item.PrepServices.Select(ps => new {
|
||||
prepServiceId = ps.PrepServiceId,
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
transferEfficiency = c.TransferEfficiency,
|
||||
powderCostPerLb = c.PowderCostPerLb,
|
||||
powderToOrder = c.PowderToOrder,
|
||||
notes = c.Notes
|
||||
notes = c.Notes,
|
||||
noExtraLayerCharge = c.NoExtraLayerCharge
|
||||
}),
|
||||
prepServices = item.PrepServices.Select(ps => new {
|
||||
prepServiceId = ps.PrepServiceId,
|
||||
|
||||
@@ -479,7 +479,8 @@
|
||||
transferEfficiency = c.TransferEfficiency,
|
||||
powderCostPerLb = c.PowderCostPerLb,
|
||||
powderToOrder = c.PowderToOrder,
|
||||
notes = c.Notes
|
||||
notes = c.Notes,
|
||||
noExtraLayerCharge = c.NoExtraLayerCharge
|
||||
})
|
||||
})))
|
||||
</script>
|
||||
|
||||
@@ -525,7 +525,8 @@
|
||||
transferEfficiency = c.TransferEfficiency,
|
||||
powderCostPerLb = c.PowderCostPerLb,
|
||||
powderToOrder = c.PowderToOrder,
|
||||
notes = c.Notes
|
||||
notes = c.Notes,
|
||||
noExtraLayerCharge = c.NoExtraLayerCharge
|
||||
})
|
||||
})))
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user