4ec55e7290
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>
235 lines
11 KiB
Plaintext
235 lines
11 KiB
Plaintext
@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'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'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>
|
||
}
|