Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor
Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace PowderCoating.Web.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub that delivers "StartIntake" push events to the front-desk tablet.
|
||||
/// Deliberately [AllowAnonymous] — the tablet runs without a logged-in user.
|
||||
/// Security is enforced at the kiosk route level via the KioskActivationToken cookie.
|
||||
///
|
||||
/// On connect the tablet passes ?companyId=N in the hub URL query string; this hub
|
||||
/// places that connection in the company-scoped group "kiosk-{companyId}" so that
|
||||
/// KioskController.StartSession can push to exactly that company's tablet.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class KioskHub : Hub
|
||||
{
|
||||
private readonly ILogger<KioskHub> _logger;
|
||||
|
||||
/// <summary>Initialises the hub with the required logger.</summary>
|
||||
public KioskHub(ILogger<KioskHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the connection to the company-scoped kiosk group on connect.
|
||||
/// companyId is read from the ?companyId query param embedded in the hub URL by the Welcome view.
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var companyId = Context.GetHttpContext()?.Request.Query["companyId"].FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(companyId))
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"kiosk-{companyId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in KioskHub.OnConnectedAsync for connection {ConnectionId}", Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
/// <summary>Logs unexpected disconnects (e.g. tablet going to sleep).</summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (exception != null)
|
||||
_logger.LogWarning(exception, "KioskHub client disconnected with error: {ConnectionId}", Context.ConnectionId);
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user