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
@@ -1,11 +1,11 @@
@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto
@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "New Account";
ViewData["PageIcon"] = "bi-journal-plus";
ViewData["PageHelpTitle"] = "New Account";
ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses.";
ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first — it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses.";
bool isInline = ViewBag.Inline == true;
}
@@ -22,7 +22,7 @@
}
<form asp-action="Create" method="post">
@Html.AntiForgeryToken()
<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-3">
<div class="col-sm-4">
@@ -31,7 +31,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Account Number"
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 10001999 Assets, 20002999 Liabilities, 30003999 Equity, 40004999 Revenue, 50005999 Cost of Goods, 60009999 Expenses. Must be unique. Sub-accounts can use decimals (e.g. 6100.1).">
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique. Sub-accounts can use decimals (e.g. 6100.1).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -55,7 +55,7 @@
</a>
</div>
<select asp-for="AccountType" asp-items="ViewBag.AccountTypes" class="form-select" id="accountTypeSelect">
<option value=""> Select Type </option>
<option value="">— Select Type —</option>
</select>
<span asp-validation-for="AccountType" class="text-danger small"></span>
</div>
@@ -70,7 +70,7 @@
</a>
</div>
<select asp-for="AccountSubType" asp-items="ViewBag.AccountSubTypes" class="form-select" id="accountSubTypeSelect">
<option value=""> Select Sub-Type </option>
<option value="">— Select Sub-Type —</option>
</select>
<span asp-validation-for="AccountSubType" class="text-danger small"></span>
<div class="form-text text-primary" id="typeAutoSetHint" style="display:none">
@@ -89,12 +89,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Parent Account"
data-bs-content="Nest this account under a parent to create a hierarchy e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
<option value=""> None (top-level account) </option>
<option value="">— None (top-level account) —</option>
</select>
</div>
@@ -152,7 +152,7 @@
<script>
(function () {
// SubType enum values AccountType enum values (mirrors server-side mapping)
// SubType enum values → AccountType enum values (mirrors server-side mapping)
const subTypeToAccountType = {
8: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Assets
10: 2, 11: 2, 12: 2, 13: 2, // Liabilities
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Accounting.EditAccountDto
@model PowderCoating.Application.DTOs.Accounting.EditAccountDto
@{
ViewData["Title"] = "Edit Account";
ViewData["PageIcon"] = "bi-pencil-square";
ViewData["PageHelpTitle"] = "Edit Account";
ViewData["PageHelpContent"] = "You can change number, name, type, sub-type, parent, and opening balance. Changing the account type or sub-type on an account that already has transactions is allowed but use caution it changes how balances are reported going forward. Inactive accounts are hidden from pickers but preserved in history.";
ViewData["PageHelpContent"] = "You can change number, name, type, sub-type, parent, and opening balance. Changing the account type or sub-type on an account that already has transactions is allowed but use caution — it changes how balances are reported going forward. Inactive accounts are hidden from pickers but preserved in history.";
}
<div class="d-flex justify-content-start mb-4">
@@ -18,7 +18,7 @@
<form asp-action="Edit" method="post">
@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-3">
<div class="col-sm-4">
@@ -27,7 +27,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Account Number"
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 10001999 Assets, 20002999 Liabilities, 30003999 Equity, 40004999 Revenue, 50005999 Cost of Goods, 60009999 Expenses. Must be unique.">
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -81,12 +81,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Parent Account"
data-bs-content="Nest this account under a parent to create a hierarchy e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
data-bs-content="Nest this account under a parent to create a hierarchy — e.g. 'Powder Costs' under 'Cost of Goods Sold'. Sub-accounts roll up into their parent on financial reports. Most accounts work fine without a parent.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select asp-for="ParentAccountId" asp-items="ViewBag.ParentAccounts" class="form-select">
<option value=""> None </option>
<option value="">— None —</option>
</select>
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = "Year-End Close";
ViewData["PageIcon"] = "bi-calendar-check";
@@ -30,10 +30,10 @@
<h5 class="mb-0 fw-semibold"><i class="bi bi-calendar-check me-2 text-primary"></i>Close a Fiscal Year</h5>
</div>
<div class="card-body">
<div class="alert alert-warning py-2 mb-4">
<div class="alert alert-warning alert-permanent py-2 mb-4">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>What this does:</strong> Posts a Journal Entry dated December 31 that zeroes all Revenue
and Expense account balances into Retained Earnings the standard accounting close.
and Expense account balances into Retained Earnings — the standard accounting close.
Run this <strong>after</strong> all entries for the year are posted and the period is locked.
A year can only be closed once.
</div>
@@ -99,7 +99,7 @@
<tr>
<td class="fw-bold">@c.ClosedYear</td>
<td>@c.ClosedAt.ToLocalTime().ToString("MM/dd/yyyy h:mm tt")</td>
<td>@(c.ClosedBy ?? "")</td>
<td>@(c.ClosedBy ?? "—")</td>
<td>
@if (c.JournalEntry != null)
{
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Appointment.CreateAppointmentDto
@model PowderCoating.Application.DTOs.Appointment.CreateAppointmentDto
@{
ViewData["Title"] = "New Appointment";
ViewData["PageIcon"] = "bi-calendar-plus";
ViewData["PageHelpTitle"] = "New Appointment";
ViewData["PageHelpContent"] = "Create an appointment to schedule a customer visit, drop-off, pick-up, or consultation. Select the Type first the Linked Job field appears once a type is chosen. Reminder notifications fire before the scheduled start time.";
ViewData["PageHelpContent"] = "Create an appointment to schedule a customer visit, drop-off, pick-up, or consultation. Select the Type first — the Linked Job field appears once a type is chosen. Reminder notifications fire before the scheduled start time.";
}
<div class="d-flex justify-content-end align-items-center mb-4">
@@ -20,7 +20,7 @@
<form asp-action="Create" method="post">
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-2"></i>Please correct the following errors:</h6>
<partial name="_ValidationSummary" />
</div>
@@ -143,7 +143,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Reminder Settings"
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance — e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Appointment.AppointmentDto
@model PowderCoating.Application.DTOs.Appointment.AppointmentDto
@{
ViewData["Title"] = $"Appointment {Model.AppointmentNumber}";
ViewData["PageIcon"] = "bi-calendar-event";
ViewData["PageHelpTitle"] = "Appointment Details";
ViewData["PageHelpContent"] = "View all details for this appointment. Edit to update status or record actual times. Deleting permanently removes the record consider setting status to Cancelled instead to preserve history.";
ViewData["PageHelpContent"] = "View all details for this appointment. Edit to update status or record actual times. Deleting permanently removes the record — consider setting status to Cancelled instead to preserve history.";
}
<div class="d-flex justify-content-end gap-2 mb-4">
@@ -52,7 +52,7 @@
}
else
{
<div class="alert alert-info mb-0">
<div class="alert alert-info alert-permanent mb-0">
<i class="bi bi-info-circle me-2"></i>
<strong>Internal Appointment</strong><br />
<small>This appointment is not associated with a customer.</small>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Subscription
@using PowderCoating.Application.DTOs.Subscription
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Billing & Subscription";
@@ -56,7 +56,7 @@
@if (isExpiringSoon)
{
<div class="alert alert-warning d-flex align-items-center justify-content-between mb-4" role="alert">
<div class="alert alert-warning alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
<div>
<i class="bi bi-clock-history me-2"></i>
<strong>Your subscription expires in @status.DaysRemaining day@(status.DaysRemaining == 1 ? "" : "s").</strong>
@@ -74,7 +74,7 @@
}
else if (status.IsGracePeriod)
{
<div class="alert alert-warning d-flex align-items-center justify-content-between mb-4" role="alert">
<div class="alert alert-warning alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
<div>
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Your subscription has expired.</strong>
@@ -93,7 +93,7 @@ else if (status.IsGracePeriod)
}
else if (status.IsExpired)
{
<div class="alert alert-danger d-flex align-items-center justify-content-between mb-4" role="alert">
<div class="alert alert-danger alert-permanent d-flex align-items-center justify-content-between mb-4" role="alert">
<div>
<i class="bi bi-x-octagon-fill me-2"></i>
<strong>Your subscription has expired and access is restricted.</strong>
@@ -136,7 +136,7 @@ else if (status.IsExpired)
}
else if (status.IsGracePeriod)
{
<span class="text-warning">Grace period expired @status.EndDate.Value.ToString("MMM d, yyyy")</span>
<span class="text-warning">Grace period — expired @status.EndDate.Value.ToString("MMM d, yyyy")</span>
}
else
{
@@ -266,7 +266,7 @@ else if (status.IsExpired)
<strong>Cancellation &amp; Refund Policy:</strong>
You may cancel your subscription at any time from this page or by contacting
<a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a>.
Cancellation takes effect at the end of your current billing period you retain full access until then.
Cancellation takes effect at the end of your current billing period — you retain full access until then.
All fees are <strong>non-refundable</strong>; unused time is not credited back.
See our <a asp-controller="Home" asp-action="TermsOfService" asp-fragment="section-5" target="_blank">full billing terms</a> for details.
</div>
+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>
@@ -1,8 +1,8 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model BudgetCreateVm
@{
ViewData["Title"] = $"Edit Budget {Model.Name}";
ViewData["Title"] = $"Edit Budget — {Model.Name}";
ViewData["PageIcon"] = "bi-pencil";
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
}
@@ -24,7 +24,7 @@
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name @Model.FiscalYear
<i class="bi bi-pie-chart me-2 text-primary"></i>@Model.Name — @Model.FiscalYear
</h5>
</div>
<div class="card-body">
@@ -56,7 +56,7 @@
</button>
</div>
</div>
<div class="alert alert-info py-2 mx-3 mt-3 mb-0 small">
<div class="alert alert-info alert-permanent py-2 mx-3 mt-3 mb-0 small">
<i class="bi bi-info-circle me-1"></i>
Enter monthly amounts for each Revenue and Expense account. Leave a row at zero to exclude that account from the budget. Amounts represent expected <strong>activity</strong> for the period (not running totals).
</div>
@@ -43,7 +43,7 @@
<input type="hidden" asp-for="CompanyName" />
<input type="hidden" asp-for="CreatedAt" />
<input type="hidden" asp-for="CompanyId" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent" role="alert"></div>
<div class="mb-3">
<label asp-for="Title" class="form-label fw-semibold">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.BugReport.CreateBugReportDto
@model PowderCoating.Application.DTOs.BugReport.CreateBugReportDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Report a Bug";
@@ -35,7 +35,7 @@
</div>
<div class="card-body">
<form asp-action="Submit" method="post" enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent" role="alert"></div>
<div class="mb-3">
<label asp-for="Title" class="form-label fw-semibold">
@@ -59,10 +59,10 @@
<div class="mb-4">
<label asp-for="Priority" class="form-label fw-semibold">Priority</label>
<select asp-for="Priority" class="form-select">
<option value="@((int)BugReportPriority.Low)">Low Minor inconvenience, workaround exists</option>
<option value="@((int)BugReportPriority.Normal)" selected>Normal Affects workflow but not critical</option>
<option value="@((int)BugReportPriority.High)">High Significantly impacts operations</option>
<option value="@((int)BugReportPriority.Critical)">Critical System unusable or data loss risk</option>
<option value="@((int)BugReportPriority.Low)">Low – Minor inconvenience, workaround exists</option>
<option value="@((int)BugReportPriority.Normal)" selected>Normal – Affects workflow but not critical</option>
<option value="@((int)BugReportPriority.High)">High – Significantly impacts operations</option>
<option value="@((int)BugReportPriority.Critical)">Critical – System unusable or data loss risk</option>
</select>
<span asp-validation-for="Priority" class="text-danger small"></span>
</div>
@@ -104,7 +104,7 @@
const li = document.createElement('li');
const sizeMb = (f.size / 1024 / 1024).toFixed(1);
if (f.size > maxBytes) {
li.innerHTML = `<i class="bi bi-exclamation-triangle text-danger"></i> ${f.name} (${sizeMb} MB) exceeds 100 MB limit`;
li.innerHTML = `<i class="bi bi-exclamation-triangle text-danger"></i> ${f.name} (${sizeMb} MB) — exceeds 100 MB limit`;
} else {
li.innerHTML = `<i class="bi bi-file-earmark text-secondary"></i> ${f.name} (${sizeMb} MB)`;
}
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Catalog.CategoryDto
@model PowderCoating.Application.DTOs.Catalog.CategoryDto
@{
ViewData["Title"] = "Delete Category";
var canDelete = ViewBag.CanDelete ?? false;
@@ -18,7 +18,7 @@
<div class="card-body">
@if (!canDelete)
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<i class="bi bi-x-circle-fill me-2"></i>
<strong>Cannot Delete:</strong> This category cannot be deleted because it contains @(hasItems ? "items" : "") @(hasItems && hasSubCategories ? "and" : "") @(hasSubCategories ? "subcategories" : "").
</div>
@@ -46,7 +46,7 @@
}
else
{
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> You are about to delete this category. This action cannot be undone.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Catalog.CatalogItemDto
@model PowderCoating.Application.DTOs.Catalog.CatalogItemDto
@{
ViewData["Title"] = "Delete Catalog Item";
}
@@ -13,7 +13,7 @@
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> You are about to delete this catalog item. This action cannot be undone.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CreateCompanyAdminDto
@model PowderCoating.Application.DTOs.Company.CreateCompanyAdminDto
@{
ViewData["Title"] = "Create Company Admin";
@@ -27,7 +27,7 @@
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-building me-2 text-primary"></i>Company
</h5>
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle me-2"></i>
Creating admin user for: <strong>@Model.CompanyName</strong>
</div>
@@ -109,7 +109,7 @@
</div>
<!-- Permissions Notice -->
<div class="alert alert-success mb-4">
<div class="alert alert-success alert-permanent mb-4">
<h6 class="alert-heading">
<i class="bi bi-shield-check me-2"></i>Administrator Permissions
</h6>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CompanyDto
@model PowderCoating.Application.DTOs.Company.CompanyDto
@{
ViewData["Title"] = Model.CompanyName;
@@ -470,7 +470,7 @@
<i class="bi bi-fire me-1"></i>Reset All Company Data
</h6>
<p class="text-muted small mb-0">
Permanently deletes <strong>all business data</strong> customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
The company record, user accounts, and system configuration are preserved.
Use this to wipe a migration and start fresh.
</p>
@@ -491,7 +491,7 @@
There is no going back.
@if (Model.UserCount > 0)
{
<br /><strong class="text-danger">This company has @Model.UserCount user(s) remove them first.</strong>
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong>
}
</p>
</div>
@@ -523,7 +523,7 @@
<!-- Loading spinner -->
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading</span>
<span class="visually-hidden">Loading…</span>
</div>
</div>
@@ -545,14 +545,14 @@
</div>
<table class="table table-sm table-borderless mb-0 small">
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role"></td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept"></td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position"></td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone"></td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire"></td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created"></td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin"></td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf"></td></tr>
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr>
</table>
</div>
@@ -680,7 +680,7 @@
@Html.AntiForgeryToken()
<input type="hidden" id="resetUserId" name="id" />
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle me-2"></i>
You are about to reset the password for <strong id="resetUserName"></strong>.
</div>
@@ -706,7 +706,7 @@
@section Scripts {
<script>
// Reset Data Modal enable submit only when user types "DELETE"
// Reset Data Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('resetDataConfirmInput');
var hidden = document.getElementById('resetDataConfirmHidden');
@@ -725,7 +725,7 @@
}
})();
// Hard Delete Modal enable submit only when user types "DELETE"
// Hard Delete Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('hardDeleteConfirmInput');
var hidden = document.getElementById('hardDeleteConfirmHidden');
@@ -760,7 +760,7 @@
});
}
// ── User Details Modal ────────────────────────────────────────────────
// ── User Details Modal ────────────────────────────────────────────────
(function () {
const offcanvasEl = document.getElementById('userDetailOffcanvas');
const oc = new bootstrap.Modal(offcanvasEl);
@@ -806,12 +806,12 @@
badge.textContent = u.isActive ? 'Active' : 'Inactive';
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '';
document.getElementById('oc-dept').textContent = u.department || '';
document.getElementById('oc-position').textContent = u.position || '';
document.getElementById('oc-phone').textContent = u.phone || '';
document.getElementById('oc-hire').textContent = u.hireDate || '';
document.getElementById('oc-created').textContent = u.createdAt || '';
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—';
document.getElementById('oc-dept').textContent = u.department || '—';
document.getElementById('oc-position').textContent = u.position || '—';
document.getElementById('oc-phone').textContent = u.phone || '—';
document.getElementById('oc-hire').textContent = u.hireDate || '—';
document.getElementById('oc-created').textContent = u.createdAt || '—';
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
@@ -1,4 +1,4 @@
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
@section Styles {
<style>
@@ -58,7 +58,7 @@
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="searchTerm" class="form-control"
placeholder="Search by name, code, email, phone"
placeholder="Search by name, code, email, phone…"
value="@searchTerm" />
</div>
</div>
@@ -230,7 +230,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var company in Model)
@@ -274,7 +274,7 @@
}
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View </span>
<span class="btn btn-sm btn-outline-primary">View →</span>
</div>
</a>
}
@@ -286,7 +286,7 @@
{
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="text-muted small">
Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
</div>
<div class="d-flex align-items-center gap-3">
<div>
@@ -413,10 +413,10 @@
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
<p class="text-muted small mb-2">
<strong class="text-danger">This cannot be undone.</strong>
All company data users, jobs, quotes, customers, invoices, and everything else will be
All company data — users, jobs, quotes, customers, invoices, and everything else — will be
<strong>permanently and irreversibly deleted</strong> from the database.
</p>
<div class="alert alert-danger py-2 mb-3">
<div class="alert alert-danger alert-permanent py-2 mb-3">
<i class="bi bi-exclamation-octagon-fill me-2"></i>
Type <strong>DELETE</strong> below to enable permanent deletion.
</div>
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.User.CreateCompanyUserDto
@model PowderCoating.Application.DTOs.User.CreateCompanyUserDto
@{
ViewData["Title"] = "Add New User";
ViewData["PageIcon"] = "bi-person-plus";
ViewData["PageHelpTitle"] = "Add New User";
ViewData["PageHelpContent"] = "Creates a new login account for a member of your company. The email address doubles as the login username. Set a temporary password the user can change it from their Profile page after their first login. Assign a Role, then fine-tune individual permissions below.";
ViewData["PageHelpContent"] = "Creates a new login account for a member of your company. The email address doubles as the login username. Set a temporary password — the user can change it from their Profile page after their first login. Assign a Role, then fine-tune individual permissions below.";
}
<div class="container">
@@ -26,7 +26,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Basic Information"
data-bs-content="First Name, Last Name, and Email are required. The email is used as the login username it must be unique across the system. Employee Number is an optional internal reference. The user can update their name and phone from their own Profile page after logging in.">
data-bs-content="First Name, Last Name, and Email are required. The email is used as the login username — it must be unique across the system. Employee Number is an optional internal reference. The user can update their name and phone from their own Profile page after logging in.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -109,7 +109,7 @@
<i class="bi bi-question-circle"></i>
</a>
</div>
<div id="companyAdminAlert" class="alert alert-info" style="display: none;">
<div id="companyAdminAlert" class="alert alert-info alert-permanent" style="display: none;">
<i class="bi bi-info-circle me-2"></i>
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
</div>
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.User.UpdateCompanyUserDto
@model PowderCoating.Application.DTOs.User.UpdateCompanyUserDto
@{
ViewData["Title"] = "Edit User";
ViewData["PageIcon"] = "bi-person-gear";
ViewData["PageHelpTitle"] = "Edit User";
ViewData["PageHelpContent"] = "Update this user&apos;s account details, role, and permissions. Unchecking User Active prevents the user from logging in without deleting their account or history. Changing the email here also changes their login username notify them so they can log in with the new address.";
ViewData["PageHelpContent"] = "Update this user&apos;s account details, role, and permissions. Unchecking User Active prevents the user from logging in without deleting their account or history. Changing the email here also changes their login username — notify them so they can log in with the new address.";
}
<div class="container">
@@ -36,7 +36,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Basic Information"
data-bs-content="Email is this user's login username changing it here means they must use the new address to log in. User Active controls whether the account can sign in; deactivating preserves all data without deleting the account. To reset a password, the user can use Forgot Password on the login page, or a SuperAdmin can set one directly.">
data-bs-content="Email is this user's login username — changing it here means they must use the new address to log in. User Active controls whether the account can sign in; deactivating preserves all data without deleting the account. To reset a password, the user can use Forgot Password on the login page, or a SuperAdmin can set one directly.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -80,7 +80,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Role &amp; Department"
data-bs-content="Changing the Role updates the user's base access level immediately on save. Termination Date is informational to actually prevent login, also uncheck User Active above. Department and Position appear on the user's profile card and in the Manage Users list.">
data-bs-content="Changing the Role updates the user's base access level immediately on save. Termination Date is informational — to actually prevent login, also uncheck User Active above. Department and Position appear on the user's profile card and in the Manage Users list.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -126,7 +126,7 @@
<i class="bi bi-question-circle"></i>
</a>
</div>
<div id="companyAdminAlert" class="alert alert-info" style="display: none;">
<div id="companyAdminAlert" class="alert alert-info alert-permanent" style="display: none;">
<i class="bi bi-info-circle me-2"></i>
<strong>Company Admins automatically have all permissions.</strong> These checkboxes are disabled because Company Admins always have full access to all features.
</div>
@@ -1,4 +1,4 @@
@model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto>
@model PagedResult<PowderCoating.Application.DTOs.User.CompanyUserListDto>
@{
ViewData["Title"] = "Manage Users";
@@ -247,7 +247,7 @@
@Html.AntiForgeryToken()
<input type="hidden" name="id" id="banUserId" />
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Banning <strong id="banUserName"></strong> will immediately prevent them from logging in.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Customer.CustomerDto
@model PowderCoating.Application.DTOs.Customer.CustomerDto
@{
ViewData["Title"] = "Delete Customer";
@@ -14,7 +14,7 @@
</div>
<!-- Warning Banner -->
<div class="alert alert-danger d-flex align-items-start mb-4">
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Are you sure you want to delete this customer?</h5>
@@ -141,7 +141,7 @@
@if (Model.CurrentBalance > 0)
{
<div class="alert alert-warning d-flex align-items-center">
<div class="alert alert-warning alert-permanent d-flex align-items-center">
<i class="bi bi-exclamation-circle me-2"></i>
<div>
<strong>Warning:</strong> This customer has an outstanding balance of @Model.CurrentBalance.ToString("C"). Please ensure all balances are settled before deletion.
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Customer.CustomerDto
@model PowderCoating.Application.DTOs.Customer.CustomerDto
@{
ViewData["Title"] = !string.IsNullOrWhiteSpace(Model.CompanyName)
@@ -123,7 +123,7 @@
}
else
{
<span class="text-muted">Not set invoices go to contact email</span>
<span class="text-muted">Not set — invoices go to contact email</span>
}
</p>
</div>
@@ -272,7 +272,7 @@
{
<div class="col-md-12">
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
<div class="alert alert-success d-flex justify-content-between align-items-center mb-0 mt-2">
<div class="alert alert-success alert-permanent d-flex justify-content-between align-items-center mb-0 mt-2">
<div>
<i class="bi bi-file-earmark-check me-2"></i>
<strong>File on record:</strong> @Model.TaxExemptCertificateFileName
@@ -287,7 +287,7 @@
{
<div class="col-md-12">
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
<div class="alert alert-warning mb-0 mt-2">
<div class="alert alert-warning alert-permanent mb-0 mt-2">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>No certificate on file.</strong> Please upload a tax exempt certificate to complete the record.
</div>
@@ -514,7 +514,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
<select name="Reason" class="form-select" required id="creditReasonSelect">
<option value=""> Select reason </option>
<option value="">— Select reason —</option>
<option value="Pre-payment / Deposit">Pre-payment / Deposit</option>
<option value="Gift / Gift Card">Gift / Gift Card</option>
<option value="Overpayment credit">Overpayment credit</option>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Customer.UpdateCustomerDto
@model PowderCoating.Application.DTOs.Customer.UpdateCustomerDto
@{
ViewData["Title"] = "Edit Customer";
@@ -26,7 +26,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Company Information"
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Customer Type controls which features are available Commercial customers get payment terms, credit limits, and pricing tier discounts. Status Inactive hides the customer from new quote/job dropdowns but preserves all history.">
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Customer Type controls which features are available — Commercial customers get payment terms, credit limits, and pricing tier discounts. Status Inactive hides the customer from new quote/job dropdowns but preserves all history.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -220,7 +220,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Business Information"
data-bs-content="Payment Terms sets the default due date on invoices (e.g., Net 30 = 30 days from invoice date). Credit Limit is a soft warning cap the system alerts when exceeded. Tax Exempt removes tax from all invoices; upload the exemption certificate in the Tax Exempt Certificate section below.">
data-bs-content="Payment Terms sets the default due date on invoices (e.g., Net 30 = 30 days from invoice date). Credit Limit is a soft warning cap — the system alerts when exceeded. Tax Exempt removes tax from all invoices; upload the exemption certificate in the Tax Exempt Certificate section below.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -246,7 +246,7 @@
<div class="col-md-3">
<label asp-for="PricingTierId" class="form-label">Pricing Tier</label>
<select asp-for="PricingTierId" asp-items="ViewBag.PricingTiers" class="form-select">
<option value=""> No tier </option>
<option value="">— No tier —</option>
</select>
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
</div>
@@ -291,7 +291,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Notification Preferences"
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent uncheck 'SMS Notifications Active' to temporarily pause without revoking consent.">
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent — uncheck 'SMS Notifications Active' to temporarily pause without revoking consent.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -314,7 +314,7 @@
<div class="mt-3">
@if (Model.SmsConsentedAt.HasValue)
{
<!-- Consent already recorded show status and allow pause/resume -->
<!-- Consent already recorded — show status and allow pause/resume -->
<div class="card border-success bg-success-subtle p-3 mb-2">
<div class="d-flex align-items-start gap-3">
<i class="bi bi-shield-fill-check text-success fs-4 mt-1"></i>
@@ -339,7 +339,7 @@
}
else
{
<!-- No consent on file show the compliance notice and consent checkbox -->
<!-- No consent on file — show the compliance notice and consent checkbox -->
<div class="alert alert-warning border-warning alert-permanent" role="alert">
<h6 class="alert-heading fw-bold mb-2">
<i class="bi bi-exclamation-triangle-fill me-2"></i>SMS Consent Requirement (TCPA)
@@ -399,7 +399,7 @@
<div class="col-md-6">
@if (Model.HasTaxExemptCertificate)
{
<div class="alert alert-success d-flex justify-content-between align-items-center">
<div class="alert alert-success alert-permanent d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-file-earmark-check me-2"></i>
<strong>Certificate on file:</strong> @Model.TaxExemptCertificateFileName
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@using Microsoft.AspNetCore.Html
@using PowderCoating.Application.DTOs.Health
@using PowderCoating.Web.ViewModels.Dashboard
@@ -24,7 +24,7 @@
<p class="mb-3" style="font-family:var(--font-display);font-size:1.35rem;font-weight:500;line-height:1.4;color:var(--pcl-ink);">
@if (_attnCount > 0)
{
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
<span>Shop is </span><span style="color:var(--pcl-bad);">running hot</span><span> — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
}
else
{
@@ -59,7 +59,7 @@
</div>
</div>
@* PWA install banner rendered by JS only on mobile, hidden once dismissed or already installed *@
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
<div id="pwa-install-banner" class="row mb-4" style="display:none!important;">
<div class="col-12">
<div class="alert alert-permanent mb-0 d-flex align-items-start gap-3 py-3"
@@ -104,7 +104,7 @@
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
}
@* Config health alert only shown when there are setup gaps *@
@* Config health alert — only shown when there are setup gaps *@
@if (configHealth != null && !configHealth.IsHealthy)
{
<div class="row mb-4">
@@ -406,15 +406,15 @@
}
@if (Model.AgingDays1To30 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>130d @Model.AgingDays1To30.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-warn);"></span>1–30d @Model.AgingDays1To30.ToString("C0")</span>
}
@if (Model.AgingDays31To60 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>3160d @Model.AgingDays31To60.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>31–60d @Model.AgingDays31To60.ToString("C0")</span>
}
@if (Model.AgingDays61To90 > 0)
{
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>6190d @Model.AgingDays61To90.ToString("C0")</span>
<span><span class="me-1" style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--pcl-bad);"></span>61–90d @Model.AgingDays61To90.ToString("C0")</span>
}
@if (Model.AgingDaysOver90 > 0)
{
@@ -536,7 +536,7 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted"></span>}
{<span class="text-muted">—</span>}
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
@@ -552,7 +552,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td></td>
</tr>
</tfoot>
@@ -571,7 +571,7 @@
<div class="card border-0 shadow-sm dashboard-card" style="border-left: 4px solid #0d6efd !important;">
<div class="card-header bg-body border-0 d-flex justify-content-between align-items-center pt-4 pb-3">
<h5 class="mb-0 fw-bold">
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered Awaiting Receipt
<i class="bi bi-box-arrow-in-down me-2 text-muted"></i>Powder Ordered — Awaiting Receipt
<span class="ms-2 text-muted fw-normal small" id="placed-count-label">@Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")</span>
</h5>
<small class="text-muted">Grouped by vendor &middot; Enter lbs received to update inventory</small>
@@ -630,13 +630,13 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted"></span>}
{<span class="text-muted">—</span>}
</td>
<td class="text-muted small">
@if (line.OrderedAt.HasValue)
{<span title="@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("g")">@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")</span>}
else
{<span></span>}
{<span>—</span>}
</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
@@ -669,7 +669,7 @@
<tr>
<td colspan="2">Vendor Total</td>
<td class="text-end">@vendorGroup.TotalLbsNeeded.ToString("N2") lbs</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "")</td>
<td class="text-end">@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")</td>
<td colspan="2"></td>
</tr>
</tfoot>
@@ -704,7 +704,7 @@
<div id="apm-ai-status" class="d-none py-2 small mb-3"></div>
<div class="alert alert-info py-2 small mb-3">
<div class="alert alert-info alert-permanent py-2 small mb-3">
<i class="bi bi-info-circle me-1"></i>
Fields pre-filled from the powder order. Fill in any missing details then click Save.
</div>
@@ -739,7 +739,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Category</label>
<select class="form-select" id="apm-categoryId" name="inventoryCategoryId">
<option value=""> Select category </option>
<option value="">— Select category —</option>
@if (ViewBag.InventoryCategories != null)
{
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
@@ -753,7 +753,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Primary Vendor</label>
<select class="form-select" id="apm-vendorId" name="primaryVendorId">
<option value=""> Select vendor </option>
<option value="">— Select vendor —</option>
@if (ViewBag.VendorList != null)
{
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
@@ -883,7 +883,7 @@
const esc = s => s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : '';
const estCost = (c.lbsToOrder && c.costPerLb) ? (c.lbsToOrder * c.costPerLb) : null;
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '';
const orderedDate = c.orderedAt ? new Date(c.orderedAt).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : '—';
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
// Find or create vendor group
@@ -928,7 +928,7 @@
${c.finish ? `<span class="badge bg-light text-dark border ms-1">${esc(c.finish)}</span>` : ''}
</td>
<td class="text-end fw-medium">${lbsFmt} lbs</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted"></span>'}</td>
<td class="text-end">${estCost ? '$' + estCost.toFixed(2) : '<span class="text-muted">—</span>'}</td>
<td class="text-muted small">${orderedDate}</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-${c.coatId}">
@@ -979,7 +979,7 @@
}
qtyInput.classList.remove('is-invalid');
// Custom powder (no inventory item) → open modal to add to inventory
// Custom powder (no inventory item) → open modal to add to inventory
if (!hasInv) {
const modal = document.getElementById('addPowderModal');
// Pre-fill hidden + text fields
@@ -1024,7 +1024,7 @@
return;
}
// Inventory item exists → receive directly
// Inventory item exists → receive directly
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
@@ -1065,7 +1065,7 @@
?? document.querySelector('meta[name="__RequestVerificationToken"]')?.content;
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving';
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving…';
try {
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
@@ -1094,7 +1094,7 @@
}
});
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
// ── AI Lookup for Add Powder modal ───────────────────────────────────────
(function () {
const apmBtn = document.getElementById('apm-ai-btn');
const apmStatusEl = document.getElementById('apm-ai-status');
@@ -1144,7 +1144,7 @@
const hasInput = manufacturer || colorName || colorCode || partNumber || itemName;
if (!hasInput) {
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field Manufacturer, Color Name, Color Code, or Item Name then try again.');
apmShowStatus('warning', '<i class="bi bi-exclamation-triangle me-1"></i>Fill in at least one field — Manufacturer, Color Name, Color Code, or Item Name — then try again.');
return;
}
@@ -1153,7 +1153,7 @@
document.getElementById('apm-bad-match-btn')?.remove();
apmBtn.disabled = true;
apmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Looking up...';
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications');
apmShowStatus('info', '<i class="bi bi-hourglass-split me-1"></i>Searching for product specifications…');
try {
const formData = new FormData();
@@ -1220,7 +1220,7 @@
: '';
apmShowStatus('success', `<i class="bi bi-check-circle me-1"></i>Auto-filled: ${filled.join(', ')}.${reasoning}`);
} else {
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill they may already be populated, or the product wasn\'t found.');
apmShowStatus('warning', '<i class="bi bi-info-circle me-1"></i>No new fields to fill — they may already be populated, or the product wasn\'t found.');
}
} catch (err) {
@@ -1274,7 +1274,7 @@
(function () {
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
// Already installed as standalone never show
// Already installed as standalone — never show
var isStandalone = window.navigator.standalone === true ||
window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) return;
@@ -1298,7 +1298,7 @@
var isSafari = /webkit/i.test(ua) && !/crios|chrome|fxios|opios/i.test(ua);
if (isSafari) {
titleEl.textContent = 'Add to Home Screen';
msgEl.innerHTML = 'For the best experience and so the camera only asks once open the ' +
msgEl.innerHTML = 'For the best experience — and so the camera only asks once — open the ' +
'<strong>Share menu</strong> <span style="font-size:1.1em">&#9650;</span> at the bottom of Safari ' +
'and tap <strong>Add to Home Screen</strong>.';
} else {
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model List<EntityPurgeStat>
@{
ViewData["Title"] = "Data Purge & Cleanup";
@@ -56,10 +56,10 @@
}
@* Warning banner *@
<div class="alert alert-warning d-flex gap-3 align-items-start mb-3">
<div class="alert alert-warning alert-permanent d-flex gap-3 align-items-start mb-3">
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
<div>
<strong>Destructive operation this cannot be undone.</strong>
<strong>Destructive operation — this cannot be undone.</strong>
Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean.
Job photo blobs in Azure Storage are also deleted when purging job photo records.
</div>
@@ -83,8 +83,8 @@
<th style="width:36px"></th>
<th>Entity</th>
<th class="text-end" style="width:90px">Total</th>
<th class="text-end" style="width:100px">030d</th>
<th class="text-end" style="width:100px">3090d</th>
<th class="text-end" style="width:100px">0–30d</th>
<th class="text-end" style="width:100px">30–90d</th>
<th class="text-end" style="width:100px">&gt;90d</th>
<th style="width:130px">Oldest</th>
<th style="width:42px">
@@ -109,7 +109,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">—</span>
}
</td>
<td class="text-end">
@@ -117,24 +117,24 @@
{
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">—</span> }
</td>
<td class="text-end">
@if (s.Deleted30To90Days > 0)
{
<span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">—</span> }
</td>
<td class="text-end">
@if (s.DeletedOlderThan90Days > 0)
{
<span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">—</span> }
</td>
<td class="text-muted">
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")
</td>
<td class="text-center">
<input type="checkbox" class="form-check-input entity-select"
@@ -149,7 +149,7 @@
</table>
</div>
<!-- Mobile card view for this group shown on screens < 992px -->
<!-- Mobile card view for this group — shown on screens < 992px -->
<div class="mobile-card-view">
<div class="px-3 pt-2 pb-1">
<span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span>
@@ -164,7 +164,7 @@
</div>
<div class="mobile-card-title">
<h6>@s.Label</h6>
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")</small>
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—")</small>
</div>
</div>
<div class="mobile-card-body">
@@ -175,11 +175,11 @@
{
<span class="badge bg-secondary">@s.Total</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">—</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">030d / 3090d / &gt;90d</span>
<span class="mobile-card-label">0–30d / 30–90d / &gt;90d</span>
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
</div>
</div>
@@ -300,7 +300,7 @@
const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
const confirmSummary= document.getElementById('confirmSummary');
// ── Select all ──────────────────────────────────────────────────────────
// ── Select all ──────────────────────────────────────────────────────────
selectAll.addEventListener('change', () => {
document.querySelectorAll('.entity-select:not(:disabled)').forEach(cb => {
cb.checked = selectAll.checked;
@@ -308,7 +308,7 @@
updatePurgeBtn();
});
// ── Group select all ────────────────────────────────────────────────────
// ── Group select all ────────────────────────────────────────────────────
document.querySelectorAll('.group-select-all').forEach(ga => {
ga.addEventListener('change', () => {
document.querySelectorAll(`.entity-select[data-group="${ga.dataset.group}"]:not(:disabled)`)
@@ -335,7 +335,7 @@
previewRes.classList.add('d-none');
}
// ── Preview ─────────────────────────────────────────────────────────────
// ── Preview ─────────────────────────────────────────────────────────────
previewBtn.addEventListener('click', async () => {
const entities = getSelectedEntities();
if (!entities.length) {
@@ -344,7 +344,7 @@
}
previewBtn.disabled = true;
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading';
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading…';
const days = document.getElementById('olderThanDays').value;
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
@@ -379,7 +379,7 @@
}
});
// ── Purge button modal ────────────────────────────────────────────────
// ── Purge button → modal ────────────────────────────────────────────────
purgeBtn.addEventListener('click', () => {
const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value;
@@ -390,7 +390,7 @@
confirmModal.show();
});
// ── Confirm submit form ───────────────────────────────────────────────
// ── Confirm → submit form ───────────────────────────────────────────────
document.getElementById('confirmPurgeBtn').addEventListener('click', () => {
const entities = getSelectedEntities();
const days = document.getElementById('olderThanDays').value;
@@ -1,4 +1,4 @@
@model PowderCoating.Web.Controllers.DiagnosticsInfo
@model PowderCoating.Web.Controllers.DiagnosticsInfo
@{
ViewData["Title"] = "System Diagnostics";
ViewData["PageIcon"] = "bi-activity";
@@ -47,11 +47,11 @@
<td>
@if (Model.CanWriteToAppPath)
{
<span class="badge bg-success"> YES</span>
<span class="badge bg-success">✓ YES</span>
}
else
{
<span class="badge bg-danger"> NO - PERMISSION ISSUE</span>
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
}
</td>
</tr>
@@ -60,11 +60,11 @@
<td>
@if (Model.LogsDirectoryExists)
{
<span class="badge bg-success"> YES</span>
<span class="badge bg-success">✓ YES</span>
}
else
{
<span class="badge bg-warning"> NO</span>
<span class="badge bg-warning">✗ NO</span>
}
</td>
</tr>
@@ -73,11 +73,11 @@
<td>
@if (Model.CanWriteToLogsPath)
{
<span class="badge bg-success"> YES</span>
<span class="badge bg-success">✓ YES</span>
}
else
{
<span class="badge bg-danger"> NO - PERMISSION ISSUE</span>
<span class="badge bg-danger">✗ NO - PERMISSION ISSUE</span>
}
</td>
</tr>
@@ -111,7 +111,7 @@
<p><strong>Message:</strong> @Model.LoggingTestMessage</p>
@if (Model.LoggingTestSuccess)
{
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle"></i> A test log entry was written. Check the log files below to see if it appears.
</div>
}
@@ -132,7 +132,7 @@
<div class="card-body">
@if (Model.LogFilesError != null)
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<strong>Error reading log files:</strong> @Model.LogFilesError
</div>
}
@@ -162,7 +162,7 @@
}
else
{
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle"></i> <strong>No log files found!</strong>
<hr>
<p class="mb-0">This means logs are not being written. Check the permissions above.</p>
@@ -175,7 +175,7 @@
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<h6><i class="bi bi-lightbulb"></i> Troubleshooting Tips</h6>
<ul class="mb-0">
<li>If "Can Write to Logs" shows NO, the IIS Application Pool doesn't have write permissions</li>
@@ -1,4 +1,4 @@
@model PowderCoating.Web.Controllers.LogViewerModel
@model PowderCoating.Web.Controllers.LogViewerModel
@{
ViewData["Title"] = "Log Viewer";
ViewData["PageIcon"] = "bi-file-text";
@@ -31,7 +31,7 @@
@if (!string.IsNullOrEmpty(Model.Error))
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<i class="bi bi-exclamation-triangle"></i> @Model.Error
</div>
}
@@ -163,7 +163,7 @@
}
else
{
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle"></i> No log files found in <code>@Model.LogsPath</code>
</div>
}
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model AdminEmailComposeModel
@{
ViewData["Title"] = "Admin Email";
@@ -94,7 +94,7 @@
<div class="row g-4 align-items-start mb-4">
<div class="col-lg-8">
<div class="alert alert-info mb-0">
<div class="alert alert-info alert-permanent mb-0">
The email sends one company at a time to that company's <strong>Primary Contact Email</strong>.
Rich text is supported, and the preview step will render one merged sample before anything sends.
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model AdminEmailPreviewModel
@{
ViewData["Title"] = "Preview Admin Email";
@@ -46,7 +46,7 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-transparent fw-semibold py-3">Delivery Summary</div>
<div class="card-body">
<div class="alert alert-info mb-0">
<div class="alert alert-info alert-permanent mb-0">
The system will process each selected company one at a time.
The sample shown on the left uses the first available recipient after token replacement.
</div>
@@ -78,7 +78,7 @@
<div class="small text-muted">@(string.IsNullOrWhiteSpace(row.RecipientEmail) ? "No primary contact email configured" : row.RecipientEmail)</div>
</td>
<td>
<div>@(string.IsNullOrWhiteSpace(row.CompanyAdminName) ? "" : row.CompanyAdminName)</div>
<div>@(string.IsNullOrWhiteSpace(row.CompanyAdminName) ? "—" : row.CompanyAdminName)</div>
@if (!string.IsNullOrWhiteSpace(row.CompanyAdminEmail))
{
<div class="small text-muted">@row.CompanyAdminEmail</div>
@@ -1,10 +1,10 @@
@model PowderCoating.Application.DTOs.Accounting.CreateExpenseDto
@model PowderCoating.Application.DTOs.Accounting.CreateExpenseDto
@{
ViewData["Title"] = "New Expense";
ViewData["PageIcon"] = "bi-receipt";
ViewData["PageHelpTitle"] = "New Expense";
ViewData["PageHelpContent"] = "Use this for purchases paid immediately credit card swipes, cash payments, debit transactions. For vendor invoices paid later, use Bills instead. Select the expense account (what was bought) and the payment account (where the money came from).";
ViewData["PageHelpContent"] = "Use this for purchases paid immediately — credit card swipes, cash payments, debit transactions. For vendor invoices paid later, use Bills instead. Select the expense account (what was bought) and the payment account (where the money came from).";
}
<div class="d-flex justify-content-start mb-4">
@@ -17,7 +17,7 @@
<div class="card-body">
<form asp-action="Create" method="post" enctype="multipart/form-data">
@Html.AntiForgeryToken()
<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-3">
<div class="col-sm-6">
@@ -40,13 +40,13 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Expense Account"
data-bs-content="The expense category this purchase belongs to e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="d-flex gap-2 align-items-center">
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select" id="expenseAccountSelect">
<option value=""> Select Account </option>
<option value="">— Select Account —</option>
</select>
<button type="button" class="btn btn-sm btn-outline-primary text-nowrap" id="expAiSuggestBtn" title="AI-suggest expense account">
<i class="bi bi-stars me-1"></i>AI Suggest
@@ -66,12 +66,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Paid From"
data-bs-content="The bank or cash account the money came out of e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
<option value=""> Select Account </option>
<option value="">— Select Account —</option>
</select>
<span asp-validation-for="PaymentAccountId" class="text-danger small"></span>
</div>
@@ -84,8 +84,8 @@
<label asp-for="VendorId" class="form-label fw-medium">Vendor <span class="text-muted small">(optional)</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=""> None </option>
<option value="__new__">+ Add New Vendor</option>
<option value="">— None —</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
</div>
<div class="col-sm-6">
@@ -99,7 +99,7 @@
</a>
</div>
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
<option value=""> None </option>
<option value="">— None —</option>
</select>
</div>
@@ -154,7 +154,7 @@
}
});
// ── AI Suggest Account ────────────────────────────────────────────────
// ── AI Suggest Account ────────────────────────────────────────────────
let _expAiSuggestedAccountId = null;
document.getElementById('expAiSuggestBtn').addEventListener('click', async function () {
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Accounting.EditExpenseDto
@model PowderCoating.Application.DTOs.Accounting.EditExpenseDto
@* Note: ReceiptFilePath is carried via hidden field to detect existing receipt *@
@{
@@ -20,7 +20,7 @@
@Html.AntiForgeryToken()
<input asp-for="Id" type="hidden" />
<input asp-for="ReceiptFilePath" 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-3">
<div class="col-sm-6">
@@ -40,12 +40,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Expense Account"
data-bs-content="The expense category this purchase belongs to e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
data-bs-content="The expense category this purchase belongs to — e.g. Supplies, Materials, Utilities, Fuel. This account is debited when the expense is saved. Choose the most specific account that fits to keep your reports accurate.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select asp-for="ExpenseAccountId" asp-items="ViewBag.ExpenseAccounts" class="form-select">
<option value=""> Select Account </option>
<option value="">— Select Account —</option>
</select>
</div>
<div class="col-sm-6">
@@ -54,12 +54,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Paid From"
data-bs-content="The bank or cash account the money came out of e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
data-bs-content="The bank or cash account the money came out of — e.g. Business Checking, Petty Cash, Company Credit Card. This account is credited when the expense is saved. Used for bank reconciliation.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select asp-for="PaymentAccountId" asp-items="ViewBag.PaymentAccounts" class="form-select">
<option value=""> Select Account </option>
<option value="">— Select Account —</option>
</select>
</div>
<div class="col-sm-6">
@@ -70,8 +70,8 @@
<label asp-for="VendorId" class="form-label fw-medium">Vendor</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=""> None </option>
<option value="__new__">+ Add New Vendor</option>
<option value="">— None —</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
</div>
<div class="col-sm-6">
@@ -85,7 +85,7 @@
</a>
</div>
<select asp-for="JobId" asp-items="ViewBag.Jobs" class="form-select">
<option value=""> None </option>
<option value="">— None —</option>
</select>
</div>
<div class="col-12">
@@ -1,4 +1,4 @@
@model List<PowderCoating.Application.DTOs.Accounting.ExpenseListDto>
@model List<PowderCoating.Application.DTOs.Accounting.ExpenseListDto>
@{
ViewData["Title"] = "Expenses";
@@ -23,7 +23,7 @@
@if ((decimal)ViewBag.TotalAmount > 0)
{
<div class="alert alert-info d-flex align-items-center gap-2 mb-4">
<div class="alert alert-info alert-permanent d-flex align-items-center gap-2 mb-4">
<i class="bi bi-info-circle fs-5"></i>
<span>Total shown: <strong>@(((decimal)ViewBag.TotalAmount).ToString("C"))</strong></span>
</div>
@@ -34,7 +34,7 @@
<form method="get" class="row g-2 align-items-end">
<div class="col-md-3">
<input type="search" name="search" value="@ViewBag.Search" class="form-control"
placeholder="Search memo or vendor" />
placeholder="Search memo or vendor…" />
</div>
<div class="col-md-3">
<select name="accountId" class="form-select">
@@ -28,7 +28,7 @@
</div>
</div>
<div class="alert alert-@statusClass d-flex align-items-center mb-4">
<div class="alert alert-@statusClass alert-permanent d-flex align-items-center mb-4">
<i class="bi bi-gift me-2" style="font-size:1.4rem;"></i>
<div>
<strong>@statusLabel</strong>
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Error";
}
@@ -20,7 +20,7 @@
@if (Context.TraceIdentifier != null)
{
<div class="alert alert-secondary mt-3">
<div class="alert alert-secondary alert-permanent mt-3">
<strong>Request ID:</strong> <code>@Context.TraceIdentifier</code>
<br />
<small class="text-muted">Please provide this ID when contacting support.</small>
@@ -56,7 +56,7 @@
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
{
var logFileName = $"logs/errors-{DateTime.Now:yyyyMMdd}.txt";
<div class="alert alert-warning mt-3">
<div class="alert alert-warning alert-permanent mt-3">
<strong><i class="bi bi-code-slash"></i> Development Mode:</strong>
Check the error logs at <code>@logFileName</code> for details.
</div>
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Welcome";
Layout = null; // Use custom layout for login page
}
@@ -249,7 +249,7 @@
@if (User.Identity?.IsAuthenticated == true)
{
<div class="alert alert-success">
<div class="alert alert-success alert-permanent">
<i class="bi bi-check-circle me-2"></i>
You're already logged in as <strong>@User.Identity.Name</strong>
</div>
@@ -272,7 +272,7 @@
<a href="/Identity/Account/Register">
<i class="bi bi-person-plus me-1"></i>Create an account
</a>
<span class="mx-2"></span>
<span class="mx-2">•</span>
<a href="/Identity/Account/ForgotPassword">
<i class="bi bi-question-circle me-1"></i>Forgot password?
</a>
@@ -297,7 +297,7 @@
<div class="text-center mt-4">
<p style="color: rgba(255, 255, 255, 0.8); font-size: 0.875rem;">
© 2024 Powder Coating Logix. All rights reserved.
© 2024 Powder Coating Logix. All rights reserved.
</p>
</div>
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
@{
ViewData["Title"] = "Delete Inventory Item";
@@ -14,7 +14,7 @@
</div>
<!-- Warning Banner -->
<div class="alert alert-danger d-flex align-items-start mb-4">
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Are you sure you want to delete this inventory item?</h5>
@@ -143,7 +143,7 @@
@if (Model.QuantityOnHand > 0)
{
<div class="alert alert-warning d-flex align-items-center">
<div class="alert alert-warning alert-permanent d-flex align-items-center">
<i class="bi bi-exclamation-circle me-2"></i>
<div>
<strong>Warning:</strong> This item still has @Model.QuantityOnHand.ToString("N2") @Model.UnitOfMeasure in stock (value: @((Model.QuantityOnHand * Model.UnitCost).ToString("C"))). Consider transferring or adjusting inventory before deletion.
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Invoice
@using PowderCoating.Application.DTOs.Invoice
@using PowderCoating.Core.Enums
@using PowderCoating.Web.Controllers
@model InvoiceDto
@@ -66,17 +66,17 @@
@if (!hasEmail)
{
<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-envelope-slash fs-5"></i>
<span>
<strong>@Model.CustomerName</strong> has no email address on file you'll be prompted to enter one when sending.
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
</span>
</div>
}
else if (emailOptedOut)
{
<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-envelope-x fs-5"></i>
<span>
<strong>@Model.CustomerName</strong> has email notifications turned off.
@@ -168,12 +168,12 @@
<div class="col-md-4">
<label class="text-muted small mb-1">Due Date</label>
<p class="mb-0 @(Model.Status == InvoiceStatus.Overdue ? "text-danger fw-bold" : "")">
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "")
@(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")
</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Sent Date</label>
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "")</p>
<p class="mb-0">@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")</p>
</div>
@if (!string.IsNullOrWhiteSpace(Model.CustomerPO))
{
@@ -339,7 +339,7 @@
</span>
</td>
<td class="text-muted">
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "")
@(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—")
</td>
<td class="text-end fw-semibold">@gcItem.TotalPrice.ToString("C")</td>
<td>
@@ -385,7 +385,7 @@
<tr>
<td>@p.PaymentDate.ToString("MM/dd/yyyy")</td>
<td>@p.PaymentMethodDisplay</td>
<td>@(p.Reference ?? "")</td>
<td>@(p.Reference ?? "—")</td>
<td>
@if (!string.IsNullOrEmpty(p.DepositAccountName))
{
@@ -393,10 +393,10 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">—</span>
}
</td>
<td>@(p.RecordedByName ?? "")</td>
<td>@(p.RecordedByName ?? "—")</td>
<td class="text-end fw-semibold text-success">@p.Amount.ToString("C")</td>
<td class="text-end">
@if (!isVoided)
@@ -452,7 +452,7 @@
<td>@r.RefundDate.ToString("MM/dd/yyyy")</td>
<td>@r.RefundMethodDisplay</td>
<td>@r.Reason</td>
<td>@(r.Reference ?? "")</td>
<td>@(r.Reference ?? "—")</td>
<td><span class="badge bg-@refundStatusColor">@r.Status</span></td>
<td class="text-end fw-semibold text-danger">(@r.Amount.ToString("C"))</td>
<td class="text-nowrap">
@@ -564,7 +564,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Invoice Actions"
data-bs-content="Workflow: Edit (Draft only) Send Invoice (locks it, emails customer) Record Payment. Partial payments are supported record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -761,7 +761,7 @@
{
<div class="mb-2">
<span class="badge bg-success-subtle text-success mb-2">
<i class="bi bi-check-circle me-1"></i>Active expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
<i class="bi bi-check-circle me-1"></i>Active — expires @Model.PaymentLinkExpiresAt!.Value.ToString("MMM d")
</span>
<div class="input-group input-group-sm">
<input type="text" id="paymentLinkInput" class="form-control font-monospace"
@@ -784,7 +784,7 @@
}
else if (linkExpired)
{
<div class="alert alert-warning py-2 small mb-2">
<div class="alert alert-warning alert-permanent py-2 small mb-2">
<i class="bi bi-clock me-1"></i>Payment link expired.
</div>
<button type="button" class="btn btn-primary btn-sm w-100"
@@ -899,7 +899,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Payment Reference"
data-bs-content="Optional identifier for reconciliation e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
data-bs-content="Optional identifier for reconciliation — e.g., the check number, last 4 digits of the card, ACH transaction ID, or Venmo/PayPal confirmation code. Appears in payment history so you can match payments to your bank statement.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -1112,13 +1112,13 @@
<form asp-action="IssueRefund" asp-route-invoiceId="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-body">
<div id="refundAlertCash" class="alert alert-info small mb-3">
<div id="refundAlertCash" class="alert alert-info alert-permanent small mb-3">
<i class="bi bi-info-circle me-1"></i>
This records the refund intent. You still need to issue the actual refund (cash, check, etc.) manually.
</div>
<div id="refundAlertCredit" class="alert alert-success small mb-3 d-none">
<i class="bi bi-piggy-bank me-1"></i>
The refund amount will be added to the customer's store credit balance immediately no manual action needed.
The refund amount will be added to the customer's store credit balance immediately — no manual action needed.
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
@@ -1188,7 +1188,7 @@
<form asp-action="IssueCreditMemo" asp-route-invoiceId="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-body">
<div class="alert alert-info small mb-3">
<div class="alert alert-info alert-permanent small mb-3">
<i class="bi bi-info-circle me-1"></i>
A credit memo adds store credit to the customer's account that can be applied against future invoices.
</div>
@@ -1240,7 +1240,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Select Credit Memo <span class="text-danger">*</span></label>
<select name="CreditMemoId" class="form-select" required>
<option value=""> Select </option>
<option value="">— Select —</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AvailableCreditMemos)
{
<option value="@item.Value">@item.Text</option>
@@ -1254,7 +1254,7 @@
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01"
max="@Model.BalanceDue.ToString("F2")" value="@Model.BalanceDue.ToString("F2")" required />
</div>
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") the system will cap at the memo's remaining balance.</div>
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C") — the system will cap at the memo's remaining balance.</div>
</div>
</div>
<div class="modal-footer">
@@ -1328,7 +1328,7 @@
<form asp-action="WriteOff" asp-route-id="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-body">
<div class="alert alert-warning py-2 mb-3">
<div class="alert alert-warning alert-permanent py-2 mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
This will write off the remaining balance of <strong>@Model.BalanceDue.ToString("C")</strong>
as bad debt. A GL journal entry will be posted. This action cannot be undone.
@@ -1336,7 +1336,7 @@
<div class="mb-3">
<label class="form-label">Bad Debt Expense Account</label>
<select name="expenseAccountId" class="form-select">
<option value=""> Use default bad debt account </option>
<option value="">— Use default bad debt account —</option>
@if (ViewBag.ExpenseAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
@@ -1397,8 +1397,8 @@
const max = Math.min(data.remainingBalance, @Model.BalanceDue.ToString("F2", System.Globalization.CultureInfo.InvariantCulture));
document.getElementById('gcAmountInput').value = max.toFixed(2);
document.getElementById('gcAmountInput').max = max;
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
const expiry = data.expiryDate ? ` · Expires ${data.expiryDate}` : '';
result.innerHTML = `<div class="alert alert-success py-1 mb-0 small"><i class="bi bi-check-circle me-1"></i><strong>${data.certificateCode}</strong> — $${data.remainingBalance.toFixed(2)} remaining${expiry}</div>`;
}
} catch { result.innerHTML = '<div class="alert alert-danger py-1 mb-0 small">Lookup failed.</div>'; }
document.getElementById('gcLookupSpinner').style.display = 'none';
@@ -1511,7 +1511,7 @@
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted"></span>'}</td>
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">—</span>'}</td>
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span>${expandBtn}</td>
</tr>${errorRow}`;
}).join('');
@@ -1555,7 +1555,7 @@
const data = await resp.json();
if (data.success) {
if (msg) msg.innerHTML = `
<div class="alert alert-success py-2 small">
<div class="alert alert-success alert-permanent py-2 small">
<i class="bi bi-check-circle me-1"></i>New link generated!
<a href="${data.paymentUrl}" target="_blank" class="alert-link ms-1">Open</a>
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Invoice.UpdateInvoiceDto
@model PowderCoating.Application.DTOs.Invoice.UpdateInvoiceDto
@{
ViewData["Title"] = "Edit Invoice";
@@ -38,7 +38,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Invoice Details"
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -79,7 +79,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 row is a billable line on the invoice. Qty × Unit Price = Total per line; you can also override Total directly. Color is optional and appears under the description when printed. Add manual lines for charges not in the original job (e.g., rush fee, pickup charge).">
data-bs-content="Each row is a billable line on the invoice. Qty × Unit Price = Total per line; you can also override Total directly. Color is optional and appears under the description when printed. Add manual lines for charges not in the original job (e.g., rush fee, pickup charge).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -163,7 +163,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Notes"
data-bs-content="Customer Notes appear on the printed and emailed invoice use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff in the app and are never sent to the customer.">
data-bs-content="Customer Notes appear on the printed and emailed invoice — use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff in the app and are never sent to the customer.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -196,7 +196,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Totals"
data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax. Tax % is applied to (Subtotal Discount). Both default from the company settings but can be overridden for this invoice.">
data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax. Tax % is applied to (Subtotal − Discount). Both default from the company settings but can be overridden for this invoice.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -252,7 +252,7 @@
</a>
</div>
<div class="card-footer border-0 pt-0">
<div class="alert alert-info mb-0 small py-2">
<div class="alert alert-info alert-permanent mb-0 small py-2">
<i class="bi bi-info-circle me-1"></i>
<strong>Draft, Sent,</strong> and <strong>Overdue</strong> invoices can be edited.
Paid invoices are locked.
@@ -1,4 +1,4 @@
@model PowderCoating.Core.Entities.JobTemplate
@model PowderCoating.Core.Entities.JobTemplate
@{
ViewData["Title"] = $"Edit Template: {Model.Name}";
ViewData["PageIcon"] = "bi-pencil-square";
@@ -31,7 +31,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Default Customer</label>
<select name="customerId" class="form-select">
<option value=""> Any customer </option>
<option value="">— Any customer —</option>
@foreach (SelectListItem item in (ViewBag.Customers as IEnumerable<SelectListItem> ?? Enumerable.Empty<SelectListItem>()))
{
if (item.Selected)
@@ -70,7 +70,7 @@
</div>
</div>
<div class="alert alert-info mt-3">
<div class="alert alert-info alert-permanent mt-3">
<i class="bi bi-info-circle me-2"></i>
To update the <strong>items and coatings</strong> on this template, create a new job with the desired configuration and save it as a template.
</div>
+10 -10
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.CreateJobDto
@model PowderCoating.Application.DTOs.Job.CreateJobDto
@using PowderCoating.Core.Entities
@{
@@ -19,7 +19,7 @@
<i class="bi bi-layout-text-window-reverse fs-5"></i>
<div>
Pre-filled from template <strong>@ViewBag.TemplateName</strong>.
Items and coatings have been loaded review and adjust before saving.
Items and coatings have been loaded — review and adjust before saving.
</div>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="alert"></button>
</div>
@@ -50,7 +50,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Details"
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order include it so it appears on invoices. Special Instructions go directly to the shop floor worker.">
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order — include it so it appears on invoices. Special Instructions go directly to the shop floor worker.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -70,7 +70,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Priority"
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint overuse of Rush dilutes its meaning for the shop floor team.">
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team.">
<i class="bi bi-question-circle"></i>
</a>
</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="Due Date"
data-bs-content="The customer's deadline when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -130,7 +130,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Special Instructions"
data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes e.g., 'Keep brackets separated, customer allergic to zinc primer'.">
data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes — e.g., 'Keep brackets separated, customer allergic to zinc primer'.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -201,7 +201,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Items"
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -276,7 +276,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -337,7 +337,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
@@ -352,7 +352,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.JobDto
@model PowderCoating.Application.DTOs.Job.JobDto
@{
ViewData["Title"] = "Delete Job";
@@ -14,7 +14,7 @@
</div>
<!-- Warning Banner -->
<div class="alert alert-danger d-flex align-items-start mb-4">
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Are you sure you want to delete this job?</h5>
@@ -125,7 +125,7 @@
@if (Model.StatusIsWIP)
{
<div class="alert alert-warning d-flex align-items-center">
<div class="alert alert-warning alert-permanent d-flex align-items-center">
<i class="bi bi-exclamation-circle me-2"></i>
<div>
<strong>Warning:</strong> This job is currently in progress. Please ensure all work is properly documented before deletion.
@@ -2107,7 +2107,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -3224,7 +3224,7 @@
placeholder="Brief description of when to use this template"></textarea>
</div>
<div class="alert alert-light border mb-0">
<div class="alert alert-light alert-permanent border mb-0">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
The customer, special instructions, and all items with their coatings will be copied.
+9 -9
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.UpdateJobDto
@model PowderCoating.Application.DTOs.Job.UpdateJobDto
@using PowderCoating.Core.Entities
@{
@@ -26,7 +26,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Details"
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and job list. Customer PO is the customer's own reference number it appears on invoices. Special Instructions go directly to the shop floor worker on the work order.">
data-bs-content="Core job information. Priority and due date are visible on the shop floor board and job list. Customer PO is the customer's own reference number — it appears on invoices. Special Instructions go directly to the shop floor worker on the work order.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -45,7 +45,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Status"
data-bs-content="Tracks where the job is in the workflow: Pending Approved Sandblasting Cleaning Coating Curing QualityCheck Completed ReadyForPickup Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress.">
data-bs-content="Tracks where the job is in the workflow: Pending → Approved → Sandblasting → Cleaning → Coating → Curing → QualityCheck → Completed → ReadyForPickup → Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress.">
<i class="bi bi-question-circle"></i>
</a>
</label>
@@ -57,7 +57,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Priority"
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint overuse of Rush dilutes its meaning for the shop floor team.">
data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team.">
<i class="bi bi-question-circle"></i>
</a>
</label>
@@ -85,7 +85,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Due Date"
data-bs-content="The customer's deadline when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list.">
<i class="bi bi-question-circle"></i>
</a>
</label>
@@ -170,7 +170,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Job Items"
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -245,7 +245,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -322,7 +322,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
@@ -337,7 +337,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -1,8 +1,8 @@
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = $"Edit Items {Model.JobNumber}";
ViewData["Title"] = $"Edit Items — {Model.JobNumber}";
ViewData["PageIcon"] = "bi-list-check";
}
@@ -22,7 +22,7 @@
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger mb-4" role="alert">
<div class="alert alert-danger alert-permanent mb-4" role="alert">
<ul class="mb-0">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
@@ -64,7 +64,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -118,7 +118,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
@@ -133,7 +133,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Job.JobDto
@model PowderCoating.Application.DTOs.Job.JobDto
@{
var emailDefault = ViewBag.EmailDefaultOnComplete == true;
var preLoggedPowder = ViewBag.PreLoggedPowder as Dictionary<int, decimal> ?? new Dictionary<int, decimal>();
@@ -97,7 +97,7 @@
@if (preFilledLbs > 0)
{
<small class="text-success d-block mt-1">
<i class="bi bi-check-circle me-1"></i>Already logged inventory adjusted
<i class="bi bi-check-circle me-1"></i>Already logged — inventory adjusted
</small>
}
</td>
@@ -111,7 +111,7 @@
<td colspan="5">
<small class="text-muted fst-italic">
<i class="bi bi-info-circle me-1"></i>
@item.Description No coat information available (legacy job item)
@item.Description — No coat information available (legacy job item)
</small>
</td>
</tr>
@@ -120,9 +120,9 @@
</tbody>
</table>
</div>
<div class="alert alert-info mb-0">
<div class="alert alert-info alert-permanent mb-0">
<i class="bi bi-info-circle me-2"></i>
<small>Pre-filled values were already logged via scan inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
<small>Pre-filled values were already logged via scan — inventory is already adjusted for those. You can edit the amount; only the difference will be applied to inventory.</small>
</div>
</div>
}
@@ -1,4 +1,4 @@
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
@using PowderCoating.Application.DTOs.Job
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Enums
@@ -51,7 +51,7 @@
</div>
</div>
@* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@
@* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@
@if (overdueJobs.Any())
{
<div class="card border-danger mb-4">
@@ -59,7 +59,7 @@
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Carried Over Not Yet Completed</strong>
<strong>Carried Over — Not Yet Completed</strong>
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Carried Over Jobs"
@@ -170,7 +170,7 @@
@if (!Model.Any())
{
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle me-2"></i>
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
</div>
@@ -185,7 +185,7 @@
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Reorder &amp; Quick Edit"
data-bs-content="Drag the grip on the left of any row to change processing order saved automatically. Click a Priority badge to change priority. Click the Worker cell to reassign. Click Scheduled Date or Due Date to update dates inline without navigating away.">
data-bs-content="Drag the ⠿ grip on the left of any row to change processing order — saved automatically. Click a Priority badge to change priority. Click the Worker cell to reassign. Click Scheduled Date or Due Date to update dates inline without navigating away.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -320,7 +320,7 @@
</div>
}
@* ── Scheduled Maintenance for the Day ──────────────────────────────── *@
@* ── Scheduled Maintenance for the Day ──────────────────────────────── *@
<div class="mt-4">
<div class="card">
<div class="card-header bg-warning text-dark">
@@ -381,7 +381,7 @@
<tr onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'"
style="cursor: pointer;">
<td>
<strong>@(item.Equipment?.EquipmentName ?? "")</strong>
<strong>@(item.Equipment?.EquipmentName ?? "—")</strong>
@if (!string.IsNullOrEmpty(item.Equipment?.Location))
{
<br /><small class="text-muted"><i class="bi bi-geo-alt me-1"></i>@item.Equipment.Location</small>
@@ -930,14 +930,14 @@
</script>
<script>
// SignalR real-time status updates from shop display
// SignalR — real-time status updates from shop display
(function () {
const connection = new signalR.HubConnectionBuilder()
.withUrl('/hubs/shop')
.withAutomaticReconnect()
.build();
// A worker advanced a job status on the shop display update badge in place
// A worker advanced a job status on the shop display — update badge in place
connection.on('JobStatusChanged', function (data) {
const badge = document.getElementById(`status-badge-${data.jobId}`);
if (badge) {
@@ -946,7 +946,7 @@
}
});
// Order/priority/worker changed by another Daily Board session reload
// Order/priority/worker changed by another Daily Board session — reload
connection.on('DailyBoardUpdated', function () {
// Only reload if this tab didn't trigger it (we can't easily tell, so just reload)
location.reload();
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Maintenance.CreateMaintenanceDto
@model PowderCoating.Application.DTOs.Maintenance.CreateMaintenanceDto
@{
ViewData["Title"] = "Schedule Maintenance";
@@ -30,7 +30,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Basic Information"
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical Critical and High priorities are highlighted in the list view and on the equipment status banner.">
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -211,7 +211,7 @@
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
</div>
</div>
<div class="alert alert-info mt-3 small mb-0">
<div class="alert alert-info alert-permanent mt-3 small mb-0">
<i class="bi bi-info-circle me-1"></i>
If no end date is set, occurrences will be generated for:
Daily = 90 days &bull; Weekly / Bi-Weekly = 1 year &bull; Monthly = 2 years &bull; Quarterly / Annually = 3 years &bull; Bi-Annually = 18 months.
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
@{
ViewData["Title"] = "Delete Maintenance Record";
@@ -14,7 +14,7 @@
</a>
</div>
<div class="alert alert-danger" role="alert">
<div class="alert alert-danger alert-permanent" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action cannot be undone.
</div>
@@ -22,7 +22,7 @@
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-danger bg-opacity-10 border-0 py-3">
<h5 class="mb-0 text-danger">
<i class="bi bi-wrench me-2"></i>@Model.MaintenanceType @Model.EquipmentName
<i class="bi bi-wrench me-2"></i>@Model.MaintenanceType — @Model.EquipmentName
</h5>
</div>
<div class="card-body">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Maintenance.UpdateMaintenanceDto
@model PowderCoating.Application.DTOs.Maintenance.UpdateMaintenanceDto
@{
ViewData["Title"] = "Edit Maintenance";
@@ -31,7 +31,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Basic Information"
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical Critical and High priorities are highlighted in the list view and on the equipment status banner.">
data-bs-content="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -188,7 +188,7 @@
</div>
@if (ViewBag.IsRecurringSeries as bool? == true)
{
<div class="alert alert-warning mb-3 small">
<div class="alert alert-warning alert-permanent mb-3 small">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>This is part of a recurring series.</strong>
Changing recurrence settings will delete all future scheduled/overdue occurrences and regenerate them from this record's date.
@@ -221,7 +221,7 @@
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
</div>
</div>
<div class="alert alert-info mt-3 small mb-0">
<div class="alert alert-info alert-permanent mt-3 small mb-0">
<i class="bi bi-info-circle me-1"></i>
If no end date is set, occurrences will be generated for:
Daily = 90 days &bull; Weekly / Bi-Weekly = 1 year &bull; Monthly = 2 years &bull; Quarterly / Annually = 3 years &bull; Bi-Annually = 18 months.
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@model ManufacturerLookupPattern
@{
ViewData["Title"] = "Add Manufacturer Pattern";
@@ -19,7 +19,7 @@
<form asp-action="Create" method="post">
@Html.AntiForgeryToken()
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
<div class="mb-3">
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
@@ -39,9 +39,9 @@
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
<div class="form-text">
Supported placeholders:
<code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> color name transformed by Slug Transform below,
<code>{colorCode}</code> color code as-is.
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is.
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
</div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -50,10 +50,10 @@
<div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen e.g. "Jet-Black"</option>
<option value="AsIs">AsIs color name unchanged</option>
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
<option value="AsIs">AsIs — color name unchanged</option>
</select>
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@model ManufacturerLookupPattern
@{
ViewData["Title"] = "Edit Pattern";
@@ -20,7 +20,7 @@
@Html.AntiForgeryToken()
<input asp-for="Id" type="hidden" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
<div class="mb-3">
<label asp-for="ManufacturerName" class="form-label fw-medium">Manufacturer Name <span class="text-danger">*</span></label>
@@ -40,9 +40,9 @@
<input asp-for="ProductUrlTemplate" class="form-control" placeholder="e.g. https://www.prismaticpowders.com/shop/powder-coating-colors/{partNumber}/{slug}" />
<div class="form-text">
Supported placeholders:
<code>{partNumber}</code> manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> color name transformed by Slug Transform below,
<code>{colorCode}</code> color code as-is.
<code>{partNumber}</code> — manufacturer part number (slashes normalized to hyphens),
<code>{slug}</code> — color name transformed by Slug Transform below,
<code>{colorCode}</code> — color code as-is.
If a required placeholder is missing at runtime the template is skipped and the system falls back to a search URL.
</div>
<span asp-validation-for="ProductUrlTemplate" class="text-danger small"></span>
@@ -51,10 +51,10 @@
<div class="mb-3">
<label asp-for="SlugTransform" class="form-label fw-medium">Slug Transform</label>
<select asp-for="SlugTransform" class="form-select">
<option value="LowerHyphen">LowerHyphen e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen e.g. "Jet-Black"</option>
<option value="AsIs">AsIs color name unchanged</option>
<option value="LowerHyphen">LowerHyphen — e.g. "jet-black"</option>
<option value="LowerUnderscore">LowerUnderscore — e.g. "jet_black"</option>
<option value="TitleHyphen">TitleHyphen — e.g. "Jet-Black"</option>
<option value="AsIs">AsIs — color name unchanged</option>
</select>
<div class="form-text">How the color name is converted to a URL slug when building the product URL.</div>
<span asp-validation-for="SlugTransform" class="text-danger small"></span>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Scheduling.OvenSchedulerViewModel
@model PowderCoating.Application.DTOs.Scheduling.OvenSchedulerViewModel
@{
ViewData["Title"] = "Oven Scheduler";
var dateStr = Model.ScheduledDate.ToString("yyyy-MM-dd");
@@ -8,7 +8,7 @@
@section Styles {
<style>
/* ─── Layout ─────────────────────────────────────── */
/* ─── Layout ─────────────────────────────────────── */
.scheduler-layout {
display: flex;
gap: 1rem;
@@ -39,7 +39,7 @@
flex-shrink: 0;
}
/* ─── Batch cards ────────────────────────────────── */
/* ─── Batch cards ────────────────────────────────── */
.batch-card {
border-radius: 10px;
border: 2px solid transparent;
@@ -52,14 +52,14 @@
.batch-card.status-completed { border-color: #198754; opacity: .85; }
.batch-card.drag-over { border-color: #0d6efd !important; box-shadow: 0 0 12px rgba(13,110,253,.4) !important; }
/* ─── Capacity bar ───────────────────────────────── */
/* ─── Capacity bar ───────────────────────────────── */
.capacity-bar-wrap { height: 6px; border-radius: 3px; background: #e9ecef; overflow: hidden; }
.capacity-bar-fill { height: 100%; border-radius: 3px; transition: width .3s; }
.cap-ok { background: #198754; }
.cap-warn { background: #ffc107; }
.cap-over { background: #dc3545; }
/* ─── Queue items ────────────────────────────────── */
/* ─── Queue items ────────────────────────────────── */
.queue-job-card {
border-radius: 8px;
cursor: grab;
@@ -90,7 +90,7 @@
.batch-item-row:hover { background: var(--bs-secondary-bg); }
.batch-item-row.dragging { opacity: .5; }
/* ─── AI panel ───────────────────────────────────── */
/* ─── AI panel ───────────────────────────────────── */
.ai-suggestion-panel {
position: fixed;
top: 0; right: 0;
@@ -115,7 +115,7 @@
}
.ai-panel-backdrop.open { display: block; }
/* ─── Drop zone hint ─────────────────────────────── */
/* ─── Drop zone hint ─────────────────────────────── */
.drop-zone-empty {
min-height: 80px;
border: 2px dashed var(--bs-border-color);
@@ -275,7 +275,7 @@
<p class="small text-muted mb-2">
The Oven Scheduler helps you plan which jobs go into which oven on a given day.
Jobs waiting to be coated appear in the <strong>Queue</strong> on the left.
Each oven gets its own column create <strong>batches</strong> and drag jobs into them
Each oven gets its own column — create <strong>batches</strong> and drag jobs into them
to build your day's run sheet.
</p>
<p class="small text-muted mb-0">
@@ -292,8 +292,8 @@
<li class="mb-1">Use the <strong>date arrows</strong> to navigate to the day you want to schedule.</li>
<li class="mb-1">Click <strong>New Batch</strong> and pick an oven to create a batch slot.</li>
<li class="mb-1"><strong>Drag job coat items</strong> from the Queue into a batch, or drag between batches to reorder.</li>
<li class="mb-1">Set a <strong>start time</strong> on each batch and monitor the capacity bar it turns yellow at 80 % and red when over capacity.</li>
<li class="mb-1">Use the batch <strong>status buttons</strong> (Planned Loading In Progress Completed) to track real-time progress.</li>
<li class="mb-1">Set a <strong>start time</strong> on each batch and monitor the capacity bar — it turns yellow at 80 % and red when over capacity.</li>
<li class="mb-1">Use the batch <strong>status buttons</strong> (Planned → Loading → In Progress → Completed) to track real-time progress.</li>
<li class="mb-0">Drag an item back to the Queue to unschedule it.</li>
</ol>
</div>
@@ -303,23 +303,23 @@
<h6 class="fw-semibold mb-2"><i class="bi bi-gear-fill text-warning me-1"></i>Setup for best results</h6>
<ul class="small text-muted mb-0 ps-3">
<li class="mb-1">
<strong>Ovens in Equipment</strong> each oven must exist as an Equipment record
<strong>Ovens in Equipment</strong> — each oven must exist as an Equipment record
with <em>Type = Oven</em> and Status = Operational for it to appear as a column.
</li>
<li class="mb-1">
<strong>Oven capacity</strong> set a <em>capacity (sq ft)</em> on each oven so
<strong>Oven capacity</strong> — set a <em>capacity (sq ft)</em> on each oven so
the capacity bar can warn you before you overload a batch.
</li>
<li class="mb-1">
<strong>Job items with surface area</strong> enter <em>surface area (sq ft)</em>
<strong>Job items with surface area</strong> — enter <em>surface area (sq ft)</em>
on each job item so the scheduler can calculate load accurately.
</li>
<li class="mb-1">
<strong>Coat quantities</strong> job items need at least one coat defined
<strong>Coat quantities</strong> — job items need at least one coat defined
(powder color + quantity) so they appear as draggable coat rows in the Queue.
</li>
<li class="mb-0">
<strong>Due dates &amp; priorities</strong> set due dates and priorities on
<strong>Due dates &amp; priorities</strong> — set due dates and priorities on
jobs so the AI and sorting tools can recommend the most urgent work first.
</li>
</ul>
@@ -332,7 +332,7 @@
<!-- No ovens warning -->
@if (!Model.Ovens.Any())
{
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle me-2"></i>
No active ovens found. Go to <a asp-controller="Equipment" asp-action="Index">Equipment</a> and add a piece of equipment with Type = "Oven".
</div>
@@ -341,7 +341,7 @@
<!-- Main layout -->
<div class="scheduler-layout">
<!-- ── JOB QUEUE (left sidebar) ───────────────── -->
<!-- ── JOB QUEUE (left sidebar) ───────────────── -->
<div class="scheduler-queue">
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center py-2">
@@ -408,11 +408,11 @@
<span class="color-dot" style="background:@GetColorHex(coat.ColorName, coat.ColorCode)"></span>
}
<span class="fw-medium">@coat.CoatName</span>
<span class="text-muted ms-1"> @coat.ItemDescription</span>
<span class="text-muted ms-1">— @coat.ItemDescription</span>
</div>
<div class="text-muted" style="font-size:.75rem;">
Pass @coat.CoatPassNumber · @coat.SurfaceAreaSqFt.ToString("F1") sqft
@if (!string.IsNullOrEmpty(coat.ColorName)) { <span>· @coat.ColorName</span> }
Pass @coat.CoatPassNumber · @coat.SurfaceAreaSqFt.ToString("F1") sqft
@if (!string.IsNullOrEmpty(coat.ColorName)) { <span>· @coat.ColorName</span> }
</div>
</div>
<i class="bi bi-grip-vertical text-muted ms-1"></i>
@@ -426,7 +426,7 @@
</div>
</div>
<!-- ── OVEN COLUMNS ───────────────────────────── -->
<!-- ── OVEN COLUMNS ───────────────────────────── -->
<div class="scheduler-board">
<div class="oven-columns" id="ovenColumns">
@foreach (var oven in Model.Ovens)
@@ -447,7 +447,7 @@
</div>
<div class="small text-muted">
@(oven.MaxLoadSqFt.HasValue ? $"{oven.MaxLoadSqFt:F0} sqft max" : "No capacity set")
· @oven.CycleMinutes min cycle
· @oven.CycleMinutes min cycle
</div>
</div>
<button class="btn btn-sm btn-outline-primary ms-auto"
@@ -503,7 +503,7 @@
<!-- Error state -->
<div id="aiError" class="d-none p-4">
<div class="alert alert-danger mb-0">
<div class="alert alert-danger alert-permanent mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<span id="aiErrorText"></span>
</div>
@@ -546,8 +546,8 @@
</ul>
@if (!Model.QueuedJobs.Any())
{
<div class="alert alert-info small">
<i class="bi bi-info-circle me-1"></i>The queue is empty no jobs need oven scheduling right now.
<div class="alert alert-info alert-permanent small">
<i class="bi bi-info-circle me-1"></i>The queue is empty — no jobs need oven scheduling right now.
</div>
}
else
@@ -561,7 +561,7 @@
@section Scripts {
<script>
// ── Info panel (always visible on load; dismiss hides for this visit only)
// ── Info panel (always visible on load; dismiss hides for this visit only) ─
document.getElementById('btnDismissInfo').addEventListener('click', function () {
document.getElementById('schedulerInfoPanel').style.display = 'none';
});
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Subscription
@using PowderCoating.Application.DTOs.Subscription
@model UpdateSubscriptionPlanConfigDto
@{
ViewData["Title"] = $"Edit {ViewBag.PlanName} Plan";
@@ -179,12 +179,12 @@
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
<div class="alert alert-info small mb-3" role="alert">
<div class="alert alert-info alert-permanent small mb-3" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>Where to find price IDs:</strong>
In your <a href="https://dashboard.stripe.com/products" target="_blank" class="alert-link">Stripe Dashboard</a>,
open the product, then look in the <strong>Pricing</strong> section for the specific price.
The ID starts with <code>price_</code> <em>not</em> the product ID which starts with <code>prod_</code>.
The ID starts with <code>price_</code> — <em>not</em> the product ID which starts with <code>prod_</code>.
</div>
<div class="row g-3 mb-4">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.User.CreateSuperAdminDto
@model PowderCoating.Application.DTOs.User.CreateSuperAdminDto
@{
ViewData["Title"] = "Create SuperAdmin";
}
@@ -13,7 +13,7 @@
</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle"></i>
<strong>Important:</strong> SuperAdmins have full platform access across all companies. Use this feature carefully.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
@{
ViewData["Title"] = "User Details";
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
@@ -132,7 +132,7 @@
@if (Model.IsBanned)
{
<hr />
<div class="alert alert-danger mb-0">
<div class="alert alert-danger alert-permanent mb-0">
<i class="bi bi-slash-circle-fill me-2"></i>
<strong>This account is banned.</strong>
@if (Model.BannedAt.HasValue)
@@ -312,7 +312,7 @@
asp-route-returnUrl="@detailsReturnUrl" method="post">
@Html.AntiForgeryToken()
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Banning <strong>@Model.FullName</strong> will immediately prevent them from logging in.
</div>
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Grant SuperAdmin Access";
ViewData["PageIcon"] = "bi-shield-plus";
}
@@ -10,7 +10,7 @@
</a>
</div>
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<i class="bi bi-shield-exclamation me-2"></i>
<strong>Caution:</strong> SuperAdmins have full platform access across <em>all companies</em>. Only promote users you trust completely with platform-level administration.
</div>
@@ -91,7 +91,7 @@
@Html.AntiForgeryToken()
<input type="hidden" name="userId" id="grantUserId" />
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> This user will gain full platform access across all companies.
</div>
@@ -1,4 +1,4 @@
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
@section Styles {
<style>
@@ -188,7 +188,7 @@
}
else
{
<span class="btn btn-outline-secondary disabled" title="Root account protected">
<span class="btn btn-outline-secondary disabled" title="Root account — protected">
<i class="bi bi-shield-lock"></i>
</span>
}
@@ -234,7 +234,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var user in Model.Items)
@@ -295,7 +295,7 @@
</div>
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View </span>
<span class="btn btn-sm btn-outline-primary">View →</span>
</div>
</a>
}
@@ -373,7 +373,7 @@
@Html.AntiForgeryToken()
<input type="hidden" name="id" id="revokeUserId" />
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> This will remove platform-wide access. The user will be demoted to Company Admin.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel
@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel
@{
var enableAiLookup = ViewData["EnableAiLookup"] as bool? == true;
@@ -27,7 +27,7 @@
</div>
}
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3 small"></div>
<div asp-validation-summary="ModelOnly" class="alert alert-danger alert-permanent mb-3 small"></div>
<div class="row g-3">
<div class="col-md-6">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@model PowderCoating.Application.DTOs.Powder.PowderInsightsDashboardDto
@{
ViewData["Title"] = "Powder Insights";
ViewData["PageIcon"] = "bi-graph-up";
@@ -11,7 +11,7 @@
</a>
</div>
@* ── KPI cards ── *@
@* ── KPI cards ── *@
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
@@ -49,7 +49,7 @@
</div>
</div>
@* ── Tabs ── *@
@* ── Tabs ── *@
<ul class="nav nav-tabs mb-3" id="insightsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="forecast-tab" data-bs-toggle="tab" data-bs-target="#forecast" type="button">
@@ -74,7 +74,7 @@
<div class="tab-content">
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
@* ── Tab 1: Stock Forecast (Layer 2, immediate value) ── *@
<div class="tab-pane fade show active" id="forecast" role="tabpanel">
@if (!Model.LowStockAlerts.Any())
{
@@ -87,7 +87,7 @@
{
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock Active Jobs</h6>
<h6 class="mb-0"><i class="bi bi-box-seam me-2"></i>Powder Demand vs. Stock — Active Jobs</h6>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
@@ -115,7 +115,7 @@
<td class="text-end fw-semibold">@item.CurrentStockLbs.ToString("0.##") lbs</td>
<td class="text-end">@item.ScheduledDemandLbs.ToString("0.##") lbs</td>
<td class="text-end @(item.ShortfallLbs > 0 ? "text-danger fw-bold" : "text-muted")">
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "")
@(item.ShortfallLbs > 0 ? $"{item.ShortfallLbs:0.##} lbs" : "—")
</td>
<td class="text-center">@item.ActiveJobCount</td>
<td class="text-center">
@@ -141,7 +141,7 @@
}
</div>
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
@* ── Tab 2: Coverage Efficiency (Layer 2) ── *@
<div class="tab-pane fade" id="efficiency" role="tabpanel">
@if (!readiness.IsLayer2Ready)
{
@@ -157,7 +157,7 @@
else if (!Model.EfficiencyBySku.Any())
{
<div class="text-center py-5 text-muted">
<p>No efficiency data yet record actual powder usage on completed jobs to see this chart.</p>
<p>No efficiency data yet — record actual powder usage on completed jobs to see this chart.</p>
</div>
}
else
@@ -188,11 +188,11 @@
<strong>@eff.Name</strong>
@if (!string.IsNullOrEmpty(eff.ColorName))
{
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
<br /><small class="text-muted">@eff.ColorName · @eff.Manufacturer</small>
}
@if (!eff.HasEnoughData)
{
<br /><small class="text-muted fst-italic">Low confidence need 5+ samples</small>
<br /><small class="text-muted fst-italic">Low confidence — need 5+ samples</small>
}
</td>
<td class="text-end">@eff.CatalogCoverageSqFtPerLb.ToString("0.#") sq ft/lb</td>
@@ -214,7 +214,7 @@
}
</div>
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
@* ── Tab 3: Predictive (Layer 3, gated) ── *@
<div class="tab-pane fade" id="predictive" role="tabpanel">
@if (!readiness.IsLayer3Ready)
{
@@ -235,12 +235,12 @@
<strong>@readiness.JobsWithActualData</strong> of <strong>@readiness.Layer3MinJobs</strong> jobs recorded
(@readiness.Layer3ProgressPercent%)
</p>
<div class="alert alert-info text-start">
<div class="alert alert-info alert-permanent text-start">
<h6 class="alert-heading"><i class="bi bi-lightbulb me-2"></i>What unlocks here</h6>
<ul class="mb-0 small">
<li><strong>Smart reorder suggestions</strong> quantity recommendations based on your actual usage history + scheduled job pipeline</li>
<li><strong>Waste pattern detection</strong> identifies jobs and powder types that consistently over-consume</li>
<li><strong>Per-powder efficiency corrections</strong> suggests updating coverage defaults based on real data</li>
<li><strong>Smart reorder suggestions</strong> — quantity recommendations based on your actual usage history + scheduled job pipeline</li>
<li><strong>Waste pattern detection</strong> — identifies jobs and powder types that consistently over-consume</li>
<li><strong>Per-powder efficiency corrections</strong> — suggests updating coverage defaults based on real data</li>
</ul>
</div>
</div>
@@ -258,7 +258,7 @@
</div>
@if (!Model.ReorderSuggestions.Any())
{
<div class="card-body text-muted text-center py-4">No reorder suggestions stock levels look good for upcoming pipeline.</div>
<div class="card-body text-muted text-center py-4">No reorder suggestions — stock levels look good for upcoming pipeline.</div>
}
else
{
@@ -284,7 +284,7 @@
<strong>@s.Name</strong>
@if (!string.IsNullOrEmpty(s.ColorName))
{
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
<br /><small class="text-muted">@s.ColorName · @s.Manufacturer</small>
}
</td>
<td class="text-end">@s.CurrentStockLbs.ToString("0.#") lbs</td>
@@ -311,7 +311,7 @@
@* Waste Patterns *@
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent">
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal"> coats that used &gt;20% more than estimated</small></h6>
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2 text-warning"></i>Waste Patterns <small class="text-muted fw-normal">— coats that used &gt;20% more than estimated</small></h6>
</div>
@if (!Model.WastePatterns.Any())
{
@@ -344,7 +344,7 @@
<br /><small class="text-muted">@w.CoatName</small>
</td>
<td class="text-muted small">@(w.InventoryItemName ?? "Custom")</td>
<td>@(w.Complexity ?? "")</td>
<td>@(w.Complexity ?? "—")</td>
<td class="text-end">@w.EstimatedLbs.ToString("0.##") lbs</td>
<td class="text-end">@w.ActualLbs.ToString("0.##") lbs</td>
<td class="text-end text-danger fw-bold">+@w.OveragePct.ToString("0.#")%</td>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.User.UserProfileDto
@model PowderCoating.Application.DTOs.User.UserProfileDto
@{
ViewData["Title"] = "My Profile";
@@ -85,7 +85,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Personal Information"
data-bs-content="First Name, Last Name, and Phone are editable and saved when you click Save Profile. Email is shown here for reference change it on the Security tab. Department, Position, Role, and Employee Number are set by an administrator and cannot be changed here.">
data-bs-content="First Name, Last Name, and Phone are editable and saved when you click Save Profile. Email is shown here for reference — change it on the Security tab. Department, Position, Role, and Employee Number are set by an administrator and cannot be changed here.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -213,7 +213,7 @@
<input type="password" class="form-control" id="confirmPassword" name="ConfirmPassword" required />
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<div class="alert alert-info alert-permanent mt-3 mb-0">
<i class="bi bi-info-circle me-2"></i>
<strong>Password requirements:</strong> At least 8 characters with uppercase, lowercase, and a digit.
</div>
@@ -290,7 +290,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Appearance Settings"
data-bs-content="Theme switches the app between Light and Dark mode. Sidebar Color changes the navigation panel background click a swatch to preview it instantly. Date Format controls how dates display throughout the app. Timezone is used to localize timestamps. Click Save Appearance to persist your choices.">
data-bs-content="Theme switches the app between Light and Dark mode. Sidebar Color changes the navigation panel background — click a swatch to preview it instantly. Date Format controls how dates display throughout the app. Timezone is used to localize timestamps. Click Save Appearance to persist your choices.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -364,39 +364,39 @@
<label class="form-label fw-semibold">Timezone</label>
<select class="form-select" id="timezoneInput" name="TimeZone" style="max-width:350px;">
<optgroup label="United States">
<option value="America/New_York" selected="@(Model.TimeZone == "America/New_York" ? "selected" : null)">Eastern (ET) New York</option>
<option value="America/Chicago" selected="@(Model.TimeZone == "America/Chicago" ? "selected" : null)">Central (CT) Chicago</option>
<option value="America/Denver" selected="@(Model.TimeZone == "America/Denver" ? "selected" : null)">Mountain (MT) Denver</option>
<option value="America/Phoenix" selected="@(Model.TimeZone == "America/Phoenix" ? "selected" : null)">Mountain no-DST Phoenix</option>
<option value="America/Los_Angeles" selected="@(Model.TimeZone == "America/Los_Angeles" ? "selected" : null)">Pacific (PT) Los Angeles</option>
<option value="America/Anchorage" selected="@(Model.TimeZone == "America/Anchorage" ? "selected" : null)">Alaska (AKT) Anchorage</option>
<option value="Pacific/Honolulu" selected="@(Model.TimeZone == "Pacific/Honolulu" ? "selected" : null)">Hawaii (HT) Honolulu</option>
<option value="America/New_York" selected="@(Model.TimeZone == "America/New_York" ? "selected" : null)">Eastern (ET) — New York</option>
<option value="America/Chicago" selected="@(Model.TimeZone == "America/Chicago" ? "selected" : null)">Central (CT) — Chicago</option>
<option value="America/Denver" selected="@(Model.TimeZone == "America/Denver" ? "selected" : null)">Mountain (MT) — Denver</option>
<option value="America/Phoenix" selected="@(Model.TimeZone == "America/Phoenix" ? "selected" : null)">Mountain no-DST — Phoenix</option>
<option value="America/Los_Angeles" selected="@(Model.TimeZone == "America/Los_Angeles" ? "selected" : null)">Pacific (PT) — Los Angeles</option>
<option value="America/Anchorage" selected="@(Model.TimeZone == "America/Anchorage" ? "selected" : null)">Alaska (AKT) — Anchorage</option>
<option value="Pacific/Honolulu" selected="@(Model.TimeZone == "Pacific/Honolulu" ? "selected" : null)">Hawaii (HT) — Honolulu</option>
</optgroup>
<optgroup label="Canada">
<option value="America/Halifax" selected="@(Model.TimeZone == "America/Halifax" ? "selected" : null)">Atlantic (AT) Halifax</option>
<option value="America/Toronto" selected="@(Model.TimeZone == "America/Toronto" ? "selected" : null)">Eastern Toronto</option>
<option value="America/Winnipeg" selected="@(Model.TimeZone == "America/Winnipeg" ? "selected" : null)">Central Winnipeg</option>
<option value="America/Edmonton" selected="@(Model.TimeZone == "America/Edmonton" ? "selected" : null)">Mountain Edmonton</option>
<option value="America/Vancouver" selected="@(Model.TimeZone == "America/Vancouver" ? "selected" : null)">Pacific Vancouver</option>
<option value="America/Halifax" selected="@(Model.TimeZone == "America/Halifax" ? "selected" : null)">Atlantic (AT) — Halifax</option>
<option value="America/Toronto" selected="@(Model.TimeZone == "America/Toronto" ? "selected" : null)">Eastern — Toronto</option>
<option value="America/Winnipeg" selected="@(Model.TimeZone == "America/Winnipeg" ? "selected" : null)">Central — Winnipeg</option>
<option value="America/Edmonton" selected="@(Model.TimeZone == "America/Edmonton" ? "selected" : null)">Mountain — Edmonton</option>
<option value="America/Vancouver" selected="@(Model.TimeZone == "America/Vancouver" ? "selected" : null)">Pacific — Vancouver</option>
</optgroup>
<optgroup label="Europe">
<option value="Europe/London" selected="@(Model.TimeZone == "Europe/London" ? "selected" : null)">GMT/BST London</option>
<option value="Europe/Paris" selected="@(Model.TimeZone == "Europe/Paris" ? "selected" : null)">CET Paris / Berlin</option>
<option value="Europe/Helsinki" selected="@(Model.TimeZone == "Europe/Helsinki" ? "selected" : null)">EET Helsinki</option>
<option value="Europe/Moscow" selected="@(Model.TimeZone == "Europe/Moscow" ? "selected" : null)">MSK Moscow</option>
<option value="Europe/London" selected="@(Model.TimeZone == "Europe/London" ? "selected" : null)">GMT/BST — London</option>
<option value="Europe/Paris" selected="@(Model.TimeZone == "Europe/Paris" ? "selected" : null)">CET — Paris / Berlin</option>
<option value="Europe/Helsinki" selected="@(Model.TimeZone == "Europe/Helsinki" ? "selected" : null)">EET — Helsinki</option>
<option value="Europe/Moscow" selected="@(Model.TimeZone == "Europe/Moscow" ? "selected" : null)">MSK — Moscow</option>
</optgroup>
<optgroup label="Asia / Pacific">
<option value="Asia/Dubai" selected="@(Model.TimeZone == "Asia/Dubai" ? "selected" : null)">GST Dubai</option>
<option value="Asia/Kolkata" selected="@(Model.TimeZone == "Asia/Kolkata" ? "selected" : null)">IST India</option>
<option value="Asia/Bangkok" selected="@(Model.TimeZone == "Asia/Bangkok" ? "selected" : null)">ICT Bangkok</option>
<option value="Asia/Shanghai" selected="@(Model.TimeZone == "Asia/Shanghai" ? "selected" : null)">CST Beijing / Shanghai</option>
<option value="Asia/Tokyo" selected="@(Model.TimeZone == "Asia/Tokyo" ? "selected" : null)">JST Tokyo</option>
<option value="Australia/Sydney" selected="@(Model.TimeZone == "Australia/Sydney" ? "selected" : null)">AEST Sydney</option>
<option value="Pacific/Auckland" selected="@(Model.TimeZone == "Pacific/Auckland" ? "selected" : null)">NZST Auckland</option>
<option value="Asia/Dubai" selected="@(Model.TimeZone == "Asia/Dubai" ? "selected" : null)">GST — Dubai</option>
<option value="Asia/Kolkata" selected="@(Model.TimeZone == "Asia/Kolkata" ? "selected" : null)">IST — India</option>
<option value="Asia/Bangkok" selected="@(Model.TimeZone == "Asia/Bangkok" ? "selected" : null)">ICT — Bangkok</option>
<option value="Asia/Shanghai" selected="@(Model.TimeZone == "Asia/Shanghai" ? "selected" : null)">CST — Beijing / Shanghai</option>
<option value="Asia/Tokyo" selected="@(Model.TimeZone == "Asia/Tokyo" ? "selected" : null)">JST — Tokyo</option>
<option value="Australia/Sydney" selected="@(Model.TimeZone == "Australia/Sydney" ? "selected" : null)">AEST — Sydney</option>
<option value="Pacific/Auckland" selected="@(Model.TimeZone == "Pacific/Auckland" ? "selected" : null)">NZST — Auckland</option>
</optgroup>
<optgroup label="South America">
<option value="America/Sao_Paulo" selected="@(Model.TimeZone == "America/Sao_Paulo" ? "selected" : null)">BRT — São Paulo</option>
<option value="America/Buenos_Aires" selected="@(Model.TimeZone == "America/Buenos_Aires" ? "selected" : null)">ART Buenos Aires</option>
<option value="America/Sao_Paulo" selected="@(Model.TimeZone == "America/Sao_Paulo" ? "selected" : null)">BRT — São Paulo</option>
<option value="America/Buenos_Aires" selected="@(Model.TimeZone == "America/Buenos_Aires" ? "selected" : null)">ART — Buenos Aires</option>
</optgroup>
<optgroup label="UTC">
<option value="UTC" selected="@(Model.TimeZone == "UTC" ? "selected" : null)">UTC</option>
@@ -538,7 +538,7 @@
});
});
// Theme radio map light/dark paper/ink surface system
// Theme radio — map light/dark → paper/ink surface system
document.querySelectorAll('input[name="theme"]').forEach(radio => {
radio.addEventListener('change', function () {
var surface = this.value === 'dark' ? 'ink' : 'paper';
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.PurchaseOrder
@using PowderCoating.Application.DTOs.PurchaseOrder
@model CreatePurchaseOrderDto
@{
@@ -25,7 +25,7 @@
<h4 class="mb-0">
@if (fromLowStock)
{
<span><i class="bi bi-lightning-charge text-warning me-1"></i> New PO From Low Stock</span>
<span><i class="bi bi-lightning-charge text-warning me-1"></i> New PO — From Low Stock</span>
}
else
{
@@ -35,7 +35,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="New Purchase Order"
data-bs-content="POs are saved as Draft and can be edited before submitting. Add line items from your inventory catalog or enter a custom description for items not in the system. Once submitted you can receive goods against the PO receiving automatically updates inventory quantities. After receiving, use Create Bill to convert the PO into a payable bill.">
data-bs-content="POs are saved as Draft and can be edited before submitting. Add line items from your inventory catalog or enter a custom description for items not in the system. Once submitted you can receive goods against the PO — receiving automatically updates inventory quantities. After receiving, use Create Bill to convert the PO into a payable bill.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -46,7 +46,7 @@
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<ul class="mb-0">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
@@ -70,7 +70,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Order Details"
data-bs-content="Select the vendor you're ordering from. Order Date defaults to today adjust if you're entering a past order. Expected Delivery is used to flag the PO as Overdue on the list view if goods haven't arrived by that date.">
data-bs-content="Select the vendor you're ordering from. Order Date defaults to today — adjust if you're entering a past order. Expected Delivery is used to flag the PO as Overdue on the list view if goods haven't arrived by that date.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -80,7 +80,7 @@
<label asp-for="VendorId" class="form-label fw-semibold">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="__new__">+ Add New Vendor</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
<span asp-validation-for="VendorId" class="text-danger small"></span>
</div>
@@ -147,12 +147,12 @@
<div class="col-md-6">
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
<textarea asp-for="Notes" class="form-control" rows="3"
placeholder="Visible on PO"></textarea>
placeholder="Visible on PO…"></textarea>
</div>
<div class="col-md-6">
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
<textarea asp-for="InternalNotes" class="form-control" rows="3"
placeholder="Internal use only"></textarea>
placeholder="Internal use only…"></textarea>
</div>
</div>
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.PurchaseOrder
@using PowderCoating.Application.DTOs.PurchaseOrder
@model UpdatePurchaseOrderDto
@{
@@ -27,14 +27,14 @@
</a>
</div>
<div class="alert alert-info d-flex align-items-center gap-2 mb-3">
<div class="alert alert-info alert-permanent d-flex align-items-center gap-2 mb-3">
<i class="bi bi-info-circle-fill"></i>
<span>Only <strong>Draft</strong> purchase orders can be edited.</span>
</div>
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<ul class="mb-0">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
@@ -57,7 +57,7 @@
<label asp-for="VendorId" class="form-label fw-semibold">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="__new__">+ Add New Vendor</option>
<option value="__new__">+ Add New Vendor…</option>
</select>
<span asp-validation-for="VendorId" class="text-danger small"></span>
</div>
@@ -114,12 +114,12 @@
<div class="col-md-6">
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
<textarea asp-for="Notes" class="form-control" rows="3"
placeholder="Visible on PO"></textarea>
placeholder="Visible on PO…"></textarea>
</div>
<div class="col-md-6">
<label asp-for="InternalNotes" class="form-label fw-semibold">Internal Notes</label>
<textarea asp-for="InternalNotes" class="form-control" rows="3"
placeholder="Internal use only"></textarea>
placeholder="Internal use only…"></textarea>
</div>
</div>
</div>
@@ -1,8 +1,8 @@
@using PowderCoating.Application.DTOs.PurchaseOrder
@using PowderCoating.Application.DTOs.PurchaseOrder
@model ReceivePurchaseOrderDto
@{
ViewData["Title"] = $"Receive Goods {ViewBag.PoNumber}";
ViewData["Title"] = $"Receive Goods — {ViewBag.PoNumber}";
int poId = (int)ViewBag.PoId;
}
@@ -17,11 +17,11 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Receive Goods"
data-bs-content="Enter the quantity actually received for each line item. Use Receive All to fill in the full remaining quantity for every item. You can receive partial quantities the PO becomes Partially Received and you can come back to record the rest later. Saving automatically adds the received quantities to inventory on hand and records purchase transactions.">
data-bs-content="Enter the quantity actually received for each line item. Use Receive All to fill in the full remaining quantity for every item. You can receive partial quantities — the PO becomes Partially Received and you can come back to record the rest later. Saving automatically adds the received quantities to inventory on hand and records purchase transactions.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<small class="text-muted">@ViewBag.PoNumber · @ViewBag.VendorName · Ordered @((DateTime)ViewBag.OrderDate).ToString("MM/dd/yyyy")</small>
<small class="text-muted">@ViewBag.PoNumber · @ViewBag.VendorName · Ordered @((DateTime)ViewBag.OrderDate).ToString("MM/dd/yyyy")</small>
</div>
<a asp-action="Details" asp-route-id="@poId" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back
@@ -40,7 +40,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Items to Receive"
data-bs-content="Remaining = Ordered minus Previously Received. Enter how many units arrived in this shipment you can enter less than Remaining for a partial delivery. Rows already fully received are shown in green and cannot be edited. Only inventory catalog items will have their stock quantities updated on save.">
data-bs-content="Remaining = Ordered minus Previously Received. Enter how many units arrived in this shipment — you can enter less than Remaining for a partial delivery. Rows already fully received are shown in green and cannot be edited. Only inventory catalog items will have their stock quantities updated on save.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -108,7 +108,7 @@
else
{
<input type="hidden" name="Items[@i].QuantityToReceive" value="0" />
<span class="text-muted"></span>
<span class="text-muted">—</span>
}
</td>
</tr>
@@ -132,12 +132,12 @@
<div class="mb-3">
<label asp-for="Notes" class="form-label fw-semibold">Notes</label>
<textarea asp-for="Notes" class="form-control" rows="3"
placeholder="Any notes about this receipt"></textarea>
placeholder="Any notes about this receipt…"></textarea>
</div>
</div>
</div>
<div class="alert alert-info small">
<div class="alert alert-info alert-permanent small">
<i class="bi bi-info-circle me-1"></i>
Receiving goods will automatically update inventory quantities and record purchase transactions.
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
@{
Layout = "_QuoteApprovalLayout";
ViewData["Title"] = $"Quote {Model.QuoteNumber}";
@@ -33,7 +33,7 @@
@if (soonExpiry)
{
<div class="alert alert-warning mt-3 mb-0 py-2">
<div class="alert alert-warning alert-permanent mt-3 mb-0 py-2">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
This quote expires in @((int)daysUntilExpiry!.Value) day(s). Please respond promptly.
</div>
@@ -168,7 +168,7 @@
<!-- Decline error alert -->
@if (!string.IsNullOrWhiteSpace(Model.DeclineError))
{
<div class="alert alert-danger">
<div class="alert alert-danger alert-permanent">
<i class="bi bi-exclamation-circle me-1"></i>@Model.DeclineError
</div>
}
@@ -1,4 +1,4 @@
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
@model PowderCoating.Web.ViewModels.QuoteApprovalViewModel
@{
Layout = "_QuoteApprovalLayout";
ViewData["Title"] = "One Last Step";
@@ -17,7 +17,7 @@
@if (!string.IsNullOrEmpty(Model.DeclineError))
{
<div class="alert alert-danger">@Model.DeclineError</div>
<div class="alert alert-danger alert-permanent">@Model.DeclineError</div>
}
<div class="card">
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.ConvertQuoteToCustomerDto
@model PowderCoating.Application.DTOs.Quote.ConvertQuoteToCustomerDto
@{
ViewData["Title"] = "Convert Prospect/Walk-In to Customer";
@@ -35,7 +35,7 @@
<p><strong>Phone:</strong> @(Model.Phone ?? "-")</p>
</div>
</div>
<div class="alert alert-warning mb-0">
<div class="alert alert-warning alert-permanent mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Note:</strong> Converting this prospect will create a new customer record
and link this quote to that customer. The quote status will change to "Converted".
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.CreateQuoteDto
@model PowderCoating.Application.DTOs.Quote.CreateQuoteDto
@using PowderCoating.Core.Entities
@{
@@ -51,7 +51,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Customer vs Prospect/Walk-In"
data-bs-content="Choose &lt;strong&gt;Existing Customer&lt;/strong&gt; if this person is already in your system. Choose &lt;strong&gt;New Prospect/Walk-In&lt;/strong&gt; if they haven't committed yet their details stay on the quote. When they approve, you can convert them to a full customer record with one click.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#prospect-conversion' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="Choose &lt;strong&gt;Existing Customer&lt;/strong&gt; if this person is already in your system. Choose &lt;strong&gt;New Prospect/Walk-In&lt;/strong&gt; if they haven't committed yet — their details stay on the quote. When they approve, you can convert them to a full customer record with one click.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#prospect-conversion' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -146,7 +146,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Information"
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional — use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -210,7 +210,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Oven &amp; Batch Pricing"
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -253,7 +253,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Item Types"
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; — upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -314,7 +314,7 @@
<div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
<label class="form-check-label small" for="hideDiscountFromCustomer">
Hide discount from customer PDFs and approval portal show final price only
Hide discount from customer — PDFs and approval portal show final price only
</label>
</div>
</div>
@@ -329,7 +329,7 @@
<a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left"
data-bs-title="Pricing Summary"
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -340,7 +340,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -379,7 +379,7 @@
<div class="row g-3" id="stagedPhotoGrid"></div>
<div id="stagedPhotoUploadProgress" class="d-none mt-2">
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
<small class="text-muted">Uploading</small>
<small class="text-muted">Uploading…</small>
</div>
</div>
</div>
@@ -446,7 +446,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
@@ -461,7 +461,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -681,7 +681,7 @@
<script src="~/lib/tom-select/js/tom-select.complete.min.js"></script>
<script src="~/js/item-wizard.js?v=@DateTime.Now.Ticks"></script>
<script>
// ── Quick / Full quote mode toggle ──────────────────────────────────
// ── Quick / Full quote mode toggle ──────────────────────────────────
(function () {
const STORAGE_KEY = 'pcl_quote_mode';
const form = document.getElementById('quoteForm');
@@ -690,7 +690,7 @@
function applyMode(mode) {
if (mode === 'simple') {
form.classList.add('quote-simple-mode');
hint.textContent = 'Advanced fields are hidden switch to Full Quote to see them.';
hint.textContent = 'Advanced fields are hidden — switch to Full Quote to see them.';
} else {
form.classList.remove('quote-simple-mode');
hint.textContent = '';
@@ -758,8 +758,8 @@
smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms
? '<i class="bi bi-phone me-1"></i>No email send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email SMS consent required';
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
} else {
smsNote.style.display = 'none';
}
+16 -16
View File
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
@model PowderCoating.Application.DTOs.Quote.UpdateQuoteDto
@using PowderCoating.Core.Entities
@{
@@ -109,7 +109,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Information"
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="Set the quote date, expiration, and any internal notes. The &lt;strong&gt;Expiration Date&lt;/strong&gt; is shown to the customer — once it passes the quote is flagged Expired and can no longer be approved without editing. The &lt;strong&gt;Customer PO&lt;/strong&gt; field is optional — use it if the customer provides their own purchase order number.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -173,7 +173,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Oven &amp; Batch Pricing"
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
data-bs-content="The oven cost is charged once per batch at the quote level, not per item. Estimate how many oven loads the full job will fill — for example, if you have 20 small parts and your oven fits 10, that's 2 batches. Cycle time is how long each batch runs. The cost is calculated from your oven's hourly rate in Settings.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -216,7 +216,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Item Types"
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="&lt;strong&gt;Calculated&lt;/strong&gt; — you enter surface area (sq ft) and the system prices it using your rates for materials, labour, and overhead.&lt;br&gt;&lt;strong&gt;Custom Work&lt;/strong&gt; — you enter a description and a manual price. Use this for flat-rate jobs or work that doesn't fit the formula.&lt;br&gt;&lt;strong&gt;AI Photo&lt;/strong&gt; — upload photos and let the AI estimate surface area and complexity for you.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-items' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -277,7 +277,7 @@
<div class="form-check">
<input asp-for="HideDiscountFromCustomer" class="form-check-input" type="checkbox" id="hideDiscountFromCustomer" />
<label class="form-check-label small" for="hideDiscountFromCustomer">
Hide discount from customer PDFs and approval portal show final price only
Hide discount from customer — PDFs and approval portal show final price only
</label>
</div>
</div>
@@ -292,7 +292,7 @@
<a tabindex="0" class="help-icon text-white" role="button"
data-bs-toggle="popover" data-bs-placement="left"
data-bs-title="Pricing Summary"
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more &lt;/a&gt;">
data-bs-content="The total is built up from materials, labour, equipment time, overhead, and profit margin — all based on the rates in Settings. A &lt;strong&gt;Tier Discount&lt;/strong&gt; appears automatically if the customer has a pricing tier assigned. A &lt;strong&gt;Rush Fee&lt;/strong&gt; is added when Rush Job is checked.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#pricing-breakdown' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -303,7 +303,7 @@
<p class="mb-1 text-muted small" id="pricingPlaceholder">Pricing will update automatically as you add items.</p>
<p class="mb-1 d-none" id="itemsSubtotalRow">Items Subtotal: <strong id="itemsSubtotalDisplay">$0.00</strong></p>
<p class="mb-1 d-none" id="ovenBatchCostRow">
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<i class="bi bi-fire me-1"></i>Oven (<span id="ovenBatchesDisplay">1</span> batch × <span id="ovenCycleMinDisplay">45</span> min):
<strong id="ovenBatchCostDisplay">$0.00</strong>
</p>
<p class="mb-1 text-success d-none" id="pricingTierDiscountRow">
@@ -407,7 +407,7 @@
</div>
<div id="editPhotoUploadProgress" class="d-none mt-2">
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
<small class="text-muted">Uploading</small>
<small class="text-muted">Uploading…</small>
</div>
</div>
</div>
@@ -435,11 +435,11 @@
<span id="smsNotifyNote" class="badge @(editHasSms ? "bg-info text-white" : "bg-warning text-dark")">
@if (editHasSms)
{
<i class="bi bi-phone me-1"></i><text>No email send via SMS from quote details</text>
<i class="bi bi-phone me-1"></i><text>No email — send via SMS from quote details</text>
}
else
{
<i class="bi bi-phone-slash me-1"></i><text>No email SMS consent required</text>
<i class="bi bi-phone-slash me-1"></i><text>No email — SMS consent required</text>
}
</span>
}
@@ -483,7 +483,7 @@
<div class="col-6"><label class="form-label">Width (@(ViewBag.UseMetric == true ? "cm" : "in"))</label>
<input type="number" id="rectWidth" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()"></div>
</div>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
<small class="text-muted">Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")</small>
</div>
<div id="cylinderInputs" style="display:none">
<div class="row g-2">
@@ -498,7 +498,7 @@
<input type="number" id="circDiameter" class="form-control" min="0" step="0.01" value="0" oninput="calculateSqFt()">
</div>
<hr />
<div class="alert alert-info mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
<div class="alert alert-info alert-permanent mb-0"><strong>Result:</strong> <span id="calcResult">0.00</span> @ViewBag.AreaUnit</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -579,7 +579,7 @@
@Html.Raw(Json.Serialize(ViewBag.BlastSetups ?? new List<object>()))
</script>
<!-- Existing items always populated on Edit -->
<!-- Existing items — always populated on Edit -->
<script id="existingItemsData" type="application/json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize((Model.QuoteItems ?? new List<PowderCoating.Application.DTOs.Quote.CreateQuoteItemDto>()).Select((item, i) => new {
description = item.Description,
@@ -740,8 +740,8 @@
smsNote.style.display = 'inline';
smsNote.className = hasSms ? 'badge bg-info text-white' : 'badge bg-warning text-dark';
smsNote.innerHTML = hasSms
? '<i class="bi bi-phone me-1"></i>No email send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email SMS consent required';
? '<i class="bi bi-phone me-1"></i>No email — send via SMS from quote details'
: '<i class="bi bi-phone-slash me-1"></i>No email — SMS consent required';
} else {
smsNote.style.display = 'none';
}
@@ -811,7 +811,7 @@
}
});
// Quote photo direct upload (Edit page quoteId is known)
// Quote photo direct upload (Edit page — quoteId is known)
(function () {
const quoteId = @Model.Id;
const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")';
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Registration.RegisterCompanyDto
@model PowderCoating.Application.DTOs.Registration.RegisterCompanyDto
@using PowderCoating.Core.Entities
@{
@@ -309,7 +309,7 @@
@if (!ViewData.ModelState.IsValid && ViewData.ModelState.ErrorCount > 0)
{
<div class="alert alert-danger mb-3">
<div class="alert alert-danger alert-permanent mb-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Please fix the errors below:</strong>
<div asp-validation-summary="All" class="mt-1 mb-0"></div>
@@ -415,7 +415,7 @@
}
else
{
<div class="alert alert-warning mt-2" role="alert">
<div class="alert alert-warning alert-permanent mt-2" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
No subscription plans are currently available. Please contact a system administrator.
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model List<Vendor1099Row>
@{
@@ -46,7 +46,7 @@
<div class="col-md-4">
<div class="card border-0 shadow-sm text-center">
<div class="card-body py-3">
<div class="text-muted small">Need 1099-NEC ( $600)</div>
<div class="text-muted small">Need 1099-NEC (≥ $600)</div>
<div class="fs-3 fw-bold text-danger">@vendorsOver600</div>
</div>
</div>
@@ -61,7 +61,7 @@
</div>
</div>
<div class="alert alert-info py-2 mb-4">
<div class="alert alert-info alert-permanent py-2 mb-4">
<i class="bi bi-info-circle me-2"></i>
This report shows payments to vendors marked as <strong>1099 Vendor</strong> during <strong>@reportYear</strong>.
IRS rules require a 1099-NEC for non-incorporated contractors, attorneys, and service providers paid <strong>$600 or more</strong> in the calendar year.
@@ -81,7 +81,7 @@ else
{
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary @reportYear</h5>
<h5 class="mb-0 fw-semibold"><i class="bi bi-table me-2 text-primary"></i>1099-NEC Summary — @reportYear</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
@@ -117,14 +117,14 @@ else
<span class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>Missing</span>
}
</td>
<td class="small">@(row.Address ?? "")</td>
<td class="small">@(row.Address ?? "—")</td>
<td class="text-end">@row.BillsPaid.ToString("C")</td>
<td class="text-end">@row.ExpensesPaid.ToString("C")</td>
<td class="text-end fw-bold @(row.NeedsForm ? "text-danger" : "")">@row.TotalPaid.ToString("C")</td>
<td class="text-center">
@if (row.NeedsForm)
{
<span class="badge bg-danger">Yes File Required</span>
<span class="badge bg-danger">Yes — File Required</span>
}
else
{
@@ -1,4 +1,4 @@
@model List<PowderCoating.Core.Entities.Company>
@model List<PowderCoating.Core.Entities.Company>
@{
ViewData["Title"] = "Seed Data Management";
ViewData["PageIcon"] = "bi-database-fill-gear";
@@ -98,7 +98,7 @@
<li><strong>Default Company:</strong> Demo Company (DEMO)</li>
<li><strong>SuperAdmin User:</strong> artemis@("@")powdercoatinglogix.com</li>
</ul>
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle-fill me-2"></i>
<strong>Note:</strong> This operation is idempotent. If data already exists, it will be skipped.
</div>
@@ -125,14 +125,14 @@
<li><strong>Operating Costs:</strong> Default labor, equipment, and overhead rates</li>
<li><strong>Demo Users:</strong> Company Admin and Manager (only for Demo Company)</li>
</ul>
<div class="alert alert-info mb-3">
<div class="alert alert-info alert-permanent mb-3">
<i class="bi bi-info-circle-fill me-2"></i>
<strong>Note:</strong> Select a company below to seed demo data. Existing data will not be overwritten.
</div>
@if (Model == null || !Model.Any())
{
<div class="alert alert-warning">
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
No companies found. Please seed system data first to create the default company.
</div>
@@ -196,7 +196,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view d-lg-none">
<div class="mobile-card-list">
@foreach (var company in Model)
@@ -357,7 +357,7 @@
</label>
</div>
<div class="alert alert-warning mt-3 mb-0">
<div class="alert alert-warning alert-permanent mt-3 mb-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>This action permanently deletes records and cannot be undone.</strong>
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@{
ViewData["Title"] = "Setup Complete!";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
@@ -78,7 +78,7 @@
</div>
<h1>You're all set!</h1>
<p style="color:rgba(255,255,255,0.8);font-size:1.05rem;max-width:500px;margin:0 auto 1.5rem;">
Your setup is complete. @progress.DoneSteps.Count of @WizardProgressDto.TotalSteps steps were configured your shop is ready to roll.
Your setup is complete. @progress.DoneSteps.Count of @WizardProgressDto.TotalSteps steps were configured — your shop is ready to roll.
</p>
@if (showGuidedActivationCta)
{
@@ -135,7 +135,7 @@
@if (progress.SkippedSteps.Any(s => !progress.IsStepDone(s)))
{
<div class="alert alert-warning d-flex gap-2 mb-4">
<div class="alert alert-warning alert-permanent d-flex gap-2 mb-4">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
You skipped @progress.SkippedSteps.Count(s => !progress.IsStepDone(s)) step(s). You can always re-run the setup wizard from
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Storage Migration";
ViewData["PageIcon"] = "bi-cloud-upload";
bool mediaExists = ViewBag.MediaExists;
@@ -26,7 +26,7 @@
<small class="text-muted">@ViewBag.MediaPath</small>
</div>
</div>
<div class="alert alert-info mb-0">
<div class="alert alert-info alert-permanent mb-0">
<i class="bi bi-info-circle me-1"></i>
These files will be uploaded to Azure Blob Storage. Files already present in Azure will be skipped automatically.
</div>
@@ -82,7 +82,7 @@
</tbody>
</table>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view d-lg-none p-3">
<div class="mobile-card-list">
<div class="mobile-data-card">
@@ -148,7 +148,7 @@
</div>
<div class="form-text text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>
Leave unchecked on the first run. Files already in Azure are always skipped re-running is safe.
Leave unchecked on the first run. Files already in Azure are always skipped — re-running is safe.
</div>
</div>
@@ -156,7 +156,7 @@
<i class="bi bi-cloud-upload me-2"></i>Start Migration
</button>
<span class="ms-3 text-muted d-none" id="migrationSpinner">
<span class="spinner-border spinner-border-sm me-1"></span>Migrating @localFileCount files, please wait
<span class="spinner-border spinner-border-sm me-1"></span>Migrating @localFileCount files, please wait…
</span>
</form>
</div>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.Interfaces
@using PowderCoating.Application.Interfaces
@model StorageMigrationResult
@{
@@ -52,7 +52,7 @@
@if (Model.HasErrors)
{
<div class="alert alert-danger mb-4">
<div class="alert alert-danger alert-permanent mb-4">
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-1"></i>Errors (@Model.Errors.Count)</h6>
<ul class="mb-0 small">
@foreach (var error in Model.Errors)
@@ -65,7 +65,7 @@
@if (Model.Total == 0)
{
<div class="alert alert-info">
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle me-1"></i>No files were found in the local media directory.
</div>
}
@@ -1,8 +1,8 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Enums
@model Company
@{
ViewData["Title"] = $"Manage {Model.CompanyName}";
ViewData["Title"] = $"Manage – {Model.CompanyName}";
var planConfigs = (dynamic)ViewBag.PlanConfigs;
string PlanName(int plan)
@@ -66,9 +66,9 @@
<dt class="col-7 text-muted">Users</dt>
<dd class="col-5 fw-semibold">@ViewBag.UserCount</dd>
<dt class="col-7 text-muted">Stripe Customer</dt>
<dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "")</code></dd>
<dd class="col-5"><code class="small">@(Model.StripeCustomerId ?? "—")</code></dd>
<dt class="col-7 text-muted">Stripe Sub</dt>
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "")</code></dd>
<dd class="col-5"><code class="small">@(Model.StripeSubscriptionId ?? "—")</code></dd>
</dl>
</div>
</div>
@@ -99,7 +99,7 @@
<form method="post" asp-action="UpdateSubscription" asp-route-id="@Model.Id">
@Html.AntiForgeryToken()
@* Comped / Internal card prominent *@
@* Comped / Internal card — prominent *@
<div class="card border-0 shadow-sm mb-3 @(Model.IsComped ? "border-success border-2" : "")">
<div class="card-header border-0 py-3 @(Model.IsComped ? "bg-success bg-opacity-10" : "bg-white")">
<h6 class="mb-0 fw-semibold">
@@ -345,17 +345,17 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning small mb-3">
<div class="alert alert-warning alert-permanent small mb-3">
<i class="bi bi-exclamation-triangle me-1"></i>
This will immediately issue a refund via Stripe. This action cannot be undone.
</div>
<dl class="row small mb-3">
<dt class="col-5 text-muted">Invoice</dt>
<dd class="col-7 font-monospace" id="refund-invoice-number"></dd>
<dd class="col-7 font-monospace" id="refund-invoice-number">—</dd>
<dt class="col-5 text-muted">Amount Paid</dt>
<dd class="col-7 fw-semibold" id="refund-amount-paid"></dd>
<dd class="col-7 fw-semibold" id="refund-amount-paid">—</dd>
<dt class="col-5 text-muted">Max Refundable</dt>
<dd class="col-7 fw-semibold text-success" id="refund-max-amount"></dd>
<dd class="col-7 fw-semibold text-success" id="refund-max-amount">—</dd>
</dl>
<div class="mb-3">
<label class="form-label fw-medium">Refund Amount</label>
@@ -402,7 +402,7 @@
@section Scripts {
<script>
// ── State for the refund modal ────────────────────────────────────────────────
// ── State for the refund modal ────────────────────────────────────────────────
let _refundPaymentIntentId = null;
let _refundMaxCents = 0;
let _refundAmountPaid = '';
@@ -442,7 +442,7 @@ async function loadPaymentHistory() {
: '';
const refundedCell = ch.amountRefunded
? `<span class="text-danger small">${ch.amountRefunded}</span>`
: '<span class="text-muted small"></span>';
: '<span class="text-muted small">—</span>';
const desc = ch.description ? `<span class="text-muted">${ch.description}</span>` : `<code class="small">${ch.id}</code>`;
// Show Refund button only for succeeded charges that still have something refundable
@@ -469,7 +469,7 @@ async function loadPaymentHistory() {
}
function openRefundModal(chargeId, refundableCents, amountPaid, displayLabel) {
_refundPaymentIntentId = chargeId; // reusing variable now holds charge ID
_refundPaymentIntentId = chargeId; // reusing variable — now holds charge ID
_refundMaxCents = refundableCents;
_refundAmountPaid = amountPaid;
_refundInvoiceNumber = displayLabel;
@@ -509,7 +509,7 @@ async function submitRefund() {
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing';
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Processing…';
try {
const formData = new FormData();
@@ -1,4 +1,4 @@
@model PowderCoating.Core.Entities.TaxRate
@model PowderCoating.Core.Entities.TaxRate
@{
ViewData["Title"] = "Add Tax Rate";
}
@@ -12,7 +12,7 @@
<div class="card-body">
<form asp-action="Create" method="post">
@Html.AntiForgeryToken()
<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="mb-3">
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
@@ -1,4 +1,4 @@
@model PowderCoating.Core.Entities.TaxRate
@model PowderCoating.Core.Entities.TaxRate
@{
ViewData["Title"] = "Edit Tax Rate";
}
@@ -12,7 +12,7 @@
<form asp-action="Edit" asp-route-id="@Model.Id" method="post">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Id" />
<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="mb-3">
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Set Up Two-Factor Authentication";
}
@@ -15,7 +15,7 @@
<div class="card-body">
<ol class="mb-0 ps-3">
<li class="mb-3">
<strong>Install an authenticator app</strong> on your phone if you haven't already
<strong>Install an authenticator app</strong> on your phone if you haven't already —
<em>Google Authenticator</em>, <em>Microsoft Authenticator</em>, or <em>Authy</em> all work.
</li>
<li class="mb-3">
@@ -26,7 +26,7 @@
class="border rounded p-1"
style="width:200px;height:200px" />
</div>
<div class="alert alert-light py-2 small">
<div class="alert alert-light alert-permanent py-2 small">
<strong>Manual entry key:</strong><br>
<code class="user-select-all">@ViewBag.SharedKey</code>
</div>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Vendor.VendorDto
@model PowderCoating.Application.DTOs.Vendor.VendorDto
@{
ViewData["Title"] = "Delete Vendor";
@@ -15,7 +15,7 @@
</div>
<!-- Warning Banner -->
<div class="alert alert-danger d-flex align-items-start mb-4">
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Are you sure you want to delete this vendor?</h5>
@@ -25,7 +25,7 @@
@if (hasInventoryItems)
{
<div class="alert alert-warning d-flex align-items-start mb-4">
<div class="alert alert-warning alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-circle-fill me-3" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Warning: This vendor has inventory items</h5>
@@ -174,7 +174,7 @@
@if (hasInventoryItems)
{
<div class="alert alert-info d-flex align-items-center">
<div class="alert alert-info alert-permanent d-flex align-items-center">
<i class="bi bi-info-circle me-2"></i>
<div>
<strong>Inventory Items:</strong> This vendor has @ViewBag.InventoryItemCount inventory item(s) on record.
@@ -979,3 +979,45 @@ a.tag-index-badge:hover {
filter: brightness(0.88);
text-decoration: none;
}
/* =============================================================
Design system utilities audit fixes 2026-05
============================================================= */
/* Card consistency
Standard is `card border-0 shadow-sm`. Any shadow card that
drifted to just `shadow-sm` gets border stripped here. */
.card.shadow-sm {
border: 0 !important;
}
/* Card header dark-mode fix
bg-white hardcodes light mode. Rebind to theme surface so
card headers follow the user's dark/light preference. */
.card-header.bg-white {
background-color: var(--bs-body-bg) !important;
}
/* Typography scale utilities
Fills the gap between Bootstrap's .small and the custom
.nav-section-title / .pcl-metric-label sizes. */
.text-2xs { font-size: 0.68rem; line-height: 1.4; }
.text-xs { font-size: 0.73rem; line-height: 1.4; }
/* Token color utilities
Use .text-ember for the amber brand accent.
Use .text-primary only for near-black ink (Bootstrap default).
Never use text-primary when you want the orange accent. */
.text-ember { color: var(--pcl-ember) !important; }
.text-ok { color: var(--pcl-ok) !important; }
.text-bad { color: var(--pcl-bad) !important; }
.text-warn { color: var(--pcl-warn) !important; }
.text-cool { color: var(--pcl-cool) !important; }
.bg-paper-2 { background-color: var(--pcl-paper-2) !important; }
/* Max-width layout constraints
Use these instead of inline style="max-width:Npx" */
.mw-xs { max-width: 280px; }
.mw-sm { max-width: 360px; }
.mw-md { max-width: 480px; }
.mw-lg { max-width: 640px; }