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,282 @@
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var token = Antiforgery.GetAndStoreTokens(Context).RequestToken;
|
||||
}
|
||||
|
||||
<!-- AI Help Widget -->
|
||||
<div id="ai-help-widget" class="ai-help-widget" aria-live="polite" aria-label="AI Help Assistant">
|
||||
|
||||
<!-- Trigger button -->
|
||||
<button id="ai-help-btn" class="ai-help-trigger" title="Ask AI Help Assistant" aria-label="Open AI Help Assistant">
|
||||
<i class="bi bi-stars fs-5"></i>
|
||||
<span class="ai-help-label">Help</span>
|
||||
</button>
|
||||
|
||||
<!-- Chat panel -->
|
||||
<div id="ai-help-panel" class="ai-help-panel" role="dialog" aria-modal="true" aria-label="AI Help Assistant" hidden>
|
||||
<!-- Drag-to-resize handle -->
|
||||
<div id="ai-help-resize" class="ai-help-resize" title="Drag to resize"></div>
|
||||
<div class="ai-help-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-stars text-warning"></i>
|
||||
<span class="fw-semibold">AI Help Assistant</span>
|
||||
<span class="badge bg-secondary" style="font-size:0.65rem;">Beta</span>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button id="ai-help-clear" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Start new chat" style="font-size:0.75rem;">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
<button id="ai-help-close" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Close" aria-label="Close AI Help">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-help-messages" class="ai-help-messages">
|
||||
<!-- Welcome message -->
|
||||
<div class="ai-help-msg ai-help-msg--assistant">
|
||||
<div class="ai-help-msg-bubble">
|
||||
<p class="mb-1">Hi! I'm your AI Help Assistant. I can answer questions like:</p>
|
||||
<ul class="mb-2 ps-3" style="font-size:0.85rem;">
|
||||
<li>Where do I find invoices?</li>
|
||||
<li>How do I convert a quote to a job?</li>
|
||||
<li>Can I track deposits?</li>
|
||||
<li>What does job status "Curing" mean?</li>
|
||||
</ul>
|
||||
<p class="mb-0" style="font-size:0.85rem;">What can I help you with?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suggested starters (hidden after first message) -->
|
||||
<div id="ai-help-starters" class="ai-help-starters">
|
||||
<button class="ai-help-starter-btn" data-q="Where do I find invoices?">Find invoices</button>
|
||||
<button class="ai-help-starter-btn" data-q="How do I create a quote?">Create a quote</button>
|
||||
<button class="ai-help-starter-btn" data-q="How do I add a customer?">Add a customer</button>
|
||||
<button class="ai-help-starter-btn" data-q="What are the job statuses?">Job statuses</button>
|
||||
</div>
|
||||
|
||||
<div class="ai-help-input-area">
|
||||
<div class="ai-help-typing d-none" id="ai-help-typing">
|
||||
<span class="ai-help-typing-dot"></span>
|
||||
<span class="ai-help-typing-dot"></span>
|
||||
<span class="ai-help-typing-dot"></span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<textarea id="ai-help-input"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Ask a question..."
|
||||
rows="2"
|
||||
maxlength="1000"
|
||||
aria-label="Your question"></textarea>
|
||||
<button id="ai-help-send" class="btn btn-primary btn-sm align-self-end" aria-label="Send">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="ai-help-token" value="@token" />
|
||||
|
||||
<script src="~/js/ai-help-widget.js" asp-append-version="true"></script>
|
||||
|
||||
<style>
|
||||
.ai-help-widget {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 24px;
|
||||
z-index: 1050;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ai-help-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #1A1A1C; /* fixed graphite — never flips with surface */
|
||||
color: #FAFAF7;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.ai-help-trigger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.4);
|
||||
background: oklch(0.68 0.17 50); /* ember */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ai-help-panel {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 0;
|
||||
width: 360px;
|
||||
height: 520px;
|
||||
min-height: 300px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bs-body-bg, #fff);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ai-help-panel[hidden] { display: none !important; }
|
||||
|
||||
.ai-help-resize {
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px 12px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ai-help-resize::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 2px;
|
||||
transform: translateX(-50%);
|
||||
width: 32px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--bs-border-color, #dee2e6);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.ai-help-resize:hover::after,
|
||||
.ai-help-resize.dragging::after {
|
||||
background: var(--bs-secondary-color, #6c757d);
|
||||
}
|
||||
|
||||
.ai-help-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: var(--bs-secondary-bg, #f8f9fa);
|
||||
border-bottom: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-help-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.ai-help-msg { display: flex; }
|
||||
.ai-help-msg--user { justify-content: flex-end; }
|
||||
.ai-help-msg--assistant { justify-content: flex-start; }
|
||||
|
||||
.ai-help-msg-bubble {
|
||||
max-width: 85%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ai-help-msg--user .ai-help-msg-bubble {
|
||||
background: #1A1A1C; /* fixed — matches trigger */
|
||||
color: #FAFAF7;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.ai-help-msg--assistant .ai-help-msg-bubble {
|
||||
background: var(--bs-secondary-bg, #f1f3f5);
|
||||
color: var(--bs-body-color, #212529);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
/* Markdown-style rendering in assistant bubbles */
|
||||
.ai-help-msg--assistant .ai-help-msg-bubble a {
|
||||
color: var(--bs-primary, #0d6efd);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ai-help-msg--assistant .ai-help-msg-bubble ul,
|
||||
.ai-help-msg--assistant .ai-help-msg-bubble ol {
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.ai-help-msg--assistant .ai-help-msg-bubble p:last-child { margin-bottom: 0; }
|
||||
|
||||
.ai-help-starters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-help-starter-btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
color: var(--bs-body-color);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.ai-help-starter-btn:hover {
|
||||
background: var(--bs-secondary-bg, #f1f3f5);
|
||||
}
|
||||
|
||||
.ai-help-input-area {
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-help-typing {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px 0 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ai-help-typing-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--bs-secondary-color, #6c757d);
|
||||
animation: ai-help-bounce 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ai-help-typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.ai-help-typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@@keyframes ai-help-bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@@media (max-width: 480px) {
|
||||
.ai-help-widget { bottom: 16px; right: 16px; }
|
||||
.ai-help-panel { width: calc(100vw - 32px); right: 0; }
|
||||
.ai-help-label { display: none; }
|
||||
.ai-help-trigger { padding: 10px 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var token = Antiforgery.GetAndStoreTokens(Context).RequestToken;
|
||||
}
|
||||
|
||||
<!-- AI Quick Quote Widget -->
|
||||
<div id="qq-widget" class="qq-widget" aria-live="polite" aria-label="AI Quick Quote">
|
||||
|
||||
<!-- Trigger button -->
|
||||
<button id="qq-btn" class="qq-trigger" title="Get a quick phone estimate" aria-label="Open AI Quick Quote">
|
||||
<i class="bi bi-lightning-charge-fill fs-5"></i>
|
||||
<span class="qq-label">Quick Quote</span>
|
||||
</button>
|
||||
|
||||
<!-- Panel -->
|
||||
<div id="qq-panel" class="qq-panel" role="dialog" aria-modal="true" aria-label="AI Quick Quote" hidden>
|
||||
<div class="qq-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-lightning-charge-fill text-warning"></i>
|
||||
<span class="fw-semibold">AI Quick Quote</span>
|
||||
<span class="badge bg-secondary" style="font-size:0.65rem;">Beta</span>
|
||||
</div>
|
||||
<button id="qq-close" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Close" aria-label="Close Quick Quote">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Input -->
|
||||
<div id="qq-step-input" class="qq-body">
|
||||
<p class="text-muted mb-3" style="font-size:0.82rem;">
|
||||
Describe what the customer needs and get an instant estimate.
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" style="font-size:0.85rem;" for="qq-description">
|
||||
What does the customer have?
|
||||
</label>
|
||||
<textarea id="qq-description"
|
||||
class="form-control form-control-sm"
|
||||
rows="4"
|
||||
maxlength="600"
|
||||
placeholder="e.g. 4 car wheels done in Alexandrite with an Alien Silver base, about 18 inch diameter…"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-qty">Quantity</label>
|
||||
<input type="number" id="qq-qty" class="form-control form-control-sm" value="1" min="1" max="999" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-coats">Coats</label>
|
||||
<input type="number" id="qq-coats" class="form-control form-control-sm" value="1" min="1" max="5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="qq-input-error" class="alert alert-danger alert-permanent d-none py-2" style="font-size:0.82rem;"></div>
|
||||
|
||||
<button id="qq-analyze-btn" class="btn btn-primary w-100">
|
||||
<i class="bi bi-lightning-charge-fill me-1"></i> Get Estimate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Results -->
|
||||
<div id="qq-step-results" class="qq-body d-none">
|
||||
<!-- AI estimates -->
|
||||
<div id="qq-result-card" class="card border-0 bg-light mb-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="fw-semibold mb-2" id="qq-res-description" style="font-size:0.9rem;"></div>
|
||||
|
||||
<div class="row g-2 text-center mb-2">
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Sq Ft</div>
|
||||
<div class="fw-semibold" id="qq-res-sqft"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Complexity</div>
|
||||
<div class="fw-semibold" id="qq-res-complexity"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Labor</div>
|
||||
<div class="fw-semibold" id="qq-res-minutes"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center border-top pt-2">
|
||||
<div>
|
||||
<div class="small text-muted">Estimate</div>
|
||||
<div class="fs-5 fw-bold text-success" id="qq-res-price"></div>
|
||||
<div id="qq-res-oven" class="text-muted" style="font-size:0.73rem;"></div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="small text-muted">Confidence</div>
|
||||
<span id="qq-res-confidence" class="badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="qq-res-reasoning" class="mt-2 text-muted" style="font-size:0.78rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Powder / color stock status -->
|
||||
<div id="qq-powder-section" class="d-none mb-3">
|
||||
<div class="fw-semibold mb-1" style="font-size:0.82rem;">Powder Stock</div>
|
||||
<div id="qq-powder-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Save inputs -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-reference">
|
||||
Reference <span class="text-muted">(caller name or memo)</span>
|
||||
</label>
|
||||
<input type="text" id="qq-reference" class="form-control form-control-sm"
|
||||
placeholder="e.g. John – 4 wheels" maxlength="100" />
|
||||
</div>
|
||||
|
||||
<div id="qq-save-error" class="alert alert-danger alert-permanent d-none py-2 mb-2" style="font-size:0.82rem;"></div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="qq-back-btn" class="btn btn-outline-secondary btn-sm flex-shrink-0">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</button>
|
||||
<button id="qq-save-btn" class="btn btn-success btn-sm flex-grow-1">
|
||||
<i class="bi bi-floppy me-1"></i> Save as Draft Quote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing indicator (shared between steps) -->
|
||||
<div id="qq-loading" class="qq-loading d-none">
|
||||
<div class="qq-typing-dot"></div>
|
||||
<div class="qq-typing-dot"></div>
|
||||
<div class="qq-typing-dot"></div>
|
||||
<span class="ms-2 text-muted" style="font-size:0.82rem;">Analyzing…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="qq-token" value="@token" />
|
||||
|
||||
<script src="~/js/ai-quick-quote.js" asp-append-version="true"></script>
|
||||
|
||||
<style>
|
||||
.qq-widget {
|
||||
position: fixed;
|
||||
bottom: 134px; /* sits above the AI Help button at 80px */
|
||||
right: 24px;
|
||||
z-index: 1050;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.qq-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #1e3a8a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.qq-trigger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.4);
|
||||
background: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.qq-panel {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 0;
|
||||
width: 380px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bs-body-bg, #fff);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qq-panel[hidden] { display: none !important; }
|
||||
|
||||
.qq-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: var(--bs-secondary-bg, #f8f9fa);
|
||||
border-bottom: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-body {
|
||||
padding: 14px;
|
||||
overflow-y: auto;
|
||||
max-height: 480px;
|
||||
}
|
||||
|
||||
.qq-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-typing-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--bs-secondary-color, #6c757d);
|
||||
animation: qq-bounce 1.2s infinite ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-typing-dot:nth-child(2) { animation-delay: 0.2s; margin: 0 4px; }
|
||||
.qq-typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@@keyframes qq-bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.qq-powder-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.78rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
@@media (max-width: 480px) {
|
||||
.qq-widget { bottom: 80px; right: 16px; }
|
||||
.qq-panel { width: calc(100vw - 32px); right: 0; bottom: 54px; }
|
||||
.qq-label { display: none; }
|
||||
.qq-trigger { padding: 10px 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<!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 — hidden on Welcome screen which renders its own centered logo *@
|
||||
@if (!(bool)(ViewBag.HideLayoutLogo ?? false))
|
||||
{
|
||||
<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 when idle too long.
|
||||
Intake steps set ViewBag.InactivityTimeoutMs = 45000 (45 s).
|
||||
Welcome screen keeps the default 5-minute timeout. *@
|
||||
@{
|
||||
bool showInactivityTimer = (bool)(ViewBag.ShowInactivityTimer ?? true);
|
||||
string welcomeUrl = ViewBag.WelcomeUrl as string ?? "/Kiosk/Welcome";
|
||||
int inactivityMs = ViewBag.InactivityTimeoutMs as int? ?? (5 * 60 * 1000);
|
||||
}
|
||||
@if (showInactivityTimer)
|
||||
{
|
||||
<script>
|
||||
(function () {
|
||||
var TIMEOUT_MS = @inactivityMs;
|
||||
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>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
@* Usage: @await Html.PartialAsync("_SectionHeader", (Kicker: "FLOOR", Title: "Active jobs", Meta: (string?)null))
|
||||
Replaces: <div class="card-header fw-bold">…</div> *@
|
||||
@model (string Kicker, string Title, string? Meta)
|
||||
<div class="pcl-section-header d-flex align-items-baseline justify-content-between gap-3">
|
||||
<div>
|
||||
<div class="pcl-metric-kicker">@Model.Kicker</div>
|
||||
<h2 class="pcl-section-title mb-0">@Model.Title</h2>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.Meta))
|
||||
{
|
||||
<span class="mono pcl-section-meta text-nowrap">@Model.Meta</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user