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:
@@ -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…</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');
|
||||
|
||||
Reference in New Issue
Block a user