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,234 @@
@model List<IGrouping<PowderCoating.Core.Enums.AccountType, PowderCoating.Application.DTOs.Accounting.AccountListDto>>
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Chart of Accounts";
ViewData["PageIcon"] = "bi-journal-bookmark";
ViewData["PageHelpTitle"] = "Chart of Accounts";
ViewData["PageHelpContent"] = "The master list of financial accounts grouped by type: Asset (what you own), Liability (what you owe), Equity (owner&apos;s stake), Revenue (income), Cost of Goods Sold (direct costs), Expense (overhead). Click any row to view its ledger. System accounts (sys badge) cannot be deleted. Recalculate Balances recomputes every account&apos;s balance from transaction history.";
string TypeIcon(AccountType t) => t switch
{
AccountType.Asset => "bi-safe",
AccountType.Liability => "bi-credit-card",
AccountType.Equity => "bi-bar-chart-line",
AccountType.Revenue => "bi-graph-up-arrow",
AccountType.CostOfGoods => "bi-box-seam",
AccountType.Expense => "bi-receipt-cutoff",
_ => "bi-journal"
};
string TypeColor(AccountType t) => t switch
{
AccountType.Asset => "success",
AccountType.Liability => "danger",
AccountType.Equity => "primary",
AccountType.Revenue => "info",
AccountType.CostOfGoods => "warning",
AccountType.Expense => "secondary",
_ => "secondary"
};
string TypeLabel(AccountType t) => t switch
{
AccountType.CostOfGoods => "Cost of Goods Sold",
_ => t.ToString()
};
}
<div class="d-flex justify-content-end mb-4">
<div class="d-flex gap-2">
<form asp-action="FixOpeningBalanceSigns" method="post"
onsubmit="return confirm('Fix opening balance signs for Revenue, Liability, and Equity accounts imported from QuickBooks? This corrects negative balances caused by QB\'s sign convention.')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-warning"
title="Fixes negative opening balances on Revenue/Liability/Equity accounts imported from QuickBooks IIF">
<i class="bi bi-sign-stop me-1"></i>Fix QB Import Signs
</button>
</form>
<form id="recalcBalancesForm" asp-action="RecalculateBalances" method="post">
@Html.AntiForgeryToken()
<button type="button" id="btnRecalcBalances" class="btn btn-outline-secondary"
title="Recompute CurrentBalance for all accounts from ledger transactions">
<i class="bi bi-arrow-repeat me-1"></i>Recalculate Balances
</button>
</form>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>New Account
</a>
</div>
</div>
@* Bootstrap toast — confirmation before recalculating balances *@
<div class="toast-container position-fixed top-50 start-50 translate-middle p-3" style="z-index:1100">
<div id="recalcConfirmToast" class="toast align-items-center border-0 bg-dark text-white" role="alert" aria-atomic="true" data-bs-autohide="false">
<div class="toast-body d-flex flex-column gap-2 py-3 px-3">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-arrow-repeat fs-5 text-warning"></i>
<span class="fw-semibold">Recalculate all account balances?</span>
</div>
<p class="mb-1 small text-white-50">This will recompute every account's balance from transaction history. The page will reload when done.</p>
<div class="d-flex gap-2">
<button id="btnRecalcConfirm" type="button" class="btn btn-warning btn-sm fw-semibold">
<i class="bi bi-check-lg me-1"></i>Yes, recalculate
</button>
<button type="button" class="btn btn-outline-light btn-sm" data-bs-dismiss="toast">Cancel</button>
</div>
</div>
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (!Model.Any())
{
<div class="card shadow-sm border-0">
<div class="card-body text-center py-5">
<i class="bi bi-journal-bookmark display-4 text-muted mb-3 d-block"></i>
<h5 class="mb-2">No accounts set up yet</h5>
<p class="text-muted mb-4">Get started quickly by loading a standard chart of accounts<br>tailored for a powder coating business.</p>
<form asp-action="SeedDefaultAccounts" method="post">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-primary">
<i class="bi bi-magic me-1"></i>Create Default Accounts
</button>
<a asp-action="Create" class="btn btn-outline-secondary ms-2">
<i class="bi bi-plus-lg me-1"></i>Add Manually
</a>
</form>
</div>
</div>
}
@foreach (var group in Model)
{
var color = TypeColor(group.Key);
var icon = TypeIcon(group.Key);
var label = TypeLabel(group.Key);
<div class="card shadow-sm mb-4">
<div class="card-header bg-@color bg-opacity-10 border-@color border-opacity-25">
<div class="d-flex align-items-center gap-2">
<i class="bi @icon text-@color fs-5"></i>
<h6 class="mb-0 fw-semibold text-@color">@label</h6>
<span class="badge bg-@color ms-auto">@group.Count() accounts</span>
</div>
</div>
<div class="card-body p-0">
<table class="table table-hover table-sm mb-0">
<thead class="table-light">
<tr>
<th style="width:110px">Number</th>
<th>Name</th>
<th>Sub-Type</th>
<th>Parent</th>
<th style="width:80px">Status</th>
<th style="width:120px" class="text-end">Balance</th>
<th style="width:150px" class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var acct in group.OrderBy(a => a.AccountNumber))
{
<tr class="@(acct.IsActive ? "" : "text-muted opacity-75") table-row-link"
data-href="@Url.Action("Ledger", new { id = acct.Id })"
style="cursor:pointer">
<td>
<code class="text-@color">@acct.AccountNumber</code>
</td>
<td>
<span class="fw-medium">@acct.Name</span>
@if (acct.IsSystem)
{
<span class="badge bg-secondary ms-1" title="System account — cannot be deleted">sys</span>
}
</td>
<td><span class="text-muted small">@acct.AccountSubType.ToDisplayName()</span></td>
<td>
@if (!string.IsNullOrEmpty(acct.ParentAccountName))
{
<span class="text-muted small">@acct.ParentAccountName</span>
}
</td>
<td>
@if (acct.IsActive)
{
<span class="badge bg-success-subtle text-success">Active</span>
}
else
{
<span class="badge bg-secondary-subtle text-secondary">Inactive</span>
}
</td>
<td class="text-end text-nowrap fw-medium @(acct.CurrentBalance >= 0 ? "text-success" : "text-danger")">
@acct.CurrentBalance.ToString("C")
</td>
<td class="text-end text-nowrap">
<a asp-action="Ledger" asp-route-id="@acct.Id" class="btn btn-sm btn-outline-secondary me-1" title="View Ledger">
<i class="bi bi-journal-text"></i>
</a>
<a asp-action="Edit" asp-route-id="@acct.Id" class="btn btn-sm btn-outline-primary me-1" title="Edit">
<i class="bi bi-pencil"></i>
</a>
@if (!acct.IsSystem)
{
<form asp-action="Delete" asp-route-id="@acct.Id" method="post" class="d-inline"
onsubmit="return confirm('Delete account @acct.AccountNumber @acct.Name?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@if (!Model.Any())
{
<div class="text-center py-5">
<i class="bi bi-journal-x display-3 text-muted"></i>
<p class="mt-3 text-muted">No accounts found. Use the Seed Data page to generate default accounts.</p>
</div>
}
@section Scripts {
<script>
document.querySelectorAll('tr.table-row-link').forEach(row => {
row.addEventListener('click', e => {
if (e.target.closest('a, button, form')) return;
window.location.href = row.dataset.href;
});
});
// Recalculate Balances — show confirmation toast instead of native confirm()
const recalcToast = new bootstrap.Toast(document.getElementById('recalcConfirmToast'));
document.getElementById('btnRecalcBalances').addEventListener('click', () => {
recalcToast.show();
});
document.getElementById('btnRecalcConfirm').addEventListener('click', () => {
recalcToast.hide();
document.getElementById('recalcBalancesForm').submit();
});
</script>
}