Add staff-presented SMS consent flow on customer record
- New GET/POST Customers/SmsConsent/{id}: full-screen kiosk-layout page staff
opens and hands to the customer to read TCPA consent language and tap I Agree
- On agreement: sets Customer.NotifyBySms, SmsConsentedAt (UTC), SmsConsentMethod
= "InPerson", clears SmsOptedOutAt
- Redirects back if customer has already consented (no double-consent)
- Customer Details: "Get SMS Consent" badge link shown when NotifyBySms is false;
SMS on badge shows consent date on hover when consented
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -877,6 +877,74 @@ public class CustomersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a full-screen SMS consent form for the customer to read and agree to.
|
||||
/// Staff opens this page on a tablet and hands it to the customer; no staff account
|
||||
/// interaction is required — the page is scoped to the customer by ID only.
|
||||
/// Redirects back to Details if the customer has already consented.
|
||||
/// </summary>
|
||||
// GET: Customers/SmsConsent/5
|
||||
public async Task<IActionResult> SmsConsent(int id)
|
||||
{
|
||||
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
|
||||
if (customer == null) return NotFound();
|
||||
|
||||
if (customer.NotifyBySms)
|
||||
{
|
||||
this.ToastInfo("This customer has already given SMS consent.");
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId.HasValue)
|
||||
{
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value);
|
||||
ViewBag.CompanyName = company?.CompanyName;
|
||||
ViewBag.CompanyLogoUrl = !string.IsNullOrEmpty(company?.LogoFilePath)
|
||||
? Url.Action("Logo", "Kiosk")
|
||||
: null;
|
||||
}
|
||||
|
||||
ViewBag.ShowInactivityTimer = false;
|
||||
ViewBag.CustomerName = $"{customer.ContactFirstName} {customer.ContactLastName}".Trim();
|
||||
if (string.IsNullOrWhiteSpace(ViewBag.CustomerName as string) && !string.IsNullOrEmpty(customer.CompanyName))
|
||||
ViewBag.CustomerName = customer.CompanyName;
|
||||
|
||||
return View(customer.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the customer's SMS consent: sets NotifyBySms, SmsConsentedAt (UTC now),
|
||||
/// and SmsConsentMethod = "InPerson". Called when the customer taps "I Agree" on the
|
||||
/// consent form presented by staff.
|
||||
/// </summary>
|
||||
// POST: Customers/SmsConsent/5
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SmsConsent(int id, bool agreed)
|
||||
{
|
||||
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
|
||||
if (customer == null) return NotFound();
|
||||
|
||||
if (!agreed)
|
||||
{
|
||||
this.ToastError("Customer did not agree to SMS consent.");
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
customer.NotifyBySms = true;
|
||||
customer.SmsConsentedAt = DateTime.UtcNow;
|
||||
customer.SmsConsentMethod = "InPerson";
|
||||
customer.SmsOptedOutAt = null;
|
||||
|
||||
await _unitOfWork.Customers.UpdateAsync(customer);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogInformation("SMS consent recorded for customer {CustomerId} via staff-presented form", id);
|
||||
|
||||
this.ToastSuccess($"SMS consent recorded for {customer.ContactFirstName} {customer.ContactLastName}.");
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issues a standalone credit memo and increments the customer's CreditBalance.
|
||||
/// Restricted to CompanyAdmin because credits affect the financial ledger. The memo
|
||||
|
||||
@@ -175,7 +175,8 @@
|
||||
}
|
||||
@if (Model.NotifyBySms)
|
||||
{
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25"
|
||||
title="@(Model.SmsConsentedAt.HasValue ? "Consented " + Model.SmsConsentedAt.Value.ToLocalTime().ToString("MM/dd/yyyy") : "")">
|
||||
<i class="bi bi-chat-fill me-1"></i>SMS on
|
||||
</span>
|
||||
}
|
||||
@@ -184,6 +185,11 @@
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25">
|
||||
<i class="bi bi-chat-slash me-1"></i>SMS off
|
||||
</span>
|
||||
<a href="/Customers/SmsConsent/@Model.Id"
|
||||
class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 text-decoration-none"
|
||||
title="Present SMS consent form to customer">
|
||||
<i class="bi bi-chat-dots me-1"></i>Get SMS Consent
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
@model int
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "SMS Consent";
|
||||
string customerName = ViewBag.CustomerName as string ?? "Customer";
|
||||
}
|
||||
|
||||
<div class="kiosk-card">
|
||||
<h2 class="fw-bold mb-1" style="font-size:1.6rem;">SMS Notifications</h2>
|
||||
<p class="text-muted mb-4">Please read the following and tap <strong>I Agree</strong> to opt in.</p>
|
||||
|
||||
<form method="post" action="/Customers/SmsConsent/@Model">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="agreed" value="true" />
|
||||
|
||||
<div class="kiosk-terms-scroll mb-4">
|
||||
<strong>SMS Consent & Opt-In</strong>
|
||||
<p class="mt-2">
|
||||
By tapping <em>I Agree</em> below, <strong>@customerName</strong> consents to receive
|
||||
SMS text messages from @(ViewBag.CompanyName ?? "this shop") regarding order status
|
||||
updates, pickup notifications, and other information related to your powder coating
|
||||
services.
|
||||
</p>
|
||||
<p>
|
||||
Message frequency varies. Message and data rates may apply.
|
||||
You may opt out at any time by replying <strong>STOP</strong> to any message.
|
||||
Reply <strong>HELP</strong> for assistance.
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
Your mobile number will not be shared with third parties or used for marketing
|
||||
unrelated to your orders.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/Customers/Details/@Model" class="btn btn-outline-secondary"
|
||||
style="min-height:64px;border-radius:12px;font-size:1.1rem;flex:0 0 auto;padding:0 2rem;">
|
||||
<i class="bi bi-x-lg me-1"></i> No Thanks
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success kiosk-btn">
|
||||
<i class="bi bi-check-circle me-2"></i> I Agree
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user