Add SMS gating, TCPA terms agreement, and compose-before-send modal
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in - CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version - SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion) - Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers - Removed redundant Ready for Pickup SMS (Job Completed covers it) - Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends - Send SMS button on job details for ad-hoc messages (Admin/Manager only) - SendJobSmsAsync auto-appends STOP opt-out language if missing - Migrations: AddSmsGating, AddCompanySmsAgreement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1346,6 +1346,14 @@
|
||||
<i class="bi bi-check-circle me-2"></i>Complete Job
|
||||
</button>
|
||||
}
|
||||
@if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false) && Model.CustomerNotifyBySms && !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone))
|
||||
{
|
||||
<button type="button" class="btn btn-outline-info" id="btnSendSms"
|
||||
data-job-id="@Model.Id"
|
||||
title="Send a custom SMS to @Model.CustomerName">
|
||||
<i class="bi bi-chat-dots me-2"></i>Send SMS
|
||||
</button>
|
||||
}
|
||||
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-outline-danger">
|
||||
<i class="bi bi-trash me-2"></i>Delete Job
|
||||
</a>
|
||||
@@ -1819,6 +1827,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMS Compose Modal (Admin/Manager only) -->
|
||||
@if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false))
|
||||
{
|
||||
<div class="modal fade" id="smsComposeModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info bg-opacity-10">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-chat-dots me-2 text-info"></i>Send SMS to @Model.CustomerName
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" id="smsModalClose"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerMobilePhone))
|
||||
{
|
||||
<p class="text-muted small mb-3">
|
||||
<i class="bi bi-phone me-1"></i>Sending to: <strong>@Model.CustomerMobilePhone</strong>
|
||||
</p>
|
||||
}
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold" for="smsMessageText">Message</label>
|
||||
<textarea class="form-control" id="smsMessageText" rows="5"
|
||||
placeholder="Type your message…" maxlength="160"></textarea>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<div id="smsStopWarning" class="text-warning small d-none">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>"Reply STOP to opt out." will be appended automatically.
|
||||
</div>
|
||||
<div class="ms-auto text-muted small"><span id="smsCharCount">0</span> / 160</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="smsSendError" class="alert alert-danger d-none mt-2"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="smsDismissBtn">
|
||||
Skip — don't send
|
||||
</button>
|
||||
<button type="button" class="btn btn-info text-white" id="smsSendBtn">
|
||||
<i class="bi bi-send me-1"></i>Send SMS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Hidden form used by item-wizard.js to collect item data and submit to UpdateItems -->
|
||||
<form asp-action="UpdateItems" asp-controller="Jobs" method="post" id="jobItemsForm" style="display:none">
|
||||
@Html.AntiForgeryToken()
|
||||
@@ -2967,6 +3020,23 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false))
|
||||
{
|
||||
<script src="~/js/jobs-sms-compose.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const pendingPreview = @Html.Raw(ViewBag.PendingSmsPreview != null
|
||||
? System.Text.Json.JsonSerializer.Serialize((string)ViewBag.PendingSmsPreview)
|
||||
: "null");
|
||||
const jobIdForSms = @Model.Id;
|
||||
const renderUrl = '@Url.Action("RenderJobSms", "Jobs")';
|
||||
const sendUrl = '@Url.Action("SendJobSms", "Jobs")';
|
||||
const customerOptedIn = @(Model.CustomerNotifyBySms ? "true" : "false");
|
||||
window.__smsCompose = { pendingPreview, jobIdForSms, renderUrl, sendUrl, customerOptedIn };
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Save as Template Modal -->
|
||||
|
||||
Reference in New Issue
Block a user