6a918c2afc
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>
56 lines
2.0 KiB
C#
56 lines
2.0 KiB
C#
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);
|
|
}
|
|
}
|