Restore all zeroed views + add bulk gift certificate creation
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).
Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,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,107 @@
|
||||
@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;
|
||||
bool quoteFirst = !string.Equals(ViewBag.KioskIntakeOutput as string, "Job", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
<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>
|
||||
@if (quoteFirst)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>
|
||||
A team member will review your intake and reach out about pricing before work begins.
|
||||
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="~/lib/signature-pad/signature_pad.umd.min.js"></script>
|
||||
<script src="~/js/kiosk-terms.js"></script>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@{
|
||||
Layout = "~/Views/Shared/_KioskLayout.cshtml";
|
||||
ViewData["Title"] = "Welcome";
|
||||
ViewBag.HideLayoutLogo = true;
|
||||
}
|
||||
|
||||
<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:#94a3b8;margin-right:6px;transition:background 0.3s;"></span>
|
||||
<span id="kiosk-conn-label">Connecting…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/kiosk-welcome.js"></script>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user