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,354 @@
@using PowderCoating.Web.Controllers
@model List<CompanyExportSummary>
@{
ViewData["Title"] = "Data Export";
}
@section Styles {
<style>
[data-bs-theme="dark"] .table-light th,
[data-bs-theme="dark"] .table-light td {
background-color: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .card {
border-color: var(--bs-border-color) !important;
}
[data-bs-theme="dark"] .card-header {
border-color: var(--bs-border-color) !important;
}
[data-bs-theme="dark"] .alert-light {
background-color: var(--bs-secondary-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .border-top {
border-color: var(--bs-border-color) !important;
}
</style>
}
<div class="container-fluid py-3">
<div class="mb-2">
<a asp-controller="PlatformAdmin" asp-action="Maintenance" class="text-muted small text-decoration-none">
<i class="bi bi-arrow-left me-1"></i>Maintenance
</a>
</div>
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h4 class="mb-0"><i class="bi bi-file-earmark-arrow-down me-2 text-primary"></i>Data Export</h4>
<small class="text-muted">Export company data to Excel for offboarding, audits, GDPR requests, or migration</small>
</div>
</div>
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-permanent mb-3"><i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]</div>
}
<div class="row g-3">
@* Left: company list *@
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-bottom py-2">
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0">Select a Company</h6>
<input type="text" id="companySearch" class="form-control form-control-sm w-auto"
placeholder="Search…" style="min-width:180px" />
</div>
</div>
<div class="table-responsive" style="max-height:520px;overflow-y:auto">
<table class="table table-hover table-sm align-middle mb-0 small">
<thead class="table-light sticky-top">
<tr>
<th>Company</th>
<th style="width:80px">Plan</th>
<th style="width:70px">Status</th>
<th style="width:100px">Created</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody id="companyTable">
@foreach (var c in Model)
{
<tr class="company-row" data-name="@c.CompanyName.ToLower()">
<td>
<div class="fw-medium">@c.CompanyName</div>
</td>
<td class="text-muted">@c.Plan</td>
<td>
@if (c.IsActive)
{
<span class="badge bg-success-subtle text-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</td>
<td class="text-muted">@c.CreatedAt.ToString("MM/dd/yyyy")</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary py-0 px-2 select-company-btn"
data-id="@c.Id"
data-name="@c.CompanyName">
Select
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var c in Model)
{
<div class="mobile-data-card" data-name="@c.CompanyName.ToLower()">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-primary"><i class="bi bi-building"></i></div>
<div class="mobile-card-title">
<h6>@c.CompanyName</h6>
<small>Created @c.CreatedAt.ToString("MM/dd/yyyy")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Plan</span>
<span class="mobile-card-value">@c.Plan</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (c.IsActive)
{
<span class="badge bg-success-subtle text-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</span>
</div>
</div>
<div class="mobile-card-footer">
<button type="button"
class="btn btn-sm btn-outline-primary select-company-btn"
data-id="@c.Id"
data-name="@c.CompanyName">
Select for Export
</button>
</div>
</div>
}
</div>
</div>
</div>
</div>
@* Right: export options — always visible *@
<div class="col-lg-5">
<div class="card border-0 shadow-sm" style="position:sticky;top:1rem">
<div class="card-header bg-primary text-white py-2">
<h6 class="mb-0"><i class="bi bi-file-earmark-spreadsheet me-2"></i>Export Options</h6>
</div>
<div class="card-body">
<!-- Company selection banner — shown/hidden by JS -->
<div id="noCompanyBanner" class="alert alert-light alert-permanent border d-flex align-items-center gap-2 mb-3 small">
<i class="bi bi-arrow-left-circle fs-5 text-muted"></i>
<span>Select a company from the list to begin.</span>
</div>
<div id="selectedBanner" class="alert alert-info alert-permanent py-2 mb-3 small" style="display:none">
<i class="bi bi-building me-1"></i>
Exporting: <strong id="selectedCompanyName">—</strong>
</div>
<form method="post" asp-action="Export" id="exportForm">
@Html.AntiForgeryToken()
<input type="hidden" name="companyId" id="companyIdInput" value="0" />
<div class="mb-3">
<label class="form-label fw-semibold">Data to export</label>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Customers" id="chkCustomers" checked />
<label class="form-check-label" for="chkCustomers">
<i class="bi bi-people me-1 text-muted"></i>Customers
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Jobs" id="chkJobs" checked />
<label class="form-check-label" for="chkJobs">
<i class="bi bi-briefcase me-1 text-muted"></i>Jobs
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Quotes" id="chkQuotes" checked />
<label class="form-check-label" for="chkQuotes">
<i class="bi bi-file-earmark-text me-1 text-muted"></i>Quotes
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Inventory" id="chkInventory" />
<label class="form-check-label" for="chkInventory">
<i class="bi bi-boxes me-1 text-muted"></i>Inventory
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Equipment" id="chkEquipment" />
<label class="form-check-label" for="chkEquipment">
<i class="bi bi-tools me-1 text-muted"></i>Equipment
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Vendors" id="chkVendors" />
<label class="form-check-label" for="chkVendors">
<i class="bi bi-truck me-1 text-muted"></i>Vendors
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="ShopWorkers" id="chkShopWorkers" />
<label class="form-check-label" for="chkShopWorkers">
<i class="bi bi-person-badge me-1 text-muted"></i>Shop Workers
</label>
</div>
<div class="form-check mb-1">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Invoices" id="chkInvoices" />
<label class="form-check-label" for="chkInvoices">
<i class="bi bi-receipt me-1 text-muted"></i>Invoices
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
value="Users" id="chkUsers" />
<label class="form-check-label" for="chkUsers">
<i class="bi bi-person-lock me-1 text-muted"></i>Users
<span class="badge bg-warning text-dark ms-1" style="font-size:0.65rem">GDPR</span>
</label>
</div>
<div class="border-top pt-2 mt-1">
<div class="small text-muted mb-1">
<i class="bi bi-shield-check me-1 text-success"></i>
The <strong>Users</strong> sheet contains personal data (names, emails, login history).
Include only when required for a data access request or offboarding.
</div>
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="selectAllSheets">
<label class="form-check-label text-muted small" for="selectAllSheets">
Select / deselect all
</label>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Format</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="fmtXlsx" value="xlsx" checked />
<label class="form-check-label" for="fmtXlsx">
<i class="bi bi-file-earmark-spreadsheet me-1 text-success"></i>Excel (.xlsx)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" id="fmtCsv" value="csv" />
<label class="form-check-label" for="fmtCsv">
<i class="bi bi-filetype-csv me-1 text-secondary"></i>CSV (.zip)
<span class="text-muted small">— one file per sheet</span>
</label>
</div>
</div>
</div>
<div id="noCompanyHint" class="alert alert-warning alert-permanent py-2 small mb-2" style="display:none">
<i class="bi bi-exclamation-triangle me-1"></i>Please select a company from the list before downloading.
</div>
<button type="submit" class="btn btn-primary w-100" id="exportBtn">
<i class="bi bi-file-earmark-arrow-down me-2" id="exportBtnIcon"></i><span id="exportBtnLabel">Download Excel (.xlsx)</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
(function () {
var selectedId = null;
// ── Company selection ────────────────────────────────────────────────────
document.querySelectorAll('.select-company-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
// Reset all buttons
document.querySelectorAll('.select-company-btn').forEach(function (b) {
b.textContent = 'Select';
b.className = 'btn btn-sm btn-outline-primary py-0 px-2 select-company-btn';
});
// Mark this one selected
btn.textContent = 'Selected';
btn.className = 'btn btn-sm btn-primary py-0 px-2 select-company-btn';
selectedId = btn.getAttribute('data-id');
var name = btn.getAttribute('data-name');
document.getElementById('companyIdInput').value = selectedId;
document.getElementById('selectedCompanyName').textContent = name;
document.getElementById('noCompanyBanner').style.display = 'none';
document.getElementById('selectedBanner').style.display = '';
document.getElementById('noCompanyHint').style.display = 'none';
});
});
// ── Company search ────────────────────────────────────────────────────────
document.getElementById('companySearch').addEventListener('input', function () {
var q = this.value.toLowerCase();
document.querySelectorAll('.company-row').forEach(function (row) {
row.style.display = row.getAttribute('data-name').indexOf(q) !== -1 ? '' : 'none';
});
document.querySelectorAll('.mobile-card-list .mobile-data-card').forEach(function (card) {
card.style.display = card.getAttribute('data-name').indexOf(q) !== -1 ? '' : 'none';
});
});
// ── Select all sheets ─────────────────────────────────────────────────────
document.getElementById('selectAllSheets').addEventListener('change', function () {
var checked = this.checked;
document.querySelectorAll('.sheet-check').forEach(function (cb) {
cb.checked = checked;
});
});
// ── Format toggle — update button label ──────────────────────────────────
document.querySelectorAll('input[name="format"]').forEach(function (radio) {
radio.addEventListener('change', function () {
var isCsv = this.value === 'csv';
document.getElementById('exportBtnLabel').textContent = isCsv ? 'Download CSV (.zip)' : 'Download Excel (.xlsx)';
document.getElementById('exportBtnIcon').className = isCsv ? 'bi bi-file-zip me-2' : 'bi bi-file-earmark-arrow-down me-2';
});
});
// ── Guard: require company before submit ──────────────────────────────────
document.getElementById('exportForm').addEventListener('submit', function (e) {
if (!selectedId) {
e.preventDefault();
document.getElementById('noCompanyHint').style.display = '';
}
});
})();
</script>
}