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:
@@ -33,6 +33,14 @@
|
||||
</p>
|
||||
<div class="d-flex gap-2 flex-wrap align-items-center">
|
||||
<a asp-controller="Jobs" asp-action="Board" class="btn btn-sm btn-primary">Open Jobs Board</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-info" id="btnStartIntake"
|
||||
title="Push the intake form to the front-desk tablet">
|
||||
<i class="bi bi-tablet me-1"></i>Start Intake
|
||||
</button>
|
||||
<a href="/Kiosk/SendRemoteLink" class="btn btn-sm btn-outline-secondary"
|
||||
title="Email a customer a link to fill out the intake form on their own device">
|
||||
<i class="bi bi-envelope-at me-1"></i>Remote Link
|
||||
</a>
|
||||
@if (!string.IsNullOrEmpty(Model.TipOfTheDay))
|
||||
{
|
||||
<span class="text-muted d-none d-xl-inline" style="font-size:0.73rem;"><i class="bi bi-lightbulb me-1"></i>@Model.TipOfTheDay</span>
|
||||
@@ -827,6 +835,40 @@
|
||||
@section Scripts {
|
||||
<script src="~/js/shop-progress-widget.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
// Start Intake — pushes SignalR event to front-desk tablet
|
||||
document.getElementById('btnStartIntake')?.addEventListener('click', async function () {
|
||||
const btn = this;
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sending…';
|
||||
try {
|
||||
const res = await fetch('/Kiosk/StartSession', {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': token, 'Content-Type': 'application/json' }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
btn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Sent!';
|
||||
btn.classList.replace('btn-outline-info', 'btn-success');
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-tablet me-1"></i>Start Intake';
|
||||
btn.classList.replace('btn-success', 'btn-outline-info');
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error('Server returned failure');
|
||||
}
|
||||
} catch (err) {
|
||||
btn.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>Failed';
|
||||
btn.classList.replace('btn-outline-info', 'btn-outline-danger');
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-tablet me-1"></i>Start Intake';
|
||||
btn.classList.replace('btn-outline-danger', 'btn-outline-info');
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Powder Orders - Mark as Ordered
|
||||
document.querySelectorAll('.mark-ordered-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function () {
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
var canResend = !isDraft && !isVoided && Model.Status != InvoiceStatus.Paid;
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail);
|
||||
var emailOptedOut = hasEmail && !Model.CustomerNotifyByEmail;
|
||||
var smsPhone = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone) ? Model.CustomerMobilePhone : Model.CustomerPhone;
|
||||
var hasSms = !string.IsNullOrWhiteSpace(smsPhone) && Model.CustomerNotifyBySms;
|
||||
var showSendModal = hasEmail && !emailOptedOut && hasSms; // both channels — show choice modal
|
||||
var directSendSms = !hasEmail && hasSms; // SMS only — skip modal
|
||||
var hasAvailableCredits = ViewBag.AvailableCreditMemos != null && ((IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos).Any();
|
||||
var canIssueRefund = !isDraft && !isVoided && Model.AmountPaid > 0;
|
||||
var canApplyCredit = !isVoided && Model.BalanceDue > 0 && hasAvailableCredits;
|
||||
@@ -579,14 +583,32 @@
|
||||
<form id="sendInvoiceForm" asp-action="Send" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="overrideEmail" id="sendInvoiceOverrideEmail" value="" />
|
||||
@if (emailOptedOut)
|
||||
<input type="hidden" name="sendEmail" id="sendInvoiceSendEmail" value="true" />
|
||||
<input type="hidden" name="sendSms" id="sendInvoiceSendSms" value="false" />
|
||||
@if (emailOptedOut && !hasSms)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
title="No delivery channel available for this customer">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else if (hasEmail)
|
||||
else if (showSendModal)
|
||||
{
|
||||
@* Both email + SMS available — let staff choose *@
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendChannelModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else if (directSendSms)
|
||||
{
|
||||
@* SMS only — send directly *@
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
onclick="submitSendInvoice(false, true)">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice via SMS
|
||||
</button>
|
||||
}
|
||||
else if (hasEmail && !emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendInvoiceModal">
|
||||
@@ -839,13 +861,50 @@
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="document.getElementById('sendInvoiceForm').submit()">
|
||||
<button type="button" class="btn btn-primary" onclick="submitSendInvoice(true, false)">
|
||||
<i class="bi bi-send me-1"></i>Yes, Send Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showSendModal)
|
||||
{
|
||||
<!-- Send Channel Choice Modal (shown when customer has both email + SMS) -->
|
||||
<div class="modal fade" id="sendChannelModal" tabindex="-1" aria-labelledby="sendChannelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title" id="sendChannelModalLabel">
|
||||
<i class="bi bi-send text-primary me-2"></i>Send Invoice
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p class="mb-3">How would you like to send <strong>@Model.InvoiceNumber</strong> to <strong>@Model.CustomerName</strong>?</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(true, false)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-envelope me-2"></i>Email only
|
||||
<small class="d-block text-muted ms-4">PDF attached · @Model.CustomerEmail</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary text-start" onclick="submitSendInvoice(false, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-phone me-2"></i>SMS only
|
||||
<small class="d-block text-muted ms-4">View link · @smsPhone</small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary text-start" onclick="submitSendInvoice(true, true)" data-bs-dismiss="modal">
|
||||
<i class="bi bi-send me-2"></i>Both Email & SMS
|
||||
<small class="d-block text-muted ms-4">PDF via email + view link via SMS</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (canPay)
|
||||
@@ -1381,6 +1440,12 @@
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function submitSendInvoice(sendEmail, sendSms) {
|
||||
document.getElementById('sendInvoiceSendEmail').value = sendEmail ? 'true' : 'false';
|
||||
document.getElementById('sendInvoiceSendSms').value = sendSms ? 'true' : 'false';
|
||||
document.getElementById('sendInvoiceForm').submit();
|
||||
}
|
||||
|
||||
function openEditPaymentModal(paymentId, invoiceId, paymentDate, paymentMethod, reference, notes, depositAccountId) {
|
||||
document.getElementById('editPaymentId').value = paymentId;
|
||||
document.getElementById('editPaymentDate').value = paymentDate;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
@{
|
||||
ViewData["Title"] = "Kiosk Setup";
|
||||
bool isActivated = ViewBag.IsActivated as bool? ?? false;
|
||||
}
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<i class="bi bi-tablet fs-3 text-primary"></i>
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-0">Kiosk Setup</h1>
|
||||
<p class="text-muted mb-0">Configure the front-desk intake tablet</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-permanent mb-4">
|
||||
<i class="bi bi-check-circle me-2"></i> @TempData["Success"]
|
||||
</div>
|
||||
}
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-permanent mb-4">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i> @TempData["Error"]
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
@* Status card *@
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title fw-semibold mb-3">Current Status</h5>
|
||||
@if (isActivated)
|
||||
{
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<span class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="bi bi-check-circle me-1"></i> Active
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
A kiosk device is currently activated. The tablet will respond to
|
||||
"Start Intake" commands from your staff.
|
||||
</p>
|
||||
<form method="post" asp-action="Activate">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="action" value="deactivate" />
|
||||
<button type="submit" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Deactivate the kiosk? The tablet will no longer receive intake requests.');">
|
||||
<i class="bi bi-tablet me-1"></i> Deactivate Kiosk
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<span class="badge bg-secondary fs-6 px-3 py-2">
|
||||
<i class="bi bi-dash-circle me-1"></i> Not Activated
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
No kiosk device is activated. Click below to activate this browser
|
||||
session as the kiosk device.
|
||||
</p>
|
||||
<form method="post" asp-action="Activate">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="action" value="activate" />
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-tablet me-1"></i> Activate This Device
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Instructions card *@
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title fw-semibold mb-3">Setup Instructions</h5>
|
||||
<ol class="text-muted" style="line-height:2;">
|
||||
<li>Open this page on the <strong>tablet</strong> and tap <em>Activate This Device</em>.</li>
|
||||
<li>After activation, navigate to <code>/Kiosk/Welcome</code> on the tablet.</li>
|
||||
<li>Bookmark that page so it survives a browser restart.</li>
|
||||
<li>Keep the tablet browser open — SignalR maintains a live connection.</li>
|
||||
<li>Use <em>Start Customer Intake</em> on the Dashboard or Jobs list to push a session to the tablet.</li>
|
||||
</ol>
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Only one device can be active at a time. Re-activating replaces the previous device token.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Thank You";
|
||||
bool isInPerson = ViewBag.IsInPerson as bool? ?? false;
|
||||
string firstName = ViewBag.FirstName as string ?? "there";
|
||||
}
|
||||
|
||||
<div class="kiosk-confirmation py-5">
|
||||
<div class="kiosk-confirmation-icon">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
</div>
|
||||
|
||||
<h2 class="fw-bold" style="font-size:2rem;">Thank you, @firstName!</h2>
|
||||
|
||||
@if (isInPerson)
|
||||
{
|
||||
<p class="text-muted mt-2" style="font-size:1.1rem;">
|
||||
A team member will be right with you.
|
||||
</p>
|
||||
<p class="kiosk-countdown" id="countdown-msg">
|
||||
Returning to the welcome screen in <span id="countdown">30</span> seconds…
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mt-2" style="font-size:1.1rem;">
|
||||
We've received your intake form and will be in touch soon.
|
||||
</p>
|
||||
<p class="text-muted mt-4" style="font-size:0.95rem;">
|
||||
You can close this window.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isInPerson)
|
||||
{
|
||||
@section Scripts {
|
||||
<script>
|
||||
(function () {
|
||||
var secs = 30;
|
||||
var el = document.getElementById("countdown");
|
||||
var interval = setInterval(function () {
|
||||
secs--;
|
||||
if (el) el.textContent = secs;
|
||||
if (secs <= 0) {
|
||||
clearInterval(interval);
|
||||
window.location.href = "@ViewBag.WelcomeUrl";
|
||||
}
|
||||
}, 1000);
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@model PowderCoating.Application.DTOs.Kiosk.SubmitKioskContactDto
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Your Information";
|
||||
var token = ViewBag.SessionToken as Guid? ?? Guid.Empty;
|
||||
}
|
||||
|
||||
<div class="kiosk-card">
|
||||
<h2 class="fw-bold mb-1" style="font-size:1.6rem;">Tell us about yourself</h2>
|
||||
<p class="text-muted mb-4">All fields are required.</p>
|
||||
|
||||
<form method="post" action="/Kiosk/Intake/@token/Contact" id="contactForm">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<label asp-for="FirstName" class="form-label">First Name</label>
|
||||
<input asp-for="FirstName" class="form-control" autocomplete="given-name"
|
||||
autocapitalize="words" spellcheck="false" placeholder="Jane" />
|
||||
<span asp-validation-for="FirstName" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label asp-for="LastName" class="form-label">Last Name</label>
|
||||
<input asp-for="LastName" class="form-control" autocomplete="family-name"
|
||||
autocapitalize="words" spellcheck="false" placeholder="Smith" />
|
||||
<span asp-validation-for="LastName" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label asp-for="Phone" class="form-label">Phone Number</label>
|
||||
<input asp-for="Phone" class="form-control" type="tel" inputmode="tel"
|
||||
autocomplete="tel" placeholder="(555) 555-0100" />
|
||||
<span asp-validation-for="Phone" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label asp-for="Email" class="form-label">Email Address</label>
|
||||
<input asp-for="Email" class="form-control" type="email" inputmode="email"
|
||||
autocomplete="email" placeholder="jane@example.com" />
|
||||
<span asp-validation-for="Email" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-3 rounded-3" style="background:#f1f5f9;">
|
||||
<div class="form-check">
|
||||
<input asp-for="IsReturningCustomer" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsReturningCustomer" class="form-check-label">
|
||||
I've been a customer before
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary kiosk-btn">
|
||||
Continue <i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
@model PowderCoating.Application.DTOs.Kiosk.SubmitKioskJobDto
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "About Your Project";
|
||||
var token = ViewBag.SessionToken as Guid? ?? Guid.Empty;
|
||||
}
|
||||
|
||||
<div class="kiosk-card">
|
||||
<h2 class="fw-bold mb-1" style="font-size:1.6rem;">What brings you in?</h2>
|
||||
<p class="text-muted mb-4">Tell us a little about what you need coated.</p>
|
||||
|
||||
<form method="post" action="/Kiosk/Intake/@token/Job">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="JobDescription" class="form-label">Describe your project</label>
|
||||
<textarea asp-for="JobDescription" class="form-control" rows="5"
|
||||
placeholder="e.g. Motorcycle frame, two-tone black and chrome, remove old coating first..."
|
||||
style="min-height:160px;resize:none;"></textarea>
|
||||
<span asp-validation-for="JobDescription" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="HowDidYouHearAboutUs" class="form-label">How did you hear about us? <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<select asp-for="HowDidYouHearAboutUs" class="form-select">
|
||||
<option value="">— Select one —</option>
|
||||
<option>Google / Online Search</option>
|
||||
<option>Friend or Family Referral</option>
|
||||
<option>Social Media</option>
|
||||
<option>Drove by the shop</option>
|
||||
<option>Returning Customer</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/Kiosk/Intake/@token/Contact" class="btn btn-outline-secondary"
|
||||
style="min-height:64px;border-radius:12px;font-size:1.1rem;flex:0 0 auto;padding:0 2rem;">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary kiosk-btn">
|
||||
Continue <i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
@model PowderCoating.Application.DTOs.Kiosk.SubmitKioskTermsDto
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Terms & Consent";
|
||||
var token = ViewBag.SessionToken as Guid? ?? Guid.Empty;
|
||||
bool isInPerson = ViewBag.IsInPerson as bool? ?? false;
|
||||
}
|
||||
|
||||
<div class="kiosk-card">
|
||||
<h2 class="fw-bold mb-1" style="font-size:1.6rem;">Terms & Consent</h2>
|
||||
<p class="text-muted mb-4">Please read and agree to the following before we proceed.</p>
|
||||
|
||||
<form method="post" action="/Kiosk/Intake/@token/Terms" id="termsForm">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
@* Terms scroll box *@
|
||||
<div class="kiosk-terms-scroll mb-4">
|
||||
<strong>Work Authorization & Liability Waiver</strong>
|
||||
<p class="mt-2">
|
||||
By signing below (or checking the box), you authorize @(ViewBag.CompanyName ?? "this shop")
|
||||
to perform the powder coating services described in your intake form.
|
||||
</p>
|
||||
<p>
|
||||
You acknowledge that you are the owner of the items submitted for coating, or you
|
||||
have authority to authorize work on them. You release the shop from liability for
|
||||
pre-existing damage, hidden defects, or items left unclaimed after 30 days.
|
||||
</p>
|
||||
<p>
|
||||
Final pricing is subject to a formal quote. Work will not begin until you approve
|
||||
the quoted amount. Payment is due upon pickup unless otherwise agreed in writing.
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
You agree to comply with all pickup and payment terms provided by the shop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@* SMS consent — separate checkbox per plan *@
|
||||
<div class="p-3 rounded-3 mb-3" style="background:#f0f9ff;border:1px solid #bae6fd;">
|
||||
<div class="form-check">
|
||||
<input asp-for="SmsOptIn" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="SmsOptIn" class="form-check-label">
|
||||
I consent to receive SMS text messages with updates about my order.
|
||||
<span class="text-muted d-block mt-1" style="font-size:0.85rem;">
|
||||
Message and data rates may apply. Reply STOP to opt out at any time.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Terms agreement *@
|
||||
<div class="p-3 rounded-3 mb-4" style="background:#f8fafc;border:1px solid #e2e8f0;">
|
||||
<div class="form-check">
|
||||
<input asp-for="AgreedToTerms" class="form-check-input" type="checkbox" required />
|
||||
<label asp-for="AgreedToTerms" class="form-check-label fw-semibold">
|
||||
I have read and agree to the terms above.
|
||||
</label>
|
||||
<span asp-validation-for="AgreedToTerms" class="text-danger d-block small mt-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Signature pad — in-person only *@
|
||||
@if (isInPerson)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Your Signature</label>
|
||||
<canvas id="signatureCanvas"></canvas>
|
||||
<div id="signatureError" class="text-danger small mt-1 d-none">
|
||||
Please sign above before continuing.
|
||||
</div>
|
||||
<input type="hidden" id="SignatureDataBase64" name="SignatureDataBase64" />
|
||||
<button type="button" id="clearSignatureBtn"
|
||||
class="btn btn-sm btn-outline-secondary mt-2">
|
||||
<i class="bi bi-eraser me-1"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/Kiosk/Intake/@token/Job" class="btn btn-outline-secondary"
|
||||
style="min-height:64px;border-radius:12px;font-size:1.1rem;flex:0 0 auto;padding:0 2rem;">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success kiosk-btn">
|
||||
<i class="bi bi-check-circle me-2"></i> Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (isInPerson)
|
||||
{
|
||||
@section Scripts {
|
||||
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"
|
||||
integrity="sha384-bQMMRVcRi5vEIBLKnB4FY7tBOA9k/Qvd/9zSWMNO4h0zfB2qLj4DV2R/JyPAbF3"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="~/js/kiosk-terms.js"></script>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
@model List<PowderCoating.Application.DTOs.Kiosk.KioskSessionListDto>
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
ViewData["Title"] = "Customer Intakes";
|
||||
string activeFilter = ViewBag.ActiveFilter as string ?? "all";
|
||||
}
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="bi bi-clipboard-check fs-3 text-primary"></i>
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-0">Customer Intakes</h1>
|
||||
<p class="text-muted mb-0">Walk-in and remote intake sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/Kiosk/SendRemoteLink" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-envelope-at me-1"></i> Send Remote Link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Filter tabs *@
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(activeFilter == "all" ? "active" : "")" href="?filter=all">All (@Model.Count)</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(activeFilter == "submitted" ? "active" : "")" href="?filter=submitted">
|
||||
Submitted (@Model.Count(d => d.Status == KioskSessionStatus.Submitted))
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(activeFilter == "active" ? "active" : "")" href="?filter=active">
|
||||
Pending (@Model.Count(d => d.Status == KioskSessionStatus.Active && !d.IsExpired))
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(activeFilter == "expired" ? "active" : "")" href="?filter=expired">
|
||||
Expired (@Model.Count(d => d.IsExpired))
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-1 mb-3 d-block"></i>
|
||||
<p>No intake sessions found.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Customer</th>
|
||||
<th>Contact</th>
|
||||
<th>Project</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>SMS</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-nowrap text-muted small">
|
||||
@(s.SubmittedAt?.ToLocalTime().ToString("MM/dd/yy h:mm tt") ?? s.ExpiresAt.AddHours(-2).ToLocalTime().ToString("MM/dd/yy h:mm tt"))
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">@s.CustomerFullName</div>
|
||||
@if (s.LinkedCustomerId.HasValue)
|
||||
{
|
||||
<a href="/Customers/Details/@s.LinkedCustomerId" class="small text-success">
|
||||
<i class="bi bi-person-check me-1"></i>Customer matched
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
@if (!string.IsNullOrEmpty(s.CustomerPhone))
|
||||
{
|
||||
<div><i class="bi bi-telephone me-1"></i>@s.CustomerPhone</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(s.CustomerEmail))
|
||||
{
|
||||
<div><i class="bi bi-envelope me-1"></i>@s.CustomerEmail</div>
|
||||
}
|
||||
</td>
|
||||
<td style="max-width:280px;">
|
||||
<span class="text-truncate d-block" style="max-width:260px;"
|
||||
title="@s.JobDescription">@s.JobDescriptionSnippet</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (s.SessionType == KioskSessionType.InPerson)
|
||||
{
|
||||
<span class="badge bg-primary-subtle text-primary">
|
||||
<i class="bi bi-tablet me-1"></i>In-Person
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-purple-subtle text-purple" style="background:#ede9fe;color:#6d28d9;">
|
||||
<i class="bi bi-envelope me-1"></i>Remote
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (s.Status == KioskSessionStatus.Submitted && s.IsConverted)
|
||||
{
|
||||
<span class="badge bg-success">Converted</span>
|
||||
}
|
||||
else if (s.Status == KioskSessionStatus.Submitted)
|
||||
{
|
||||
<span class="badge bg-info text-dark">Submitted</span>
|
||||
}
|
||||
else if (s.Status == KioskSessionStatus.Active && !s.IsExpired)
|
||||
{
|
||||
<span class="badge bg-warning text-dark">In Progress</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Expired</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (s.SmsOptIn)
|
||||
{
|
||||
<i class="bi bi-check-circle-fill text-success" title="SMS opt-in"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-dash text-muted"></i>
|
||||
}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
@if (s.LinkedJobId.HasValue)
|
||||
{
|
||||
<a href="/Jobs/Details/@s.LinkedJobId" class="btn btn-sm btn-outline-success me-1">
|
||||
<i class="bi bi-briefcase me-1"></i>View Job
|
||||
</a>
|
||||
}
|
||||
@if (s.LinkedCustomerId.HasValue)
|
||||
{
|
||||
<a href="/Customers/Details/@s.LinkedCustomerId" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-person me-1"></i>Customer
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
@model string
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Unable to Start";
|
||||
ViewBag.ShowInactivityTimer = false;
|
||||
}
|
||||
|
||||
<div class="kiosk-card text-center py-5">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning" style="font-size:4rem;"></i>
|
||||
<h2 class="mt-3 fw-bold">Something went wrong</h2>
|
||||
<p class="text-muted mt-2">@Model</p>
|
||||
<p class="mt-4 text-muted" style="font-size:0.9rem;">Please ask a staff member for assistance.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
@model PowderCoating.Application.DTOs.Kiosk.SendRemoteLinkDto
|
||||
@{
|
||||
ViewData["Title"] = "Send Intake Link";
|
||||
}
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<i class="bi bi-envelope-at fs-3 text-primary"></i>
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-0">Send Remote Intake Link</h1>
|
||||
<p class="text-muted mb-0">Email a customer an intake form they can fill out on their own device</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-permanent mb-4">
|
||||
<i class="bi bi-check-circle me-2"></i> @TempData["Success"]
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" asp-action="SendRemoteLink">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Email" class="form-label fw-semibold">Customer Email Address</label>
|
||||
<input asp-for="Email" class="form-control" type="email"
|
||||
placeholder="customer@example.com" autofocus />
|
||||
<span asp-validation-for="Email" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="CustomerName" class="form-label fw-semibold">
|
||||
Customer Name <span class="text-muted fw-normal">(optional)</span>
|
||||
</label>
|
||||
<input asp-for="CustomerName" class="form-control"
|
||||
placeholder="Used to personalise the email greeting" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-send me-2"></i> Send Intake Link
|
||||
</button>
|
||||
<a href="/Dashboard" class="btn btn-link ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-semibold mb-2"><i class="bi bi-info-circle me-2 text-primary"></i>How it works</h6>
|
||||
<ul class="text-muted small mb-0" style="line-height:1.8;">
|
||||
<li>The customer receives an email with a unique, secure link.</li>
|
||||
<li>They fill out their contact info and describe their project on their own phone or computer.</li>
|
||||
<li>When they submit, a Pending job is automatically created and you're notified.</li>
|
||||
<li>The link expires in 48 hours.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Welcome";
|
||||
}
|
||||
|
||||
<div id="kiosk-welcome-root"
|
||||
data-company-id="@ViewBag.CompanyId"
|
||||
class="kiosk-welcome-screen">
|
||||
|
||||
@if (!string.IsNullOrEmpty(ViewBag.CompanyLogoUrl as string))
|
||||
{
|
||||
<img src="@ViewBag.CompanyLogoUrl"
|
||||
alt="@ViewBag.CompanyName"
|
||||
class="kiosk-welcome-logo" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1 class="kiosk-welcome-title">@ViewBag.CompanyName</h1>
|
||||
}
|
||||
|
||||
<p class="kiosk-welcome-subtitle">Welcome! A staff member will start your intake shortly.</p>
|
||||
|
||||
<div class="kiosk-idle-indicator">
|
||||
<span id="kiosk-conn-dot" style="display:inline-block;width:10px;height:10px;
|
||||
border-radius:50%;background:#16a34a;margin-right:6px;transition:background 0.3s;"></span>
|
||||
Ready
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/kiosk-welcome.js"></script>
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
@model InvoiceViewViewModel
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
Layout = "~/Views/Shared/_QuoteApprovalLayout.cshtml";
|
||||
ViewData["Title"] = $"Invoice {Model.InvoiceNumber}";
|
||||
var isPaid = Model.BalanceDue <= 0;
|
||||
}
|
||||
|
||||
<div class="container py-4" style="max-width:780px;">
|
||||
|
||||
@* ── Header ── *@
|
||||
<div class="text-center mb-4">
|
||||
@if (!string.IsNullOrEmpty(Model.LogoFilePath))
|
||||
{
|
||||
<img src="/media/@(Model.LogoFilePath.TrimStart('/'))" alt="@Model.CompanyName" style="max-height:80px;max-width:240px;object-fit:contain;" class="mb-3" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/images/pcl-logo.png" alt="@Model.CompanyName" style="max-height:60px;" class="mb-3" />
|
||||
}
|
||||
<h4 class="fw-semibold mb-0">@Model.CompanyName</h4>
|
||||
@if (!string.IsNullOrEmpty(Model.CompanyPhone))
|
||||
{
|
||||
<p class="text-muted small mb-0">@Model.CompanyPhone</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.CompanyAddress))
|
||||
{
|
||||
<p class="text-muted small">@Model.CompanyAddress</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── Invoice meta ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Invoice</p>
|
||||
<p class="fw-semibold mb-0">@Model.InvoiceNumber</p>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<p class="text-muted small mb-1">Date</p>
|
||||
<p class="fw-semibold mb-0">@Model.InvoiceDate.ToString("MMM d, yyyy")</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Bill To</p>
|
||||
<p class="fw-semibold mb-0">@Model.CustomerName</p>
|
||||
</div>
|
||||
@if (Model.DueDate.HasValue)
|
||||
{
|
||||
<div class="col-6 text-end">
|
||||
<p class="text-muted small mb-1">Due Date</p>
|
||||
<p class="fw-semibold mb-0 @(Model.DueDate < DateTime.UtcNow && !isPaid ? "text-danger" : "")">
|
||||
@Model.DueDate.Value.ToString("MMM d, yyyy")
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.JobNumber))
|
||||
{
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-1">Job</p>
|
||||
<p class="fw-semibold mb-0">@Model.JobNumber</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Line items ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Description</th>
|
||||
<th class="text-center" style="width:70px;">Qty</th>
|
||||
<th class="text-end" style="width:100px;">Unit Price</th>
|
||||
<th class="text-end pe-3" style="width:110px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.LineItems)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">@item.Description</td>
|
||||
<td class="text-center">@item.Quantity.ToString("G29")</td>
|
||||
<td class="text-end">@item.UnitPrice.ToString("C")</td>
|
||||
<td class="text-end pe-3">@item.TotalPrice.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Totals ── *@
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span>@Model.SubTotal.ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.DiscountAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-1 text-success">
|
||||
<span>Discount</span>
|
||||
<span>-@Model.DiscountAmount.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.TaxAmount > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-muted">Tax (@Model.TaxPercent.ToString("0.##")%)</span>
|
||||
<span>@Model.TaxAmount.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
<hr class="my-2" />
|
||||
<div class="d-flex justify-content-between fw-semibold">
|
||||
<span>Total</span>
|
||||
<span>@Model.Total.ToString("C")</span>
|
||||
</div>
|
||||
@if (Model.AmountPaid > 0)
|
||||
{
|
||||
<div class="d-flex justify-content-between text-success mt-1">
|
||||
<span>Amount Paid</span>
|
||||
<span>-@Model.AmountPaid.ToString("C")</span>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
<div class="d-flex justify-content-between fw-bold fs-5 @(isPaid ? "text-success" : "text-danger")">
|
||||
<span>Balance Due</span>
|
||||
<span>@Model.BalanceDue.ToString("C")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Pay button ── *@
|
||||
@if (!isPaid && !string.IsNullOrEmpty(Model.PaymentUrl))
|
||||
{
|
||||
<div class="text-center mb-4">
|
||||
<a href="@Model.PaymentUrl" class="btn btn-success btn-lg px-5">
|
||||
<i class="bi bi-credit-card me-2"></i>Pay @Model.BalanceDue.ToString("C") Online
|
||||
</a>
|
||||
<p class="text-muted small mt-2">Secure payment powered by Stripe. This pay link expires in 5 days.</p>
|
||||
</div>
|
||||
}
|
||||
else if (isPaid)
|
||||
{
|
||||
<div class="alert alert-success text-center" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>This invoice has been paid in full. Thank you!
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>To arrange payment, please contact @Model.CompanyName@(!string.IsNullOrEmpty(Model.CompanyPhone) ? $" at {Model.CompanyPhone}" : "").
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Notes / Terms ── *@
|
||||
@if (!string.IsNullOrEmpty(Model.Notes))
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1 fw-semibold">Notes</p>
|
||||
<p class="mb-0 small">@Model.Notes</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Terms))
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1 fw-semibold">Payment Terms</p>
|
||||
<p class="mb-0 small">@Model.Terms</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="text-center text-muted small mt-4">
|
||||
Questions? Contact @Model.CompanyName@(!string.IsNullOrEmpty(Model.CompanyPhone) ? $" at {Model.CompanyPhone}" : "").
|
||||
</p>
|
||||
</div>
|
||||
@@ -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