Add SMS quote approval, fix Twilio credentials, fix passkey post-login redirect

- Add 'Send Quote via SMS' button on quote details page that sends the approval
  link to the customer via SMS (respects NotifyBySms, handles prospects via ProspectPhone)
- Reuses existing valid approval token rather than regenerating, so a previously
  emailed link stays valid when SMS is also sent
- Fix Twilio appsettings.json placeholders (real credentials moved to gitignored
  appsettings.Development.json)
- Fix passkey login ignoring ReturnUrl: biometric login on the login page now
  respects the form's ReturnUrl hidden field so QR-code and deep-link flows
  redirect correctly after authentication instead of always going to the dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 09:38:55 -04:00
parent d9bf80cc9a
commit a2d48c8b58
6 changed files with 238 additions and 5 deletions
@@ -1478,7 +1478,10 @@
</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 to Customer
<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 (!Model.ConvertedToJobId.HasValue)
{
@@ -2012,6 +2015,33 @@
</style>
}
<!-- Send Quote via SMS Modal -->
<div class="modal fade" id="sendQuoteSmsModal" tabindex="-1" aria-labelledby="sendQuoteSmsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header" id="sendQuoteSmsModalHeader">
<h5 class="modal-title" id="sendQuoteSmsModalLabel">
<i class="bi bi-chat-dots me-2"></i>Send Quote via SMS
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center" id="sendQuoteSmsBody">
<div id="sendQuoteSmsSending">
<div class="spinner-border text-info mb-3" role="status"></div>
<div class="text-muted">Sending SMS&hellip;</div>
</div>
<div id="sendQuoteSmsResult" class="d-none">
<i id="sendQuoteSmsIcon" class="fs-1 d-block mb-3"></i>
<p id="sendQuoteSmsMessage" class="mb-0"></p>
</div>
</div>
<div class="modal-footer d-none" id="sendQuoteSmsFooter">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Send Quote Modal -->
<div class="modal fade" id="sendQuoteModal" tabindex="-1" aria-labelledby="sendQuoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
@@ -2131,6 +2161,53 @@
});
}
function sendQuoteSms(quoteId) {
document.getElementById('sendQuoteSmsSending').classList.remove('d-none');
document.getElementById('sendQuoteSmsResult').classList.add('d-none');
document.getElementById('sendQuoteSmsFooter').classList.add('d-none');
document.getElementById('sendQuoteSmsModalHeader').className = 'modal-header';
const modal = new bootstrap.Modal(document.getElementById('sendQuoteSmsModal'));
modal.show();
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
fetch('@Url.Action("SendQuoteApprovalSms", "Quotes")?id=' + quoteId, {
method: 'POST',
headers: { 'RequestVerificationToken': token, 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(data => {
document.getElementById('sendQuoteSmsSending').classList.add('d-none');
document.getElementById('sendQuoteSmsResult').classList.remove('d-none');
document.getElementById('sendQuoteSmsFooter').classList.remove('d-none');
const icon = document.getElementById('sendQuoteSmsIcon');
const msg = document.getElementById('sendQuoteSmsMessage');
const header = document.getElementById('sendQuoteSmsModalHeader');
if (data.success) {
icon.className = 'bi bi-check-circle-fill text-success fs-1 d-block mb-3';
header.className = 'modal-header bg-success text-white';
showInfo(data.message, 'SMS Sent');
} else {
icon.className = 'bi bi-x-circle-fill text-danger fs-1 d-block mb-3';
header.className = 'modal-header bg-danger text-white';
showWarning(data.message, 'SMS Not Sent');
}
msg.textContent = data.message;
})
.catch(() => {
document.getElementById('sendQuoteSmsSending').classList.add('d-none');
document.getElementById('sendQuoteSmsResult').classList.remove('d-none');
document.getElementById('sendQuoteSmsFooter').classList.remove('d-none');
document.getElementById('sendQuoteSmsIcon').className = 'bi bi-x-circle-fill text-danger fs-1 d-block mb-3';
document.getElementById('sendQuoteSmsModalHeader').className = 'modal-header bg-danger text-white';
document.getElementById('sendQuoteSmsMessage').textContent = 'A network error occurred. Please try again.';
showWarning('A network error occurred. Please try again.', 'SMS Not Sent');
});
}
function loadNotifications(quoteId) {
const modal = new bootstrap.Modal(document.getElementById('notificationsModal'));
document.getElementById('notificationsLoading').classList.remove('d-none');