Hide email controls when no email on file; show SMS hint for quote/job events
- Quotes Create/Edit: hide 'Send via email' checkbox when customer has no email; show badge 'send via SMS from details' or 'SMS consent required' when customer has a mobile number. JS responds to customer dropdown change. - Quotes Details: hide 'Send Quote via Email' button and approval email checkbox; hide SMS button when no mobile; show consent-required note. - Jobs Details (Mark Complete modal): hide email checkbox; show 'SMS notification will be sent' badge or consent-required note. - Jobs Index (status modal): hide email row when customer has no email. - Jobs Edit: hide 'Notify customer if status changes' when no email. - Invoices Details: hide Send/Re-send buttons when no email (vs. disabled). DTOs: added CustomerEmail + CustomerNotifyByEmail to JobDto/JobListDto; added CustomerNotifyByEmail/CustomerMobilePhone/CustomerNotifyBySms to QuoteDto. Mapped in JobProfile and QuotesController customer blocks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,8 @@ public class JobDto
|
||||
public bool RequiresCustomerApproval { get; set; }
|
||||
public bool IsCustomerApproved { get; set; }
|
||||
|
||||
public string? CustomerEmail { get; set; }
|
||||
public bool CustomerNotifyByEmail { get; set; } = true;
|
||||
// Customer SMS opt-in — used for SMS compose modal on job details
|
||||
public bool CustomerNotifyBySms { get; set; }
|
||||
public string? CustomerMobilePhone { get; set; }
|
||||
@@ -103,6 +105,7 @@ public class JobListDto
|
||||
public string PriorityDisplayName { get; set; } = string.Empty;
|
||||
public string PriorityColorClass { get; set; } = "secondary";
|
||||
|
||||
public string? CustomerEmail { get; set; }
|
||||
public bool CustomerNotifyByEmail { get; set; } = true;
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
|
||||
@@ -45,6 +45,9 @@ public class QuoteDto
|
||||
public string? CustomerContactLastName { get; set; }
|
||||
public string? CustomerEmail { get; set; }
|
||||
public string? CustomerPhone { get; set; }
|
||||
public bool CustomerNotifyByEmail { get; set; } = true;
|
||||
public string? CustomerMobilePhone { get; set; }
|
||||
public bool CustomerNotifyBySms { get; set; }
|
||||
|
||||
public string? ProspectCompanyName { get; set; }
|
||||
public string? ProspectContactFirstName { get; set; }
|
||||
|
||||
@@ -58,6 +58,10 @@ public class JobProfile : Profile
|
||||
opt => opt.MapFrom(src => src.OriginalJob != null ? src.OriginalJob.JobNumber : null))
|
||||
.ForMember(dest => dest.IntakeCheckedByName,
|
||||
opt => opt.MapFrom(src => src.IntakeCheckedBy != null ? src.IntakeCheckedBy.FullName : null))
|
||||
.ForMember(dest => dest.CustomerEmail,
|
||||
opt => opt.MapFrom(src => src.Customer != null ? src.Customer.Email : null))
|
||||
.ForMember(dest => dest.CustomerNotifyByEmail,
|
||||
opt => opt.MapFrom(src => src.Customer == null || src.Customer.NotifyByEmail))
|
||||
.ForMember(dest => dest.CustomerNotifyBySms,
|
||||
opt => opt.MapFrom(src => src.Customer != null && src.Customer.NotifyBySms))
|
||||
.ForMember(dest => dest.CustomerMobilePhone,
|
||||
@@ -116,6 +120,8 @@ public class JobProfile : Profile
|
||||
.ForMember(dest => dest.PriorityCode, opt => opt.MapFrom(src => src.JobPriority.PriorityCode))
|
||||
.ForMember(dest => dest.PriorityDisplayName, opt => opt.MapFrom(src => src.JobPriority.DisplayName))
|
||||
.ForMember(dest => dest.PriorityColorClass, opt => opt.MapFrom(src => src.JobPriority.ColorClass))
|
||||
.ForMember(dest => dest.CustomerEmail,
|
||||
opt => opt.MapFrom(src => src.Customer != null ? src.Customer.Email : null))
|
||||
.ForMember(dest => dest.CustomerNotifyByEmail,
|
||||
opt => opt.MapFrom(src => src.Customer == null || src.Customer.NotifyByEmail));
|
||||
|
||||
|
||||
@@ -1279,6 +1279,9 @@ public class JobsController : Controller
|
||||
dto.SendEmailOnStatusChange = (bool)(ViewBag.EmailDefaultOnStatusChange ?? false);
|
||||
}
|
||||
|
||||
// Used by view to hide the email checkbox when the customer has no email address on file
|
||||
ViewBag.CustomerEmail = job.Customer?.Email;
|
||||
|
||||
return View(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -533,6 +533,9 @@ public class QuotesController : Controller
|
||||
quoteDto.CustomerContactLastName = customer.ContactLastName;
|
||||
quoteDto.CustomerEmail = customer.Email;
|
||||
quoteDto.CustomerPhone = customer.Phone;
|
||||
quoteDto.CustomerNotifyByEmail = customer.NotifyByEmail;
|
||||
quoteDto.CustomerMobilePhone = customer.MobilePhone;
|
||||
quoteDto.CustomerNotifyBySms = customer.NotifyBySms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1264,6 +1267,8 @@ public class QuotesController : Controller
|
||||
ViewBag.CustomerContactName = $"{quote.Customer.ContactFirstName} {quote.Customer.ContactLastName}".Trim();
|
||||
ViewBag.CustomerEmail = quote.Customer.Email ?? "";
|
||||
ViewBag.CustomerPhone = quote.Customer.Phone ?? "";
|
||||
ViewBag.CustomerNotifyBySms = quote.Customer.NotifyBySms;
|
||||
ViewBag.CustomerMobilePhone = quote.Customer.MobilePhone ?? "";
|
||||
}
|
||||
|
||||
var editCurrentUser = await _userManager.GetUserAsync(User);
|
||||
@@ -2627,6 +2632,16 @@ public class QuotesController : Controller
|
||||
.Where(c => !c.NotifyByEmail)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
// Customers with no email address on file — JS hides the email section entirely
|
||||
ViewBag.CustomerNoEmailIds = customers
|
||||
.Where(c => string.IsNullOrWhiteSpace(c.Email))
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
// Customers who have given SMS consent with a mobile number on file
|
||||
ViewBag.CustomerSmsConsentIds = customers
|
||||
.Where(c => c.NotifyBySms && !string.IsNullOrWhiteSpace(c.MobilePhone))
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
// Stored separately so views can restore the company default when switching away from an exempt customer
|
||||
// (ViewBag.CompanyTaxPercent is set by the calling action if it has access to operatingCosts)
|
||||
if (ViewBag.CompanyTaxPercent == null && customers.Any())
|
||||
@@ -2817,6 +2832,9 @@ public class QuotesController : Controller
|
||||
quoteDto.CustomerContactLastName = customer.ContactLastName;
|
||||
quoteDto.CustomerEmail = customer.Email;
|
||||
quoteDto.CustomerPhone = customer.Phone;
|
||||
quoteDto.CustomerNotifyByEmail = customer.NotifyByEmail;
|
||||
quoteDto.CustomerMobilePhone = customer.MobilePhone;
|
||||
quoteDto.CustomerNotifyBySms = customer.NotifyBySms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
var isVoided = Model.Status == InvoiceStatus.Voided || Model.Status == InvoiceStatus.WrittenOff;
|
||||
var canPay = !isVoided && Model.BalanceDue > 0;
|
||||
var canResend = !isDraft && !isVoided && Model.Status != InvoiceStatus.Paid;
|
||||
var emailOptedOut = !Model.CustomerNotifyByEmail;
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail);
|
||||
var emailOptedOut = hasEmail && !Model.CustomerNotifyByEmail;
|
||||
var hasAvailableCredits = ViewBag.AvailableCreditMemos != null && ((IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos).Any();
|
||||
var canIssueRefund = !isDraft && !isVoided && Model.AmountPaid > 0;
|
||||
var canApplyCredit = !isVoided && Model.BalanceDue > 0 && hasAvailableCredits;
|
||||
@@ -58,7 +59,17 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (emailOptedOut)
|
||||
@if (!hasEmail)
|
||||
{
|
||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-envelope-slash fs-5"></i>
|
||||
<span>
|
||||
<strong>@Model.CustomerName</strong> has no email address on file — email buttons are hidden.
|
||||
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else if (emailOptedOut)
|
||||
{
|
||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-envelope-x fs-5"></i>
|
||||
@@ -560,23 +571,26 @@
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil me-2"></i>Edit Invoice
|
||||
</a>
|
||||
<form id="sendInvoiceForm" asp-action="Send" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
@if (emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendInvoiceModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
@if (hasEmail)
|
||||
{
|
||||
<form id="sendInvoiceForm" asp-action="Send" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
@if (emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendInvoiceModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@if (canPay)
|
||||
{
|
||||
@@ -587,7 +601,7 @@
|
||||
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-file-pdf me-2"></i>Download PDF
|
||||
</a>
|
||||
@if (canResend)
|
||||
@if (canResend && hasEmail)
|
||||
{
|
||||
@if (emailOptedOut)
|
||||
{
|
||||
|
||||
@@ -1840,13 +1840,28 @@
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="completeJobSendEmail" name="SendEmailToCustomer" value="true"
|
||||
@(ViewBag.EmailDefaultOnComplete == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="completeJobSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerEmail))
|
||||
{
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="completeJobSendEmail" name="SendEmailToCustomer" value="true"
|
||||
@(ViewBag.EmailDefaultOnComplete == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="completeJobSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
@if (Model.CustomerNotifyBySms && !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone))
|
||||
{
|
||||
<span class="badge bg-info text-white">
|
||||
<i class="bi bi-phone me-1"></i>SMS notification will be sent
|
||||
</span>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(Model.CustomerMobilePhone) && !Model.CustomerNotifyBySms)
|
||||
{
|
||||
<span class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent required</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">Cancel</button>
|
||||
|
||||
@@ -274,14 +274,17 @@
|
||||
@{
|
||||
var sendEmailDefault = (bool)(ViewBag.EmailDefaultOnStatusChange ?? Model.SendEmailOnStatusChange);
|
||||
}
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailOnStatusChange" name="SendEmailOnStatusChange" value="true"
|
||||
checked="@(sendEmailDefault ? "checked" : null)" />
|
||||
<label class="form-check-label small" for="SendEmailOnStatusChange">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer if status changes
|
||||
</label>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(ViewBag.CustomerEmail as string))
|
||||
{
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailOnStatusChange" name="SendEmailOnStatusChange" value="true"
|
||||
checked="@(sendEmailDefault ? "checked" : null)" />
|
||||
<label class="form-check-label small" for="SendEmailOnStatusChange">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer if status changes
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancel
|
||||
|
||||
@@ -196,6 +196,7 @@
|
||||
data-status-id="@job.JobStatusId"
|
||||
data-status-name="@job.StatusDisplayName"
|
||||
data-customer-notify="@job.CustomerNotifyByEmail.ToString().ToLower()"
|
||||
data-customer-email="@(string.IsNullOrWhiteSpace(job.CustomerEmail) ? "false" : "true")"
|
||||
title="Click to change status">
|
||||
<span class="pcl-chip-dot"></span>@job.StatusDisplayName
|
||||
</span>
|
||||
@@ -504,16 +505,18 @@
|
||||
<option value="">Loading statuses...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="statusModalSendEmail"
|
||||
@(ViewBag.EmailDefaultOnStatusChange == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="statusModalSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
</div>
|
||||
<div id="statusModalEmailOptOutNote" class="alert alert-warning alert-permanent py-1 px-2 mt-2 small" style="display:none;">
|
||||
<i class="bi bi-bell-slash me-1"></i>This customer has email notifications turned off.
|
||||
<div id="statusModalEmailRow">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="statusModalSendEmail"
|
||||
@(ViewBag.EmailDefaultOnStatusChange == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="statusModalSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
</div>
|
||||
<div id="statusModalEmailOptOutNote" class="alert alert-warning alert-permanent py-1 px-2 mt-2 small" style="display:none;">
|
||||
<i class="bi bi-bell-slash me-1"></i>This customer has email notifications turned off.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -750,20 +753,25 @@
|
||||
const jobNumber = this.getAttribute('data-job-number');
|
||||
const statusName = this.getAttribute('data-status-name');
|
||||
const customerNotify = this.getAttribute('data-customer-notify') !== 'false';
|
||||
const customerHasEmail = this.getAttribute('data-customer-email') !== 'false';
|
||||
|
||||
// Update modal content
|
||||
document.getElementById('modalStatusJobNumber').textContent = jobNumber;
|
||||
document.getElementById('modalCurrentStatus').textContent = statusName;
|
||||
document.getElementById('statusSelect').value = currentJobStatusId;
|
||||
|
||||
// Show email controls only when the customer has an email address on file
|
||||
const emailRow = document.getElementById('statusModalEmailRow');
|
||||
if (emailRow) emailRow.style.display = customerHasEmail ? '' : 'none';
|
||||
|
||||
// Reflect customer email opt-out preference
|
||||
const emailCheckbox = document.getElementById('statusModalSendEmail');
|
||||
const emailOptOutNote = document.getElementById('statusModalEmailOptOutNote');
|
||||
if (emailCheckbox) {
|
||||
if (emailCheckbox && customerHasEmail) {
|
||||
emailCheckbox.disabled = !customerNotify;
|
||||
if (!customerNotify) emailCheckbox.checked = false;
|
||||
}
|
||||
if (emailOptOutNote) emailOptOutNote.style.display = customerNotify ? 'none' : 'block';
|
||||
if (emailOptOutNote) emailOptOutNote.style.display = (customerHasEmail && !customerNotify) ? 'block' : 'none';
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('statusModal'));
|
||||
|
||||
@@ -376,18 +376,21 @@
|
||||
<!-- Form Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body d-flex align-items-center justify-content-end flex-wrap gap-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailToCustomer" name="SendEmailToCustomer" value="true" />
|
||||
<label class="form-check-label fw-semibold" for="SendEmailToCustomer">
|
||||
<i class="bi bi-envelope me-1"></i>Send quote via email
|
||||
</label>
|
||||
<div class="d-flex align-items-center gap-1" id="notifyCustomerSection">
|
||||
<div id="emailNotifySection">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailToCustomer" name="SendEmailToCustomer" value="true" />
|
||||
<label class="form-check-label fw-semibold" for="SendEmailToCustomer">
|
||||
<i class="bi bi-envelope me-1"></i>Send quote via email
|
||||
</label>
|
||||
</div>
|
||||
<span id="emailOptOutNote" class="badge bg-warning text-dark ms-1" style="display:none;"
|
||||
title="This customer has email notifications turned off">
|
||||
<i class="bi bi-bell-slash me-1"></i>Notifications off
|
||||
</span>
|
||||
</div>
|
||||
<span id="emailOptOutNote" class="badge bg-warning text-dark ms-1" style="display:none;"
|
||||
title="This customer has email notifications turned off">
|
||||
<i class="bi bi-bell-slash me-1"></i>Notifications off
|
||||
</span>
|
||||
<span id="smsNotifyNote" style="display:none;"></span>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="top"
|
||||
data-bs-title="Send via Email"
|
||||
@@ -567,6 +570,8 @@
|
||||
"companyTaxPercent": @((ViewBag.CompanyTaxPercent ?? Model.TaxPercent).ToString()),
|
||||
"taxExemptCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerTaxExemptIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"emailOptOutCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerEmailOptOutIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"noEmailCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerNoEmailIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"smsConsentCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerSmsConsentIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"discountType": @Json.Serialize(Model.DiscountType),
|
||||
"discountValue": @Model.DiscountValue,
|
||||
"isRushJob": @Json.Serialize(Model.IsRushJob),
|
||||
@@ -703,11 +708,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Update tax rate and email opt-out state when customer changes
|
||||
// Update tax rate, email visibility, and SMS note when customer changes
|
||||
function onQuoteCustomerChanged(select) {
|
||||
const meta = JSON.parse(document.getElementById('quoteMetaData').textContent);
|
||||
const exemptIds = new Set(meta.taxExemptCustomerIds || []);
|
||||
const optOutIds = new Set(meta.emailOptOutCustomerIds || []);
|
||||
const noEmailIds = new Set(meta.noEmailCustomerIds || []);
|
||||
const smsConsentIds = new Set(meta.smsConsentCustomerIds || []);
|
||||
const customerId = parseInt(select.value) || 0;
|
||||
|
||||
const taxField = document.querySelector('[name="TaxPercent"]');
|
||||
@@ -715,13 +722,34 @@
|
||||
taxField.value = exemptIds.has(customerId) ? 0 : (meta.companyTaxPercent ?? meta.taxPercent);
|
||||
}
|
||||
|
||||
const emailCheckbox = document.getElementById('SendEmailToCustomer');
|
||||
const emailNote = document.getElementById('emailOptOutNote');
|
||||
if (emailCheckbox) {
|
||||
const optedOut = optOutIds.has(customerId);
|
||||
emailCheckbox.disabled = optedOut;
|
||||
if (optedOut) emailCheckbox.checked = false;
|
||||
if (emailNote) emailNote.style.display = optedOut ? 'inline' : 'none';
|
||||
const noEmail = customerId > 0 && noEmailIds.has(customerId);
|
||||
const emailSection = document.getElementById('emailNotifySection');
|
||||
const smsNote = document.getElementById('smsNotifyNote');
|
||||
|
||||
if (emailSection) emailSection.style.display = noEmail ? 'none' : '';
|
||||
|
||||
if (!noEmail) {
|
||||
const emailCheckbox = document.getElementById('SendEmailToCustomer');
|
||||
const emailNote = document.getElementById('emailOptOutNote');
|
||||
if (emailCheckbox) {
|
||||
const optedOut = optOutIds.has(customerId);
|
||||
emailCheckbox.disabled = optedOut;
|
||||
if (optedOut) emailCheckbox.checked = false;
|
||||
if (emailNote) emailNote.style.display = optedOut ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (smsNote) {
|
||||
if (noEmail && customerId > 0) {
|
||||
const hasSms = smsConsentIds.has(customerId);
|
||||
smsNote.style.display = 'inline';
|
||||
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
||||
smsNote.innerHTML = hasSms
|
||||
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
||||
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
||||
} else {
|
||||
smsNote.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1524,29 +1524,53 @@
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil me-1"></i>Edit Quote
|
||||
</a>
|
||||
@{
|
||||
var detHasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail);
|
||||
var detHasMobile = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone);
|
||||
var detHasSmsConsent = Model.CustomerNotifyBySms && detHasMobile;
|
||||
}
|
||||
@if (Model.StatusCode != "APPROVED" && Model.StatusCode != "CONVERTED")
|
||||
{
|
||||
<form asp-action="ApproveQuote" asp-route-id="@Model.Id" method="post" id="approveQuoteForm">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="approveQuoteSendEmail" name="sendEmail" value="true"
|
||||
@(ViewBag.EmailDefaultOnApprove == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="approveQuoteSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
</div>
|
||||
@if (detHasEmail)
|
||||
{
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="approveQuoteSendEmail" name="sendEmail" value="true"
|
||||
@(ViewBag.EmailDefaultOnApprove == true ? "checked" : "") />
|
||||
<label class="form-check-label small" for="approveQuoteSendEmail">
|
||||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-success w-100" data-bs-toggle="modal" data-bs-target="#approveQuoteModal">
|
||||
<i class="bi bi-check-circle me-1"></i>Approve Quote
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-primary" onclick="resendQuote(@Model.Id)">
|
||||
<i class="bi bi-envelope-arrow-up me-1"></i>Send Quote via Email
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" onclick="sendQuoteSms(@Model.Id)">
|
||||
<i class="bi bi-chat-dots me-1"></i>Send Quote via SMS
|
||||
</button>
|
||||
@if (detHasEmail)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary" onclick="resendQuote(@Model.Id)">
|
||||
<i class="bi bi-envelope-arrow-up me-1"></i>Send Quote via Email
|
||||
</button>
|
||||
}
|
||||
@if (detHasMobile)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-info" onclick="sendQuoteSms(@Model.Id)">
|
||||
<i class="bi bi-chat-dots me-1"></i>Send Quote via SMS
|
||||
</button>
|
||||
}
|
||||
@if (!detHasMobile && !detHasEmail)
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent py-1 px-2 small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>No email or phone — update the customer record to send this quote.
|
||||
</div>
|
||||
}
|
||||
@if (detHasMobile && !detHasSmsConsent)
|
||||
{
|
||||
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent required to send via text.</div>
|
||||
}
|
||||
@if (!Model.ConvertedToJobId.HasValue)
|
||||
{
|
||||
<form asp-action="ConvertToJob" asp-route-id="@Model.Id" method="post" class="d-inline" id="createJobForm">
|
||||
|
||||
@@ -403,12 +403,37 @@
|
||||
<!-- Form Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body d-flex align-items-center justify-content-end flex-wrap gap-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailToCustomer" name="SendEmailToCustomer" value="true" />
|
||||
<label class="form-check-label fw-semibold" for="SendEmailToCustomer">
|
||||
<i class="bi bi-envelope me-1"></i>Send updated quote via email
|
||||
</label>
|
||||
@{
|
||||
var editHasEmail = !string.IsNullOrWhiteSpace(ViewBag.CustomerEmail as string);
|
||||
var editHasSms = (bool)(ViewBag.CustomerNotifyBySms ?? false) && !string.IsNullOrWhiteSpace(ViewBag.CustomerMobilePhone as string);
|
||||
}
|
||||
<div class="d-flex align-items-center gap-1" id="notifyCustomerSection">
|
||||
<div id="emailNotifySection" style="@(editHasEmail ? "" : "display:none;")">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="SendEmailToCustomer" name="SendEmailToCustomer" value="true" />
|
||||
<label class="form-check-label fw-semibold" for="SendEmailToCustomer">
|
||||
<i class="bi bi-envelope me-1"></i>Send updated quote via email
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (!editHasEmail)
|
||||
{
|
||||
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
|
||||
@if (editHasSms)
|
||||
{
|
||||
<i class="bi bi-phone me-1"></i><text>No email — send via SMS from quote details</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-phone-slash me-1"></i><text>No email — SMS consent required</text>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span id="smsNotifyNote" style="display:none;"></span>
|
||||
}
|
||||
</div>
|
||||
<a asp-action="Details" asp-controller="Quotes" asp-route-id="@Model.Id" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancel
|
||||
@@ -601,7 +626,10 @@
|
||||
"aiAnalyzeUrl": "@Url.Action("AiAnalyzeItem", "Quotes")",
|
||||
"aiPhotoQuotesEnabled": @Json.Serialize((bool)(ViewBag.AiPhotoQuotesEnabled ?? true)),
|
||||
"itemsFieldPrefix": "QuoteItems",
|
||||
"aiRecalcUrl": "@Url.Action("AiRecalcPrice", "Quotes")"
|
||||
"aiRecalcUrl": "@Url.Action("AiRecalcPrice", "Quotes")",
|
||||
"emailOptOutCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerEmailOptOutIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"noEmailCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerNoEmailIds ?? new System.Collections.Generic.HashSet<int>())),
|
||||
"smsConsentCustomerIds": @Html.Raw(System.Text.Json.JsonSerializer.Serialize(ViewBag.CustomerSmsConsentIds ?? new System.Collections.Generic.HashSet<int>()))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -652,9 +680,49 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initTagInput('quoteTags', 'quoteTagsContainer');
|
||||
var custEl = document.getElementById('customerSelect');
|
||||
if (custEl) new TomSelect(custEl, { placeholder: '-- Select Customer --', openOnFocus: true, maxOptions: false });
|
||||
if (custEl) new TomSelect(custEl, { placeholder: '-- Select Customer --', openOnFocus: true, maxOptions: false,
|
||||
onChange: function(value) { onQuoteCustomerChanged({ value: value }); }
|
||||
});
|
||||
});
|
||||
|
||||
function onQuoteCustomerChanged(select) {
|
||||
const meta = JSON.parse(document.getElementById('quoteMetaData').textContent);
|
||||
const optOutIds = new Set(meta.emailOptOutCustomerIds || []);
|
||||
const noEmailIds = new Set(meta.noEmailCustomerIds || []);
|
||||
const smsConsentIds = new Set(meta.smsConsentCustomerIds || []);
|
||||
const customerId = parseInt(select.value) || 0;
|
||||
|
||||
const noEmail = customerId > 0 && noEmailIds.has(customerId);
|
||||
const emailSection = document.getElementById('emailNotifySection');
|
||||
const smsNote = document.getElementById('smsNotifyNote');
|
||||
|
||||
if (emailSection) emailSection.style.display = noEmail ? 'none' : '';
|
||||
|
||||
if (!noEmail) {
|
||||
const emailCheckbox = document.getElementById('SendEmailToCustomer');
|
||||
const emailNote = document.getElementById('emailOptOutNote');
|
||||
if (emailCheckbox) {
|
||||
const optedOut = optOutIds.has(customerId);
|
||||
emailCheckbox.disabled = optedOut;
|
||||
if (optedOut) emailCheckbox.checked = false;
|
||||
if (emailNote) emailNote.style.display = optedOut ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (smsNote) {
|
||||
if (noEmail && customerId > 0) {
|
||||
const hasSms = smsConsentIds.has(customerId);
|
||||
smsNote.style.display = 'inline';
|
||||
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
|
||||
smsNote.innerHTML = hasSms
|
||||
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
|
||||
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
|
||||
} else {
|
||||
smsNote.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discount type toggle
|
||||
function onDiscountTypeChange() {
|
||||
const type = document.getElementById('discountTypeSelect').value;
|
||||
|
||||
Reference in New Issue
Block a user