Design consistency audit fixes: alerts, cards, dark mode, utilities

Alert sweep (113 alerts, 79 files):
  All persistent static banners now carry alert-permanent so the
  layout's 5-second auto-dismiss cannot swallow guidance, warnings,
  or validation errors. Transient dismissible toasts left untouched.

CSS fixes (site.css):
  .card.shadow-sm      — strips rogue border from ~40 drifted cards
  .card-header.bg-white — rebinds to var(--bs-body-bg) so card
                          headers follow dark/light theme correctly
  Typography utilities  — .text-2xs (.68rem), .text-xs (.73rem)
  Token color classes   — .text-ember, .text-ok, .text-bad,
                          .text-warn, .text-cool, .bg-paper-2
  Layout utilities      — .mw-xs/sm/md/lg replace inline max-width
  Comment              — documents text-ember vs text-primary intent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 18:05:29 -04:00
parent f6d457fe0e
commit 328b195127
80 changed files with 603 additions and 561 deletions
+23 -23
View File
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Accounting.CreateBillDto
@model PowderCoating.Application.DTOs.Accounting.CreateBillDto
@{
ViewData["Title"] = "New Bill";
ViewData["PageIcon"] = "bi-receipt-cutoff";
ViewData["PageHelpTitle"] = "New Bill";
ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking.";
ViewData["PageHelpContent"] = "Record a vendor invoice to track what you owe. Bills start as Draft (editable) and become Open once confirmed. Partial payments are supported — each payment reduces the balance. Link line items to expense accounts and optionally to specific jobs for cost tracking.";
string? fromPoNumber = ViewBag.FromPoNumber as string;
int? fromPoId = ViewBag.FromPoId as int?;
}
@@ -13,7 +13,7 @@
<div>
@if (!string.IsNullOrEmpty(fromPoNumber))
{
<p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> review and save</p>
<p class="text-muted mb-0 small"><i class="bi bi-box-arrow-in-down text-success me-1"></i> Pre-filled from <strong>@fromPoNumber</strong> — review and save</p>
}
</div>
@if (fromPoId.HasValue)
@@ -32,7 +32,7 @@
{
<input type="hidden" name="PurchaseOrderId" value="@Model.PurchaseOrderId" />
}
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
<div class="row g-4">
<!-- Left column: Bill header -->
@@ -44,7 +44,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Bill Details"
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record.">
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation. Payment Terms auto-fill from the vendor record.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -66,8 +66,8 @@
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select" id="vendorSelect"
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
<option value=""> Select Vendor </option>
<option value="__new__">+ Add New Vendor</option>
<option value="">— Select Vendor —</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
<span asp-validation-for="VendorId" class="text-danger small"></span>
</div>
@@ -100,7 +100,7 @@
<label for="receiptFile" class="form-label fw-medium">Attach Receipt / Document</label>
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
<div class="form-text">JPG, PNG, GIF, WebP, or PDF up to 10 MB.</div>
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
</div>
</div>
</div>
@@ -114,7 +114,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Line Items"
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -150,7 +150,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Bill Summary"
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed each payment recorded reduces the balance due until the bill is fully paid.">
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -198,7 +198,7 @@
<div class="mb-2">
<label class="form-label small fw-medium">Bank / Cash Account <span class="text-danger">*</span></label>
<select name="bankAccountId" class="form-select form-select-sm" id="payNowBankAccount">
<option value=""> Select Account </option>
<option value="">— Select Account —</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -232,7 +232,7 @@
<tr class="line-item-row">
<td>
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
<option value=""> Account </option>
<option value="">— Account —</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -242,7 +242,7 @@
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
<td>
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
<option value=""></option>
<option value="">—</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
{
<option value="@item.Value">@item.Text</option>
@@ -273,7 +273,7 @@
<div class="mb-3">
<label for="scanReceiptFile" class="form-label fw-medium">Receipt / Invoice Document</label>
<input type="file" id="scanReceiptFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
<div class="form-text">JPG, PNG, GIF, WebP, or PDF up to 10 MB.</div>
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
</div>
<div id="scanReceiptStatus" class="text-muted small mt-2"></div>
</div>
@@ -393,8 +393,8 @@
}
if (lineCount === 0) addLineItem();
// ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts handle common cases with zero API cost
// ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts — handle common cases with zero API cost
const _keywordMap = [
{ words: ['electric','power','utility','gas','water','internet','phone','telecom'], hint: 'utilities' },
{ words: ['powder','paint','coat','material','supply','supplies','chemical','resin'], hint: 'materials' },
@@ -407,7 +407,7 @@
{ words: ['advertising','marketing','promo'], hint: 'advertising' },
];
// Session cache: description (lowercased) { accountId, accountName }
// Session cache: description (lowercased) → { accountId, accountName }
const _suggestCache = new Map();
function _keywordGuess(description) {
@@ -480,7 +480,7 @@
hint2.className = 'ai-account-hint text-muted small mt-1';
accountSel.parentNode.appendChild(hint2);
}
hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking';
hint2.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:.75rem;height:.75rem"></span> Thinking…';
try {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
@@ -501,14 +501,14 @@
}
}
// Event delegation works for dynamically added rows
// Event delegation — works for dynamically added rows
document.getElementById('lineItemsBody').addEventListener('blur', function (e) {
if (e.target.matches('[name$=".Description"]')) {
_suggestAccountForRow(e.target.closest('tr'));
}
}, true); // capture phase so blur bubbles
// ── Scan Receipt ─────────────────────────────────────────────────────
// ── Scan Receipt ─────────────────────────────────────────────────────
document.getElementById('scanReceiptUploadBtn').addEventListener('click', async function () {
const fileInput = document.getElementById('scanReceiptFile');
if (!fileInput.files.length) { alert('Please select a file.'); return; }
@@ -535,7 +535,7 @@
return;
}
// Auto-fill bill header try to match vendor name to dropdown
// Auto-fill bill header — try to match vendor name to dropdown
if (data.vendorName) {
const vendorSel = document.getElementById('vendorSelect');
if (vendorSel && !vendorSel.value) {
@@ -553,7 +553,7 @@
vendorSel.value = bestOption.value;
vendorSel.dispatchEvent(new Event('change'));
} else {
// No match put the name in Memo so user knows what the AI saw
// No match — put the name in Memo so user knows what the AI saw
const memo = document.querySelector('[name="Memo"]');
if (memo && !memo.value) memo.value = data.vendorName;
}
@@ -598,7 +598,7 @@
const modal = bootstrap.Modal.getInstance(document.getElementById('scanReceiptModal'));
if (modal) modal.hide();
statusEl.textContent = 'Scan complete review and adjust as needed.';
statusEl.textContent = 'Scan complete — review and adjust as needed.';
} catch (e) {
statusEl.textContent = 'Error connecting to AI service.';
} finally {
+11 -11
View File
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Accounting.EditBillDto
@model PowderCoating.Application.DTOs.Accounting.EditBillDto
@{
ViewData["Title"] = "Edit Bill";
ViewData["PageIcon"] = "bi-pencil-square";
ViewData["PageHelpTitle"] = "Edit Bill";
ViewData["PageHelpContent"] = "Bills can only be edited while in Draft status. Once marked Open, they are locked Void the bill and recreate it if corrections are needed after confirmation.";
ViewData["PageHelpContent"] = "Bills can only be edited while in Draft status. Once marked Open, they are locked — Void the bill and recreate it if corrections are needed after confirmation.";
}
<div class="d-flex justify-content-start mb-4">
@@ -14,7 +14,7 @@
<form asp-action="Edit" asp-route-id="@Model.Id" method="post" enctype="multipart/form-data" id="billForm">
@Html.AntiForgeryToken()
<input asp-for="Id" type="hidden" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3"></div>
<div class="row g-4">
<div class="col-lg-8">
@@ -24,7 +24,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Bill Details"
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation.">
data-bs-content="Vendor: who you're paying. AP Account: the liability account this bill posts to (e.g. Accounts Payable). Bill Date: date on the vendor's invoice. Due Date: when payment is due — drives overdue status. Vendor Invoice #: the vendor's own reference number for reconciliation.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -34,8 +34,8 @@
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-danger">*</span></label>
<select asp-for="VendorId" asp-items="ViewBag.Vendors" class="form-select"
data-quick-add-url="/Vendors/Create" data-quick-add-title="Add New Vendor">
<option value=""> Select Vendor </option>
<option value="__new__">+ Add New Vendor</option>
<option value="">— Select Vendor —</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
</div>
<div class="col-md-6">
@@ -87,7 +87,7 @@
}
<input type="file" name="receiptFile" id="receiptFile" class="form-control"
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf" />
<div class="form-text">JPG, PNG, GIF, WebP, or PDF up to 10 MB.</div>
<div class="form-text">JPG, PNG, GIF, WebP, or PDF — up to 10 MB.</div>
</div>
</div>
</div>
@@ -100,7 +100,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Line Items"
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
data-bs-content="Each line maps to an expense account (e.g. Supplies, Materials, Subcontractors). Optionally link a line to a Job to track costs against specific work orders. Qty × Unit Price = Amount. Use multiple lines to split one bill across different expense categories.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -134,7 +134,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Bill Summary"
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed each payment recorded reduces the balance due until the bill is fully paid.">
data-bs-content="Tax % is applied to the line-item subtotal. The resulting Total is the full amount owed to the vendor. Partial payments are allowed — each payment recorded reduces the balance due until the bill is fully paid.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -171,7 +171,7 @@
<tr class="line-item-row">
<td>
<select class="form-select form-select-sm account-select" name="LineItems[INDEX].AccountId" required>
<option value=""> Account </option>
<option value="">— Account —</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -181,7 +181,7 @@
<td><input type="text" class="form-control form-control-sm" name="LineItems[INDEX].Description" placeholder="Description" /></td>
<td>
<select class="form-select form-select-sm" name="LineItems[INDEX].JobId">
<option value=""></option>
<option value="">—</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.Jobs)
{
<option value="@item.Value">@item.Text</option>
@@ -1,4 +1,4 @@
@model List<PowderCoating.Application.DTOs.Accounting.BillExpenseListDto>
@model List<PowderCoating.Application.DTOs.Accounting.BillExpenseListDto>
@{
ViewData["Title"] = "Bills / Expenses";
@@ -45,7 +45,7 @@
@if ((decimal)ViewBag.TotalOwed > 0)
{
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-4">
<i class="bi bi-exclamation-circle fs-5"></i>
<span>Outstanding bills: <strong>@(((decimal)ViewBag.TotalOwed).ToString("C"))</strong></span>
<a asp-action="Index" asp-route-status="Unpaid" class="btn btn-sm btn-warning ms-auto">
@@ -59,7 +59,7 @@
<form method="get" class="row g-2 align-items-end">
<div class="col-md-4">
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
placeholder="Search by #, vendor, memo, amount" />
placeholder="Search by #, vendor, memo, amount…" />
</div>
<div class="col-md-2">
<select name="type" class="form-select">
@@ -153,13 +153,13 @@
}
else if (entry.EntryType == "Expense")
{
<span class="text-muted"></span>
<span class="text-muted">—</span>
}
</td>
<td><span class="badge bg-@entry.StatusColor">@entry.StatusLabel</span></td>
<td class="text-end">@entry.Total.ToString("C")</td>
<td class="text-end fw-medium @(entry.BalanceDue > 0 ? "text-danger" : "text-muted")">
@(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "")
@(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "—")
</td>
<td>
@if (entry.EntryType == "Bill")
@@ -206,7 +206,7 @@ else
asp-route-status="@ViewBag.StatusFilter"
asp-route-search="@ViewBag.Search"
asp-route-page="@((int)ViewBag.Page - 1)"
asp-route-pageSize="@ViewBag.PageSize"> Prev</a>
asp-route-pageSize="@ViewBag.PageSize">‹ Prev</a>
</li>
@for (var p = 1; p <= (int)ViewBag.TotalPages; p++)
{
@@ -225,11 +225,11 @@ else
asp-route-status="@ViewBag.StatusFilter"
asp-route-search="@ViewBag.Search"
asp-route-page="@((int)ViewBag.Page + 1)"
asp-route-pageSize="@ViewBag.PageSize">Next </a>
asp-route-pageSize="@ViewBag.PageSize">Next ›</a>
</li>
</ul>
<p class="text-center text-muted small">
Showing @(((int)ViewBag.Page - 1) * (int)ViewBag.PageSize + 1)@(Math.Min((int)ViewBag.Page * (int)ViewBag.PageSize, (int)ViewBag.TotalCount))
Showing @(((int)ViewBag.Page - 1) * (int)ViewBag.PageSize + 1)–@(Math.Min((int)ViewBag.Page * (int)ViewBag.PageSize, (int)ViewBag.TotalCount))
of @ViewBag.TotalCount entries
</p>
</nav>