From 0af31c39b376fbfa034f5a4dc3d31cd0a24a6726 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 13 May 2026 23:25:37 -0400 Subject: [PATCH] Fix kiosk SMS consent routing loop and stuck tablet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Route param renamed customerId→id so /Kiosk/SmsConsent/15307 binds correctly (default MVC route uses {id}; mismatched name caused GetByIdAsync(0)→404→loop) - Cache entry cleared in GET (not just POST) so returning to Welcome after seeing the form never redirects again - Added POST /Kiosk/CancelSmsConsent for staff to free the kiosk if they pushed consent accidentally — Customer Details shows a Cancel button after pushing Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/KioskController.cs | 39 +++++++++++++------ .../Views/Customers/Details.cshtml | 9 ++++- .../wwwroot/js/customer-details.js | 20 ++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/KioskController.cs b/src/PowderCoating.Web/Controllers/KioskController.cs index 5ceb1cb..0f18df7 100644 --- a/src/PowderCoating.Web/Controllers/KioskController.cs +++ b/src/PowderCoating.Web/Controllers/KioskController.cs @@ -158,17 +158,34 @@ public class KioskController : Controller return Json(new { success = true }); } + /// + /// Cancels a pending kiosk SMS consent request, freeing the kiosk to return to the Welcome + /// screen. Called by staff if they pushed consent accidentally or the customer isn't coming. + /// + [HttpPost, ValidateAntiForgeryToken] + public IActionResult CancelSmsConsent() + { + var companyId = HttpContext.User.FindFirst("CompanyId")?.Value; + if (int.TryParse(companyId, out var cid)) + _cache.Remove(SmsConsentCacheKey(cid)); + return Json(new { success = true }); + } + /// /// Displays the full-screen SMS consent form on the kiosk tablet (anonymous, kiosk layout). /// Loads the customer by ID with ignoreQueryFilters because the kiosk has no tenant context. /// [AllowAnonymous] - public async Task SmsConsent(int customerId) + public async Task SmsConsent(int id) { var cookie = ReadKioskCookie(); if (cookie == null) return Forbid(); - var customer = await _unitOfWork.Customers.GetByIdAsync(customerId, ignoreQueryFilters: true); + // Clear the pending entry immediately — the kiosk is now showing the form, + // so Welcome must not redirect again if the customer cancels or navigates back. + _cache.Remove(SmsConsentCacheKey(cookie.Value.companyId)); + + var customer = await _unitOfWork.Customers.GetByIdAsync(id, ignoreQueryFilters: true); if (customer == null) return NotFound(); var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true); @@ -179,25 +196,23 @@ public class KioskController : Controller ? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim() : customer.CompanyName ?? "Customer"; - return View(customerId); + return View(id); } /// - /// Records the customer's SMS consent from the kiosk tablet and clears the pending cache entry. + /// Records the customer's SMS consent from the kiosk tablet. /// Sets NotifyBySms, SmsConsentedAt, SmsConsentMethod = "KioskInPerson" on the customer record. + /// Cache is already cleared by the GET; this handles the agree/decline outcome. /// [AllowAnonymous, HttpPost] - public async Task SmsConsent(int customerId, bool agreed) + public async Task SmsConsent(int id, bool agreed) { var cookie = ReadKioskCookie(); if (cookie == null) return Forbid(); - // Always clear the pending consent so the kiosk stops showing the form - _cache.Remove(SmsConsentCacheKey(cookie.Value.companyId)); - if (agreed) { - var customer = await _unitOfWork.Customers.GetByIdAsync(customerId, ignoreQueryFilters: true); + var customer = await _unitOfWork.Customers.GetByIdAsync(id, ignoreQueryFilters: true); if (customer != null) { customer.NotifyBySms = true; @@ -206,15 +221,15 @@ public class KioskController : Controller customer.SmsOptedOutAt = null; await _unitOfWork.Customers.UpdateAsync(customer); await _unitOfWork.CompleteAsync(); - _logger.LogInformation("SMS consent recorded via kiosk for customer {CustomerId}", customerId); + _logger.LogInformation("SMS consent recorded via kiosk for customer {CustomerId}", id); await _inApp.CreateAsync( customer.CompanyId, "SMS Consent Recorded", $"{customer.ContactFirstName} {customer.ContactLastName} agreed to SMS notifications on the kiosk.", "KioskConsent", - link: $"/Customers/Details/{customerId}", - customerId: customerId); + link: $"/Customers/Details/{id}", + customerId: id); } } diff --git a/src/PowderCoating.Web/Views/Customers/Details.cshtml b/src/PowderCoating.Web/Views/Customers/Details.cshtml index cf2bfec..53fc64e 100644 --- a/src/PowderCoating.Web/Views/Customers/Details.cshtml +++ b/src/PowderCoating.Web/Views/Customers/Details.cshtml @@ -185,13 +185,20 @@ SMS off - + } diff --git a/src/PowderCoating.Web/wwwroot/js/customer-details.js b/src/PowderCoating.Web/wwwroot/js/customer-details.js index 53f71f9..fb081b3 100644 --- a/src/PowderCoating.Web/wwwroot/js/customer-details.js +++ b/src/PowderCoating.Web/wwwroot/js/customer-details.js @@ -10,6 +10,8 @@ async function pushSmsConsent(customerId) { const data = await res.json(); if (data.success) { toastr.success('Consent form sent to the kiosk tablet — hand it to the customer.', 'Sent to Kiosk'); + document.getElementById('btnGetSmsConsent')?.classList.add('d-none'); + document.getElementById('btnCancelSmsConsent')?.classList.remove('d-none'); } else { toastr.warning(data.message || 'Could not send consent to kiosk.'); } @@ -17,3 +19,21 @@ async function pushSmsConsent(customerId) { toastr.error('An error occurred. Please try again.'); } } + +async function cancelSmsConsent() { + const tok = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; + try { + const res = await fetch('/Kiosk/CancelSmsConsent', { + method: 'POST', + headers: { 'RequestVerificationToken': tok } + }); + const data = await res.json(); + if (data.success) { + toastr.info('Consent request cancelled — kiosk is free.'); + document.getElementById('btnCancelSmsConsent')?.classList.add('d-none'); + document.getElementById('btnGetSmsConsent')?.classList.remove('d-none'); + } + } catch { + toastr.error('An error occurred. Please try again.'); + } +}