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,150 @@
@{
ViewData["Title"] = "Enable Biometric Login";
Layout = "/Views/Shared/_AuthLayout.cshtml";
var returnUrl = ViewBag.ReturnUrl as string ?? "/";
}
@section Styles {
<style>
.auth-brand-panel {
background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 3rem 2.5rem;
color: white;
}
.auth-brand-panel h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.75rem;
text-align: center;
}
.auth-brand-panel .tagline {
font-size: 1rem;
color: rgba(255,255,255,0.65);
margin-bottom: 2.5rem;
text-align: center;
}
.feature-list {
list-style: none;
padding: 0;
width: 100%;
max-width: 280px;
}
.feature-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0;
font-size: 0.95rem;
color: rgba(255,255,255,0.82);
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.feature-list li:last-child { border-bottom: none; }
.feature-list li i {
color: #4fc3f7;
font-size: 1.1rem;
flex-shrink: 0;
}
.auth-form-panel {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2.5rem 1.5rem;
background-color: #ffffff;
min-height: 100vh;
}
.auth-form-container {
width: 100%;
max-width: 420px;
}
.auth-form-container h2 {
font-size: 1.75rem;
font-weight: 700;
color: #0f172a;
}
.auth-form-container .subtext {
color: #64748b;
font-size: 0.95rem;
}
</style>
}
<div class="d-flex" style="min-height:100vh;">
<!-- Left brand panel — hidden on mobile, same as login page -->
<div class="col-lg-5 d-none d-lg-flex auth-brand-panel">
<img src="/images/pcl-logo.png" alt="Powder Coating Logix" style="max-width:220px; margin-bottom:1.5rem;" />
<h1>Powder Coating Logix</h1>
<p class="tagline">The complete management platform for powder coating businesses</p>
<ul class="feature-list">
<li><i class="bi bi-fingerprint"></i> Fast biometric login</li>
<li><i class="bi bi-shield-check-fill"></i> Secure — your biometrics never leave your device</li>
<li><i class="bi bi-phone-fill"></i> Works with Face ID, Touch ID &amp; Windows Hello</li>
<li><i class="bi bi-trash3-fill"></i> Remove any device at any time</li>
</ul>
</div>
<!-- Right prompt panel -->
<div class="auth-form-panel col-lg-7">
<div class="auth-form-container text-center">
<div id="prompt-step">
<div class="mb-4" style="font-size:4rem; color:#0284c7; line-height:1;">
<i class="bi bi-fingerprint"></i>
</div>
<h2 class="mb-2">Speed up future logins</h2>
<p class="subtext mb-4">
Enable biometric login so next time you can sign in with a single tap —
no password needed.
</p>
<p id="pk-status" class="small mb-3" style="min-height:1.25em;"></p>
<div class="d-grid gap-2" style="max-width:320px; margin:0 auto;">
<button id="pk-enable-btn" type="button" class="btn btn-primary btn-lg">
<i class="bi bi-fingerprint me-2"></i><span id="pk-btn-label">Enable Biometric Login</span>
</button>
<a id="pk-skip-link" href="@returnUrl" class="btn btn-outline-secondary btn-lg">
Maybe later
</a>
<form method="post" action="/Passkey/DismissPrompt">
@Html.AntiForgeryToken()
<input type="hidden" name="returnUrl" value="@returnUrl" />
<button type="submit" class="btn btn-link text-muted w-100" style="font-size:0.85rem;">
Don't ask me again
</button>
</form>
</div>
</div>
<div id="success-step" class="d-none">
<div class="mb-4" style="font-size:4rem; color:#16a34a; line-height:1;">
<i class="bi bi-check-circle-fill"></i>
</div>
<h2 class="mb-2">All set!</h2>
<p class="subtext mb-4">Biometric login is enabled for this device.</p>
<a href="@returnUrl" class="btn btn-primary btn-lg px-5">Continue</a>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/js/passkey.js"></script>
<script src="~/js/passkey-enroll.js"></script>
}
@@ -0,0 +1,97 @@
@model IEnumerable<PowderCoating.Core.Entities.UserPasskey>
@{
ViewData["Title"] = "My Passkeys";
}
<div class="container-fluid py-4" style="max-width:760px;">
<div class="d-flex align-items-center gap-3 mb-4">
<div class="rounded-circle d-flex align-items-center justify-content-center"
style="width:48px;height:48px;background:#e0f2fe;">
<i class="bi bi-fingerprint" style="font-size:1.5rem;color:#0284c7;"></i>
</div>
<div>
<h4 class="mb-0 fw-semibold">Passkeys &amp; Biometric Login</h4>
<p class="text-muted small mb-0">
Passkeys let you sign in with Face ID, fingerprint, or your device PIN — no password needed.
</p>
</div>
</div>
@if (TempData["Success"] is string msg)
{
<div class="alert alert-success alert-permanent">
<i class="bi bi-check-circle-fill me-2"></i>@msg
</div>
}
<!-- Add new passkey -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h6 class="card-title mb-1">Add a passkey for this device</h6>
<p class="text-muted small mb-3">
You'll be prompted to authenticate using Face ID, Touch ID, Windows Hello, or a security key.
</p>
<div class="d-flex gap-2 align-items-center flex-wrap">
<input type="text" id="pk-device-name" class="form-control" style="max-width:220px;"
placeholder="Device name (e.g. iPhone 15)" maxlength="64" />
<button type="button" id="pk-add-btn" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i>Add Passkey
</button>
</div>
<p id="pk-add-status" class="mt-2 small mb-0"></p>
</div>
</div>
<!-- Existing passkeys -->
@if (!Model.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-fingerprint" style="font-size:3rem;opacity:.3;"></i>
<p class="mt-3">No passkeys registered yet.<br />Add one above to enable biometric login on this device.</p>
</div>
}
else
{
<div class="list-group shadow-sm">
@foreach (var pk in Model)
{
<div class="list-group-item list-group-item-action d-flex align-items-center gap-3">
<i class="bi bi-phone" style="font-size:1.4rem;color:#64748b;flex-shrink:0;"></i>
<div class="flex-grow-1 min-width-0">
<div class="fw-medium text-truncate">
@(pk.DeviceFriendlyName ?? "Unnamed device")
</div>
<div class="text-muted small">
Added @pk.CreatedAt.ToLocalTime().ToString("MMM d, yyyy")
@if (pk.LastUsedAt.HasValue)
{
<span class="ms-2">&bull; Last used @pk.LastUsedAt.Value.ToLocalTime().ToString("MMM d, yyyy")</span>
}
</div>
</div>
<form method="post" asp-action="Remove" asp-route-id="@pk.Id"
onsubmit="return confirm('Remove this passkey?');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash3"></i> Remove
</button>
</form>
</div>
}
</div>
<p class="text-muted small mt-3">
Removing a passkey means you'll need to use your password on that device next time.
</p>
}
<div class="mt-4">
<a asp-controller="CompanySettings" asp-action="Index" class="text-decoration-none">
<i class="bi bi-arrow-left me-1"></i>Back to Settings
</a>
</div>
</div>
@section Scripts {
<script src="~/js/passkey.js"></script>
<script src="~/js/passkey-manage.js"></script>
}