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:
2026-05-14 20:09:22 -04:00
parent 3eda91f170
commit 4ec55e7290
240 changed files with 73116 additions and 0 deletions
@@ -0,0 +1,341 @@
@{
ViewData["Title"] = "Welcome to Powder Coating Logix";
var firstName = (ViewBag.FirstName as string) ?? "there";
var isOnTrial = (bool)(ViewBag.IsOnTrial ?? false);
var trialEndDate = ViewBag.TrialEndDate as DateTime?;
var trialEnd = trialEndDate?.ToString("MMMM d, yyyy") ?? DateTime.UtcNow.AddDays(7).ToString("MMMM d, yyyy");
}
@section Styles {
<style>
.welcome-hero {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
border-radius: 1rem;
color: white;
padding: 2.5rem 2rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.welcome-hero::after {
content: '\f1e3';
font-family: 'bootstrap-icons';
position: absolute;
right: -1rem;
top: -1rem;
font-size: 10rem;
color: rgba(255,255,255,0.04);
pointer-events: none;
}
.welcome-hero h1 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.welcome-hero .subtitle {
color: rgba(255,255,255,0.7);
font-size: 1rem;
margin-bottom: 1.5rem;
}
.trial-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: rgba(79, 195, 247, 0.15);
border: 1px solid rgba(79, 195, 247, 0.4);
color: #4fc3f7;
font-size: 0.85rem;
font-weight: 500;
padding: 0.375rem 0.875rem;
border-radius: 2rem;
}
/* Info cards */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 2rem;
}
@@media (max-width: 576px) {
.info-grid { grid-template-columns: 1fr; }
}
.info-tile {
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.875rem;
padding: 1.25rem;
display: flex;
align-items: flex-start;
gap: 0.875rem;
}
.info-tile .tile-icon {
width: 40px;
height: 40px;
border-radius: 0.625rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
flex-shrink: 0;
}
.info-tile .tile-text h6 {
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 0.2rem;
color: var(--bs-emphasis-color);
}
.info-tile .tile-text p {
font-size: 0.8rem;
color: var(--bs-secondary-color);
margin: 0;
}
/* Screenshot rotator */
.screenshot-rotator {
margin-bottom: 2rem;
max-width: 50%;
margin-left: auto;
margin-right: auto;
}
.rotator-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--bs-secondary-color);
margin-bottom: 0.875rem;
}
.rotator-track {
position: relative;
border-radius: 0.875rem;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
background: #0f172a;
aspect-ratio: 16/9;
}
.rotator-track img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 0.6s ease;
}
.rotator-track img.active {
opacity: 1;
}
.rotator-nav {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0.75rem;
pointer-events: none;
}
.rotator-nav button {
pointer-events: all;
background: rgba(0,0,0,0.45);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s;
backdrop-filter: blur(4px);
}
.rotator-nav button:hover {
background: rgba(0,0,0,0.7);
}
.rotator-dots {
display: flex;
justify-content: center;
gap: 0.4rem;
margin-top: 0.75rem;
}
.rotator-dots button {
width: 7px;
height: 7px;
border-radius: 50%;
border: none;
background: var(--bs-border-color);
padding: 0;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.rotator-dots button.active {
background: #4f46e5;
transform: scale(1.3);
}
.rotator-caption {
text-align: center;
font-size: 0.8rem;
color: var(--bs-secondary-color);
margin-top: 0.5rem;
min-height: 1.2em;
}
</style>
}
<!-- Welcome Hero -->
<div class="welcome-hero">
<div class="mb-3">
<i class="bi bi-check-circle-fill me-2" style="color:#4fc3f7;font-size:1.5rem;"></i>
<span style="color:rgba(255,255,255,0.8);font-size:0.95rem;">Account created successfully</span>
</div>
<h1>Welcome, @firstName!</h1>
<p class="subtitle">
Your account is created and ready — but <strong style="color:white;">it still needs to be configured
before it's ready to use.</strong> Run the Setup Wizard to get your shop up and running.
We <strong style="color:#4fc3f7;">strongly recommend</strong> completing it before doing anything else.
</p>
<div class="d-flex flex-wrap align-items-center gap-3 mt-4">
<form asp-controller="SetupWizard" asp-action="Launch" method="post">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-lg fw-semibold px-4 py-2" style="background:#4fc3f7;color:#0f3460;border:none;">
<i class="bi bi-rocket-takeoff me-2"></i>Start Setup Wizard
</button>
</form>
@if (isOnTrial)
{
<span class="trial-pill">
<i class="bi bi-calendar-check"></i>
Free trial &mdash; ends @trialEnd
</span>
}
</div>
</div>
<!-- Info tiles -->
<div class="info-grid">
<div class="info-tile">
<div class="tile-icon" style="background:#ecfdf5;color:#059669;">
<i class="bi bi-list-check"></i>
</div>
<div class="tile-text">
<h6>Lookups Pre-Seeded</h6>
<p>Job statuses, priorities, quote statuses, appointment types, and inventory categories are ready to use — and fully customizable.</p>
</div>
</div>
<div class="info-tile">
<div class="tile-icon" style="background:#eff6ff;color:#2563eb;">
<i class="bi bi-shield-check"></i>
</div>
<div class="tile-text">
<h6>You're the Admin</h6>
<p>Your account has full admin access. You can invite team members and control their permissions from Company Settings.</p>
</div>
</div>
</div>
<!-- Screenshot rotator -->
<div class="screenshot-rotator">
<p class="rotator-label"><i class="bi bi-grid-1x2 me-1"></i>A quick look around</p>
<div class="rotator-track" id="rotatorTrack">
<img src="/images/welcome/Dashboard.png" alt="Dashboard" class="active" data-caption="Dashboard — live KPIs, alerts, and daily tips at a glance" />
<img src="/images/welcome/Jobs.png" alt="Jobs" data-caption="Jobs — track every job from intake through delivery across 16 statuses" />
<img src="/images/welcome/JobBoard.png" alt="Job Board" data-caption="Job Board — drag-and-drop priority view of all active work" />
<img src="/images/welcome/Quotes.png" alt="Quotes" data-caption="Quotes — multi-item pricing engine with AI photo quoting" />
<img src="/images/welcome/Schedule.png" alt="Schedule" data-caption="Schedule — calendar view for appointments and job deadlines" />
<img src="/images/welcome/ShopDisplay.png" alt="Shop Display" data-caption="Shop Display — full-screen board for the shop floor" />
<img src="/images/welcome/SetupWizard.png" alt="Setup Wizard" data-caption="Setup Wizard — 10-step guided configuration for your shop" />
<img src="/images/welcome/CompanySettings.png" alt="Company Settings" data-caption="Company Settings — pricing rates, branding, notifications, and more" />
<img src="/images/welcome/DailyBoard.png" alt="Daily Board" data-caption="Daily Board — at-a-glance view of today's work" />
<img src="/images/welcome/QBMigrationWizard.png" alt="QB Migration" data-caption="QuickBooks Migration Wizard — import your existing customers and data" />
<img src="/images/welcome/Tools.png" alt="Tools" data-caption="Tools — equipment management and maintenance scheduling" />
<img src="/images/welcome/Help.png" alt="Help Center" data-caption="Help Center — built-in documentation and AI assistant" />
<div class="rotator-nav">
<button id="rotatorPrev" aria-label="Previous"><i class="bi bi-chevron-left"></i></button>
<button id="rotatorNext" aria-label="Next"><i class="bi bi-chevron-right"></i></button>
</div>
</div>
<div class="rotator-dots" id="rotatorDots"></div>
<p class="rotator-caption" id="rotatorCaption"></p>
</div>
<!-- Footer actions -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div class="text-secondary" style="font-size:0.875rem;">
<i class="bi bi-info-circle me-1"></i>
You can re-run the setup wizard anytime from <a asp-controller="CompanySettings" asp-action="Index">Company Settings</a>.
</div>
<div class="d-flex gap-2">
<a asp-controller="Billing" asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-credit-card me-1"></i>Set Up Billing
</a>
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary px-4">
<i class="bi bi-house me-1"></i>Go to Dashboard
</a>
</div>
</div>
@section Scripts {
<script>
(function () {
var imgs = Array.from(document.querySelectorAll('#rotatorTrack img'));
var dotsEl = document.getElementById('rotatorDots');
var caption = document.getElementById('rotatorCaption');
var current = 0;
var timer;
// Build dots
imgs.forEach(function (_, i) {
var btn = document.createElement('button');
btn.setAttribute('aria-label', 'Go to slide ' + (i + 1));
btn.addEventListener('click', function () { go(i); resetTimer(); });
dotsEl.appendChild(btn);
});
function go(index) {
imgs[current].classList.remove('active');
dotsEl.children[current].classList.remove('active');
current = (index + imgs.length) % imgs.length;
imgs[current].classList.add('active');
dotsEl.children[current].classList.add('active');
caption.textContent = imgs[current].dataset.caption || '';
}
function resetTimer() {
clearInterval(timer);
timer = setInterval(function () { go(current + 1); }, 3000);
}
document.getElementById('rotatorPrev').addEventListener('click', function () { go(current - 1); resetTimer(); });
document.getElementById('rotatorNext').addEventListener('click', function () { go(current + 1); resetTimer(); });
// Pause on hover
document.getElementById('rotatorTrack').addEventListener('mouseenter', function () { clearInterval(timer); });
document.getElementById('rotatorTrack').addEventListener('mouseleave', resetTimer);
go(0);
resetTimer();
})();
</script>
}