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,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>@(ViewData["Title"] ?? "Customer Intake") — @(ViewBag.CompanyName ?? "Intake Form")</title>
|
||||
<link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap-icons/font/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="~/css/kiosk.css" />
|
||||
@await RenderSectionAsync("Styles", required: false)
|
||||
</head>
|
||||
<body class="kiosk-body">
|
||||
|
||||
@{
|
||||
int kioskStep = ViewBag.KioskStep ?? 0; // 1, 2, or 3 — 0 means no step dots
|
||||
int kioskSteps = ViewBag.KioskSteps ?? 3;
|
||||
}
|
||||
|
||||
<div class="container py-4" style="max-width:720px;">
|
||||
|
||||
@* Logo *@
|
||||
<div class="text-center mb-3">
|
||||
@if (!string.IsNullOrEmpty(ViewBag.CompanyLogoUrl as string))
|
||||
{
|
||||
<img src="@ViewBag.CompanyLogoUrl" alt="@ViewBag.CompanyName"
|
||||
style="max-height:80px;max-width:220px;object-fit:contain;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fw-bold fs-5 text-muted">@ViewBag.CompanyName</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Step dots *@
|
||||
@if (kioskStep > 0)
|
||||
{
|
||||
<div class="kiosk-steps mb-4" aria-label="Step @kioskStep of @kioskSteps">
|
||||
@for (int i = 1; i <= kioskSteps; i++)
|
||||
{
|
||||
string dotClass = i < kioskStep ? "done" : (i == kioskStep ? "active" : "");
|
||||
<div class="kiosk-step-dot @dotClass" title="Step @i"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Validation summary *@
|
||||
@if (ViewData.ModelState.IsValid == false)
|
||||
{
|
||||
<div class="alert alert-danger alert-permanent mb-4">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Please correct the highlighted fields below.
|
||||
</div>
|
||||
}
|
||||
|
||||
@RenderBody()
|
||||
|
||||
</div>
|
||||
|
||||
@* Inactivity timer — redirect to Welcome after 5 minutes of no input *@
|
||||
@{
|
||||
bool showInactivityTimer = (bool)(ViewBag.ShowInactivityTimer ?? true);
|
||||
string welcomeUrl = ViewBag.WelcomeUrl as string ?? "/Kiosk/Welcome";
|
||||
}
|
||||
@if (showInactivityTimer)
|
||||
{
|
||||
<script>
|
||||
(function () {
|
||||
var TIMEOUT_MS = 5 * 60 * 1000;
|
||||
var timer;
|
||||
function reset() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
window.location.href = "@Html.Raw(welcomeUrl)";
|
||||
}, TIMEOUT_MS);
|
||||
}
|
||||
["touchstart", "touchmove", "click", "keydown", "scroll"].forEach(function (evt) {
|
||||
document.addEventListener(evt, reset, { passive: true });
|
||||
});
|
||||
reset();
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -1136,6 +1136,13 @@
|
||||
<span>Daily Board</span>
|
||||
</a>
|
||||
}
|
||||
@if (hasJobs)
|
||||
{
|
||||
<a asp-controller="Kiosk" asp-action="Intakes" class="nav-link" data-nav="ops">
|
||||
<i class="bi bi-tablet"></i>
|
||||
<span>Intake Sessions</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@* ── Billing & Payments ───────────────────────────────────── *@
|
||||
@if (hasInvoices)
|
||||
@@ -1492,6 +1499,7 @@
|
||||
<li><a class="dropdown-item" asp-controller="CompanyUsers" asp-action="Index"><i class="bi bi-people-fill me-2"></i>Manage Users</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="PricingTiers" asp-action="Index"><i class="bi bi-tags me-2"></i>Pricing Tiers</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="TaxRates" asp-action="Index"><i class="bi bi-percent me-2"></i>Tax Rates</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Kiosk" asp-action="Activate"><i class="bi bi-tablet me-2"></i>Kiosk Setup</a></li>
|
||||
}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
@if (gearIsAdmin)
|
||||
|
||||
Reference in New Issue
Block a user