Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc3cd75ea4 | |||
| a73f14fa7f | |||
| 0af31c39b3 | |||
| e1256503be |
@@ -877,74 +877,6 @@ 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>
|
/// <summary>
|
||||||
/// Issues a standalone credit memo and increments the customer's CreditBalance.
|
/// Issues a standalone credit memo and increments the customer's CreditBalance.
|
||||||
/// Restricted to CompanyAdmin because credits affect the financial ledger. The memo
|
/// Restricted to CompanyAdmin because credits affect the financial ledger. The memo
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using AutoMapper;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PowderCoating.Application.DTOs.Kiosk;
|
using PowderCoating.Application.DTOs.Kiosk;
|
||||||
using PowderCoating.Application.Interfaces;
|
using PowderCoating.Application.Interfaces;
|
||||||
@@ -39,6 +40,9 @@ public class KioskController : Controller
|
|||||||
private readonly IHubContext<KioskHub> _kioskHub;
|
private readonly IHubContext<KioskHub> _kioskHub;
|
||||||
private readonly ILogger<KioskController> _logger;
|
private readonly ILogger<KioskController> _logger;
|
||||||
private readonly ICompanyLogoService _logoService;
|
private readonly ICompanyLogoService _logoService;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
private static string SmsConsentCacheKey(int companyId) => $"kiosk-sms-consent:{companyId}";
|
||||||
|
|
||||||
/// <summary>Initialises all dependencies for the kiosk controller.</summary>
|
/// <summary>Initialises all dependencies for the kiosk controller.</summary>
|
||||||
public KioskController(
|
public KioskController(
|
||||||
@@ -49,7 +53,8 @@ public class KioskController : Controller
|
|||||||
IEmailService emailService,
|
IEmailService emailService,
|
||||||
IHubContext<KioskHub> kioskHub,
|
IHubContext<KioskHub> kioskHub,
|
||||||
ILogger<KioskController> logger,
|
ILogger<KioskController> logger,
|
||||||
ICompanyLogoService logoService)
|
ICompanyLogoService logoService,
|
||||||
|
IMemoryCache cache)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
@@ -59,6 +64,7 @@ public class KioskController : Controller
|
|||||||
_kioskHub = kioskHub;
|
_kioskHub = kioskHub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_logoService = logoService;
|
_logoService = logoService;
|
||||||
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -104,6 +110,10 @@ public class KioskController : Controller
|
|||||||
if (company == null || company.KioskActivationToken != cookie.Value.token)
|
if (company == null || company.KioskActivationToken != cookie.Value.token)
|
||||||
return Json(new { hasSession = false });
|
return Json(new { hasSession = false });
|
||||||
|
|
||||||
|
// Check for a staff-pushed SMS consent request before checking for intake sessions.
|
||||||
|
if (_cache.TryGetValue(SmsConsentCacheKey(cookie.Value.companyId), out (int customerId, string customerName) pending))
|
||||||
|
return Json(new { hasSession = false, smsConsentPending = true, customerId = pending.customerId, customerName = pending.customerName });
|
||||||
|
|
||||||
var window = DateTime.UtcNow.AddSeconds(-60);
|
var window = DateTime.UtcNow.AddSeconds(-60);
|
||||||
var session = await _unitOfWork.KioskSessions.FirstOrDefaultAsync(
|
var session = await _unitOfWork.KioskSessions.FirstOrDefaultAsync(
|
||||||
s => s.CompanyId == cookie.Value.companyId
|
s => s.CompanyId == cookie.Value.companyId
|
||||||
@@ -116,6 +126,116 @@ public class KioskController : Controller
|
|||||||
return Json(new { hasSession = true, sessionToken = session.SessionToken });
|
return Json(new { hasSession = true, sessionToken = session.SessionToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// SMS CONSENT (staff pushes to kiosk; customer agrees on tablet)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Staff calls this (authenticated) from the Customer Details page to push an SMS
|
||||||
|
/// consent request to the front-desk kiosk tablet. Stores the customer ID in
|
||||||
|
/// IMemoryCache under a company-scoped key; the kiosk's PollSession endpoint picks
|
||||||
|
/// it up and returns smsConsentPending so the tablet can navigate to the consent page.
|
||||||
|
/// The cache entry expires in 10 minutes in case the customer never approaches the tablet.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> PushSmsConsent(int customerId)
|
||||||
|
{
|
||||||
|
var customer = await _unitOfWork.Customers.GetByIdAsync(customerId);
|
||||||
|
if (customer == null) return Json(new { success = false, message = "Customer not found." });
|
||||||
|
|
||||||
|
if (customer.NotifyBySms)
|
||||||
|
return Json(new { success = false, message = "Customer has already given SMS consent." });
|
||||||
|
|
||||||
|
var companyId = customer.CompanyId;
|
||||||
|
var name = !string.IsNullOrWhiteSpace(customer.ContactFirstName)
|
||||||
|
? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()
|
||||||
|
: customer.CompanyName ?? "Customer";
|
||||||
|
|
||||||
|
_cache.Set(SmsConsentCacheKey(companyId), (customerId, name),
|
||||||
|
new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
|
||||||
|
|
||||||
|
_logger.LogInformation("SMS consent pushed to kiosk for customer {CustomerId} by staff", customerId);
|
||||||
|
return Json(new { success = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> SmsConsent(int id)
|
||||||
|
{
|
||||||
|
var cookie = ReadKioskCookie();
|
||||||
|
if (cookie == null) return Forbid();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
ViewBag.CompanyName = company?.CompanyName;
|
||||||
|
ViewBag.CompanyLogoUrl = !string.IsNullOrEmpty(company?.LogoFilePath) ? Url.Action("Logo", "Kiosk") : null;
|
||||||
|
ViewBag.ShowInactivityTimer = false;
|
||||||
|
ViewBag.CustomerName = !string.IsNullOrWhiteSpace(customer.ContactFirstName)
|
||||||
|
? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()
|
||||||
|
: customer.CompanyName ?? "Customer";
|
||||||
|
|
||||||
|
return View(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous, HttpPost]
|
||||||
|
public async Task<IActionResult> SmsConsent(int id, bool agreed)
|
||||||
|
{
|
||||||
|
var cookie = ReadKioskCookie();
|
||||||
|
if (cookie == null) return Forbid();
|
||||||
|
|
||||||
|
if (agreed)
|
||||||
|
{
|
||||||
|
var customer = await _unitOfWork.Customers.GetByIdAsync(id, ignoreQueryFilters: true);
|
||||||
|
if (customer != null)
|
||||||
|
{
|
||||||
|
customer.NotifyBySms = true;
|
||||||
|
customer.SmsConsentedAt = DateTime.UtcNow;
|
||||||
|
customer.SmsConsentMethod = "KioskInPerson";
|
||||||
|
customer.SmsOptedOutAt = null;
|
||||||
|
await _unitOfWork.Customers.UpdateAsync(customer);
|
||||||
|
await _unitOfWork.CompleteAsync();
|
||||||
|
_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/{id}",
|
||||||
|
customerId: id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect("/Kiosk/Welcome");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serves the company logo for anonymous kiosk pages. Resolves the company from the
|
/// Serves the company logo for anonymous kiosk pages. Resolves the company from the
|
||||||
/// KioskDevice cookie so no tenant context is needed on the anonymous request.
|
/// KioskDevice cookie so no tenant context is needed on the anonymous request.
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public class InAppNotificationService : IInAppNotificationService
|
|||||||
message = notification.Message,
|
message = notification.Message,
|
||||||
link = notification.Link,
|
link = notification.Link,
|
||||||
notificationType = notification.NotificationType,
|
notificationType = notification.NotificationType,
|
||||||
|
customerId = notification.CustomerId,
|
||||||
createdAt = now.ToString("o")
|
createdAt = now.ToString("o")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
<i class="bi bi-envelope-slash me-1"></i>Email off
|
<i class="bi bi-envelope-slash me-1"></i>Email off
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
<span id="sms-status-section">
|
||||||
@if (Model.NotifyBySms)
|
@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"
|
||||||
@@ -185,12 +186,22 @@
|
|||||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25">
|
<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
|
<i class="bi bi-chat-slash me-1"></i>SMS off
|
||||||
</span>
|
</span>
|
||||||
<a href="/Customers/SmsConsent/@Model.Id"
|
<button type="button" id="btnGetSmsConsent"
|
||||||
class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 text-decoration-none"
|
class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 border-0"
|
||||||
title="Present SMS consent form to customer">
|
style="cursor:pointer;"
|
||||||
|
title="Send SMS consent form to the front-desk kiosk tablet"
|
||||||
|
onclick="pushSmsConsent(@Model.Id)">
|
||||||
<i class="bi bi-chat-dots me-1"></i>Get SMS Consent
|
<i class="bi bi-chat-dots me-1"></i>Get SMS Consent
|
||||||
</a>
|
</button>
|
||||||
|
<button type="button" id="btnCancelSmsConsent"
|
||||||
|
class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25 border-0 d-none"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
title="Cancel the pending kiosk consent request"
|
||||||
|
onclick="cancelSmsConsent()">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Cancel Consent
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -549,3 +560,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="~/js/customer-details.js" asp-append-version="true"></script>
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -9,7 +9,7 @@
|
|||||||
<h2 class="fw-bold mb-1" style="font-size:1.6rem;">SMS Notifications</h2>
|
<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>
|
<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">
|
<form method="post" action="/Kiosk/SmsConsent/@Model">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<input type="hidden" name="agreed" value="true" />
|
<input type="hidden" name="agreed" value="true" />
|
||||||
|
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
<a href="/Customers/Details/@Model" class="btn btn-outline-secondary"
|
<a href="/Kiosk/SmsConsent/@Model?agreed=false"
|
||||||
|
onclick="event.preventDefault(); document.getElementById('declineForm').submit();"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
style="min-height:64px;border-radius:12px;font-size:1.1rem;flex:0 0 auto;padding:0 2rem;">
|
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
|
<i class="bi bi-x-lg me-1"></i> No Thanks
|
||||||
</a>
|
</a>
|
||||||
@@ -42,4 +44,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@* Separate form for decline so "No Thanks" can POST with agreed=false *@
|
||||||
|
<form id="declineForm" method="post" action="/Kiosk/SmsConsent/@Model" style="display:none;">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="agreed" value="false" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -1914,7 +1914,8 @@
|
|||||||
const icons = {
|
const icons = {
|
||||||
QuoteApproved: { icon: 'bi-check-circle-fill', cls: 'success', title: 'Quote Approved' },
|
QuoteApproved: { icon: 'bi-check-circle-fill', cls: 'success', title: 'Quote Approved' },
|
||||||
QuoteDeclined: { icon: 'bi-x-circle-fill', cls: 'danger', title: 'Quote Declined' },
|
QuoteDeclined: { icon: 'bi-x-circle-fill', cls: 'danger', title: 'Quote Declined' },
|
||||||
InvoicePaid: { icon: 'bi-cash-coin', cls: 'primary', title: 'Payment Received' }
|
InvoicePaid: { icon: 'bi-cash-coin', cls: 'primary', title: 'Payment Received' },
|
||||||
|
KioskConsent: { icon: 'bi-chat-fill', cls: 'success', title: 'SMS Consent' }
|
||||||
};
|
};
|
||||||
const t = icons[data.notificationType] || { icon: 'bi-bell', cls: 'info', title: 'Notification' };
|
const t = icons[data.notificationType] || { icon: 'bi-bell', cls: 'info', title: 'Notification' };
|
||||||
toastr[t.cls === 'danger' ? 'warning' : t.cls === 'primary' ? 'info' : 'success'](
|
toastr[t.cls === 'danger' ? 'warning' : t.cls === 'primary' ? 'info' : 'success'](
|
||||||
@@ -1922,6 +1923,12 @@
|
|||||||
`<i class="bi ${t.icon} me-1"></i>${t.title}`,
|
`<i class="bi ${t.icon} me-1"></i>${t.title}`,
|
||||||
{ timeOut: 10000, extendedTimeOut: 3000, closeButton: true, enableHtml: true }
|
{ timeOut: 10000, extendedTimeOut: 3000, closeButton: true, enableHtml: true }
|
||||||
);
|
);
|
||||||
|
if (data.notificationType === 'KioskConsent' && data.customerId) {
|
||||||
|
const path = window.location.pathname.toLowerCase();
|
||||||
|
if (path === `/customers/details/${data.customerId}`) {
|
||||||
|
window.updateCustomerSmsStatus?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.start().catch(err => console.warn('SignalR connection failed:', err));
|
connection.start().catch(err => console.warn('SignalR connection failed:', err));
|
||||||
@@ -2101,8 +2108,14 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load on page ready
|
// Load on page ready and refresh when dropdown is opened
|
||||||
document.addEventListener('DOMContentLoaded', load);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
load();
|
||||||
|
btn?.addEventListener('show.bs.dropdown', load);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback poll every 60 s in case SignalR misses a push
|
||||||
|
setInterval(load, 60_000);
|
||||||
|
|
||||||
return { addItem, incrementBadge, markAllRead, openDetail, markRead };
|
return { addItem, incrementBadge, markAllRead, openDetail, markRead };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ body.kiosk-body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vertically centre content in any tall kiosk button (covers <a> and <button>) */
|
||||||
|
.kiosk-body .btn,
|
||||||
|
.kiosk-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Suppress all hover effects on touch screens */
|
/* Suppress all hover effects on touch screens */
|
||||||
@media (hover: none) {
|
@media (hover: none) {
|
||||||
.kiosk-body .btn:hover { filter: none; opacity: 1; }
|
.kiosk-body .btn:hover { filter: none; opacity: 1; }
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function pushSmsConsent(customerId) {
|
||||||
|
const tok = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/Kiosk/PushSmsConsent?customerId=${customerId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'RequestVerificationToken': tok }
|
||||||
|
});
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.updateCustomerSmsStatus = function () {
|
||||||
|
const section = document.getElementById('sms-status-section');
|
||||||
|
if (!section) return;
|
||||||
|
const today = new Date().toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
|
||||||
|
section.innerHTML = `<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25"
|
||||||
|
title="Consented ${today}">
|
||||||
|
<i class="bi bi-chat-fill me-1"></i>SMS on
|
||||||
|
</span>`;
|
||||||
|
};
|
||||||
@@ -26,6 +26,12 @@
|
|||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setStatus("#16a34a", "Ready");
|
setStatus("#16a34a", "Ready");
|
||||||
|
if (data.smsConsentPending && data.customerId) {
|
||||||
|
active = false;
|
||||||
|
setStatus("#2563eb", "Loading consent…");
|
||||||
|
window.location.href = `/Kiosk/SmsConsent/${data.customerId}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.hasSession && data.sessionToken) {
|
if (data.hasSession && data.sessionToken) {
|
||||||
active = false;
|
active = false;
|
||||||
setStatus("#2563eb", "Starting…");
|
setStatus("#2563eb", "Starting…");
|
||||||
|
|||||||
Reference in New Issue
Block a user