Sweep all .cshtml files for encoding corruption; add pre-commit guard

Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII

tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun

.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.

Build clean; 225 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 21:37:10 -04:00
parent 21b39161a3
commit a0bdd2b5b4
252 changed files with 1785 additions and 1633 deletions
@@ -72,14 +72,14 @@
<input class="form-check-input" type="radio" name="format" value="xlsx" id="fmt_xlsx" checked />
<label class="form-check-label" for="fmt_xlsx">
<i class="bi bi-file-earmark-spreadsheet me-1 text-success"></i>
Excel (.xlsx) all sheets in one file
Excel (.xlsx) &mdash; all sheets in one file
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="format" value="csv" id="fmt_csv" />
<label class="form-check-label" for="fmt_csv">
<i class="bi bi-file-zip me-1 text-warning"></i>
CSV (.zip) one file per sheet
CSV (.zip) &mdash; one file per sheet
</label>
</div>
</div>
@@ -111,7 +111,7 @@
document.getElementById('exportForm').addEventListener('submit', function () {
var btn = document.getElementById('exportBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating&hellip;';
// Re-enable after 10s in case browser blocks the download dialog
setTimeout(function () {
btn.disabled = false;
@@ -49,7 +49,7 @@
<div class="mt-1 text-success" style="font-size:1.6rem;"><i class="bi bi-building"></i></div>
<div>
<div class="fw-semibold">QuickBooks Desktop</div>
<div class="text-muted small">IIF files import directly via File &gt; Utilities &gt; Import</div>
<div class="text-muted small">IIF files &mdash; import directly via File &gt; Utilities &gt; Import</div>
<div class="mt-2">
<span class="badge bg-light text-dark border me-1">customers.iif</span>
<span class="badge bg-light text-dark border me-1">invoices_payments.iif</span>
@@ -146,7 +146,7 @@
});
}
// Init mark CSV as selected by default
// Init &mdash; mark CSV as selected by default
document.getElementById('fmt-csv').checked = true;
updateFormatCards();
@@ -154,7 +154,7 @@
document.getElementById('exportForm').addEventListener('submit', function() {
const btn = document.getElementById('exportBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generating&hellip;';
setTimeout(function() { btn.disabled = false; btn.innerHTML = '<i class="bi bi-download me-2"></i>Download Export Package'; }, 8000);
});
@@ -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 &mdash; 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;
}
@@ -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: 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).">
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000&ndash;1999 Assets, 2000&ndash;2999 Liabilities, 3000&ndash;3999 Equity, 4000&ndash;4999 Revenue, 5000&ndash;5999 Cost of Goods, 6000&ndash;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="">&mdash; Select Type &mdash;</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="">&mdash; Select Sub-Type &mdash;</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 &mdash; 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="">&mdash; None (top-level account) &mdash;</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 &mdash; 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">
@@ -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: 1000–1999 Assets, 2000–2999 Liabilities, 3000–3999 Equity, 4000–4999 Revenue, 5000–5999 Cost of Goods, 6000–9999 Expenses. Must be unique.">
data-bs-content="A numeric code for sorting and organizing accounts. Convention: 1000&ndash;1999 Assets, 2000&ndash;2999 Liabilities, 3000&ndash;3999 Equity, 4000&ndash;4999 Revenue, 5000&ndash;5999 Cost of Goods, 6000&ndash;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 &mdash; 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="">&mdash; None &mdash;</option>
</select>
</div>
@@ -59,7 +59,7 @@
</div>
</div>
@* Bootstrap toast confirmation before recalculating balances *@
@* Bootstrap toast &mdash; confirmation before recalculating balances *@
<div class="toast-container position-fixed top-50 start-50 translate-middle p-3" style="z-index:1100">
<div id="recalcConfirmToast" class="toast align-items-center border-0 bg-dark text-white" role="alert" aria-atomic="true" data-bs-autohide="false">
<div class="toast-body d-flex flex-column gap-2 py-3 px-3">
@@ -153,7 +153,7 @@
<span class="fw-medium">@acct.Name</span>
@if (acct.IsSystem)
{
<span class="badge bg-secondary ms-1" title="System account cannot be deleted">sys</span>
<span class="badge bg-secondary ms-1" title="System account &mdash; cannot be deleted">sys</span>
}
</td>
<td><span class="text-muted small">@acct.AccountSubType.ToDisplayName()</span></td>
@@ -186,7 +186,7 @@
@if (!acct.IsSystem)
{
<form asp-action="Delete" asp-route-id="@acct.Id" method="post" class="d-inline"
onsubmit="return confirm('Delete account @acct.AccountNumber @acct.Name?')">
onsubmit="return confirm('Delete account @acct.AccountNumber &ndash; @acct.Name?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
@@ -219,7 +219,7 @@
});
});
// Recalculate Balances show confirmation toast instead of native confirm()
// Recalculate Balances &mdash; show confirmation toast instead of native confirm()
const recalcToast = new bootstrap.Toast(document.getElementById('recalcConfirmToast'));
document.getElementById('btnRecalcBalances').addEventListener('click', () => {
@@ -2,7 +2,7 @@
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = $"Ledger {Model.AccountNumber} {Model.Name}";
ViewData["Title"] = $"Ledger &mdash; {Model.AccountNumber} {Model.Name}";
ViewData["PageIcon"] = "bi-journal-text";
ViewData["PageHelpTitle"] = "Account Ledger";
ViewData["PageHelpContent"] = "A chronological list of every transaction posted to this account. Click any Reference to open the source record. Debit increases asset and expense accounts; credit increases liability, equity, and revenue accounts. Use the date range or quick buttons (This Month, YTD, etc.) to narrow the view.";
@@ -60,7 +60,7 @@
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-action="Index">Chart of Accounts</a></li>
<li class="breadcrumb-item active">@Model.AccountNumber @Model.Name</li>
<li class="breadcrumb-item active">@Model.AccountNumber &ndash; @Model.Name</li>
</ol>
</nav>
@@ -166,7 +166,7 @@
<i class="bi bi-journal-text me-1"></i>
Transactions
<span class="text-muted fw-normal small ms-1">
@Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy")
@Model.From.ToString("MMM d") &ndash; @Model.To.ToString("MMM d, yyyy")
</span>
</span>
<a tabindex="0" class="help-icon" role="button"
@@ -205,7 +205,7 @@
<!-- Opening balance row -->
<tr class="table-light">
<td class="text-muted small">@Model.From.ToString("MM/dd/yyyy")</td>
<td><span class="fw-medium text-muted"></span></td>
<td><span class="fw-medium text-muted">&mdash;</span></td>
<td><span class="badge bg-dark-subtle text-dark">Opening Balance</span></td>
<td class="text-muted small">Balance brought forward as of @Model.From.ToString("MMM d, yyyy")</td>
<td></td>
@@ -1,4 +1,4 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = "Year-End Close";
ViewData["PageIcon"] = "bi-calendar-check";
@@ -33,7 +33,7 @@
<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 &mdash; 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 ?? "&mdash;")</td>
<td>
@if (c.JournalEntry != null)
{
@@ -40,7 +40,7 @@
</div>
<div>
<div class="fs-3 fw-bold">@Model.TotalCallsLast30Days.ToString("N0")</div>
<div class="text-muted small">AI Calls Last 30 Days</div>
<div class="text-muted small">AI Calls &mdash; Last 30 Days</div>
</div>
</div>
</div>
@@ -96,11 +96,11 @@
<!-- Tier Legend -->
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
<span class="text-muted small fw-semibold me-1">Usage Tier (last 30 days):</span>
<span class="badge bg-secondary">Inactive 0 calls</span>
<span class="badge bg-success">Light — 110</span>
<span class="badge bg-primary">Regular — 1150</span>
<span class="badge bg-warning text-dark">Heavy — 51200</span>
<span class="badge bg-danger">Power User 200+</span>
<span class="badge bg-secondary">Inactive &mdash; 0 calls</span>
<span class="badge bg-success">Light &mdash; 1&ndash;10</span>
<span class="badge bg-primary">Regular &mdash; 11&ndash;50</span>
<span class="badge bg-warning text-dark">Heavy &mdash; 51&ndash;200</span>
<span class="badge bg-danger">Power User &mdash; 200+</span>
</div>
<!-- Main Table -->
@@ -207,16 +207,16 @@
</span>
</td>
<td class="text-center @(row.Today > 0 ? "fw-semibold" : "text-muted")">
@(row.Today > 0 ? row.Today.ToString("N0") : "")
@(row.Today > 0 ? row.Today.ToString("N0") : "&mdash;")
</td>
<td class="text-center @(row.Last7Days > 0 ? "fw-semibold" : "text-muted")">
@(row.Last7Days > 0 ? row.Last7Days.ToString("N0") : "")
@(row.Last7Days > 0 ? row.Last7Days.ToString("N0") : "&mdash;")
</td>
<td class="text-center @(row.Last30Days > 0 ? "fw-semibold" : "text-muted")">
@(row.Last30Days > 0 ? row.Last30Days.ToString("N0") : "")
@(row.Last30Days > 0 ? row.Last30Days.ToString("N0") : "&mdash;")
</td>
<td class="text-center @(row.AllTime > 0 ? "" : "text-muted")">
@(row.AllTime > 0 ? row.AllTime.ToString("N0") : "")
@(row.AllTime > 0 ? row.AllTime.ToString("N0") : "&mdash;")
</td>
<td class="text-center @(row.PhotoCount > 0 ? "" : "text-muted")">
@if (row.PhotoCount > 0)
@@ -225,7 +225,7 @@
}
else
{
<span></span>
<span>&mdash;</span>
}
</td>
<td>
@@ -244,7 +244,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="text-center">
@@ -60,7 +60,7 @@
onclick="window.location='@Url.Action("Edit", new { id = a.Id })'">
<td>
<div class="fw-medium">@a.Title</div>
<small class="text-muted">@a.Message.Substring(0, Math.Min(60, a.Message.Length))@(a.Message.Length > 60 ? "" : "")</small>
<small class="text-muted">@a.Message.Substring(0, Math.Min(60, a.Message.Length))@(a.Message.Length > 60 ? "&hellip;" : "")</small>
</td>
<td><span class="badge @TypeBadge(a.Type)">@a.Type</span></td>
<td>
@@ -110,7 +110,7 @@
</tbody>
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@if (!Model.Any())
@@ -15,7 +15,7 @@
<div class="col-12">
<label class="form-label fw-medium">Message <span class="text-danger">*</span></label>
<textarea asp-for="Message" class="form-control" rows="3"
placeholder="The platform will be offline for maintenance on Saturday from 24 AM ET." required></textarea>
placeholder="The platform will be offline for maintenance on Saturday from 2&ndash;4 AM ET." required></textarea>
<span asp-validation-for="Message" class="text-danger small"></span>
</div>
@@ -42,7 +42,7 @@
<div id="planTargetGroup" style="display:none">
<label class="form-label fw-medium">Plan</label>
<select asp-for="TargetPlan" class="form-select">
<option value=""> select </option>
<option value="">&mdash; select &mdash;</option>
@foreach (var p in planConfigs)
{
<option value="@p.Plan">@p.DisplayName</option>
@@ -52,7 +52,7 @@
<div id="companyTargetGroup" style="display:none">
<label class="form-label fw-medium">Company</label>
<select asp-for="TargetCompanyId" class="form-select">
<option value=""> select </option>
<option value="">&mdash; select &mdash;</option>
@foreach (var c in companies)
{
<option value="@c.Id">@c.CompanyName</option>
@@ -93,7 +93,7 @@
<label class="form-label fw-medium text-muted">Preview</label>
<div id="announcementPreview" class="alert mb-0" role="alert">
<strong id="previewTitle">@Model.Title</strong>
<span id="previewMessage"> @Model.Message</span>
<span id="previewMessage"> &mdash; @Model.Message</span>
</div>
</div>
</div>
@@ -113,7 +113,7 @@
const preview = document.getElementById('announcementPreview');
preview.className = 'alert mb-0 ' + (typeMap[type] || 'alert-info');
document.getElementById('previewTitle').textContent = document.getElementById('Title').value || 'Title';
document.getElementById('previewMessage').textContent = ' ' + (document.getElementById('Message').value || 'Message');
document.getElementById('previewMessage').textContent = ' &mdash; ' + (document.getElementById('Message').value || 'Message');
}
document.getElementById('Type')?.addEventListener('change', updatePreview);
document.getElementById('Title')?.addEventListener('input', updatePreview);
@@ -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 &mdash; 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">
@@ -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 &mdash; 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 &mdash; consider setting status to Cancelled instead to preserve history.";
}
<div class="d-flex justify-content-end gap-2 mb-4">
@@ -115,7 +115,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Actual Times"
data-bs-content="Record when the customer actually arrived and when the appointment finished. These are optional and separate from the scheduled times useful for tracking punctuality and measuring how accurately appointments are estimated.">
data-bs-content="Record when the customer actually arrived and when the appointment finished. These are optional and separate from the scheduled times &mdash; useful for tracking punctuality and measuring how accurately appointments are estimated.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -169,7 +169,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 &mdash; 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>
@@ -46,19 +46,19 @@
<dd class="col-7">@Model.EntityType</dd>
<dt class="col-5 text-muted">Entity ID</dt>
<dd class="col-7">@(Model.EntityId ?? "")</dd>
<dd class="col-7">@(Model.EntityId ?? "&mdash;")</dd>
<dt class="col-5 text-muted">Description</dt>
<dd class="col-7">@(Model.EntityDescription ?? "")</dd>
<dd class="col-7">@(Model.EntityDescription ?? "&mdash;")</dd>
<dt class="col-5 text-muted">User</dt>
<dd class="col-7">@Model.UserName</dd>
<dt class="col-5 text-muted">Company</dt>
<dd class="col-7">@(Model.CompanyName ?? (Model.CompanyId?.ToString() ?? ""))</dd>
<dd class="col-7">@(Model.CompanyName ?? (Model.CompanyId?.ToString() ?? "&mdash;"))</dd>
<dt class="col-5 text-muted">IP Address</dt>
<dd class="col-7">@(Model.IpAddress ?? "")</dd>
<dd class="col-7">@(Model.IpAddress ?? "&mdash;")</dd>
</dl>
</div>
</div>
@@ -95,8 +95,8 @@
var newVal = newData.ValueKind == JsonValueKind.Object && newData.TryGetProperty(key, out var nv) ? nv.ToString() : null;
<tr>
<td class="fw-medium">@key</td>
<td class="text-danger font-monospace">@(oldVal ?? "")</td>
<td class="text-success font-monospace">@(newVal ?? "")</td>
<td class="text-danger font-monospace">@(oldVal ?? "&mdash;")</td>
<td class="text-success font-monospace">@(newVal ?? "&mdash;")</td>
</tr>
}
}
@@ -55,7 +55,7 @@
<form method="get" class="row g-2 align-items-end">
<div class="col-md-3">
<input name="search" value="@ViewBag.Search" class="form-control form-control-sm"
placeholder="User, entity name, ID" />
placeholder="User, entity name, ID&hellip;" />
</div>
<div class="col-md-2">
<select name="entityType" class="form-select form-select-sm">
@@ -145,7 +145,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<!-- Mobile card view &mdash; shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@if (!Model.Any())
@@ -196,7 +196,7 @@
{
<div class="card-footer d-flex align-items-center justify-content-between py-2">
<small class="text-muted">
Showing @((page - 1) * pageSize + 1)@Math.Min(page * pageSize, totalCount) of @totalCount.ToString("N0")
Showing @((page - 1) * pageSize + 1)&ndash;@Math.Min(page * pageSize, totalCount) of @totalCount.ToString("N0")
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
@@ -18,7 +18,7 @@
<div class="mb-3">
<label class="form-label fw-semibold">Bank Account <span class="text-danger">*</span></label>
<select asp-for="AccountId" asp-items="accounts" class="form-select" required>
<option value=""> select account </option>
<option value="">&mdash; select account &mdash;</option>
</select>
<div class="form-text">Only Checking, Savings, and Cash accounts are listed.</div>
</div>
@@ -43,7 +43,7 @@
</div>
<div class="col-md-3 text-center">
<div class="card shadow-sm py-3">
<div class="fw-bold fs-5" id="difference"></div>
<div class="fw-bold fs-5" id="difference">&mdash;</div>
<div class="text-muted small">Difference</div>
</div>
</div>
@@ -212,7 +212,7 @@
document.getElementById('aiMatchBtn')?.addEventListener('click', async function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analyzing';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analyzing&hellip;';
try {
const resp = await fetch('/BankReconciliations/AiSuggestMatches', {
@@ -244,7 +244,7 @@
const td = row.querySelector('td:last-child');
if (td) {
const pct = Math.round(s.confidence * 100);
td.insertAdjacentHTML('afterend', `<td class="small text-info" style="white-space:nowrap">${pct}% ${s.reason}</td>`);
td.insertAdjacentHTML('afterend', `<td class="small text-info" style="white-space:nowrap">${pct}% &mdash; ${s.reason}</td>`);
}
}
});
@@ -256,7 +256,7 @@
if (aiSuggestions.length > 0) {
document.getElementById('aiMatchAccept').classList.remove('d-none');
} else {
insightsEl.innerHTML += '<br><span class="text-muted">No high-confidence suggestions found review items manually.</span>';
insightsEl.innerHTML += '<br><span class="text-muted">No high-confidence suggestions found &mdash; review items manually.</span>';
}
} catch (err) {
document.getElementById('aiMatchInsights').innerHTML = '<span class="text-danger">Error contacting AI service.</span>';
@@ -1,7 +1,7 @@
@model PowderCoating.Core.Entities.BankReconciliation
@using PowderCoating.Web.Controllers
@{
ViewData["Title"] = $"Reconciliation Report {Model.Account?.Name}";
ViewData["Title"] = $"Reconciliation Report &ndash; {Model.Account?.Name}";
var clearedDeposits = ViewBag.ClearedDeposits as IEnumerable<PowderCoating.Core.Entities.Payment> ?? Enumerable.Empty<PowderCoating.Core.Entities.Payment>();
var clearedPayments = ViewBag.ClearedPayments as List<ReconciliationItem> ?? new();
}
@@ -38,7 +38,7 @@
<td class="fw-semibold text-end text-success">@clearedDeposits.Sum(p => p.Amount).ToString("C")</td>
</tr>
<tr>
<td class="text-muted"> Cleared Payments:</td>
<td class="text-muted">&ndash; Cleared Payments:</td>
<td class="fw-semibold text-end text-danger">@clearedPayments.Sum(p => p.Amount).ToString("C")</td>
</tr>
<tr class="border-top">
@@ -248,7 +248,7 @@
{
<tr class="text-muted">
<td><code>@ban.IpAddress</code></td>
<td><small>@(ban.Reason ?? "")</small></td>
<td><small>@(ban.Reason ?? "&mdash;")</small></td>
<td><small>@ban.BannedAt.ToString("MMM dd, yyyy")</small></td>
<td>
@if (!ban.IsActive)
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Subscription
@using PowderCoating.Application.DTOs.Subscription
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Billing & Subscription";
@@ -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 &mdash; 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 &mdash; 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>
+17 -17
View File
@@ -4,7 +4,7 @@
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 &mdash; 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> &mdash; review and save</p>
}
</div>
@if (fromPoId.HasValue)
@@ -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 &mdash; 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="">&mdash; Select Vendor &mdash;</option>
<option value="__new__">+ Add New Vendor&hellip;</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 &mdash; up to 10 MB.</div>
</div>
</div>
</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 &mdash; 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="">&mdash; Select Account &mdash;</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="">&mdash; Account &mdash;</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="">&mdash;</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 &mdash; up to 10 MB.</div>
</div>
<div id="scanReceiptStatus" class="text-muted small mt-2"></div>
</div>
@@ -394,7 +394,7 @@
if (lineCount === 0) addLineItem();
// ── AI Auto-suggest Account on description blur ───────────────────────
// Keyword shortcuts handle common cases with zero API cost
// Keyword shortcuts &mdash; 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' },
@@ -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&hellip;';
try {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
@@ -501,7 +501,7 @@
}
}
// Event delegation works for dynamically added rows
// Event delegation &mdash; works for dynamically added rows
document.getElementById('lineItemsBody').addEventListener('blur', function (e) {
if (e.target.matches('[name$=".Description"]')) {
_suggestAccountForRow(e.target.closest('tr'));
@@ -535,7 +535,7 @@
return;
}
// Auto-fill bill header try to match vendor name to dropdown
// Auto-fill bill header &mdash; 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 &mdash; 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 &mdash; review and adjust as needed.';
} catch (e) {
statusEl.textContent = 'Error connecting to AI service.';
} finally {
@@ -5,7 +5,7 @@
ViewData["Title"] = $"Bill {Model.BillNumber}";
ViewData["PageIcon"] = "bi-receipt-cutoff";
ViewData["PageHelpTitle"] = "Bill Status";
ViewData["PageHelpContent"] = "Draft: editable, not yet confirmed. Open: awaiting payment. Partially Paid: some payments recorded. Paid: fully settled. Voided: cancelled preserves history. Edit is only available in Draft status. Use Void instead of deleting to keep a complete audit trail.";
ViewData["PageHelpContent"] = "Draft: editable, not yet confirmed. Open: awaiting payment. Partially Paid: some payments recorded. Paid: fully settled. Voided: cancelled &mdash; preserves history. Edit is only available in Draft status. Use Void instead of deleting to keep a complete audit trail.";
string StatusBadge(BillStatus s) => s switch
{
@@ -266,7 +266,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Balance Summary"
data-bs-content="Balance Due = Bill Total minus all payments recorded. Multiple partial payments are supported each reduces the balance until fully paid. Deleting a payment reverses it and restores the balance due.">
data-bs-content="Balance Due = Bill Total minus all payments recorded. Multiple partial payments are supported &mdash; each reduces the balance until fully paid. Deleting a payment reverses it and restores the balance due.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -322,7 +322,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Record Payment @Model.BillNumber</h5>
<h5 class="modal-title">Record Payment &mdash; @Model.BillNumber</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form asp-action="RecordPayment" method="post">
@@ -358,12 +358,12 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
data-bs-title="Bank Account"
data-bs-content="The bank or cash account this payment is drawn from. Used for bank reconciliation helps match this payment to the corresponding debit on your bank statement.">
data-bs-content="The bank or cash account this payment is drawn from. Used for bank reconciliation &mdash; helps match this payment to the corresponding debit on your bank statement.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<select name="BankAccountId" class="form-select" required>
<option value=""> Select Account </option>
<option value="">&mdash; Select Account &mdash;</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -423,7 +423,7 @@
<div class="col-12">
<label class="form-label fw-medium">Bank Account <span class="text-danger">*</span></label>
<select name="BankAccountId" id="editBillBankAccountId" class="form-select" required>
<option value=""> Select Account </option>
<option value="">&mdash; Select Account &mdash;</option>
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
{
<option value="@item.Value">@item.Text</option>
@@ -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 &mdash; Void the bill and recreate it if corrections are needed after confirmation.";
}
<div class="d-flex justify-content-start mb-4">
@@ -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 &mdash; 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="">&mdash; Select Vendor &mdash;</option>
<option value="__new__">+ Add New Vendor&hellip;</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 &mdash; up to 10 MB.</div>
</div>
</div>
</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 &mdash; 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="">&mdash; Account &mdash;</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="">&mdash;</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";
@@ -156,13 +156,13 @@
}
else if (entry.EntryType == "Expense")
{
<span class=text-muted>&mdash;</span>
<span class="text-muted">&mdash;</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”) : &mdash;)
@(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "&mdash;")
</td>
<td>
@if (entry.EntryType == "Bill")
@@ -31,7 +31,7 @@
<div id="resultArea" class="d-none">
<div id="spinnerArea" class="text-center py-5 d-none">
<div class="spinner-border text-primary" style="width:2.5rem;height:2.5rem;" role="status"></div>
<p class="text-muted mt-3">Claude is reviewing your bill history</p>
<p class="text-muted mt-3">Claude is reviewing your bill history&hellip;</p>
</div>
<div id="errorArea" class="alert alert-danger alert-permanent d-none"></div>
@@ -2,7 +2,7 @@
@model BudgetCreateVm
@{
ViewData["Title"] = $"Edit Budget {Model.Name}";
ViewData["Title"] = $"Edit Budget &mdash; {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 &mdash; @Model.FiscalYear
</h5>
</div>
<div class="card-body">
@@ -78,7 +78,7 @@ else
</td>
<td class="text-center">@b.Lines.Count</td>
<td class="text-end text-success">@b.Lines.Sum(l => l.Annual).ToString("C")</td>
<td class="text-end text-danger"></td>
<td class="text-end text-danger">&mdash;</td>
<td class="text-center">
@if (b.IsDefault)
{
@@ -246,7 +246,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var report in Model)
@@ -313,7 +313,7 @@
{
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
<small class="text-muted">
Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
Showing @((pageNumber - 1) * pageSize + 1)&ndash;@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
@@ -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";
@@ -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 &ndash; Minor inconvenience, workaround exists</option>
<option value="@((int)BugReportPriority.Normal)" selected>Normal &ndash; Affects workflow but not critical</option>
<option value="@((int)BugReportPriority.High)">High &ndash; Significantly impacts operations</option>
<option value="@((int)BugReportPriority.Critical)">Critical &ndash; 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) &mdash; exceeds 100 MB limit`;
} else {
li.innerHTML = `<i class="bi bi-file-earmark text-secondary"></i> ${f.name} (${sizeMb} MB)`;
}
@@ -34,7 +34,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Parent Category"
data-bs-content="Leave as '(None)' to create a top-level category. Choose a parent to nest this under it e.g., place 'Aluminum Wheels' under a parent 'Wheels' category. This creates a browsable hierarchy in the catalog and quote wizard.">
data-bs-content="Leave as '(None)' to create a top-level category. Choose a parent to nest this under it &mdash; e.g., place 'Aluminum Wheels' under a parent 'Wheels' category. This creates a browsable hierarchy in the catalog and quote wizard.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -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="Parent Category"
data-bs-content="Leave as '(None)' to keep this as a top-level category. Choose a parent to nest it e.g., 'Aluminum Wheels' under 'Wheels'. The system prevents circular references (you cannot make a category its own ancestor).">
data-bs-content="Leave as '(None)' to keep this as a top-level category. Choose a parent to nest it &mdash; e.g., 'Aluminum Wheels' under 'Wheels'. The system prevents circular references (you cannot make a category its own ancestor).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -3,7 +3,7 @@
ViewData["Title"] = "AI Catalog Price Check";
ViewData["PageIcon"] = "bi-robot";
ViewData["PageHelpTitle"] = "AI Catalog Price Check";
ViewData["PageHelpContent"] = "The AI Price Check reviews every item in your catalog against your actual operating costs and flags items that may be priced below cost, have thin margins, or appear unusually high. Results are estimates based on industry knowledge and your shop's rates always apply your own judgment before changing prices.";
ViewData["PageHelpContent"] = "The AI Price Check reviews every item in your catalog against your actual operating costs and flags items that may be priced below cost, have thin margins, or appear unusually high. Results are estimates based on industry knowledge and your shop's rates &mdash; always apply your own judgment before changing prices.";
var sortedResults = Model?.Results
.OrderBy(r => r.Verdict switch
@@ -82,7 +82,7 @@
<div class="progress-card">
<div class="icon"><i class="bi bi-robot"></i></div>
<h5>Analyzing your catalog</h5>
<p class="status-msg" id="overlay-status">Preparing items</p>
<p class="status-msg" id="overlay-status">Preparing items&hellip;</p>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="overlay-bar"></div>
</div>
@@ -151,22 +151,22 @@
<div>
<h6 class="fw-semibold mb-1">What this analysis does</h6>
<p class="small text-muted mb-2">
Our AI system reviews every active, priced item in your catalog against your shop's actual operating costs
Our AI system reviews every active, priced item in your catalog against your shop's actual operating costs &mdash;
labor, oven time, sandblasting, coating booth, and powder material. For each item it estimates a
realistic surface area and processing time, calculates a cost floor, then compares that to your
current price and returns one of four verdicts:
</p>
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="verdict-badge verdict-below-cost">Below Cost</span><span class="small text-muted align-self-center"> you're losing money on this item</span>
<span class="verdict-badge verdict-below-cost">Below Cost</span><span class="small text-muted align-self-center">&mdash; you're losing money on this item</span>
</div>
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="verdict-badge verdict-low">Thin Margin</span><span class="small text-muted align-self-center"> above cost floor but below your target margin</span>
<span class="verdict-badge verdict-low">Thin Margin</span><span class="small text-muted align-self-center">&mdash; above cost floor but below your target margin</span>
</div>
<div class="d-flex flex-wrap gap-2 mb-2">
<span class="verdict-badge verdict-high">High</span><span class="small text-muted align-self-center"> significantly above typical market rates</span>
<span class="verdict-badge verdict-high">High</span><span class="small text-muted align-self-center">&mdash; significantly above typical market rates</span>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="verdict-badge verdict-ok">OK</span><span class="small text-muted align-self-center"> price is within a reasonable range</span>
<span class="verdict-badge verdict-ok">OK</span><span class="small text-muted align-self-center">&mdash; price is within a reasonable range</span>
</div>
<p class="small text-muted mb-0">
<i class="bi bi-exclamation-triangle me-1 text-warning"></i>
@@ -301,15 +301,15 @@ else
<div class="col-4 text-center">
<div class="small text-muted">Suggested</div>
<div class="fw-semibold text-primary">
@item.SuggestedPriceMin.ToString("C") @item.SuggestedPriceMax.ToString("C")
@item.SuggestedPriceMin.ToString("C") &ndash; @item.SuggestedPriceMax.ToString("C")
</div>
</div>
</div>
<div class="small text-muted mb-1">
<i class="bi bi-rulers me-1"></i>
Est. @item.EstimatedSqFtMin@item.EstimatedSqFtMax sqft &bull;
@item.EstimatedMinutesMin@item.EstimatedMinutesMax min
Est. @item.EstimatedSqFtMin&ndash;@item.EstimatedSqFtMax sqft &bull;
@item.EstimatedMinutesMin&ndash;@item.EstimatedMinutesMax min
</div>
<p class="small mb-1">@item.Reasoning</p>
@@ -20,7 +20,7 @@
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
<strong>Catalog item prices are fixed.</strong> The price you enter here is exactly what gets charged when this item is added to a quote or job no markup, no prep service charges, and no complexity adjustments are added on top. Make sure your price already includes labor, materials, and margin.
<strong>Catalog item prices are fixed.</strong> The price you enter here is exactly what gets charged when this item is added to a quote or job &mdash; no markup, no prep service charges, and no complexity adjustments are added on top. Make sure your price already includes labor, materials, and margin.
</div>
</div>
@@ -41,7 +41,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Item Name"
data-bs-content="Use a specific, recognizable name. The name appears on quotes, invoices, and in the picker dropdown. Good names include material and size where relevant e.g., 'Steel Bracket (6in)', '18in Alloy Wheel'.">
data-bs-content="Use a specific, recognizable name. The name appears on quotes, invoices, and in the picker dropdown. Good names include material and size where relevant &mdash; e.g., 'Steel Bracket (6in)', '18in Alloy Wheel'.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -75,7 +75,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Pricing"
data-bs-content="The Default Price is the exact amount charged when this item is added to a quote no markup, prep services, or complexity adjustments are applied on top. Set the all-in price you want to bill. Approximate Area is optional if set, it helps estimate powder needed for reporting purposes.">
data-bs-content="The Default Price is the exact amount charged when this item is added to a quote &mdash; no markup, prep services, or complexity adjustments are applied on top. Set the all-in price you want to bill. Approximate Area is optional &mdash; if set, it helps estimate powder needed for reporting purposes.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -87,7 +87,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Default Price"
data-bs-content="This is the final price charged to the customer no markup, prep services, or complexity charges are added on top. Enter the all-in amount you want to bill for this item. Staff can still override it on individual quotes if needed.">
data-bs-content="This is the final price charged to the customer &mdash; no markup, prep services, or complexity charges are added on top. Enter the all-in amount you want to bill for this item. Staff can still override it on individual quotes if needed.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -122,7 +122,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Financial Accounts"
data-bs-content="Links this item to your chart of accounts for accounting exports. Revenue Account is credited when invoiced; COGS Account is debited when materials are consumed. Leave both blank to use the company default accounts most shops only need to set these for items with special accounting treatment.">
data-bs-content="Links this item to your chart of accounts for accounting exports. Revenue Account is credited when invoiced; COGS Account is debited when materials are consumed. Leave both blank to use the company default accounts &mdash; most shops only need to set these for items with special accounting treatment.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -140,7 +140,7 @@
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
<option value="">(Default COGS account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Account debited when materials are consumed.</small>
</div>
@@ -164,7 +164,7 @@
<div class="mb-3">
<label for="image" class="form-label fw-semibold">Upload Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewCatalogImage(this)" />
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp max 10 MB. A 200×200 thumbnail is generated automatically.</div>
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp &mdash; max 10 MB. A 200×200 thumbnail is generated automatically.</div>
<div id="imagePreview" class="mt-2 d-none">
<img id="imagePreviewImg" src="" alt="Preview" style="max-width:200px;max-height:200px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;" />
</div>
@@ -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="Item Name"
data-bs-content="Use a specific, recognizable name. The name appears on quotes, invoices, and in the picker dropdown. Good names include material and size where relevant e.g., 'Steel Bracket (6in)', '18in Alloy Wheel'.">
data-bs-content="Use a specific, recognizable name. The name appears on quotes, invoices, and in the picker dropdown. Good names include material and size where relevant &mdash; e.g., 'Steel Bracket (6in)', '18in Alloy Wheel'.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -69,7 +69,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Pricing &amp; Status"
data-bs-content="The Default Price is the exact amount charged when this item is added to a quote no markup, prep services, or complexity adjustments are applied on top. Staff can override it on individual quotes. Set Status to Inactive to hide the item from the picker without deleting it.">
data-bs-content="The Default Price is the exact amount charged when this item is added to a quote &mdash; no markup, prep services, or complexity adjustments are applied on top. Staff can override it on individual quotes. Set Status to Inactive to hide the item from the picker without deleting it.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -81,7 +81,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Default Price"
data-bs-content="This is the final price charged to the customer no markup, prep services, or complexity charges are added on top. Enter the all-in amount you want to bill for this item. Staff can still override it on individual quotes if needed.">
data-bs-content="This is the final price charged to the customer &mdash; no markup, prep services, or complexity charges are added on top. Enter the all-in amount you want to bill for this item. Staff can still override it on individual quotes if needed.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -125,7 +125,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Financial Accounts"
data-bs-content="Links this item to your chart of accounts for accounting exports. Revenue Account is credited when invoiced; COGS Account is debited when materials are consumed. Leave both blank to use the company default accounts most shops only need to set these for items with special accounting treatment.">
data-bs-content="Links this item to your chart of accounts for accounting exports. Revenue Account is credited when invoiced; COGS Account is debited when materials are consumed. Leave both blank to use the company default accounts &mdash; most shops only need to set these for items with special accounting treatment.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -143,7 +143,7 @@
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
<option value="">(Default COGS account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Account debited when materials are consumed.</small>
</div>
@@ -186,7 +186,7 @@
<div class="mb-3">
<label for="image" class="form-label fw-semibold">Upload Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewCatalogImage(this)" />
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp max 10 MB. A 200×200 thumbnail is generated automatically.</div>
<div class="form-text">Accepted formats: jpg, jpeg, png, gif, webp &mdash; max 10 MB. A 200×200 thumbnail is generated automatically.</div>
<div id="imagePreview" class="mt-2 d-none">
<img id="imagePreviewImg" src="" alt="Preview" style="max-width:200px;max-height:200px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;" />
</div>
@@ -3,7 +3,7 @@
ViewData["Title"] = "Product Catalog";
ViewData["PageIcon"] = "bi-book";
ViewData["PageHelpTitle"] = "Product Catalog";
ViewData["PageHelpContent"] = "The Product Catalog is a library of standard items (wheels, brackets, panels, etc.) that your shop regularly quotes and invoices. Each item has a fixed price when a catalog item is added to a quote or job, that price is used exactly as entered. No markup, no prep services, and no complexity charges are added on top. Organize items into categories to keep the catalog easy to browse.";
ViewData["PageHelpContent"] = "The Product Catalog is a library of standard items (wheels, brackets, panels, etc.) that your shop regularly quotes and invoices. Each item has a fixed price &mdash; when a catalog item is added to a quote or job, that price is used exactly as entered. No markup, no prep services, and no complexity charges are added on top. Organize items into categories to keep the catalog easy to browse.";
var totalItemsCount = ViewBag.TotalItemsCount ?? 0;
var activeItemsCount = ViewBag.ActiveItemsCount ?? 0;
var averagePrice = ViewBag.AveragePrice ?? 0m;
@@ -148,7 +148,7 @@
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr><th style="width:40%">Company Name</th><td>@Model.CompanyName</td></tr>
<tr><th>Code</th><td>@(Model.CompanyCode ?? "")</td></tr>
<tr><th>Code</th><td>@(Model.CompanyCode ?? "&mdash;")</td></tr>
<tr><th>Status</th><td><span class="badge @(Model.IsActive ? "bg-success" : "bg-danger")">@(Model.IsActive ? "Active" : "Inactive")</span></td></tr>
<tr><th>Time Zone</th><td>@(Model.TimeZone ?? "America/New_York")</td></tr>
<tr><th>Created</th><td>@Model.CreatedAt.ToString("MMM d, yyyy h:mm tt")</td></tr>
@@ -174,7 +174,7 @@
<table class="table table-sm table-borderless mb-0">
<tr><th style="width:40%">Contact Name</th><td>@Model.PrimaryContactName</td></tr>
<tr><th>Email</th><td><a href="mailto:@Model.PrimaryContactEmail">@Model.PrimaryContactEmail</a></td></tr>
<tr><th>Phone</th><td>@(Model.Phone ?? "")</td></tr>
<tr><th>Phone</th><td>@(Model.Phone ?? "&mdash;")</td></tr>
</table>
</div>
</div>
@@ -283,7 +283,7 @@
}
else { <span class="text-muted">N/A</span> }
</td>
<td>@(user.Department ?? "")</td>
<td>@(user.Department ?? "&mdash;")</td>
<td>
<span class="badge @(user.IsActive ? "bg-success" : "bg-danger")">
@(user.IsActive ? "Active" : "Inactive")
@@ -527,20 +527,20 @@
@{
var firstActivity = onboarding.FirstJobCreatedAt ?? onboarding.FirstQuoteCreatedAt;
}
@(firstActivity.HasValue ? firstActivity.Value.ToString("MMM d, yyyy") : "")
@(firstActivity.HasValue ? firstActivity.Value.ToString("MMM d, yyyy") : "&mdash;")
</td>
</tr>
<tr>
<th>First Invoice</th>
<td>@(onboarding.FirstInvoiceCreatedAt.HasValue ? onboarding.FirstInvoiceCreatedAt.Value.ToString("MMM d, yyyy") : "")</td>
<td>@(onboarding.FirstInvoiceCreatedAt.HasValue ? onboarding.FirstInvoiceCreatedAt.Value.ToString("MMM d, yyyy") : "&mdash;")</td>
</tr>
<tr>
<th>Workflow Completed</th>
<td>@(onboarding.FirstWorkflowCompletedAt.HasValue ? onboarding.FirstWorkflowCompletedAt.Value.ToString("MMM d, yyyy") : "")</td>
<td>@(onboarding.FirstWorkflowCompletedAt.HasValue ? onboarding.FirstWorkflowCompletedAt.Value.ToString("MMM d, yyyy") : "&mdash;")</td>
</tr>
<tr>
<th>Widget Dismissed</th>
<td>@(onboarding.GuidedActivationDismissedAt.HasValue ? onboarding.GuidedActivationDismissedAt.Value.ToString("MMM d, yyyy") : "")</td>
<td>@(onboarding.GuidedActivationDismissedAt.HasValue ? onboarding.GuidedActivationDismissedAt.Value.ToString("MMM d, yyyy") : "&mdash;")</td>
</tr>
</table>
</div>
@@ -618,7 +618,7 @@
</div><!-- /tab-content -->
<!-- Danger Zone (outside tabs always present) -->
<!-- Danger Zone (outside tabs &mdash; always present) -->
<div class="card shadow-sm border-danger mt-4">
<div class="card-header bg-light">
<h6 class="card-title mb-0 text-danger">
@@ -630,7 +630,7 @@
<div>
<h6 class="mb-1 text-warning-emphasis"><i class="bi bi-fire me-1"></i>Reset All Company Data</h6>
<p class="text-muted small mb-0">
Permanently deletes all business data customers, jobs, quotes, invoices, inventory, and more.
Permanently deletes all business data &mdash; customers, jobs, quotes, invoices, inventory, and more.
The company record, users, and settings are preserved. Use this to wipe a migration and start fresh.
</p>
</div>
@@ -646,7 +646,7 @@
Permanently deletes the company and everything in it. 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) &mdash; remove them first.</strong>
}
</p>
</div>
@@ -672,7 +672,7 @@
</div>
<div class="modal-body p-0">
<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></div>
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading&hellip;</span></div>
</div>
<div id="oc-content" style="display:none;">
<div class="px-3 pt-3 pb-2">
@@ -688,14 +688,14 @@
<span id="oc-status-badge" class="badge ms-auto"></span>
</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">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">&mdash;</td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">&mdash;</td></tr>
</table>
</div>
<hr class="my-0" />
@@ -127,12 +127,12 @@
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">Feature Overrides</h5>
<p class="text-muted small mb-3">Override plan-level feature access for this company. Leave blank () to inherit from the subscription plan.</p>
<p class="text-muted small mb-3">Override plan-level feature access for this company. Leave blank (&mdash;) to inherit from the subscription plan.</p>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Online Payments</label>
<select asp-for="OnlinePaymentsOverride" class="form-select">
<option value=""> Use plan default </option>
<option value="">&mdash; Use plan default &mdash;</option>
<option value="true">Force Enable</option>
<option value="false">Force Disable</option>
</select>
@@ -141,7 +141,7 @@
<div class="col-md-6">
<label class="form-label fw-medium">Accounting Module</label>
<select asp-for="AccountingOverride" class="form-select">
<option value=""> Use plan default </option>
<option value="">&mdash; Use plan default &mdash;</option>
<option value="true">Force Enable</option>
<option value="false">Force Disable</option>
</select>
@@ -150,7 +150,7 @@
</div>
<h5 class="card-title mb-3 pb-2 border-bottom">SMS Override</h5>
<p class="text-muted small mb-3">Use this to immediately cut off SMS for a company for example if they are sending abusive messages or have a billing dispute. This overrides the plan entitlement and the company's own opt-in setting.</p>
<p class="text-muted small mb-3">Use this to immediately cut off SMS for a company &mdash; for example if they are sending abusive messages or have a billing dispute. This overrides the plan entitlement and the company's own opt-in setting.</p>
<div class="mb-4">
<div class="form-check form-switch">
<input asp-for="SmsDisabledByAdmin" class="form-check-input" type="checkbox" role="switch" id="SmsDisabledByAdmin" />
@@ -61,7 +61,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&hellip;"
value="@searchTerm" />
</div>
</div>
@@ -265,7 +265,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<!-- Mobile card view &mdash; 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)
@@ -321,7 +321,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)&ndash;@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
</div>
<div class="d-flex align-items-center gap-3">
<div>
@@ -448,7 +448,7 @@
<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 &mdash; users, jobs, quotes, customers, invoices, and everything else &mdash; will be
<strong>permanently and irreversibly deleted</strong> from the database.
</p>
<div class="alert alert-danger alert-permanent py-2 mb-3">
@@ -198,7 +198,7 @@
<form method="get" class="row g-2 align-items-end">
<div class="col-md-4">
<input name="search" value="@ViewBag.Search" class="form-control form-control-sm"
placeholder="Company name or email" />
placeholder="Company name or email&hellip;" />
</div>
<div class="col-md-3">
<select name="risk" class="form-select form-select-sm">
@@ -274,7 +274,7 @@
<td>
@if (h.RiskLevel == ChurnRisk.NeverActivated)
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
else
{
@@ -338,7 +338,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var h in Model)
@@ -394,7 +394,7 @@
@if (!Model.Any(h => h.RiskLevel != ChurnRisk.Healthy) && Model.Any())
{
<div class="card-footer text-center py-3 text-success">
<i class="bi bi-check-circle-fill me-2"></i>All tenants are healthy no churn signals detected.
<i class="bi bi-check-circle-fill me-2"></i>All tenants are healthy &mdash; no churn signals detected.
</div>
}
</div>
@@ -53,7 +53,7 @@
<ul class="list-unstyled mb-3">
<li class="mb-2">
<i class="bi bi-person-x-fill text-danger me-2"></i>
<strong>@userCount user account@(userCount != 1 ? "s" : "")</strong> will be deactivated no one will be able to log in.
<strong>@userCount user account@(userCount != 1 ? "s" : "")</strong> will be deactivated &mdash; no one will be able to log in.
</li>
<li class="mb-2">
<i class="bi bi-briefcase-fill text-danger me-2"></i>
@@ -90,13 +90,13 @@
<div class="card-body">
<ul class="mb-0">
<li class="mb-1">
<strong>Pause instead of delete</strong> Contact support to temporarily suspend your account.
<strong>Pause instead of delete</strong> &mdash; Contact support to temporarily suspend your account.
</li>
<li class="mb-1">
<strong>Cancel your subscription</strong> Stop future billing without deleting your data.
<strong>Cancel your subscription</strong> &mdash; Stop future billing without deleting your data.
</li>
<li>
<strong>Export your data first</strong> Go to
<strong>Export your data first</strong> &mdash; Go to
<a asp-controller="Reports" asp-action="Index">Reports</a>
to export jobs, customers, invoices, and more before proceeding.
</li>
@@ -146,7 +146,7 @@
<i class="bi bi-trash3 me-1"></i>Delete My Account Permanently
</button>
<a asp-controller="CompanySettings" asp-action="Index" class="btn btn-outline-secondary px-4">
Cancel Keep My Account
Cancel &mdash; Keep My Account
</a>
</div>
</form>
@@ -176,7 +176,7 @@
// Extra guard: prevent accidental double-submit after the form is submitted.
document.getElementById('deleteAccountForm').addEventListener('submit', function () {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Deleting';
deleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Deleting&hellip;';
});
})();
</script>
@@ -1,6 +1,6 @@
@model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto
@{
ViewData["Title"] = $"Edit Template {Model.DisplayName}";
ViewData["Title"] = $"Edit Template &mdash; {Model.DisplayName}";
ViewData["PageIcon"] = "bi-envelope-gear";
var placeholders = ViewBag.Placeholders as List<(string Placeholder, string Description)>
?? new List<(string, string)>();
@@ -70,7 +70,7 @@
@if (isEmail)
{
<!-- Raw HTML textarea for email supports {{placeholders}} and full HTML -->
<!-- Raw HTML textarea for email &mdash; supports {{placeholders}} and full HTML -->
<textarea asp-for="Body" class="form-control font-monospace" rows="16"
placeholder="Enter HTML email body..."></textarea>
<div class="form-text text-muted mt-1">
@@ -131,7 +131,7 @@
<div>
<span class="badge bg-light text-dark border placeholder-pill px-2 py-1"
onclick="copyPlaceholder('@placeholder', this)"
title="@description click to copy">
title="@description &mdash; click to copy">
@placeholder
</span>
<span class="copy-feedback ms-1">Copied!</span>
@@ -1,4 +1,4 @@
@model PowderCoating.Application.DTOs.Company.CompanySettingsDto
@model PowderCoating.Application.DTOs.Company.CompanySettingsDto
@{
ViewData["Title"] = "Company Settings";
ViewData["PageIcon"] = "bi-building";
@@ -118,7 +118,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Company Information"
data-bs-content="This information appears on every customer-facing document quotes, invoices, and PDFs. Keep the company name, address, and email accurate so customers see the right details. The &lt;strong&gt;Primary Contact Email&lt;/strong&gt; is used as the reply-to address on all outgoing notifications.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#company-information' target='_blank'&gt;Learn more →&lt;/a&gt;">
data-bs-content="This information appears on every customer-facing document &mdash; quotes, invoices, and PDFs. Keep the company name, address, and email accurate so customers see the right details. The &lt;strong&gt;Primary Contact Email&lt;/strong&gt; is used as the reply-to address on all outgoing notifications.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#company-information' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -165,39 +165,39 @@
<label for="timeZone" class="form-label">Time Zone</label>
<select class="form-select" id="timeZone" name="TimeZone">
<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) &mdash; New York</option>
<option value="America/Chicago" selected="@(Model.TimeZone == "America/Chicago" ? "selected" : null)">Central (CT) &mdash; Chicago</option>
<option value="America/Denver" selected="@(Model.TimeZone == "America/Denver" ? "selected" : null)">Mountain (MT) &mdash; Denver</option>
<option value="America/Phoenix" selected="@(Model.TimeZone == "America/Phoenix" ? "selected" : null)">Mountain no-DST &mdash; Phoenix</option>
<option value="America/Los_Angeles" selected="@(Model.TimeZone == "America/Los_Angeles" ? "selected" : null)">Pacific (PT) &mdash; Los Angeles</option>
<option value="America/Anchorage" selected="@(Model.TimeZone == "America/Anchorage" ? "selected" : null)">Alaska (AKT) &mdash; Anchorage</option>
<option value="Pacific/Honolulu" selected="@(Model.TimeZone == "Pacific/Honolulu" ? "selected" : null)">Hawaii (HT) &mdash; 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) &mdash; Halifax</option>
<option value="America/Toronto" selected="@(Model.TimeZone == "America/Toronto" ? "selected" : null)">Eastern &mdash; Toronto</option>
<option value="America/Winnipeg" selected="@(Model.TimeZone == "America/Winnipeg" ? "selected" : null)">Central &mdash; Winnipeg</option>
<option value="America/Edmonton" selected="@(Model.TimeZone == "America/Edmonton" ? "selected" : null)">Mountain &mdash; Edmonton</option>
<option value="America/Vancouver" selected="@(Model.TimeZone == "America/Vancouver" ? "selected" : null)">Pacific &mdash; 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 &mdash; London</option>
<option value="Europe/Paris" selected="@(Model.TimeZone == "Europe/Paris" ? "selected" : null)">CET &mdash; Paris / Berlin</option>
<option value="Europe/Helsinki" selected="@(Model.TimeZone == "Europe/Helsinki" ? "selected" : null)">EET &mdash; Helsinki</option>
<option value="Europe/Moscow" selected="@(Model.TimeZone == "Europe/Moscow" ? "selected" : null)">MSK &mdash; 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 &mdash; Dubai</option>
<option value="Asia/Kolkata" selected="@(Model.TimeZone == "Asia/Kolkata" ? "selected" : null)">IST &mdash; India</option>
<option value="Asia/Bangkok" selected="@(Model.TimeZone == "Asia/Bangkok" ? "selected" : null)">ICT &mdash; Bangkok</option>
<option value="Asia/Shanghai" selected="@(Model.TimeZone == "Asia/Shanghai" ? "selected" : null)">CST &mdash; Beijing / Shanghai</option>
<option value="Asia/Tokyo" selected="@(Model.TimeZone == "Asia/Tokyo" ? "selected" : null)">JST &mdash; Tokyo</option>
<option value="Australia/Sydney" selected="@(Model.TimeZone == "Australia/Sydney" ? "selected" : null)">AEST &mdash; Sydney</option>
<option value="Pacific/Auckland" selected="@(Model.TimeZone == "Pacific/Auckland" ? "selected" : null)">NZST &mdash; 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 &mdash; São Paulo</option>
<option value="America/Buenos_Aires" selected="@(Model.TimeZone == "America/Buenos_Aires" ? "selected" : null)">ART &mdash; Buenos Aires</option>
</optgroup>
<optgroup label="UTC">
<option value="UTC" selected="@(Model.TimeZone == "UTC" ? "selected" : null)">UTC</option>
@@ -243,7 +243,7 @@
}
else
{
<div class="text-muted small mb-2">No period lock set all dates are open.</div>
<div class="text-muted small mb-2">No period lock set &mdash; all dates are open.</div>
}
</div>
<div class="col-md-4">
@@ -353,7 +353,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Operating Costs"
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. &lt;strong&gt;New quotes use the current rates&lt;/strong&gt; changing a rate here does not retroactively reprice existing quotes.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#pricing-configuration' target='_blank'&gt;Learn more →&lt;/a&gt;">
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. &lt;strong&gt;New quotes use the current rates&lt;/strong&gt; &mdash; changing a rate here does not retroactively reprice existing quotes.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Settings#pricing-configuration' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -368,7 +368,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Rates &amp; Costs"
data-bs-content="&lt;strong&gt;Standard Labor Rate&lt;/strong&gt; is the baseline $/hr for all coating work sandblasting and masking are multiplied from this. &lt;strong&gt;Powder Coating Cost/sq ft&lt;/strong&gt; is the fallback material rate used when you don't select a specific powder inventory item on a quote item. &lt;strong&gt;Additional Coat Labor&lt;/strong&gt; is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
data-bs-content="&lt;strong&gt;Standard Labor Rate&lt;/strong&gt; is the baseline $/hr for all coating work &mdash; sandblasting and masking are multiplied from this. &lt;strong&gt;Powder Coating Cost/sq ft&lt;/strong&gt; is the fallback material rate used when you don't select a specific powder inventory item on a quote item. &lt;strong&gt;Additional Coat Labor&lt;/strong&gt; is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -506,7 +506,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Equipment Operating Costs"
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The &lt;strong&gt;Default Oven Rate&lt;/strong&gt; is used on quotes where no named oven is chosen add individual shop ovens below if you have multiple ovens with different capacities and costs.">
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The &lt;strong&gt;Default Oven Rate&lt;/strong&gt; is used on quotes where no named oven is chosen &mdash; add individual shop ovens below if you have multiple ovens with different capacities and costs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -554,7 +554,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Pricing &amp; Profit"
data-bs-content="&lt;strong&gt;Markup mode&lt;/strong&gt; adds a % on top of material costs only (labor and equipment pass through at cost). &lt;strong&gt;Margin mode&lt;/strong&gt; targets a gross margin % of the total selling price e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. &lt;strong&gt;Shop Minimum&lt;/strong&gt; sets a floor price for any job.">
data-bs-content="&lt;strong&gt;Markup mode&lt;/strong&gt; adds a % on top of material costs only (labor and equipment pass through at cost). &lt;strong&gt;Margin mode&lt;/strong&gt; targets a gross margin % of the total selling price &mdash; e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. &lt;strong&gt;Shop Minimum&lt;/strong&gt; sets a floor price for any job.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -678,7 +678,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Part Complexity Multipliers"
data-bs-content="A percentage added to the price of &lt;strong&gt;calculated items&lt;/strong&gt; based on how intricate the part is. When adding an item in a quote, staff select a complexity level the system then applies this multiplier to account for the extra time and care needed. &lt;em&gt;Simple&lt;/em&gt; = 0% (flat panels, basic shapes). &lt;em&gt;Extreme&lt;/em&gt; = highly detailed, tight recesses, masking-intensive parts.">
data-bs-content="A percentage added to the price of &lt;strong&gt;calculated items&lt;/strong&gt; based on how intricate the part is. When adding an item in a quote, staff select a complexity level &mdash; the system then applies this multiplier to account for the extra time and care needed. &lt;em&gt;Simple&lt;/em&gt; = 0% (flat panels, basic shapes). &lt;em&gt;Extreme&lt;/em&gt; = highly detailed, tight recesses, masking-intensive parts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -775,11 +775,11 @@
</div>
<div class="d-flex align-items-center gap-1 ms-auto">
<span class="text-muted">=</span>
<span id="ovenDimResult" class="fw-semibold small text-primary" style="min-width:65px;"></span>
<span id="ovenDimResult" class="fw-semibold small text-primary" style="min-width:65px;">&mdash;</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="ovenDimApply" disabled>Use</button>
</div>
</div>
<div class="text-muted mt-1" style="font-size:.72rem;">W × D × H of oven interior 20% deducted for rack &amp; wall depth</div>
<div class="text-muted mt-1" style="font-size:.72rem;">W × D × H of oven interior &mdash; 20% deducted for rack &amp; wall depth</div>
</div>
<div class="mb-3">
<label for="ovenOrderInput" class="form-label">Display Order</label>
@@ -810,7 +810,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="AI Photo Quote Profile"
data-bs-content="Describe your shop's specialties, typical items, and pricing style in plain language. This text is injected into the AI's system prompt every time a photo quote is analysed the more specific you are, the better calibrated the estimates will be for your business. You can also describe anything the AI tends to get wrong. &lt;br&gt;&lt;br&gt;&lt;strong&gt;Additionally&lt;/strong&gt;, the AI automatically learns from quotes your team accepted without overriding those become calibration examples that improve accuracy over time.">
data-bs-content="Describe your shop's specialties, typical items, and pricing style in plain language. This text is injected into the AI's system prompt every time a photo quote is analysed &mdash; the more specific you are, the better calibrated the estimates will be for your business. You can also describe anything the AI tends to get wrong. &lt;br&gt;&lt;br&gt;&lt;strong&gt;Additionally&lt;/strong&gt;, the AI automatically learns from quotes your team accepted without overriding &mdash; those become calibration examples that improve accuracy over time.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -821,9 +821,9 @@
<div class="mb-3">
<label for="aiContextProfile" class="form-label fw-semibold">Shop Description</label>
<textarea id="aiContextProfile" class="form-control" rows="8" maxlength="2000"
placeholder="Examples:&#10;• We specialise in automotive restoration wheels, frames, suspension brackets, and roll cages are our bread and butter.&#10;• Our customers expect premium pricing. We rarely work on items over 20 sqft.&#10;• Most items come to us already stripped; sandblasting adds roughly 15 min per item on average.&#10;• We use a 2-stage cure cycle pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)</textarea>
placeholder="Examples:&#10;• We specialise in automotive restoration &mdash; wheels, frames, suspension brackets, and roll cages are our bread and butter.&#10;• Our customers expect premium pricing. We rarely work on items over 20 sqft.&#10;• Most items come to us already stripped; sandblasting adds roughly 15 min per item on average.&#10;• We use a 2-stage cure cycle &mdash; pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)</textarea>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted">Plain language write it as if briefing a new estimator on your shop.</small>
<small class="text-muted">Plain language &mdash; write it as if briefing a new estimator on your shop.</small>
<small class="text-muted"><span id="aiProfileCharCount">@(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)</span>/2000</small>
</div>
</div>
@@ -832,7 +832,7 @@
<i class="bi bi-floppy me-1"></i> Save AI Profile
</button>
<button type="button" class="btn btn-outline-secondary" id="btnGenerateAiDraft"
title="Build a suggested profile from your existing settings ovens, workers, inventory categories, and rates">
title="Build a suggested profile from your existing settings &mdash; ovens, workers, inventory categories, and rates">
<i class="bi bi-stars me-1"></i> Generate from my settings
</button>
<span id="aiProfileStatus" class="small"></span>
@@ -843,9 +843,9 @@
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-lightbulb text-warning me-1"></i> How AI Learning Works</h6>
<p class="small mb-2"><strong>Layer 1 Pricing config:</strong> Your operating costs (labor, equipment, markup) are always injected automatically.</p>
<p class="small mb-2"><strong>Layer 2 Your shop profile:</strong> The description you write here is added to every AI analysis, guiding estimates toward your typical work.</p>
<p class="small mb-0"><strong>Layer 3 Automatic learning:</strong> Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.</p>
<p class="small mb-2"><strong>Layer 1 &mdash; Pricing config:</strong> Your operating costs (labor, equipment, markup) are always injected automatically.</p>
<p class="small mb-2"><strong>Layer 2 &mdash; Your shop profile:</strong> The description you write here is added to every AI analysis, guiding estimates toward your typical work.</p>
<p class="small mb-0"><strong>Layer 3 &mdash; Automatic learning:</strong> Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.</p>
</div>
</div>
</div>
@@ -874,10 +874,10 @@
<div class="mb-3">
<label class="form-label fw-medium">Shop Size</label>
<select class="form-select" id="shopCapabilityTier" style="max-width:320px">
<option value="0" selected="@(tierVal == 0 ? "selected" : null)">Garage Home setup, part-time</option>
<option value="1" selected="@(tierVal == 1 ? "selected" : null)">Small — 15 person shop</option>
<option value="2" selected="@(tierVal == 2 ? "selected" : null)">Medium Established shop, 510 people</option>
<option value="3" selected="@(tierVal == 3 ? "selected" : null)">Large High-volume, 10+ people</option>
<option value="0" selected="@(tierVal == 0 ? "selected" : null)">Garage &mdash; Home setup, part-time</option>
<option value="1" selected="@(tierVal == 1 ? "selected" : null)">Small &mdash; 1&ndash;5 person shop</option>
<option value="2" selected="@(tierVal == 2 ? "selected" : null)">Medium &mdash; Established shop, 5&ndash;10 people</option>
<option value="3" selected="@(tierVal == 3 ? "selected" : null)">Large &mdash; High-volume, 10+ people</option>
</select>
<div class="form-text">Used by the AI when estimating job complexity and throughput.</div>
</div>
@@ -978,11 +978,11 @@
<div class="mb-3">
<label class="form-label">Default Currency</label>
<select class="form-select" id="defaultCurrency" name="DefaultCurrency">
<option value="USD" selected="@(Model.Preferences?.DefaultCurrency == "USD" ? "selected" : null)">USD US Dollar</option>
<option value="CAD" selected="@(Model.Preferences?.DefaultCurrency == "CAD" ? "selected" : null)">CAD Canadian Dollar</option>
<option value="EUR" selected="@(Model.Preferences?.DefaultCurrency == "EUR" ? "selected" : null)">EUR Euro</option>
<option value="GBP" selected="@(Model.Preferences?.DefaultCurrency == "GBP" ? "selected" : null)">GBP British Pound</option>
<option value="AUD" selected="@(Model.Preferences?.DefaultCurrency == "AUD" ? "selected" : null)">AUD Australian Dollar</option>
<option value="USD" selected="@(Model.Preferences?.DefaultCurrency == "USD" ? "selected" : null)">USD &mdash; US Dollar</option>
<option value="CAD" selected="@(Model.Preferences?.DefaultCurrency == "CAD" ? "selected" : null)">CAD &mdash; Canadian Dollar</option>
<option value="EUR" selected="@(Model.Preferences?.DefaultCurrency == "EUR" ? "selected" : null)">EUR &mdash; Euro</option>
<option value="GBP" selected="@(Model.Preferences?.DefaultCurrency == "GBP" ? "selected" : null)">GBP &mdash; British Pound</option>
<option value="AUD" selected="@(Model.Preferences?.DefaultCurrency == "AUD" ? "selected" : null)">AUD &mdash; Australian Dollar</option>
</select>
</div>
</div>
@@ -1046,7 +1046,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Number Prefixes"
data-bs-content="The prefix is combined with a date stamp and sequence number to form record IDs for example prefix &lt;strong&gt;QT&lt;/strong&gt; produces &lt;em&gt;QT-2603-0042&lt;/em&gt;. Change the prefix to match your preferred numbering convention. Changing it only affects &lt;strong&gt;new&lt;/strong&gt; records; existing numbers are not renamed.">
data-bs-content="The prefix is combined with a date stamp and sequence number to form record IDs &mdash; for example prefix &lt;strong&gt;QT&lt;/strong&gt; produces &lt;em&gt;QT-2603-0042&lt;/em&gt;. Change the prefix to match your preferred numbering convention. Changing it only affects &lt;strong&gt;new&lt;/strong&gt; records; existing numbers are not renamed.">
<i class="bi bi-question-circle"></i>
</a>
</h6>
@@ -1082,7 +1082,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Job &amp; Workflow Defaults"
data-bs-content="Controls how jobs are created and flow through your shop. &lt;strong&gt;Require Customer PO&lt;/strong&gt; enforces that a PO number is entered before a job can be saved useful for commercial accounts. &lt;strong&gt;Allow Customer Approval&lt;/strong&gt; enables the approval step in the job workflow when a quote is approved, the job moves to an Approved status before work begins.">
data-bs-content="Controls how jobs are created and flow through your shop. &lt;strong&gt;Require Customer PO&lt;/strong&gt; enforces that a PO number is entered before a job can be saved &mdash; useful for commercial accounts. &lt;strong&gt;Allow Customer Approval&lt;/strong&gt; enables the approval step in the job workflow &mdash; when a quote is approved, the job moves to an Approved status before work begins.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -1141,7 +1141,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Notifications &amp; Alerts"
data-bs-content="Controls which events send emails to your team and customers. Set the &lt;strong&gt;From Email Address&lt;/strong&gt; to a domain you control using a domain-verified address prevents emails landing in spam. If left blank, the system default address is used. Turn off notification types you don't need to avoid inbox noise.">
data-bs-content="Controls which events send emails to your team and customers. Set the &lt;strong&gt;From Email Address&lt;/strong&gt; to a domain you control &mdash; using a domain-verified address prevents emails landing in spam. If left blank, the system default address is used. Turn off notification types you don't need to avoid inbox noise.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -1311,7 +1311,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Notification Templates"
data-bs-content="Customise the subject and body of every automated email sent by the system job status updates, quote approvals, invoice reminders, and more. Templates use &lt;strong&gt;&#123;&#123;placeholder&#125;&#125;&lt;/strong&gt; tokens that are replaced with live data when the email is sent. Click &lt;strong&gt;Edit&lt;/strong&gt; on any row to modify it; use &lt;strong&gt;Reset to Default&lt;/strong&gt; to restore the original wording at any time.&lt;br&gt;&lt;br&gt;Changes take effect immediately the next triggered notification will use the updated template.">
data-bs-content="Customise the subject and body of every automated email sent by the system &mdash; job status updates, quote approvals, invoice reminders, and more. Templates use &lt;strong&gt;&#123;&#123;placeholder&#125;&#125;&lt;/strong&gt; tokens that are replaced with live data when the email is sent. Click &lt;strong&gt;Edit&lt;/strong&gt; on any row to modify it; use &lt;strong&gt;Reset to Default&lt;/strong&gt; to restore the original wording at any time.&lt;br&gt;&lt;br&gt;Changes take effect immediately &mdash; the next triggered notification will use the updated template.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -1371,7 +1371,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Data Retention"
data-bs-content="Controls how long records are kept. Most businesses set quote and job retention to &lt;strong&gt;7 years&lt;/strong&gt; to satisfy tax and audit requirements. &lt;strong&gt;Deleted record retention&lt;/strong&gt; is the grace period after a soft-delete before the record is permanently purged useful if someone accidentally deletes something.">
data-bs-content="Controls how long records are kept. Most businesses set quote and job retention to &lt;strong&gt;7 years&lt;/strong&gt; to satisfy tax and audit requirements. &lt;strong&gt;Deleted record retention&lt;/strong&gt; is the grace period after a soft-delete before the record is permanently purged &mdash; useful if someone accidentally deletes something.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -1433,7 +1433,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Data Lookups"
data-bs-content="Lookups are the dropdown options that appear throughout the app job statuses, priorities, quote statuses, and more. You can rename labels, change colours, and reorder them to match your shop's terminology. &lt;strong&gt;Status codes&lt;/strong&gt; drive workflow logic and should not be changed unless you understand the impact.">
data-bs-content="Lookups are the dropdown options that appear throughout the app &mdash; job statuses, priorities, quote statuses, and more. You can rename labels, change colours, and reorder them to match your shop's terminology. &lt;strong&gt;Status codes&lt;/strong&gt; drive workflow logic and should not be changed unless you understand the impact.">
<i class="bi bi-question-circle"></i>
</a>
</h5>
@@ -1869,7 +1869,7 @@
<textarea id="woTerms" class="form-control" rows="5" maxlength="2000"
placeholder="e.g. *Products must be picked up within 5 days of notification of completion or a storage fee may apply."
>@(Model.Preferences?.WoTerms ?? "")</textarea>
<div class="form-text">Printed in italic at the bottom of every blank work order. Supports plain text use * or ** for visual emphasis.</div>
<div class="form-text">Printed in italic at the bottom of every blank work order. Supports plain text &mdash; use * or ** for visual emphasis.</div>
</div>
<div class="d-flex gap-2 align-items-center">
@@ -2168,7 +2168,7 @@
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="smsTermsModalLabel">
<i class="bi bi-phone me-2"></i>SMS Notifications Terms of Service
<i class="bi bi-phone me-2"></i>SMS Notifications &mdash; Terms of Service
</h5>
</div>
<div class="modal-body">
@@ -2178,9 +2178,9 @@
</div>
<h6 class="fw-bold">1. Prior Express Written Consent Required</h6>
<p class="text-muted small">You <strong>must obtain clear, documented consent</strong> from each customer before sending them any SMS message. This means each customer must have explicitly agreed in writing or through a recorded digital interaction that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.</p>
<p class="text-muted small">You <strong>must obtain clear, documented consent</strong> from each customer before sending them any SMS message. This means each customer must have explicitly agreed &mdash; in writing or through a recorded digital interaction &mdash; that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.</p>
<h6 class="fw-bold">2. Federal Law Governs SMS Fines Are Real</h6>
<h6 class="fw-bold">2. Federal Law Governs SMS &mdash; Fines Are Real</h6>
<p class="text-muted small">The <strong>Telephone Consumer Protection Act (TCPA)</strong>, enforced by the Federal Communications Commission (FCC), imposes fines of <strong>$500 to $1,500 per individual message</strong> sent without proper authorization. These fines apply per text, not per customer. A single campaign to 100 unconsented recipients could result in exposure of $50,000 to $150,000. The FCC and private plaintiffs both actively pursue TCPA violations.</p>
<h6 class="fw-bold">3. Opt-Out Requests Must Be Honored Immediately</h6>
@@ -2189,7 +2189,7 @@
<h6 class="fw-bold">4. Message Rates &amp; Content Restrictions</h6>
<p class="text-muted small">Every message sent must include your business name and an opt-out reminder (e.g., "Reply STOP to opt out"). Messages must be directly relevant to the service the customer consented to receive and must not contain solicitations, promotions, or third-party offers unless the customer has separately consented to those.</p>
<h6 class="fw-bold">5. Your Responsibility Not Ours</h6>
<h6 class="fw-bold">5. Your Responsibility &mdash; Not Ours</h6>
<p class="text-muted small">Powder Coating Logix provides this feature as a communication tool only. <strong>We are not responsible for how you use it.</strong> You agree that your company is solely responsible for obtaining proper consent, maintaining records of that consent, honoring opt-outs, and ensuring all outbound messages comply with the TCPA, FCC regulations, and any applicable state laws. You agree to indemnify and hold Powder Coating Logix harmless from any claims, fines, or damages arising from your company's use of SMS.</p>
<hr />
@@ -2201,7 +2201,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="smsTermsDeclineBtn">Cancel Keep SMS Disabled</button>
<button type="button" class="btn btn-secondary" id="smsTermsDeclineBtn">Cancel &mdash; Keep SMS Disabled</button>
<button type="button" class="btn btn-primary" id="smsTermsAcceptBtn" disabled>
<i class="bi bi-check-circle me-1"></i>I Agree &amp; Enable SMS
</button>
@@ -2469,7 +2469,7 @@
});
});
// AI Profile char counter and save (elements only exist when AiPhotoQuotesEnabled)
// AI Profile &mdash; char counter and save (elements only exist when AiPhotoQuotesEnabled)
$('#aiContextProfile').on('input', function () {
$('#aiProfileCharCount').text($(this).val().length);
});
@@ -2511,7 +2511,7 @@
if (response.success) {
$('#aiContextProfile').val(response.draft);
$('#aiProfileCharCount').text(response.draft.length);
showToast('info', 'Draft generated review and edit it, then click Save AI Profile.');
showToast('info', 'Draft generated &mdash; review and edit it, then click Save AI Profile.');
} else {
showToast('error', response.message);
}
@@ -2525,7 +2525,7 @@
});
});
// Quoting Calibration save
// Quoting Calibration &mdash; save
$('#saveBlastProfile').on('click', function () {
var btn = $(this);
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Saving...');
@@ -2631,7 +2631,7 @@
};
}, 'Save Retention Policy');
// SMS toggle shows terms modal on first enable (or after terms version change)
// SMS toggle &mdash; shows terms modal on first enable (or after terms version change)
(function () {
const toggle = document.getElementById('smsEnabledToggle');
if (!toggle) return;
@@ -2737,7 +2737,7 @@
<script src="~/js/company-settings-lookups-modals.js" asp-append-version="true"></script>
<script>
// ── Oven Costs Management ──────────────────────────────────────────────
// Cache element references once modal is now outside all forms
// Cache element references once &mdash; modal is now outside all forms
const _ovenModal = new bootstrap.Modal(document.getElementById('ovenModal'));
const _ovenTitle = document.getElementById('ovenModalTitle');
@@ -2765,7 +2765,7 @@
if (!data.success) throw new Error(data.message);
renderOvenTable(data.data);
_ovenCount.textContent = data.data.length === 0
? 'No shop ovens default rate will be used on all quotes.'
? 'No shop ovens &mdash; default rate will be used on all quotes.'
: `${data.data.length} oven(s) configured`;
} catch (e) {
_ovenBody.innerHTML = `<tr><td colspan="6" class="text-center text-danger">Failed to load ovens: ${escHtml(e.message)}</td></tr>`;
@@ -2783,7 +2783,7 @@
<tr>
<td><strong>${escHtml(o.label)}</strong></td>
<td>$${parseFloat(o.costPerHour).toFixed(2)}/hr</td>
<td class="text-muted small">${o.maxLoadSqFt != null ? parseFloat(o.maxLoadSqFt).toFixed(0) + ' sqft' : ''}</td>
<td class="text-muted small">${o.maxLoadSqFt != null ? parseFloat(o.maxLoadSqFt).toFixed(0) + ' sqft' : '&mdash;'}</td>
<td>${o.displayOrder}</td>
<td>${o.isActive
? '<span class="badge bg-success">Active</span>'
@@ -2887,7 +2887,7 @@
document.getElementById('ovenCalcToggle').addEventListener('click', function (e) {
e.preventDefault();
const hidden = _calcPanel.classList.toggle('d-none');
if (!hidden) { _calcW.value = ''; _calcD.value = ''; _calcH.value = ''; _calcResult.textContent = ''; _calcApply.disabled = true; _calcW.focus(); }
if (!hidden) { _calcW.value = ''; _calcD.value = ''; _calcH.value = ''; _calcResult.textContent = '&mdash;'; _calcApply.disabled = true; _calcW.focus(); }
});
function _updateCalc() {
@@ -2907,7 +2907,7 @@
_calcApply.disabled = false;
_calcApply.dataset.val = val;
} else {
_calcResult.textContent = '';
_calcResult.textContent = '&mdash;';
_calcApply.disabled = true;
}
}
@@ -2925,7 +2925,7 @@
document.getElementById('ovenModal').addEventListener('hidden.bs.modal', function () {
_calcPanel.classList.add('d-none');
_calcW.value = ''; _calcD.value = ''; _calcH.value = '';
_calcResult.textContent = ''; _calcApply.disabled = true;
_calcResult.textContent = '&mdash;'; _calcApply.disabled = true;
});
// ─────────────────────────────────────────────────────────────────────
@@ -3108,7 +3108,7 @@
onmouseenter="this.classList.replace('bg-light','bg-primary');this.classList.replace('text-dark','text-white')"
onmouseleave="this.classList.replace('bg-primary','bg-light');this.classList.replace('text-white','text-dark')"
onclick="ntplCopyPlaceholder('${p.placeholder}', this)"
title="${p.description} click to copy">
title="${p.description} &mdash; click to copy">
${p.placeholder}
</span>
<span class="ms-1 text-success small" style="display:none;">Copied!</span>
@@ -435,13 +435,13 @@
<div class="col-sm-6">
<label for="blastSetupNozzleSize" class="form-label">Nozzle Size</label>
<select class="form-select blast-modal-input" id="blastSetupNozzleSize">
<option value="2">#2 (1/8") Very small / entry level</option>
<option value="3">#3 (3/16") Small / hobby</option>
<option value="4">#4 (1/4") Light duty</option>
<option value="5" selected>#5 (5/16") Medium (most common)</option>
<option value="6">#6 (3/8") Heavy duty</option>
<option value="7">#7 (7/16") High volume</option>
<option value="8">#8 (1/2") Industrial</option>
<option value="2">#2 (1/8") &mdash; Very small / entry level</option>
<option value="3">#3 (3/16") &mdash; Small / hobby</option>
<option value="4">#4 (1/4") &mdash; Light duty</option>
<option value="5" selected>#5 (5/16") &mdash; Medium (most common)</option>
<option value="6">#6 (3/8") &mdash; Heavy duty</option>
<option value="7">#7 (7/16") &mdash; High volume</option>
<option value="8">#8 (1/2") &mdash; Industrial</option>
</select>
</div>
<div class="col-sm-6">
@@ -465,7 +465,7 @@
<div class="col-sm-6 d-flex align-items-end">
<div class="w-100 p-3 bg-light rounded text-center">
<div class="text-muted small">Derived Rate</div>
<div class="fw-bold fs-5" id="blastSetupDerivedRate"></div>
<div class="fw-bold fs-5" id="blastSetupDerivedRate">&mdash;</div>
<div class="text-muted small">sqft/hr</div>
</div>
</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 &mdash; 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 &mdash; 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>
@@ -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 &mdash; 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 &mdash; 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 &mdash; 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>
@@ -55,7 +55,7 @@
</div>
<div class="flex-grow-1">
<div class="fw-semibold">AI Assistant</div>
<div class="text-muted small">Ask anything available 24/7 in the bottom-right corner.</div>
<div class="text-muted small">Ask anything &mdash; available 24/7 in the bottom-right corner.</div>
</div>
<button type="button" class="btn btn-sm btn-outline-success flex-shrink-0"
onclick="document.getElementById('aiHelpTrigger')?.click()">
@@ -122,7 +122,7 @@
<div class="col-md-6">
<label asp-for="Form.Category" class="form-label fw-semibold"></label>
<select asp-for="Form.Category" class="form-select">
<option value=""> Select a Category </option>
<option value="">&mdash; Select a Category &mdash;</option>
@foreach (var cat in ContactFormModel.Categories)
{
<option value="@cat" selected="@(Model.Form.Category == cat)">@cat</option>
@@ -105,7 +105,7 @@ else
<div class="mb-0">
<label class="form-label fw-semibold">Admin Note <span class="text-muted fw-normal">(optional)</span></label>
<textarea name="adminNotes" class="form-control" rows="3"
placeholder="e.g. Replied via email, escalated to billing, resolved"></textarea>
placeholder="e.g. Replied via email, escalated to billing, resolved&hellip;"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -33,7 +33,7 @@
<div class="mb-3">
<label asp-for="CustomerId" class="form-label">Customer <span class="text-danger">*</span></label>
<select asp-for="CustomerId" asp-items="customers" class="form-select">
<option value="0"> select customer </option>
<option value="0">&mdash; select customer &mdash;</option>
</select>
<span asp-validation-for="CustomerId" class="text-danger small"></span>
</div>
@@ -51,7 +51,7 @@
<div class="mb-3">
<label asp-for="Reason" class="form-label">Reason <span class="text-danger">*</span></label>
<input asp-for="Reason" class="form-control"
placeholder="e.g. Price adjustment, billing error, goodwill credit" />
placeholder="e.g. Price adjustment, billing error, goodwill credit&hellip;" />
<span asp-validation-for="Reason" class="text-danger small"></span>
</div>
@@ -65,7 +65,7 @@
<div class="mb-4">
<label asp-for="ExpiryDate" class="form-label">
Expiry Date
<span class="text-muted small ms-1">(optional leave blank for no expiry)</span>
<span class="text-muted small ms-1">(optional &mdash; leave blank for no expiry)</span>
</label>
<input asp-for="ExpiryDate" type="date" class="form-control" />
<span asp-validation-for="ExpiryDate" class="text-danger small"></span>
@@ -181,7 +181,7 @@
</td>
<td>@a.AppliedDate.ToLocalTime().ToString("MM/dd/yyyy")</td>
<td class="text-end fw-semibold text-success">@a.AmountApplied.ToString("C")</td>
<td class="small text-muted">@(a.AppliedBy?.FullName ?? "")</td>
<td class="small text-muted">@(a.AppliedBy?.FullName ?? "&mdash;")</td>
</tr>
}
</tbody>
@@ -220,12 +220,12 @@
<div class="mb-3">
<label class="form-label">Select Invoice</label>
<select name="invoiceId" id="applyInvoiceId" class="form-select" required>
<option value=""> choose invoice </option>
<option value="">&mdash; choose invoice &mdash;</option>
@foreach (var inv in openInvoices)
{
<option value="@inv.Id"
data-balance="@inv.BalanceDue.ToString("F2")">
@inv.InvoiceNumber Due @inv.BalanceDue.ToString("C")
@inv.InvoiceNumber &mdash; Due @inv.BalanceDue.ToString("C")
@if (inv.DueDate.HasValue && inv.DueDate.Value < DateTime.UtcNow)
{ <text>(Overdue)</text> }
</option>
@@ -73,7 +73,7 @@
<div class="col-md-4">
<label class="form-label small mb-1">Search</label>
<input name="search" value="@search" class="form-control form-control-sm"
placeholder="Customer, memo #, or reason" />
placeholder="Customer, memo #, or reason&hellip;" />
</div>
<div class="col-md-3">
<label class="form-label small mb-1">Status</label>
@@ -204,7 +204,7 @@ else
</td>
<td>@m.IssueDate.ToLocalTime().ToString("MM/dd/yyyy")</td>
<td class="@(expired ? "text-danger fw-semibold" : "")">
@(m.ExpiryDate.HasValue ? m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yyyy") : "")
@(m.ExpiryDate.HasValue ? m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yyyy") : "&mdash;")
@if (expired) { <small>(Expired)</small> }
</td>
<td>
@@ -239,7 +239,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="fw-semibold">@job.FinalPrice.ToString("C")</td>
@@ -267,7 +267,7 @@
{
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
<div class="text-muted small">
Showing @jobs.StartIndex@jobs.EndIndex of @jobs.TotalCount jobs
Showing @jobs.StartIndex&ndash;@jobs.EndIndex of @jobs.TotalCount jobs
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-1">
@@ -399,7 +399,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="fw-semibold">@quote.Total.ToString("C")</td>
@@ -426,7 +426,7 @@
{
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
<div class="text-muted small">
Showing @quotes.StartIndex@quotes.EndIndex of @quotes.TotalCount quotes
Showing @quotes.StartIndex&ndash;@quotes.EndIndex of @quotes.TotalCount quotes
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-1">
@@ -25,7 +25,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. Leave it blank for individual (non-business) customers. Customer Type controls which features are available Commercial customers get payment terms, credit limits, and pricing tier discounts; Individual customers are for simpler one-off work.">
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Leave it blank for individual (non-business) customers. Customer Type controls which features are available &mdash; Commercial customers get payment terms, credit limits, and pricing tier discounts; Individual customers are for simpler one-off work.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -59,7 +59,7 @@
</h5>
<div class="alert alert-info alert-permanent py-2 px-3 mb-3" style="font-size:.875rem;">
<i class="bi bi-info-circle me-1"></i>
<strong>Required:</strong> At least one of Company Name, First Name, or Last Name and at least one of Email, Phone, or Mobile Phone.
<strong>Required:</strong> At least one of Company Name, First Name, or Last Name &mdash; and at least one of Email, Phone, or Mobile Phone.
</div>
<div class="row g-3">
<div class="col-md-6">
@@ -216,7 +216,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="These fields govern billing and compliance. Payment Terms sets the default for invoices (e.g., Net 30 = payment due within 30 days). Credit Limit is a soft cap on outstanding balance the system will warn when exceeded. Tax Exempt removes tax from all invoices for this customer (upload the exemption certificate on the Edit page).">
data-bs-content="These fields govern billing and compliance. Payment Terms sets the default for invoices (e.g., Net 30 = payment due within 30 days). Credit Limit is a soft cap on outstanding balance &mdash; the system will warn when exceeded. Tax Exempt removes tax from all invoices for this customer (upload the exemption certificate on the Edit page).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -232,7 +232,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Payment Terms"
data-bs-content="Sets the default due date on invoices for this customer. 'Net 30' means payment is due 30 days after the invoice date. This is a default you can override it on individual invoices.">
data-bs-content="Sets the default due date on invoices for this customer. 'Net 30' means payment is due 30 days after the invoice date. This is a default &mdash; you can override it on individual invoices.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -250,7 +250,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="">&mdash; No tier &mdash;</option>
</select>
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
</div>
@@ -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 &mdash; invoices go to contact email</span>
<span class="text-muted">Not set &mdash; invoices go to contact email</span>
}
</p>
</div>
@@ -531,7 +531,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="">&mdash; Select reason &mdash;</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 &mdash; 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 &mdash; 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="">&mdash; No tier &mdash;</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 &mdash; 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 &mdash; 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 &mdash; 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)
@@ -116,7 +116,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@@ -128,7 +128,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@@ -140,7 +140,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@@ -224,7 +224,7 @@
}
<div class="mobile-card-row">
<span class="mobile-card-label">Phone</span>
<span class="mobile-card-value">@(customer.Phone ?? "")</span>
<span class="mobile-card-value">@(customer.Phone ?? "&mdash;")</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Type</span>
@@ -102,7 +102,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="align-middle text-end fw-semibold">@inv.Total.ToString("C")</td>
@@ -1,12 +1,12 @@
@model PowderCoating.Application.DTOs.Accounting.CustomerStatementDto
@{
ViewData["Title"] = $"Statement {Model.CustomerName}";
ViewData["Title"] = $"Statement &ndash; {Model.CustomerName}";
}
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">
<div>
<h4 class="mb-0">Customer Statement</h4>
<p class="text-muted mb-0">@Model.CustomerName &nbsp;·&nbsp; @Model.From.ToString("MMM d, yyyy") @Model.To.ToString("MMM d, yyyy")</p>
<p class="text-muted mb-0">@Model.CustomerName &nbsp;·&nbsp; @Model.From.ToString("MMM d, yyyy") &ndash; @Model.To.ToString("MMM d, yyyy")</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<form method="get" class="d-flex gap-2 align-items-center">
@@ -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> &mdash; @_attnCount item@(_attnCount == 1 ? "" : "s") need attention.</span>
}
else
{
@@ -70,7 +70,7 @@
</div>
</div>
@* PWA install banner rendered by JS only on mobile, hidden once dismissed or already installed *@
@* PWA install banner &mdash; 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"
@@ -115,7 +115,7 @@
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
}
@* Config health alert only shown when there are setup gaps *@
@* Config health alert &mdash; only shown when there are setup gaps *@
@if (configHealth != null && !configHealth.IsHealthy)
{
<div class="row mb-4">
@@ -417,15 +417,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&ndash;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&ndash;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&ndash;90d @Model.AgingDays61To90.ToString("C0")</span>
}
@if (Model.AgingDaysOver90 > 0)
{
@@ -547,7 +547,7 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted"></span>}
{<span class="text-muted">&mdash;</span>}
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger mark-ordered-btn text-nowrap"
@@ -563,7 +563,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") : "&mdash;")</td>
<td></td>
</tr>
</tfoot>
@@ -582,7 +582,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 &mdash; 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>
@@ -641,13 +641,13 @@
@if (line.EstCost.HasValue)
{<span>@line.EstCost.Value.ToString("C")</span>}
else
{<span class="text-muted"></span>}
{<span class="text-muted">&mdash;</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>&mdash;</span>}
</td>
<td class="text-center">
<div class="d-flex align-items-center gap-1 justify-content-center receive-form-@line.CoatId">
@@ -680,7 +680,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") : "&mdash;")</td>
<td colspan="2"></td>
</tr>
</tfoot>
@@ -750,7 +750,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="">&mdash; Select category &mdash;</option>
@if (ViewBag.InventoryCategories != null)
{
foreach (var cat in (IEnumerable<dynamic>)ViewBag.InventoryCategories)
@@ -764,7 +764,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="">&mdash; Select vendor &mdash;</option>
@if (ViewBag.VendorList != null)
{
foreach (var v in (IEnumerable<dynamic>)ViewBag.VendorList)
@@ -838,12 +838,12 @@
@section Scripts {
<script src="~/js/shop-progress-widget.js" asp-append-version="true"></script>
<script>
// Start Intake pushes SignalR event to front-desk tablet
// Start Intake &mdash; pushes SignalR event to front-desk tablet
document.getElementById('btnStartIntake')?.addEventListener('click', async function () {
const btn = this;
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sending';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sending&hellip;';
try {
const res = await fetch('/Kiosk/StartSession', {
method: 'POST',
@@ -928,7 +928,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'}) : '&mdash;';
const lbsFmt = c.lbsToOrder ? parseFloat(c.lbsToOrder).toFixed(2) : '0.00';
// Find or create vendor group
@@ -973,7 +973,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">&mdash;</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}">
@@ -1024,7 +1024,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
@@ -1069,7 +1069,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;
@@ -1110,7 +1110,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&hellip;';
try {
const resp = await fetch('@Url.Action("AddCustomPowderToInventory", "Dashboard")', {
@@ -1139,7 +1139,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');
@@ -1189,7 +1189,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 &mdash; Manufacturer, Color Name, Color Code, or Item Name &mdash; then try again.');
return;
}
@@ -1198,7 +1198,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&hellip;');
try {
const formData = new FormData();
@@ -1265,7 +1265,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 &mdash; they may already be populated, or the product wasn\'t found.');
}
} catch (err) {
@@ -1319,7 +1319,7 @@
(function () {
var DISMISSED_KEY = 'pcl_pwa_banner_dismissed';
// Already installed as standalone never show
// Already installed as standalone &mdash; never show
var isStandalone = window.navigator.standalone === true ||
window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) return;
@@ -1343,7 +1343,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 &mdash; and so the camera only asks once &mdash; 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 {
@@ -173,7 +173,7 @@
</tbody>
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@if (!Model.Any())
@@ -188,7 +188,7 @@
}
@foreach (var tip in Model)
{
var tipPreview = tip.TipText.Length > 60 ? tip.TipText.Substring(0, 60) + "" : tip.TipText;
var tipPreview = tip.TipText.Length > 60 ? tip.TipText.Substring(0, 60) + "&hellip;" : tip.TipText;
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-warning"><i class="bi bi-lightbulb"></i></div>
@@ -54,7 +54,7 @@
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0">Select a Company</h6>
<input type="text" id="companySearch" class="form-control form-control-sm w-auto"
placeholder="Search" style="min-width:180px" />
placeholder="Search&hellip;" style="min-width:180px" />
</div>
</div>
<div class="table-responsive" style="max-height:520px;overflow-y:auto">
@@ -101,7 +101,7 @@
</table>
</div>
<!-- Mobile card view shown on screens < 992px -->
<!-- Mobile card view &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var c in Model)
@@ -148,7 +148,7 @@
</div>
</div>
@* Right: export options always visible *@
@* Right: export options &mdash; always visible *@
<div class="col-lg-5">
<div class="card border-0 shadow-sm" style="position:sticky;top:1rem">
<div class="card-header bg-primary text-white py-2">
@@ -156,14 +156,14 @@
</div>
<div class="card-body">
<!-- Company selection banner shown/hidden by JS -->
<!-- Company selection banner &mdash; shown/hidden by JS -->
<div id="noCompanyBanner" class="alert alert-light alert-permanent border d-flex align-items-center gap-2 mb-3 small">
<i class="bi bi-arrow-left-circle fs-5 text-muted"></i>
<span>Select a company from the list to begin.</span>
</div>
<div id="selectedBanner" class="alert alert-info alert-permanent py-2 mb-3 small" style="display:none">
<i class="bi bi-building me-1"></i>
Exporting: <strong id="selectedCompanyName"></strong>
Exporting: <strong id="selectedCompanyName">&mdash;</strong>
</div>
<form method="post" asp-action="Export" id="exportForm">
@@ -266,7 +266,7 @@
<input class="form-check-input" type="radio" name="format" id="fmtCsv" value="csv" />
<label class="form-check-label" for="fmtCsv">
<i class="bi bi-filetype-csv me-1 text-secondary"></i>CSV (.zip)
<span class="text-muted small"> one file per sheet</span>
<span class="text-muted small">&mdash; one file per sheet</span>
</label>
</div>
</div>
@@ -333,7 +333,7 @@
});
});
// ── Format toggle update button label ──────────────────────────────────
// ── Format toggle &mdash; update button label ──────────────────────────────────
document.querySelectorAll('input[name="format"]').forEach(function (radio) {
radio.addEventListener('change', function () {
var isCsv = this.value === 'csv';
@@ -64,7 +64,7 @@
<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 &mdash; 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>
@@ -88,8 +88,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&ndash;30d</th>
<th class="text-end" style="width:100px">30&ndash;90d</th>
<th class="text-end" style="width:100px">&gt;90d</th>
<th style="width:130px">Oldest</th>
<th style="width:42px">
@@ -114,7 +114,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="text-end">
@@ -122,24 +122,24 @@
{
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</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">&mdash;</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">&mdash;</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") : "&mdash;")
</td>
<td class="text-center">
<input type="checkbox" class="form-check-input entity-select"
@@ -154,7 +154,7 @@
</table>
</div>
<!-- Mobile card view for this group shown on screens < 992px -->
<!-- Mobile card view for this group &mdash; 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>
@@ -169,7 +169,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") : "&mdash;")</small>
</div>
</div>
<div class="mobile-card-body">
@@ -180,11 +180,11 @@
{
<span class="badge bg-secondary">@s.Total</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">030d / 3090d / &gt;90d</span>
<span class="mobile-card-label">0&ndash;30d / 30&ndash;90d / &gt;90d</span>
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
</div>
</div>
@@ -349,7 +349,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&hellip;';
const days = document.getElementById('olderThanDays').value;
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
@@ -1,4 +1,4 @@
@model PowderCoating.Web.Controllers.DiagnosticsInfo
@model PowderCoating.Web.Controllers.DiagnosticsInfo
@{
ViewData["Title"] = "System Diagnostics";
ViewData["PageIcon"] = "bi-activity";
@@ -52,11 +52,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">âœ&mdash; NO - PERMISSION ISSUE</span>
}
</td>
</tr>
@@ -65,11 +65,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">âœ&mdash; NO</span>
}
</td>
</tr>
@@ -78,11 +78,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">âœ&mdash; NO - PERMISSION ISSUE</span>
}
</td>
</tr>
@@ -1,4 +1,4 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model AdminEmailPreviewModel
@{
ViewData["Title"] = "Preview Admin Email";
@@ -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) ? "&mdash;" : row.CompanyAdminName)</div>
@if (!string.IsNullOrWhiteSpace(row.CompanyAdminEmail))
{
<div class="small text-muted">@row.CompanyAdminEmail</div>
@@ -75,7 +75,7 @@
<div class="fw-semibold">@company.CompanyName</div>
<div class="small text-muted">#@company.CompanyId</div>
</td>
<td>@(string.IsNullOrWhiteSpace(company.PrimaryContactName) ? "" : company.PrimaryContactName)</td>
<td>@(string.IsNullOrWhiteSpace(company.PrimaryContactName) ? "&mdash;" : company.PrimaryContactName)</td>
<td>
@if (string.IsNullOrWhiteSpace(company.PrimaryContactEmail))
{
@@ -87,7 +87,7 @@
}
</td>
<td>
<div>@(string.IsNullOrWhiteSpace(company.CompanyAdminName) ? "" : company.CompanyAdminName)</div>
<div>@(string.IsNullOrWhiteSpace(company.CompanyAdminName) ? "&mdash;" : company.CompanyAdminName)</div>
@if (!string.IsNullOrWhiteSpace(company.CompanyAdminEmail))
{
<div class="small text-muted">@company.CompanyAdminEmail</div>
@@ -4,7 +4,7 @@
ViewData["Title"] = "Add New Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Add New Equipment";
ViewData["PageHelpContent"] = "Equipment records track physical assets in your shop ovens, spray booths, compressors, and other machinery. Enter a name, type, and location at minimum. Serial number and warranty details help with service claims. The maintenance interval drives the Next Scheduled date shown on the equipment list.";
ViewData["PageHelpContent"] = "Equipment records track physical assets in your shop &mdash; ovens, spray booths, compressors, and other machinery. Enter a name, type, and location at minimum. Serial number and warranty details help with service claims. The maintenance interval drives the Next Scheduled date shown on the equipment list.";
var statusList = ViewBag.StatusList as Array ?? Array.Empty<object>();
}
@@ -42,7 +42,7 @@
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Location</label>
<p class="mb-0">@(Model.Location ?? "")</p>
<p class="mb-0">@(Model.Location ?? "&mdash;")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Status</label>
@@ -76,15 +76,15 @@
<div class="row g-3">
<div class="col-md-4">
<label class="text-muted small mb-1">Manufacturer</label>
<p class="mb-0">@(Model.Manufacturer ?? "")</p>
<p class="mb-0">@(Model.Manufacturer ?? "&mdash;")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Model</label>
<p class="mb-0">@(Model.Model ?? "")</p>
<p class="mb-0">@(Model.Model ?? "&mdash;")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Serial Number</label>
<p class="mb-0">@(Model.SerialNumber ?? "")</p>
<p class="mb-0">@(Model.SerialNumber ?? "&mdash;")</p>
</div>
</div>
</div>
@@ -94,15 +94,15 @@
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Manufacturer</label>
<p class="mb-0">@(Model.Manufacturer ?? "")</p>
<p class="mb-0">@(Model.Manufacturer ?? "&mdash;")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Model</label>
<p class="mb-0">@(Model.Model ?? "")</p>
<p class="mb-0">@(Model.Model ?? "&mdash;")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Serial Number</label>
<p class="mb-0">@(Model.SerialNumber ?? "")</p>
<p class="mb-0">@(Model.SerialNumber ?? "&mdash;")</p>
</div>
</div>
</div>
@@ -157,7 +157,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Maintenance Schedule"
data-bs-content="Maintenance Interval is how many days between scheduled services set this on the Edit page. Next Scheduled is calculated automatically as Last Maintenance date plus the interval. If no maintenance has been completed yet, Next Scheduled will be blank until the first service is recorded as Completed.">
data-bs-content="Maintenance Interval is how many days between scheduled services &mdash; set this on the Edit page. Next Scheduled is calculated automatically as Last Maintenance date plus the interval. If no maintenance has been completed yet, Next Scheduled will be blank until the first service is recorded as Completed.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -4,7 +4,7 @@
ViewData["Title"] = "Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Equipment";
ViewData["PageHelpContent"] = "Track all shop equipment ovens, spray booths, compressors, and other machinery. Status shows whether each piece is Operational, Needs Maintenance, Under Maintenance, or Out of Service. Next Maintenance date is calculated from the last completed maintenance plus the equipment's maintenance interval. Click any row to view full details and maintenance history.";
ViewData["PageHelpContent"] = "Track all shop equipment &mdash; ovens, spray booths, compressors, and other machinery. Status shows whether each piece is Operational, Needs Maintenance, Under Maintenance, or Out of Service. Next Maintenance date is calculated from the last completed maintenance plus the equipment's maintenance interval. Click any row to view full details and maintenance history.";
}
<div class="pcl-metric-strip">
@@ -123,7 +123,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@@ -4,7 +4,7 @@
ViewData["Title"] = $"Expense {Model.ExpenseNumber}";
ViewData["PageIcon"] = "bi-receipt";
ViewData["PageHelpTitle"] = "Expense";
ViewData["PageHelpContent"] = "A direct purchase paid at the time of transaction. Expense Account shows what was bought; Paid From shows which bank/cash account was debited. Edit to correct any details. Delete permanently removes the record there is no Void for expenses.";
ViewData["PageHelpContent"] = "A direct purchase paid at the time of transaction. Expense Account shows what was bought; Paid From shows which bank/cash account was debited. Edit to correct any details. Delete permanently removes the record &mdash; there is no Void for expenses.";
}
<div class="d-flex justify-content-between align-items-center mb-4">
@@ -170,7 +170,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="receiptModalLabel">
<i class="bi bi-receipt me-2"></i>Receipt @Model.ExpenseNumber
<i class="bi bi-receipt me-2"></i>Receipt &mdash; @Model.ExpenseNumber
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
@@ -71,7 +71,7 @@
<div class="col-md-4">
<label class="form-label">Asset Account</label>
<select name="AssetAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.AssetAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AssetAccounts)
@@ -85,7 +85,7 @@
<div class="col-md-4">
<label class="form-label">Depreciation Expense Account</label>
<select name="DepreciationExpenseAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.ExpenseAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
@@ -99,7 +99,7 @@
<div class="col-md-4">
<label class="form-label">Accumulated Depreciation Account</label>
<select name="AccumDepreciationAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.AccumDeprecAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AccumDeprecAccounts)
@@ -108,7 +108,7 @@
}
}
</select>
<div class="form-text">Contra-asset account (e.g., 1510 Accum. Depreciation Equipment).</div>
<div class="form-text">Contra-asset account (e.g., 1510 Accum. Depreciation &mdash; Equipment).</div>
</div>
</div>
@@ -142,21 +142,21 @@
{
<div class="mb-2">
<div class="text-muted small">Asset Account</div>
<div class="fw-semibold">@Model.AssetAccount.AccountNumber @Model.AssetAccount.Name</div>
<div class="fw-semibold">@Model.AssetAccount.AccountNumber &ndash; @Model.AssetAccount.Name</div>
</div>
}
@if (Model.DepreciationExpenseAccount != null)
{
<div class="mb-2">
<div class="text-muted small">Depreciation Expense</div>
<div class="fw-semibold">@Model.DepreciationExpenseAccount.AccountNumber @Model.DepreciationExpenseAccount.Name</div>
<div class="fw-semibold">@Model.DepreciationExpenseAccount.AccountNumber &ndash; @Model.DepreciationExpenseAccount.Name</div>
</div>
}
@if (Model.AccumDepreciationAccount != null)
{
<div>
<div class="text-muted small">Accumulated Depreciation</div>
<div class="fw-semibold">@Model.AccumDepreciationAccount.AccountNumber @Model.AccumDepreciationAccount.Name</div>
<div class="fw-semibold">@Model.AccumDepreciationAccount.AccountNumber &ndash; @Model.AccumDepreciationAccount.Name</div>
</div>
}
</div>
@@ -206,7 +206,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="text-muted small">@e.CreatedAt.ToLocalTime().ToString("MM/dd/yyyy")</td>
@@ -75,7 +75,7 @@
<div class="col-md-4">
<label asp-for="AssetAccountId" class="form-label">Asset Account</label>
<select asp-for="AssetAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.AssetAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AssetAccounts)
@@ -88,7 +88,7 @@
<div class="col-md-4">
<label asp-for="DepreciationExpenseAccountId" class="form-label">Depreciation Expense Account</label>
<select asp-for="DepreciationExpenseAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.ExpenseAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.ExpenseAccounts)
@@ -101,7 +101,7 @@
<div class="col-md-4">
<label asp-for="AccumDepreciationAccountId" class="form-label">Accumulated Depreciation Account</label>
<select asp-for="AccumDepreciationAccountId" class="form-select">
<option value="">— None —</option>
<option value="">&mdash; None &mdash;</option>
@if (ViewBag.AccumDeprecAccounts != null)
{
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.AccumDeprecAccounts)
@@ -87,7 +87,7 @@
</div>
<div class="col-md-6">
<label asp-for="RecipientEmail" class="form-label fw-semibold"></label>
<input asp-for="RecipientEmail" type="email" class="form-control" placeholder="Optional for emailing the certificate" />
<input asp-for="RecipientEmail" type="email" class="form-control" placeholder="Optional &mdash; for emailing the certificate" />
</div>
</div>
</div>
@@ -34,7 +34,7 @@
<strong>@statusLabel</strong>
@if (isActive)
{
<span class="ms-2"> <strong class="fs-5">@Model.RemainingBalance.ToString("C")</strong> remaining of @Model.OriginalAmount.ToString("C") face value</span>
<span class="ms-2">&mdash; <strong class="fs-5">@Model.RemainingBalance.ToString("C")</strong> remaining of @Model.OriginalAmount.ToString("C") face value</span>
}
@if (Model.ExpiryDate.HasValue)
{
@@ -28,7 +28,7 @@
<p>
When you make a payment to a vendor, you record it against the bill and the balance reduces.
This gives you a clear, real-time picture of your upcoming financial obligations and helps you
avoid late payments and the late fees or strained vendor relationships that come with them.
avoid late payments &mdash; and the late fees or strained vendor relationships that come with them.
</p>
<p>
Bills can be created manually or generated automatically from a received Purchase Order.
@@ -46,14 +46,14 @@
<p>
The fastest and most accurate way to create a bill is from a received Purchase Order. Open the
received PO and click <strong>Create Bill</strong>. The system generates a bill pre-filled with
all line items, quantities, and prices from the PO linked to the vendor and expense accounts
all line items, quantities, and prices from the PO &mdash; linked to the vendor and expense accounts
automatically. See the <a asp-controller="Help" asp-action="PurchaseOrders">Purchase Orders help page</a>
for step-by-step instructions.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Manually</h3>
<p>
To create a bill that is not linked to a PO for example, a utility bill, a service invoice,
To create a bill that is not linked to a PO &mdash; for example, a utility bill, a service invoice,
or a vendor charge that arrived without a matching order:
</p>
<ol class="mb-3">
@@ -64,8 +64,8 @@
<li class="mb-2">
Add one or more <strong>line items</strong>:
<ul class="mt-1">
<li><strong>Expense Account</strong> the accounting category this cost belongs to (e.g., Cost of Goods Sold, Shop Supplies, Equipment Maintenance).</li>
<li><strong>Description</strong> a brief note about what this line covers.</li>
<li><strong>Expense Account</strong> &mdash; the accounting category this cost belongs to (e.g., Cost of Goods Sold, Shop Supplies, Equipment Maintenance).</li>
<li><strong>Description</strong> &mdash; a brief note about what this line covers.</li>
<li><strong>Quantity</strong> and <strong>Unit Price</strong>.</li>
</ul>
</li>
@@ -129,8 +129,8 @@
<i class="bi bi-check-circle text-primary me-2"></i>Marking a Bill as Open
</h2>
<p>
When you have verified that a bill is accurate it matches the goods you received and the
prices you agreed on you mark it as Open to post it to your AP ledger.
When you have verified that a bill is accurate &mdash; it matches the goods you received and the
prices you agreed on &mdash; you mark it as Open to post it to your AP ledger.
</p>
<ol class="mb-3">
<li class="mb-2">Open the Draft bill from <strong>Accounting &rsaquo; Bills</strong>.</li>
@@ -160,7 +160,7 @@
<i class="bi bi-cash-coin text-primary me-2"></i>Recording a Payment
</h2>
<p>
When you pay a vendor whether in full or as a partial payment you record the payment against
When you pay a vendor &mdash; whether in full or as a partial payment &mdash; you record the payment against
the open bill. The system supports multiple partial payments on a single bill.
</p>
<ol class="mb-3">
@@ -169,11 +169,11 @@
<li class="mb-2">
Enter the payment details:
<ul class="mt-1">
<li><strong>Amount</strong> how much you are paying now. Can be less than the full balance for partial payments.</li>
<li><strong>Payment Method</strong> Check, ACH / Bank Transfer, Credit Card, Cash, or Wire Transfer.</li>
<li><strong>Payment Date</strong> the date the payment was made or will be made.</li>
<li><strong>Reference Number</strong> check number, wire confirmation, or ACH batch ID. Always fill this in for non-cash payments to simplify reconciliation.</li>
<li><strong>Notes</strong> any additional information about this payment.</li>
<li><strong>Amount</strong> &mdash; how much you are paying now. Can be less than the full balance for partial payments.</li>
<li><strong>Payment Method</strong> &mdash; Check, ACH / Bank Transfer, Credit Card, Cash, or Wire Transfer.</li>
<li><strong>Payment Date</strong> &mdash; the date the payment was made or will be made.</li>
<li><strong>Reference Number</strong> &mdash; check number, wire confirmation, or ACH batch ID. Always fill this in for non-cash payments to simplify reconciliation.</li>
<li><strong>Notes</strong> &mdash; any additional information about this payment.</li>
</ul>
</li>
<li class="mb-2">Click <strong>Save Payment</strong>.</li>
@@ -197,7 +197,7 @@
<li class="mb-2">Go to <strong>Accounting &rsaquo; Bills</strong> and click <strong>Scan Receipt</strong>.</li>
<li class="mb-2">Upload a photo (JPG/PNG) or a PDF of the vendor's invoice.</li>
<li class="mb-2">The AI reads the document and pre-fills the vendor, bill date, line items, and amounts into a new draft bill.</li>
<li class="mb-2">Review the extracted data correct any fields that were misread and save the bill.</li>
<li class="mb-2">Review the extracted data &mdash; correct any fields that were misread &mdash; and save the bill.</li>
</ol>
<p>
The uploaded file is automatically attached to the bill so you always have the original
@@ -245,13 +245,13 @@
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Vendor name</strong> and detected frequency (monthly, quarterly, biannual, annual).</li>
<li class="mb-1"><strong>Typical amount</strong> the usual charge from that vendor.</li>
<li class="mb-1"><strong>Next expected date</strong> Claude's estimate of when the next bill is likely to arrive.</li>
<li class="mb-1"><strong>Confidence badge</strong> High (4+ consistent occurrences), Medium (23 occurrences or variable timing), Low (weak pattern, worth monitoring).</li>
<li class="mb-1"><strong>Suggested action</strong> for example, "Set a monthly reminder for this bill."</li>
<li class="mb-1"><strong>Typical amount</strong> &mdash; the usual charge from that vendor.</li>
<li class="mb-1"><strong>Next expected date</strong> &mdash; Claude's estimate of when the next bill is likely to arrive.</li>
<li class="mb-1"><strong>Confidence badge</strong> &mdash; High (4+ consistent occurrences), Medium (2&ndash;3 occurrences or variable timing), Low (weak pattern, worth monitoring).</li>
<li class="mb-1"><strong>Suggested action</strong> &mdash; for example, "Set a monthly reminder for this bill."</li>
</ul>
<p>
This is useful for cash flow planning knowing that a $1,200 electricity bill arrives on the
This is useful for cash flow planning &mdash; knowing that a $1,200 electricity bill arrives on the
15th every month, or that your insurance renews every January, lets you reserve funds in advance
and avoid surprises. High-confidence patterns are reliable enough to act on; Low-confidence
patterns are worth keeping an eye on but should not be treated as certain.
@@ -261,7 +261,7 @@
<div>
Recurring bill detection requires at least 2 occurrences of a vendor bill at a similar
interval to detect a pattern. Shops with less than 2 months of history will see few or no
results. The scan covers bills only direct expenses are not included.
results. The scan covers bills only &mdash; direct expenses are not included.
</div>
</div>
</section>
@@ -271,17 +271,17 @@
<i class="bi bi-folder2-open text-primary me-2"></i>Expense Accounts
</h2>
<p>
Each line item on a bill must be assigned to an <strong>expense account</strong> an accounting
Each line item on a bill must be assigned to an <strong>expense account</strong> &mdash; an accounting
category that determines where the cost appears in your financial reports. Common expense accounts
used in a powder coating shop include:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Cost of Goods Sold (COGS)</strong> raw materials, powder coatings, and consumables that go directly into jobs.</li>
<li class="mb-1"><strong>Shop Supplies</strong> items used in the shop that are not directly tied to a specific job (masking tape, gloves, cleaning solvents).</li>
<li class="mb-1"><strong>Equipment Maintenance</strong> service, repairs, and parts for your oven, sandblaster, and coating booths.</li>
<li class="mb-1"><strong>Utilities</strong> gas, electricity, and water bills that power the shop.</li>
<li class="mb-1"><strong>Rent / Occupancy</strong> monthly rent or lease payments for your shop premises.</li>
<li class="mb-1"><strong>Operating Expenses</strong> general overhead that does not fit another specific category.</li>
<li class="mb-1"><strong>Cost of Goods Sold (COGS)</strong> &mdash; raw materials, powder coatings, and consumables that go directly into jobs.</li>
<li class="mb-1"><strong>Shop Supplies</strong> &mdash; items used in the shop that are not directly tied to a specific job (masking tape, gloves, cleaning solvents).</li>
<li class="mb-1"><strong>Equipment Maintenance</strong> &mdash; service, repairs, and parts for your oven, sandblaster, and coating booths.</li>
<li class="mb-1"><strong>Utilities</strong> &mdash; gas, electricity, and water bills that power the shop.</li>
<li class="mb-1"><strong>Rent / Occupancy</strong> &mdash; monthly rent or lease payments for your shop premises.</li>
<li class="mb-1"><strong>Operating Expenses</strong> &mdash; general overhead that does not fit another specific category.</li>
</ul>
<p>
The <strong>vendor's default expense account</strong> is set on the vendor record and pre-fills
@@ -21,7 +21,7 @@
</h2>
<p>
The Customer Intake Kiosk lets walk-in customers fill out their own intake form on a front-desk tablet
no staff assistance required. When they're done, a <strong>customer record</strong> is automatically
&mdash; no staff assistance required. When they're done, a <strong>customer record</strong> is automatically
created (or matched to an existing one), a <strong>Draft Quote or Pending Job</strong> is created
depending on your setting, and your team receives an in-app notification.
</p>
@@ -45,7 +45,7 @@
</li>
<li class="mb-2">
On the tablet, open a browser and navigate to <code>/Kiosk/Welcome</code>. You'll see your
company logo and a "Ready" indicator the tablet is now in kiosk mode.
company logo and a "Ready" indicator &mdash; the tablet is now in kiosk mode.
</li>
<li class="mb-2">
<strong>Add to Home Screen</strong> on iOS/Android for a full-screen, app-like experience that
@@ -67,7 +67,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">In-Person (tablet at front desk)</h3>
<ol>
<li class="mb-1">The tablet sits on the Welcome screen the customer sees your logo and a "Ready" status dot.</li>
<li class="mb-1">The tablet sits on the Welcome screen &mdash; the customer sees your logo and a "Ready" status dot.</li>
<li class="mb-1">A staff member clicks <strong>Start Intake</strong> on the Dashboard (in the Kiosk card).</li>
<li class="mb-1">The tablet detects the new session within 3 seconds and automatically navigates to the intake form.</li>
<li class="mb-1">The customer fills out <strong>3 steps</strong>: Contact info → Job description → Terms &amp; signature.</li>
@@ -85,7 +85,7 @@
<li class="mb-1">Or use the <strong>Send Intake Link</strong> button on the Dashboard Kiosk card.</li>
<li class="mb-1">Enter the customer's email address and send.</li>
<li class="mb-1">The customer receives an email with a secure link and completes the same 3-step form on their own device.</li>
<li class="mb-1">Remote sessions don't require a drawn signature a checkbox agreement is used instead.</li>
<li class="mb-1">Remote sessions don't require a drawn signature &mdash; a checkbox agreement is used instead.</li>
</ol>
</section>
@@ -99,12 +99,12 @@
</p>
<ul>
<li>
<strong>Create a Quote</strong> (default) a Draft quote is created for staff to review and price
<strong>Create a Quote</strong> (default) &mdash; a Draft quote is created for staff to review and price
before work begins. The terms shown to the customer will say "subject to a formal quote." Use this
if you price after seeing the parts.
</li>
<li>
<strong>Create a Job</strong> a Pending job is created immediately. The terms will say "a team
<strong>Create a Job</strong> &mdash; a Pending job is created immediately. The terms will say "a team
member will reach out about pricing." Use this if you price on the spot and want the work order
ready right away.
</li>
@@ -117,16 +117,16 @@
</h2>
<p>When a customer submits their intake form, the system automatically:</p>
<ul>
<li><strong>Matches or creates a Customer</strong> searches by email first, then phone. If no match, a new non-commercial customer record is created.</li>
<li><strong>Matches or creates a Customer</strong> &mdash; searches by email first, then phone. If no match, a new non-commercial customer record is created.</li>
<li>
<strong>Creates a Draft Quote or Pending Job</strong> depending on your
<strong>Creates a Draft Quote or Pending Job</strong> &mdash; depending on your
<a href="/CompanySettings?tab=kiosk">Kiosk Output Setting</a>. Quote mode creates a Draft quote
(Normal priority); Job mode creates a Pending job with the customer's description and intake source
in Special Instructions.
</li>
<li><strong>Applies SMS consent</strong> if the customer opted in, their customer record is updated with <code>NotifyBySms = true</code> and the consent timestamp (TCPA-compliant).</li>
<li><strong>Applies SMS consent</strong> &mdash; if the customer opted in, their customer record is updated with <code>NotifyBySms = true</code> and the consent timestamp (TCPA-compliant).</li>
<li>
<strong>Fires an in-app notification</strong> your team's notification bell shows
<strong>Fires an in-app notification</strong> &mdash; your team's notification bell shows
"Walk-in Intake Submitted" (or "Remote Intake Submitted" for remote sessions) with a link to
the Intakes page.
</li>
@@ -147,9 +147,9 @@
<li>Job description snippet</li>
<li>Session type (In-Person or Remote) and status badge</li>
<li>SMS opt-in indicator</li>
<li><strong>View Quote</strong> button appears when the kiosk is set to Quote mode; opens the auto-created draft quote</li>
<li><strong>View Job</strong> button appears when the kiosk is set to Job mode; opens the auto-created job</li>
<li><strong>Customer</strong> button opens the matched or created customer record</li>
<li><strong>View Quote</strong> button &mdash; appears when the kiosk is set to Quote mode; opens the auto-created draft quote</li>
<li><strong>View Job</strong> button &mdash; appears when the kiosk is set to Job mode; opens the auto-created job</li>
<li><strong>Customer</strong> button &mdash; opens the matched or created customer record</li>
</ul>
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle me-2"></i>
@@ -164,23 +164,23 @@
<i class="bi bi-exclamation-triangle text-primary me-2"></i>Troubleshooting
</h2>
<dl>
<dt>Kiosk Welcome screen shows "Connection issue retrying"</dt>
<dd class="mb-3">The tablet can't reach the server. Check the tablet's Wi-Fi connection. Once connectivity is restored the status dot automatically turns green no refresh needed.</dd>
<dt>Kiosk Welcome screen shows "Connection issue &mdash; retrying&hellip;"</dt>
<dd class="mb-3">The tablet can't reach the server. Check the tablet's Wi-Fi connection. Once connectivity is restored the status dot automatically turns green &mdash; no refresh needed.</dd>
<dt>Kiosk doesn't respond when staff clicks Start Intake</dt>
<dd class="mb-3">The tablet polls every 3 seconds. Wait up to 3 seconds after clicking Start Intake. If it still doesn't respond, reload the Welcome page on the tablet. Make sure the tablet is on the same domain as the server (use HTTPS).</dd>
<dt>The tablet shows the wrong company logo or no logo</dt>
<dd class="mb-3">Upload your company logo at Settings → Company Settings → Logo. The kiosk reads your logo directly no separate kiosk logo setting is needed.</dd>
<dd class="mb-3">Upload your company logo at Settings → Company Settings → Logo. The kiosk reads your logo directly &mdash; no separate kiosk logo setting is needed.</dd>
<dt>Signature pad doesn't work on the tablet</dt>
<dd class="mb-3">Use a capacitive stylus or fingertip the signature pad requires touch input. Make sure the browser isn't in desktop mode (check "Request Desktop Site" is off). The signature is only required for In-Person sessions.</dd>
<dd class="mb-3">Use a capacitive stylus or fingertip &mdash; the signature pad requires touch input. Make sure the browser isn't in desktop mode (check "Request Desktop Site" is off). The signature is only required for In-Person sessions.</dd>
<dt>Submission fails no job or customer created</dt>
<dt>Submission fails &mdash; no job or customer created</dt>
<dd class="mb-3">This usually means Seed Data hasn't been run for your company. Ask your administrator to go to Platform Management → Seed Data and run the seed. This creates the required job status and priority lookup rows.</dd>
<dt>AI quote on the quote wizard times out on mobile</dt>
<dd class="mb-3">Photos are automatically compressed before upload. If it still times out, your connection may be slow the spinner will say "Still analyzing" if it's taking longer than 30 seconds. Try again on a stronger connection.</dd>
<dd class="mb-3">Photos are automatically compressed before upload. If it still times out, your connection may be slow &mdash; the spinner will say "Still analyzing&hellip;" if it's taking longer than 30 seconds. Try again on a stronger connection.</dd>
</dl>
</section>
@@ -22,7 +22,7 @@
<p>
The Customers section is the starting point for all work in the shop. Every quote, job, and invoice
belongs to a customer record. Keeping your customer list accurate and up to date ensures you can
quickly pull up a customer's full history all their past jobs, outstanding quotes, and unpaid
quickly pull up a customer's full history &mdash; all their past jobs, outstanding quotes, and unpaid
invoices in one place.
</p>
<p>
@@ -49,7 +49,7 @@
</div>
<div class="card-body">
<p class="card-text small mb-0">
Use this for businesses auto body shops, fabricators, manufacturers, or any company
Use this for businesses &mdash; auto body shops, fabricators, manufacturers, or any company
that sends you work regularly. Commercial customers can have a <strong>pricing tier</strong>
applied (e.g., volume discounts) and a <strong>credit limit</strong> set. They typically
have a company name in addition to a contact person.
@@ -64,7 +64,7 @@
</div>
<div class="card-body">
<p class="card-text small mb-0">
Use this for individuals homeowners who bring in patio furniture, hobbyists with
Use this for individuals &mdash; homeowners who bring in patio furniture, hobbyists with
motorcycle parts, or anyone who is not representing a business. Non-commercial customers
typically do not have a company name and are priced at standard retail rates.
</p>
@@ -81,17 +81,17 @@
<p>Follow these steps to add a new customer:</p>
<ol class="mb-3">
<li class="mb-2">Go to <strong>Operations &rsaquo; Customers</strong> and click the <strong>New Customer</strong> button in the top-right corner.</li>
<li class="mb-2">Choose the <strong>Customer Type</strong> Commercial or Non-Commercial.</li>
<li class="mb-2">Choose the <strong>Customer Type</strong> &mdash; Commercial or Non-Commercial.</li>
<li class="mb-2">
Fill in the customer details:
<ul class="mt-1">
<li><strong>Company Name</strong> required for Commercial customers.</li>
<li><strong>Contact Name</strong> the person you deal with day to day.</li>
<li><strong>Email</strong> used for quote and invoice notifications.</li>
<li><strong>Phone</strong> primary contact number.</li>
<li><strong>Mobile Phone</strong> used for SMS notifications. Required if you want to text this customer.</li>
<li><strong>SMS Opt-In</strong> check this only after you have obtained the customer's consent to receive text messages. Visible only when SMS is enabled for your company.</li>
<li><strong>Address</strong> billing and shipping address fields.</li>
<li><strong>Company Name</strong> &mdash; required for Commercial customers.</li>
<li><strong>Contact Name</strong> &mdash; the person you deal with day to day.</li>
<li><strong>Email</strong> &mdash; used for quote and invoice notifications.</li>
<li><strong>Phone</strong> &mdash; primary contact number.</li>
<li><strong>Mobile Phone</strong> &mdash; used for SMS notifications. Required if you want to text this customer.</li>
<li><strong>SMS Opt-In</strong> &mdash; check this only after you have obtained the customer's consent to receive text messages. Visible only when SMS is enabled for your company.</li>
<li><strong>Address</strong> &mdash; billing and shipping address fields.</li>
</ul>
</li>
<li class="mb-2">For Commercial customers, optionally set a <strong>Pricing Tier</strong> and <strong>Credit Limit</strong>.</li>
@@ -136,24 +136,24 @@
</p>
<p>The details page shows:</p>
<ul>
<li><strong>Contact information</strong> name, email, phone, and address.</li>
<li><strong>Account summary</strong> current balance, credit limit, and pricing tier.</li>
<li><strong>Contact information</strong> &mdash; name, email, phone, and address.</li>
<li><strong>Account summary</strong> &mdash; current balance, credit limit, and pricing tier.</li>
<li>
<strong>Jobs tab</strong> every job created for this customer, with status and date. Click
<strong>Jobs tab</strong> &mdash; every job created for this customer, with status and date. Click
a job number to open it.
</li>
<li>
<strong>Quotes tab</strong> all quotes sent to this customer, including pending and
<strong>Quotes tab</strong> &mdash; all quotes sent to this customer, including pending and
historical quotes. Click a quote number to open it.
</li>
<li>
<strong>Invoices tab</strong> all invoices with payment status. Quickly see who owes money
<strong>Invoices tab</strong> &mdash; all invoices with payment status. Quickly see who owes money
and how much.
</li>
<li>
<strong>Deposits tab</strong> all deposits recorded for this customer across any job or quote.
<strong>Deposits tab</strong> &mdash; all deposits recorded for this customer across any job or quote.
</li>
<li><strong>Notes</strong> any notes saved against the customer record.</li>
<li><strong>Notes</strong> &mdash; any notes saved against the customer record.</li>
</ul>
</section>
@@ -169,7 +169,7 @@
<p>
When a customer's outstanding balance approaches or exceeds their credit limit, the system displays
a warning flag on their record and on any new jobs or invoices you try to create for them. This is
a visual warning only the system does not automatically block new work but it gives your team
a visual warning only &mdash; the system does not automatically block new work &mdash; but it gives your team
a clear signal to follow up on payment before starting more jobs.
</p>
<p>
@@ -189,7 +189,7 @@
</p>
<p>
When a tax-exempt customer is selected on a new quote or invoice, the tax rate automatically
defaults to <strong>0%</strong> no manual adjustment needed. Tax-exempt customers are marked
defaults to <strong>0%</strong> &mdash; no manual adjustment needed. Tax-exempt customers are marked
with a ★ in the customer dropdown when creating quotes and invoices so your team can spot them
at a glance.
</p>
@@ -20,14 +20,14 @@
<i class="bi bi-info-circle text-primary me-2"></i>Overview
</h2>
<p>
The Equipment module lets you keep a full register of every piece of machinery in your shop
The Equipment module lets you keep a full register of every piece of machinery in your shop &mdash;
curing ovens, sandblasters, coating booths, compressors, conveyors, and anything else you
rely on to do the work. Each equipment record includes its current operating status and a
complete maintenance log.
</p>
<p>
Keeping this information up to date pays off in two ways. First, your team always knows
which machines are available and which are down for service preventing jobs from being
which machines are available and which are down for service &mdash; preventing jobs from being
scheduled on equipment that is not ready. Second, the maintenance history gives you a
paper trail for warranty claims, insurance, and resale.
</p>
@@ -46,14 +46,14 @@
<li class="mb-2">
Fill in the equipment details:
<ul class="mt-1">
<li><strong>Name</strong> a short, descriptive name (e.g., "Main Curing Oven" or "Blast Cabinet #2").</li>
<li><strong>Model / Serial Number</strong> from the manufacturer's plate on the machine. Useful for warranty and service calls.</li>
<li><strong>Manufacturer</strong> who made the equipment.</li>
<li><strong>Purchase Date</strong> when your shop acquired it.</li>
<li><strong>Last Service Date</strong> the date it was last professionally serviced or inspected.</li>
<li><strong>Next Service Due</strong> when the next scheduled service is due. This triggers alerts on the Dashboard.</li>
<li><strong>Location</strong> where in the shop the equipment is located (e.g., "Bay 1", "Back Room").</li>
<li><strong>Notes</strong> any important operational notes, quirks, or warnings for the team.</li>
<li><strong>Name</strong> &mdash; a short, descriptive name (e.g., "Main Curing Oven" or "Blast Cabinet #2").</li>
<li><strong>Model / Serial Number</strong> &mdash; from the manufacturer's plate on the machine. Useful for warranty and service calls.</li>
<li><strong>Manufacturer</strong> &mdash; who made the equipment.</li>
<li><strong>Purchase Date</strong> &mdash; when your shop acquired it.</li>
<li><strong>Last Service Date</strong> &mdash; the date it was last professionally serviced or inspected.</li>
<li><strong>Next Service Due</strong> &mdash; when the next scheduled service is due. This triggers alerts on the Dashboard.</li>
<li><strong>Location</strong> &mdash; where in the shop the equipment is located (e.g., "Bay 1", "Back Room").</li>
<li><strong>Notes</strong> &mdash; any important operational notes, quirks, or warnings for the team.</li>
</ul>
</li>
<li class="mb-2">Set the initial <strong>Status</strong> (see below).</li>
@@ -67,7 +67,7 @@
</h2>
<p>
Every piece of equipment has a status that reflects its current condition. Update the status
whenever the equipment's situation changes this keeps the Dashboard accurate and lets
whenever the equipment's situation changes &mdash; this keeps the Dashboard accurate and lets
supervisors quickly see what is available.
</p>
@@ -146,14 +146,14 @@
<li class="mb-2">
Fill in the details:
<ul class="mt-1">
<li><strong>Task Description</strong> what was done or needs to be done (e.g., "Replace heating element", "Annual burner service", "Belt tension check").</li>
<li><strong>Type</strong> choose <strong>Scheduled</strong> for planned preventive maintenance, or <strong>Corrective</strong> for repairs to fix a problem.</li>
<li><strong>Scheduled Date</strong> when the task is planned for (or when it was done).</li>
<li><strong>Completion Date</strong> leave blank if the task has not been done yet.</li>
<li><strong>Cost</strong> what the service cost (parts, labor, contractor fees).</li>
<li><strong>Priority</strong> how urgent this task is (see below).</li>
<li><strong>Assigned To</strong> the shop worker responsible for this task.</li>
<li><strong>Notes</strong> parts used, observations, instructions for next time.</li>
<li><strong>Task Description</strong> &mdash; what was done or needs to be done (e.g., "Replace heating element", "Annual burner service", "Belt tension check").</li>
<li><strong>Type</strong> &mdash; choose <strong>Scheduled</strong> for planned preventive maintenance, or <strong>Corrective</strong> for repairs to fix a problem.</li>
<li><strong>Scheduled Date</strong> &mdash; when the task is planned for (or when it was done).</li>
<li><strong>Completion Date</strong> &mdash; leave blank if the task has not been done yet.</li>
<li><strong>Cost</strong> &mdash; what the service cost (parts, labor, contractor fees).</li>
<li><strong>Priority</strong> &mdash; how urgent this task is (see below).</li>
<li><strong>Assigned To</strong> &mdash; the shop worker responsible for this task.</li>
<li><strong>Notes</strong> &mdash; parts used, observations, instructions for next time.</li>
</ul>
</li>
<li class="mb-2">Click <strong>Save</strong>.</li>
@@ -167,7 +167,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Recurring Maintenance</h3>
<p>
For maintenance that happens on a regular schedule (e.g., monthly filter cleaning, quarterly
burner service), set the <strong>Recurrence</strong> field when creating the record choose
burner service), set the <strong>Recurrence</strong> field when creating the record &mdash; choose
from Daily, Weekly, Monthly, Quarterly, or Annually. When you complete the record, the system
automatically creates the next maintenance record at the appropriate interval. This ensures
preventive maintenance tasks never fall through the cracks.
@@ -248,7 +248,7 @@
<ol class="mb-3">
<li class="mb-2">Go to the Oven Scheduler from the sidebar.</li>
<li class="mb-2">Select the oven you are loading.</li>
<li class="mb-2">Add jobs to the batch the remaining capacity updates as you add items.</li>
<li class="mb-2">Add jobs to the batch &mdash; the remaining capacity updates as you add items.</li>
<li class="mb-2">Progress batches through Loading → In Progress → Completed as work is done.</li>
</ol>
<p>
@@ -262,7 +262,7 @@
<i class="bi bi-person-gear text-primary me-2"></i>Assigning Maintenance Tasks
</h2>
<p>
Each maintenance record can be assigned to one shop worker typically someone in the
Each maintenance record can be assigned to one shop worker &mdash; typically someone in the
<strong>Maintenance</strong> role, or a <strong>Supervisor</strong> who will coordinate
with an outside service technician.
</p>
@@ -21,7 +21,7 @@
</h2>
<p>
This system is purpose-built to run a powder coating shop from end to end. At its core it helps you
track every job that comes through the door from the first quote all the way through coating,
track every job that comes through the door &mdash; from the first quote all the way through coating,
curing, quality check, and final delivery. Along the way it manages your customers, vendors,
inventory of powders and supplies, shop equipment, and the workers on your floor.
</p>
@@ -54,7 +54,7 @@
<li class="mb-1">Click <strong>Sign In</strong>. You will land on the Dashboard.</li>
</ol>
<h5 class="fw-semibold">First login set your permanent password</h5>
<h5 class="fw-semibold">First login &mdash; set your permanent password</h5>
<p>
When a new company account is created, the system generates a secure temporary password and emails
it to the address used during signup. The first time you log in with that temporary password, you
@@ -73,7 +73,7 @@
<i class="bi bi-envelope-check-fill flex-shrink-0 mt-1"></i>
<div>
<strong>Didn't receive the welcome email?</strong> Check your spam or junk folder first.
If it's still not there, contact your company administrator they can reset your password
If it's still not there, contact your company administrator &mdash; they can reset your password
from the User Management section.
</div>
</div>
@@ -85,7 +85,7 @@
instructions in the email. Reset links expire after a short time, so use them promptly.
</p>
<p>
If you do not receive an email or cannot log in, contact your company administrator they can reset
If you do not receive an email or cannot log in, contact your company administrator &mdash; they can reset
your password from the User Management section.
</p>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
@@ -103,7 +103,7 @@
</h2>
<p>
The left sidebar is your main navigation. It is divided into sections so related features are grouped
together. Click any item to go to that section. On smaller screens the sidebar collapses tap the
together. Click any item to go to that section. On smaller screens the sidebar collapses &mdash; tap the
menu icon at the top to open it.
</p>
@@ -122,7 +122,7 @@
</tr>
<tr>
<td><i class="bi bi-briefcase me-1 text-primary"></i><strong>Operations</strong></td>
<td>Customers, Quotes, Jobs, Invoices, Appointments/Calendar, Job Priority Board, Gift Certificates, and Shop Workers the day-to-day work of the shop.</td>
<td>Customers, Quotes, Jobs, Invoices, Appointments/Calendar, Job Priority Board, Gift Certificates, and Shop Workers &mdash; the day-to-day work of the shop.</td>
</tr>
<tr>
<td><i class="bi bi-box-seam me-1 text-primary"></i><strong>Inventory</strong></td>
@@ -155,7 +155,7 @@
</p>
<p>
The <strong>notification bell</strong> (<i class="bi bi-bell-fill"></i>) alerts you to customer
actions in real time a quote approved or declined online, an invoice or deposit paid online,
actions in real time &mdash; a quote approved or declined online, an invoice or deposit paid online,
and any platform announcements from the Powder Coating Logix team. A red badge shows the count of
unread items. Click the bell to see recent notifications and navigate directly to the related record.
Notifications are stored persistently so you will not miss one if you were not logged in when it
@@ -170,7 +170,7 @@
<p>
Every user is assigned a role that controls what they can see and do. Your administrator assigns
your role when they create your account. If you cannot access something you expect to, ask your
administrator they may need to update your role.
administrator &mdash; they may need to update your role.
</p>
<div class="table-responsive">
@@ -197,7 +197,7 @@
<span class="badge bg-warning text-dark">Manager</span>
</td>
<td>
Can manage all operational data customers, jobs, quotes, invoices, inventory,
Can manage all operational data &mdash; customers, jobs, quotes, invoices, inventory,
equipment, and workers. Can view reports and adjust settings, but cannot manage
user accounts or change platform-level configuration.
</td>
@@ -248,13 +248,13 @@
<div>
<strong>New to the system?</strong> Use the <a href="/SetupWizard">Setup Wizard</a> to
configure your company profile, operating costs, named ovens, and notifications in a guided
5-step walkthrough takes about 510 minutes. After the wizard, the Dashboard will show
5-step walkthrough &mdash; takes about 5&ndash;10 minutes. After the wizard, the Dashboard will show
a progress checklist to guide you through your first live workflow.
</div>
</div>
<p>
If you prefer to configure things manually, or if you are returning to complete partial setup,
work through these steps in order. Each step builds on the last for example, inventory items
work through these steps in order. Each step builds on the last &mdash; for example, inventory items
need vendors before you can record purchases, and jobs need customers before you can create them.
</p>
@@ -330,7 +330,7 @@
<section id="after-the-wizard" class="mb-5">
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
<i class="bi bi-rocket-takeoff text-primary me-2"></i>After the Wizard Your First Workflow
<i class="bi bi-rocket-takeoff text-primary me-2"></i>After the Wizard &mdash; Your First Workflow
</h2>
<p>
Once the Setup Wizard is complete, two things appear on your Dashboard to guide you through your
@@ -339,13 +339,13 @@
<h5 class="fw-semibold">Guided Activation</h5>
<p>
A banner prompts you to run a short first-workflow walkthrough. Choose a starting path either
A banner prompts you to run a short first-workflow walkthrough. Choose a starting path &mdash; either
<strong>Quote First</strong> (create a quote, get it approved, convert it to a job) or
<strong>Job First</strong> (create a job directly). The system creates a sample customer
record for you to use during the walkthrough.
</p>
<p>
After the job is created you are taken to the <strong>Daily Board</strong> your shop in real
After the job is created you are taken to the <strong>Daily Board</strong> &mdash; your shop in real
time. Every active job appears on the board by stage. The new job is highlighted so you can
find it easily. Drag it to its next stage to see how your workflow updates live. Once you have
moved the job, the board prompts you to create the invoice when the work is done.
@@ -367,7 +367,7 @@
<p>
Each incomplete step shows a description and a button that takes you directly to the right place.
The next recommended step is highlighted. The widget disappears once all six steps are done.
You can collapse it using the chevron button the collapsed state is saved in your browser.
You can collapse it using the chevron button &mdash; the collapsed state is saved in your browser.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -388,21 +388,21 @@
gives your shop floor workers a much better experience:
</p>
<ul class="mb-3">
<li class="mb-1">Opens full-screen with no browser chrome feels like a native app.</li>
<li class="mb-1">Opens full-screen with no browser chrome &mdash; feels like a native app.</li>
<li class="mb-1">The camera (used by the inventory label scanner) only asks for permission <strong>once</strong> after installation, instead of every browser session.</li>
<li class="mb-1">Faster to launch one tap from the home screen.</li>
<li class="mb-1">Faster to launch &mdash; one tap from the home screen.</li>
</ul>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-4" role="alert">
<i class="bi bi-apple flex-shrink-0 mt-1"></i>
<div>
<strong>iPhone / iPad users must use Safari.</strong> Adding to the home screen from Chrome,
Firefox, or other iOS browsers creates a regular bookmark that opens in that browser not
Firefox, or other iOS browsers creates a regular bookmark that opens in that browser &mdash; not
a standalone app. Only Safari on iOS supports the full home screen install.
</div>
</div>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-apple me-1"></i>iOS (iPhone / iPad) Safari only</h3>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-apple me-1"></i>iOS (iPhone / iPad) &mdash; Safari only</h3>
<ol class="mb-4">
<li class="mb-1">Open the app in <strong>Safari</strong>.</li>
<li class="mb-1">Tap the <strong>Share</strong> button <i class="bi bi-box-arrow-up"></i> at the bottom of the screen.</li>
@@ -411,10 +411,10 @@
<li class="mb-1">The app icon appears on your home screen. Tap it to open in full-screen mode.</li>
</ol>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-android2 me-1"></i>Android Chrome</h3>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-android2 me-1"></i>Android &mdash; Chrome</h3>
<ol class="mb-4">
<li class="mb-1">Open the app in <strong>Chrome</strong>.</li>
<li class="mb-1">Chrome may show an <strong>Install App</strong> banner at the bottom automatically tap it to install.</li>
<li class="mb-1">Chrome may show an <strong>Install App</strong> banner at the bottom automatically &mdash; tap it to install.</li>
<li class="mb-1">If no banner appears, tap the <strong>menu (&#8942;)</strong> in the top-right corner and choose <strong>Add to Home Screen</strong> or <strong>Install App</strong>.</li>
<li class="mb-1">Confirm and the icon is added to your home screen.</li>
</ol>
@@ -219,7 +219,7 @@
</div>
<div>
<h5 class="card-title mb-1">Reports</h5>
<p class="card-text text-muted small mb-2">Financial summaries, AR aging, job throughput, inventory levels, and equipment status all in one place.</p>
<p class="card-text text-muted small mb-2">Financial summaries, AR aging, job throughput, inventory levels, and equipment status &mdash; all in one place.</p>
<a asp-controller="Help" asp-action="Reports" class="btn btn-sm btn-outline-primary">Read more <i class="bi bi-arrow-right ms-1"></i></a>
</div>
</div>
@@ -26,7 +26,7 @@
</p>
<p>
Keeping inventory accurate matters for two reasons. First, your job and quote pricing is only
as accurate as the unit costs stored in inventory outdated costs lead to under-pricing.
as accurate as the unit costs stored in inventory &mdash; outdated costs lead to under-pricing.
Second, knowing how much powder you have on hand before a job starts prevents the frustrating
situation of running out of material mid-job.
</p>
@@ -45,19 +45,19 @@
<li class="mb-2">
Fill in the item details:
<ul class="mt-1">
<li><strong>Item Name</strong> a clear, descriptive name (e.g., "Gloss Black Powder Tiger Drylac 49/90005").</li>
<li><strong>SKU / Part Number</strong> the manufacturer's part number or your internal SKU.</li>
<li><strong>Category</strong> Powder, Primer, Consumable, Shop Supply, or other category as appropriate.</li>
<li><strong>Unit of Measure</strong> lbs, kg, each, litre, etc.</li>
<li><strong>Unit Cost</strong> your purchase cost per unit. Used in quote and job pricing calculations.</li>
<li><strong>Current Quantity on Hand</strong> the number of units you have right now. This becomes the opening stock level.</li>
<li><strong>Reorder Point</strong> the quantity at which you want to be alerted to reorder. See the Reorder Points section below.</li>
<li><strong>Vendor / Supplier</strong> the vendor you purchase this item from. Linking a vendor lets you quickly see who to call when stock runs low.</li>
<li><strong>Item Name</strong> &mdash; a clear, descriptive name (e.g., "Gloss Black Powder &mdash; Tiger Drylac 49/90005").</li>
<li><strong>SKU / Part Number</strong> &mdash; the manufacturer's part number or your internal SKU.</li>
<li><strong>Category</strong> &mdash; Powder, Primer, Consumable, Shop Supply, or other category as appropriate.</li>
<li><strong>Unit of Measure</strong> &mdash; lbs, kg, each, litre, etc.</li>
<li><strong>Unit Cost</strong> &mdash; your purchase cost per unit. Used in quote and job pricing calculations.</li>
<li><strong>Current Quantity on Hand</strong> &mdash; the number of units you have right now. This becomes the opening stock level.</li>
<li><strong>Reorder Point</strong> &mdash; the quantity at which you want to be alerted to reorder. See the Reorder Points section below.</li>
<li><strong>Vendor / Supplier</strong> &mdash; the vendor you purchase this item from. Linking a vendor lets you quickly see who to call when stock runs low.</li>
</ul>
</li>
<li class="mb-2">
For powder coatings, the <strong>Coverage Rate</strong> (sq ft per lb) and <strong>Transfer Efficiency %</strong>
default to <strong>30 sq ft/lb</strong> and <strong>65%</strong> respectively typical starting values for most powder
default to <strong>30 sq ft/lb</strong> and <strong>65%</strong> respectively &mdash; typical starting values for most powder
and application setups. Adjust these to match your specific powder and equipment. Both values are used when
calculating powder needed on quotes and jobs.
</li>
@@ -86,12 +86,12 @@
<p>
Click the <strong>Lookup</strong> button next to the SKU/Part Number field. Type a color name,
SKU, or part number and the system searches a built-in catalog of thousands of Prismatic Powders
and other manufacturer SKUs. Select a match and the form fills in automatically item name,
and other manufacturer SKUs. Select a match and the form fills in automatically &mdash; item name,
manufacturer, color code, finish, coverage rate, SDS/TDS links, and cure specifications.
</p>
<ul class="mb-3">
<li class="mb-1">The catalog only shows products <strong>not already in your inventory</strong>, preventing duplicates. When editing an existing item, its own catalog entry is always shown.</li>
<li class="mb-1">If no catalog match is found, the lookup falls back to <strong>AI Lookup</strong> Claude searches the web for product specs and fills in whatever it can find.</li>
<li class="mb-1">If no catalog match is found, the lookup falls back to <strong>AI Lookup</strong> &mdash; Claude searches the web for product specs and fills in whatever it can find.</li>
<li class="mb-1">If a vendor name is selected in the Vendor field before searching, results are scoped to that vendor first, then broadened automatically if nothing matches.</li>
</ul>
@@ -102,11 +102,11 @@
The scanner reads the code and attempts to identify the product:
</p>
<ol class="mb-3">
<li class="mb-1">If the QR code matches a product in the platform catalog, the form fills in automatically same as a manual catalog lookup.</li>
<li class="mb-1">If the QR code matches a product in the platform catalog, the form fills in automatically &mdash; same as a manual catalog lookup.</li>
<li class="mb-1">If no catalog match is found, the AI analyzes the label image and fills in whatever details it can extract (color name, SKU, manufacturer, finish).</li>
<li class="mb-1">
If the scanned product is <strong>already in your inventory</strong>, a prompt appears to
<strong>Add Stock</strong> to the existing item instead enter the quantity received and an
<strong>Add Stock</strong> to the existing item instead &mdash; enter the quantity received and an
optional updated unit cost, then save. No duplicate item is created.
</li>
</ol>
@@ -127,7 +127,7 @@
</h2>
<p>
The <strong>quantity on hand</strong> for each item is updated automatically whenever a transaction
is recorded a purchase receipt increases stock, a job consumption decreases it, and a manual
is recorded &mdash; a purchase receipt increases stock, a job consumption decreases it, and a manual
adjustment sets it to the corrected count.
</p>
<p>
@@ -142,8 +142,8 @@
A good reorder point accounts for two factors:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Lead time</strong> how many days it typically takes for your vendor to deliver after you place an order. If lead time is 5 days, your reorder point should cover at least 5 days of usage.</li>
<li class="mb-1"><strong>Daily usage rate</strong> how much of the item you typically consume per day based on your job volume.</li>
<li class="mb-1"><strong>Lead time</strong> &mdash; how many days it typically takes for your vendor to deliver after you place an order. If lead time is 5 days, your reorder point should cover at least 5 days of usage.</li>
<li class="mb-1"><strong>Daily usage rate</strong> &mdash; how much of the item you typically consume per day based on your job volume.</li>
</ul>
<p>
For example, if you use 3 lbs of a powder per day and your vendor takes 5 days to deliver, a
@@ -163,9 +163,9 @@
</p>
<p>Click <strong>Stock Adjustment</strong> in the Actions panel and choose one of three modes:</p>
<ul class="mb-3">
<li class="mb-2"><strong>Add Stock</strong> increases the current quantity by the amount you enter. Use for received goods, returns, or found stock.</li>
<li class="mb-2"><strong>Remove Stock</strong> decreases the current quantity by the amount you enter. Use for waste, spillage, or damage write-offs.</li>
<li class="mb-2"><strong>Set Exact</strong> sets the quantity on hand to the exact number you enter, regardless of the current value. Use after a physical inventory count to correct the balance.</li>
<li class="mb-2"><strong>Add Stock</strong> &mdash; increases the current quantity by the amount you enter. Use for received goods, returns, or found stock.</li>
<li class="mb-2"><strong>Remove Stock</strong> &mdash; decreases the current quantity by the amount you enter. Use for waste, spillage, or damage write-offs.</li>
<li class="mb-2"><strong>Set Exact</strong> &mdash; sets the quantity on hand to the exact number you enter, regardless of the current value. Use after a physical inventory count to correct the balance.</li>
</ul>
<p>
A <strong>reason</strong> is required for every adjustment. Common reasons are listed in the dropdown
@@ -230,7 +230,7 @@
</tr>
<tr>
<td><strong>Job Usage</strong></td>
<td>Powder consumed during a job recorded automatically when actual usage is entered on a job coat.</td>
<td>Powder consumed during a job &mdash; recorded automatically when actual usage is entered on a job coat.</td>
<td class="text-danger fw-semibold">&minus; Decreases</td>
</tr>
<tr>
@@ -268,12 +268,12 @@
<p>It has two tabs:</p>
<ul class="mb-3">
<li class="mb-2">
<strong>Stock Transactions</strong> every transaction recorded against your inventory items,
<strong>Stock Transactions</strong> &mdash; every transaction recorded against your inventory items,
showing date, type, quantity (green for additions, red for deductions), unit cost, total cost,
running balance after the transaction, and a link to the source Purchase Order if applicable.
</li>
<li class="mb-2">
<strong>Powder Usage by Job</strong> every instance of powder being consumed on a job coat,
<strong>Powder Usage by Job</strong> &mdash; every instance of powder being consumed on a job coat,
showing the job number (linked to the job), customer, color applied, estimated vs actual pounds
used, and the variance. A totals row at the bottom summarises the full filtered selection.
</li>
@@ -287,7 +287,7 @@
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
To see the history for a single powder, open its Details page and click
<strong>View Activity History</strong> the Inventory Activity page will open pre-filtered
<strong>View Activity History</strong> &mdash; the Inventory Activity page will open pre-filtered
to that item.
</div>
</div>
@@ -299,14 +299,14 @@
</h2>
<p>
Every inventory item has a printable <strong>QR code label</strong>. Stick it on the bag, bin,
or shelf and shop floor workers can scan it with their phone to log how much they used
or shelf and shop floor workers can scan it with their phone to log how much they used &mdash;
without ever touching a desktop.
</p>
<h3 class="h6 fw-semibold mt-4 mb-2">Printing a label</h3>
<ol class="mb-3">
<li class="mb-1">Open the inventory item's Details page.</li>
<li class="mb-1">Click <strong>Print QR Label</strong> in the Actions panel the label opens in a new tab.</li>
<li class="mb-1">Click <strong>Print QR Label</strong> in the Actions panel &mdash; the label opens in a new tab.</li>
<li class="mb-1">Click <strong>Print Label</strong> and send it to your printer. The label is sized for a standard 3.5&Prime; label and includes the item name, SKU, colour, finish, and manufacturer.</li>
</ol>
@@ -316,9 +316,9 @@
<li class="mb-1">
<strong>Select a job</strong> (optional but recommended):
<ul class="mt-1">
<li><em>My Jobs</em> active jobs assigned to your account appear first.</li>
<li><em>Other Jobs</em> any other open job in the system.</li>
<li><em>No Job</em> log usage without a job reference (e.g. a waste event).</li>
<li><em>My Jobs</em> &mdash; active jobs assigned to your account appear first.</li>
<li><em>Other Jobs</em> &mdash; any other open job in the system.</li>
<li><em>No Job</em> &mdash; log usage without a job reference (e.g. a waste event).</li>
</ul>
</li>
<li class="mb-1">Enter the <strong>quantity</strong> used. A live preview shows what the new stock balance will be.</li>
@@ -331,8 +331,8 @@
The success screen gives you two options:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Log Another Item for This Job</strong> returns to the scan page with the same job pre-selected, so you can quickly log the next powder without re-picking the job.</li>
<li class="mb-1"><strong>Back to Inventory</strong> or <strong>View Item Details</strong> returns to a neutral state.</li>
<li class="mb-1"><strong>Log Another Item for This Job</strong> &mdash; returns to the scan page with the same job pre-selected, so you can quickly log the next powder without re-picking the job.</li>
<li class="mb-1"><strong>Back to Inventory</strong> or <strong>View Item Details</strong> &mdash; returns to a neutral state.</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -341,7 +341,7 @@
Every scan-based usage log is recorded as a <strong>JobUsage</strong> or <strong>Adjustment</strong>
transaction and immediately reduces the item's quantity on hand. You can review it on the
<a href="/Inventory/Ledger" class="alert-link">Inventory Activity</a> page.
The first time a worker scans on a new device they will be asked to log in after that the
The first time a worker scans on a new device they will be asked to log in &mdash; after that the
browser keeps them signed in.
</div>
</div>
@@ -374,7 +374,7 @@
<div>
An item continues to show as Low Stock or Out of Stock even after you have placed a Purchase
Order, until the goods are physically received and the PO is marked as Received in the system.
This is intentional it reminds you that stock has not yet arrived.
This is intentional &mdash; it reminds you that stock has not yet arrived.
</div>
</div>
</section>
@@ -428,12 +428,12 @@
<li class="mb-2">Go to <strong><a asp-controller="CompanySettings" asp-action="Index">Company Settings</a> &rsaquo; Data Lookups &rsaquo; Inventory Categories</strong>.</li>
<li class="mb-2">Find the category that contains your powder coating colors (e.g. "Powder Coatings").</li>
<li class="mb-2">Click the edit icon and check the <strong>Is Coating</strong> checkbox.</li>
<li class="mb-2">Save. Items in that category will immediately appear in the powder color dropdown on all quotes and jobs no restart required.</li>
<li class="mb-2">Save. Items in that category will immediately appear in the powder color dropdown on all quotes and jobs &mdash; no restart required.</li>
</ol>
<h3 class="h6 fw-semibold mt-3 mb-2">Which categories should have "Is Coating" enabled?</h3>
<p>
Only categories that contain actual powder coating colors the materials that go into the oven
Only categories that contain actual powder coating colors &mdash; the materials that go into the oven
and bond to the part. Do <strong>not</strong> enable this on categories for primers, masking
supplies, consumables, or equipment. Enabling it on non-coating categories will pollute the
color dropdown with irrelevant items and make it harder to find the right powder.
@@ -443,7 +443,7 @@
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
The "Is Coating" flag also controls where the <strong>sample panel toggle</strong> appears
on an item's Details page. The toggle "I have a swatch/sample of this color" only
on an item's Details page. The toggle &mdash; "I have a swatch/sample of this color" &mdash; only
shows up for items in a coating category, and those items are the ones tracked on the
<strong>Sample Panels</strong> page.
</div>
@@ -459,9 +459,9 @@
will be needed before the job begins. This estimate is based on three values:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Surface area</strong> the total square footage to be coated (entered per item in the job or quote wizard).</li>
<li class="mb-1"><strong>Coverage rate</strong> how many square feet one pound of the selected powder covers (set on the inventory item).</li>
<li class="mb-1"><strong>Number of coats</strong> selected when you add the coating to the item.</li>
<li class="mb-1"><strong>Surface area</strong> &mdash; the total square footage to be coated (entered per item in the job or quote wizard).</li>
<li class="mb-1"><strong>Coverage rate</strong> &mdash; how many square feet one pound of the selected powder covers (set on the inventory item).</li>
<li class="mb-1"><strong>Number of coats</strong> &mdash; selected when you add the coating to the item.</li>
</ul>
<p>
The <strong>Powder Needed</strong> figure appears in the item wizard as you build the quote or job,
@@ -486,30 +486,30 @@
The AI Price Check reviews every active, priced item in your
<a asp-controller="CatalogItems" asp-action="Index">Catalog Items</a> list against your
shop's actual operating costs. It estimates a realistic surface area and processing time
for each item, calculates a cost floor, and compares that to your current price flagging
for each item, calculates a cost floor, and compares that to your current price &mdash; flagging
anything that may be losing money, leaving margin on the table, or priced above market rates.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Verdicts</h3>
<ul class="mb-3">
<li class="mb-2"><strong>Below Cost</strong> price is at or below the estimated cost floor. The shop loses money on every sale of this item.</li>
<li class="mb-2"><strong>Thin Margin</strong> price covers costs but falls below your target margin percentage.</li>
<li class="mb-2"><strong>High</strong> price appears significantly above typical market rates, which may cost you work.</li>
<li class="mb-2"><strong>OK</strong> price is within a reasonable range given your costs and market context.</li>
<li class="mb-2"><strong>Below Cost</strong> &mdash; price is at or below the estimated cost floor. The shop loses money on every sale of this item.</li>
<li class="mb-2"><strong>Thin Margin</strong> &mdash; price covers costs but falls below your target margin percentage.</li>
<li class="mb-2"><strong>High</strong> &mdash; price appears significantly above typical market rates, which may cost you work.</li>
<li class="mb-2"><strong>OK</strong> &mdash; price is within a reasonable range given your costs and market context.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">How to run it</h3>
<ol class="mb-3">
<li class="mb-2">Make sure your <a asp-controller="CompanySettings" asp-action="Index">operating costs</a> are up to date stale rates produce inaccurate verdicts.</li>
<li class="mb-2">Make sure your <a asp-controller="CompanySettings" asp-action="Index">operating costs</a> are up to date &mdash; stale rates produce inaccurate verdicts.</li>
<li class="mb-2">Go to <a asp-controller="CatalogItems" asp-action="Index">Catalog Items</a> and click <strong>AI Price Check</strong> in the top-right.</li>
<li class="mb-2">Click <strong>Analyze Catalog with AI</strong>. A progress overlay appears while the analysis runs (allow 710 minutes for large catalogs).</li>
<li class="mb-2">Review results sorted by severity Below Cost items appear first. Click <strong>Edit Price</strong> on any item to update it directly from the results page.</li>
<li class="mb-2">Click <strong>Analyze Catalog with AI</strong>. A progress overlay appears while the analysis runs (allow 7&ndash;10 minutes for large catalogs).</li>
<li class="mb-2">Review results sorted by severity &mdash; Below Cost items appear first. Click <strong>Edit Price</strong> on any item to update it directly from the results page.</li>
</ol>
<h3 class="h6 fw-semibold mt-3 mb-2">Things to know</h3>
<ul class="mb-3">
<li class="mb-2"><strong>Run limit:</strong> Analysis can be run once per quarter (90 days). The button shows the next available date when a recent run exists.</li>
<li class="mb-2"><strong>Confidence levels:</strong> Each result shows High, Medium, or Low confidence. Vague item names like "Custom Part" will be Low verify those manually.</li>
<li class="mb-2"><strong>Confidence levels:</strong> Each result shows High, Medium, or Low confidence. Vague item names like "Custom Part" will be Low &mdash; verify those manually.</li>
<li class="mb-2"><strong>Category paths matter:</strong> The AI uses the full category path (e.g. "Cerakote &rsaquo; Firearms") to determine the coating type. Make sure specialty items are in the correct category.</li>
<li class="mb-2"><strong>$0 items skipped:</strong> Placeholder items and category headers with no price are automatically excluded from analysis.</li>
</ul>
@@ -518,7 +518,7 @@
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
Results are estimates based on industry knowledge and your shop's rates. Always apply
your own judgment before changing prices especially for items flagged as Low confidence.
your own judgment before changing prices &mdash; especially for items flagged as Low confidence.
</div>
</div>
</section>
@@ -21,13 +21,13 @@
</h2>
<p>
Invoices are the formal request for payment you send to customers after their work is complete.
Each job can have one invoice. The system tracks payment status in real time you can see at a
Each job can have one invoice. The system tracks payment status in real time &mdash; you can see at a
glance which customers owe money, how much, and how long the balance has been outstanding.
</p>
<p>
Invoices can be emailed to customers directly from the system (when email is configured) and
downloaded as PDFs to print or send manually. Payments whether in full or in partial
installments are logged against the invoice, and the customer's outstanding balance on their
downloaded as PDFs to print or send manually. Payments &mdash; whether in full or in partial
installments &mdash; are logged against the invoice, and the customer's outstanding balance on their
account is updated automatically with every transaction.
</p>
<p>
@@ -64,14 +64,14 @@
<p>
Invoice numbers are generated in the format <code>INV-YYMM-####</code>
(for example, <code>INV-2503-0007</code>). Each job can only have one invoice if an invoice
(for example, <code>INV-2503-0007</code>). Each job can only have one invoice &mdash; if an invoice
already exists for a job, the Create Invoice button on the Job Details page is replaced with a
link to the existing invoice.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
You can create an invoice for a job at any time you do not need to wait until the job
You can create an invoice for a job at any time &mdash; you do not need to wait until the job
reaches a Completed or Delivered status. Some shops invoice on deposit when a job is
approved; others invoice on pickup. The system is flexible.
</div>
@@ -161,7 +161,7 @@
<i class="bi bi-cash-coin text-primary me-2"></i>Recording a Payment
</h2>
<p>
When a customer pays whether in full or as a partial payment you record it against the invoice.
When a customer pays &mdash; whether in full or as a partial payment &mdash; you record it against the invoice.
The system supports multiple partial payments on a single invoice.
</p>
<ol class="mb-3">
@@ -169,17 +169,17 @@
<li class="mb-2">
Fill in the payment details:
<ul class="mt-1">
<li><strong>Amount</strong> how much was received this time. Can be less than the full balance for partial payments.</li>
<li><strong>Payment Method</strong> Cash, Check, Credit/Debit Card, Bank Transfer / ACH, or Digital Payment.</li>
<li><strong>Payment Date</strong> defaults to today.</li>
<li><strong>Reference Number</strong> optional. Use for check numbers, transaction IDs, or wire reference numbers.</li>
<li><strong>Notes</strong> any additional notes about this payment.</li>
<li><strong>Amount</strong> &mdash; how much was received this time. Can be less than the full balance for partial payments.</li>
<li><strong>Payment Method</strong> &mdash; Cash, Check, Credit/Debit Card, Bank Transfer / ACH, or Digital Payment.</li>
<li><strong>Payment Date</strong> &mdash; defaults to today.</li>
<li><strong>Reference Number</strong> &mdash; optional. Use for check numbers, transaction IDs, or wire reference numbers.</li>
<li><strong>Notes</strong> &mdash; any additional notes about this payment.</li>
</ul>
</li>
<li class="mb-2">Click <strong>Save Payment</strong>.</li>
</ol>
<p>
The invoice status updates automatically to <strong>Partially Paid</strong> if there is still
The invoice status updates automatically &mdash; to <strong>Partially Paid</strong> if there is still
a remaining balance, or <strong>Paid</strong> if the full amount has been received. The customer's
outstanding balance on their account is reduced by the payment amount. All payments are shown in
a payment log on the invoice Details page.
@@ -199,8 +199,8 @@
<i class="bi bi-x-circle text-primary me-2"></i>Voiding an Invoice
</h2>
<p>
If an invoice was created in error for example, against the wrong job or with incorrect line
items that cannot be corrected you can void it to remove it from the customer's outstanding
If an invoice was created in error &mdash; for example, against the wrong job or with incorrect line
items that cannot be corrected &mdash; you can void it to remove it from the customer's outstanding
balance.
</p>
<ol class="mb-3">
@@ -215,7 +215,7 @@
<strong>Restrictions:</strong> Only invoices in <strong>Draft</strong> or <strong>Sent</strong>
status can be voided. If an invoice has payments recorded against it, you must delete those
payments first before you can void the invoice. Invoices that are Partially Paid or Paid
cannot be voided directly consider writing off the remaining balance instead if needed.
cannot be voided directly &mdash; consider writing off the remaining balance instead if needed.
</div>
</div>
</section>
@@ -238,7 +238,7 @@
<p>
If a customer's outstanding balance is approaching or has exceeded their credit limit, a warning
flag is shown on their customer record, on new jobs you try to create for them, and on new invoices.
This is a visual warning only the system does not automatically block new work but it provides
This is a visual warning only &mdash; the system does not automatically block new work &mdash; but it provides
a clear signal to follow up on payment.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -263,7 +263,7 @@
<p>
When you create an invoice from a job, <strong>all unapplied deposits are automatically applied
as payments</strong> on the new invoice. The invoice's Amount Paid and status update accordingly
you may find the invoice is already partially or fully paid at creation time.
&mdash; you may find the invoice is already partially or fully paid at creation time.
</p>
<p>
Each deposit generates a receipt (receipt number format: <code>DEP-YYMM-####</code>) that can
@@ -281,7 +281,7 @@
</p>
<p>
To apply a gift certificate to an invoice, open the Invoice Details page and click
<strong>Apply Gift Certificate</strong>. Enter the certificate code the system looks up
<strong>Apply Gift Certificate</strong>. Enter the certificate code &mdash; the system looks up
the remaining balance and applies it as a payment up to the invoice amount.
</p>
</section>
@@ -307,7 +307,7 @@
<ol class="mb-3">
<li class="mb-2">Go to <strong>Settings &rsaquo; Billing</strong> (<a href="/Billing">/Billing</a>).</li>
<li class="mb-2">Click <strong>Connect with Stripe</strong> (or <em>Set Up Online Payments</em>).</li>
<li class="mb-2">You are redirected to Stripe create a new Stripe account or connect an existing one.</li>
<li class="mb-2">You are redirected to Stripe &mdash; create a new Stripe account or connect an existing one.</li>
<li class="mb-2">Complete Stripe&rsquo;s onboarding: enter your business details, add a bank account for payouts, and verify your identity as required by Stripe.</li>
<li class="mb-2">Once Stripe approves the account, you are returned to the app and Stripe Connect status shows <strong>Active</strong>.</li>
<li class="mb-2">Payment links now appear on Invoice Details and on the Online Payments page.</li>
@@ -326,8 +326,8 @@
Once Stripe Connect is active, open any Invoice Details page and use one of these options:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Copy Payment Link</strong> copies the URL to your clipboard so you can paste it into an email, text, or any other message.</li>
<li class="mb-1"><strong>Send Payment Link</strong> emails the payment link directly to the customer&rsquo;s email address on file, with a brief message and the invoice amount.</li>
<li class="mb-1"><strong>Copy Payment Link</strong> &mdash; copies the URL to your clipboard so you can paste it into an email, text, or any other message.</li>
<li class="mb-1"><strong>Send Payment Link</strong> &mdash; emails the payment link directly to the customer&rsquo;s email address on file, with a brief message and the invoice amount.</li>
</ul>
<p>
The link is unique to each invoice and does not expire as long as the invoice remains unpaid.
@@ -340,17 +340,17 @@
<i class="bi bi-bell text-primary me-2"></i>Automated Payment Reminders
</h2>
<p>
The system can automatically email customers when their invoices become overdue without you
The system can automatically email customers when their invoices become overdue &mdash; without you
having to remember to follow up manually. This feature is controlled from
<strong>Settings &rsaquo; Notifications &rsaquo; Automated Payment Reminders</strong>.
</p>
<ul class="mb-3">
<li class="mb-2">
<strong>Enable Payment Reminders</strong> turn the feature on or off for your company at any time.
<strong>Enable Payment Reminders</strong> &mdash; turn the feature on or off for your company at any time.
When off, no automated emails are sent.
</li>
<li class="mb-2">
<strong>Reminder Days</strong> a comma-separated list of day milestones past the due date at which
<strong>Reminder Days</strong> &mdash; a comma-separated list of day milestones past the due date at which
reminders are sent. The default is <code>7,14,30</code> (one reminder at 7 days overdue, another at
14, and a final one at 30).
</li>
+48 -48
View File
@@ -38,7 +38,7 @@
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
The fastest way to create a job is to start with a quote. Build and approve the quote, then
click <strong>Convert to Job</strong> all items, coatings, and pricing carry over automatically.
click <strong>Convert to Job</strong> &mdash; all items, coatings, and pricing carry over automatically.
</div>
</div>
</section>
@@ -50,10 +50,10 @@
<p>To create a job manually:</p>
<ol class="mb-3">
<li class="mb-2">Go to <strong>Operations &rsaquo; Jobs</strong> and click the <strong>New Job</strong> button in the top-right corner.</li>
<li class="mb-2">Select the <strong>Customer</strong> this field is required. Type to search by name or company.</li>
<li class="mb-2">Enter a <strong>Job Description</strong> summarising the work (e.g., "Powder coat motorcycle frame gloss black").</li>
<li class="mb-2">Select the <strong>Customer</strong> &mdash; this field is required. Type to search by name or company.</li>
<li class="mb-2">Enter a <strong>Job Description</strong> summarising the work (e.g., "Powder coat motorcycle frame &mdash; gloss black").</li>
<li class="mb-2">Set the <strong>Scheduled Date</strong> (when work is expected to begin) and the <strong>Due Date</strong> (when the customer expects pickup or delivery).</li>
<li class="mb-2">Choose a <strong>Priority</strong> Normal is the default; see the Job Priority section below for all levels.</li>
<li class="mb-2">Choose a <strong>Priority</strong> &mdash; Normal is the default; see the Job Priority section below for all levels.</li>
<li class="mb-2">Optionally assign a <strong>Worker</strong> from your shop workers list.</li>
<li class="mb-2">Enter the customer's <strong>PO Number</strong> if they require one for their own records.</li>
<li class="mb-2">Add any <strong>Special Instructions</strong> your team needs to know before starting work.</li>
@@ -81,7 +81,7 @@
at any time by opening the job and editing it.
</p>
<p>
The two special statuses <strong>On Hold</strong> and <strong>Cancelled</strong> can be applied
The two special statuses &mdash; <strong>On Hold</strong> and <strong>Cancelled</strong> &mdash; can be applied
at any point in the workflow regardless of the current status. Use On Hold to pause a job temporarily
(waiting for customer approval, missing materials, etc.) and Cancelled to formally close a job that
will not be completed.
@@ -89,7 +89,7 @@
<p>
On the Jobs list, click any status badge to open a quick-change modal. The modal includes a
<strong>Notify customer via email</strong> toggle. If the customer has email notifications turned off,
that toggle is automatically disabled and a warning note is shown no email will be sent regardless.
that toggle is automatically disabled and a warning note is shown &mdash; no email will be sent regardless.
</p>
<div class="table-responsive">
@@ -176,7 +176,7 @@
</h2>
<p>
Every job has a priority level that tells your team how urgently a job needs to move through the shop.
Priorities are color-coded throughout the system in the job list, on the job details page, and on
Priorities are color-coded throughout the system &mdash; in the job list, on the job details page, and on
any dashboards. Set or change the priority in the Create or Edit form.
</p>
@@ -234,21 +234,21 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Item Types</h3>
<ul class="mb-3">
<li class="mb-2">
<strong>Calculated Item</strong> enter surface area, quantity, and complexity. The system
<strong>Calculated Item</strong> &mdash; enter surface area, quantity, and complexity. The system
calculates material, labor, and equipment costs automatically. Select one or more powder
coatings and optional prep services (sandblasting, masking, cleaning).
</li>
<li class="mb-2">
<strong>Custom Work Item</strong> enter a free-text description and a manual price. Use this for
<strong>Custom Work Item</strong> &mdash; enter a free-text description and a manual price. Use this for
one-off work that does not fit the standard calculation model.
</li>
<li class="mb-2">
<strong>AI Photo Quote Item</strong> upload photos of the parts and let our AI agent estimate
<strong>AI Photo Quote Item</strong> &mdash; upload photos of the parts and let our AI agent estimate
the surface area, complexity, and labor time. Review and override any value before accepting.
Up to two follow-up rounds of questions are supported.
</li>
<li class="mb-2">
<strong>Labor Item</strong> a line item representing time and labor charges only, without
<strong>Labor Item</strong> &mdash; a line item representing time and labor charges only, without
material costs.
</li>
</ul>
@@ -268,8 +268,8 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Prep Services</h3>
<p>
Each item can also include optional <strong>prep services</strong> sandblasting, masking, or
chemical cleaning that will be carried out before coating. These are selected in the wizard and
Each item can also include optional <strong>prep services</strong> &mdash; sandblasting, masking, or
chemical cleaning &mdash; that will be carried out before coating. These are selected in the wizard and
appear as sub-lines under the item on the job details page.
</p>
@@ -278,32 +278,32 @@
After completing the coatings and prep services steps, <strong>Calculated</strong> and
<strong>AI Photo Quote</strong> items include one final optional step: <strong>Save to Product Catalog</strong>.
This lets you turn the item you just configured into a reusable catalog entry so it can be selected
instantly on future quotes or jobs without re-entering dimensions, coatings, or prep services.
instantly on future quotes or jobs &mdash; without re-entering dimensions, coatings, or prep services.
</p>
<p>The wizard pre-fills the catalog form with:</p>
<ul class="mb-3">
<li><strong>Name</strong> taken from the item description (or AI-generated description); you can edit it</li>
<li><strong>Default Price</strong> copied from the item's calculated or manually adjusted unit price</li>
<li><strong>Description</strong> the item description</li>
<li><strong>Sandblasting / Masking</strong> automatically checked if those prep services were selected</li>
<li><strong>Category</strong> choose from your active catalog categories</li>
<li><strong>Name</strong> &mdash; taken from the item description (or AI-generated description); you can edit it</li>
<li><strong>Default Price</strong> &mdash; copied from the item's calculated or manually adjusted unit price</li>
<li><strong>Description</strong> &mdash; the item description</li>
<li><strong>Sandblasting / Masking</strong> &mdash; automatically checked if those prep services were selected</li>
<li><strong>Category</strong> &mdash; choose from your active catalog categories</li>
</ul>
<p>At the bottom of the step you have two options:</p>
<ul class="mb-3">
<li><strong>Save to Catalog &amp; Add</strong> saves the catalog item immediately and then adds the line item to the job.</li>
<li><strong>Skip Add to Job Only</strong> skips the save and adds the item to the job without creating a catalog entry.</li>
<li><strong>Save to Catalog &amp; Add</strong> &mdash; saves the catalog item immediately and then adds the line item to the job.</li>
<li><strong>Skip &mdash; Add to Job Only</strong> &mdash; skips the save and adds the item to the job without creating a catalog entry.</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-info-circle-fill flex-shrink-0 mt-1"></i>
<div>
The catalog save happens immediately when you click <strong>Save to Catalog &amp; Add</strong> before the job form is submitted. The catalog item is preserved even if you later discard the job. You can view and manage all saved items at <a asp-controller="CatalogItems" asp-action="Index">Catalog Items</a>.
The catalog save happens immediately when you click <strong>Save to Catalog &amp; Add</strong> &mdash; before the job form is submitted. The catalog item is preserved even if you later discard the job. You can view and manage all saved items at <a asp-controller="CatalogItems" asp-action="Index">Catalog Items</a>.
</div>
</div>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
Items on the job details page are grouped by type Catalog Items, Custom Work, and Labor to
Items on the job details page are grouped by type &mdash; Catalog Items, Custom Work, and Labor &mdash; to
make it easy to see exactly what work is being performed and which materials are required.
</div>
</div>
@@ -350,7 +350,7 @@
<i class="bi bi-receipt text-primary me-2"></i>Creating an Invoice from a Job
</h2>
<p>
Once a job is complete or at any time when you are ready to bill the customer you can create an
Once a job is complete &mdash; or at any time when you are ready to bill the customer &mdash; you can create an
invoice directly from the job's Details page. There is no need to manually re-enter pricing.
</p>
<ol class="mb-3">
@@ -428,7 +428,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Job Notes</h3>
<p>
Add internal notes to a job from the Details page. Notes are private they are not visible
Add internal notes to a job from the Details page. Notes are private &mdash; they are not visible
to the customer. Use them for team communication, special handling instructions, or to log
anything notable that happened during production.
</p>
@@ -460,7 +460,7 @@
<i class="bi bi-clipboard2-check text-primary me-2"></i>Job Templates
</h2>
<p>
If you do the same type of work repeatedly for example, a standard wheel refinish package
If you do the same type of work repeatedly &mdash; for example, a standard wheel refinish package &mdash;
you can save a job's line items as a template and reuse it for future jobs.
</p>
<p>Templates are managed at <strong>/JobTemplates</strong>. To use a template:</p>
@@ -500,7 +500,7 @@
</h2>
<p>
The <strong>Part Intake</strong> workflow lets you formally check in a customer's parts at
drop-off before any work begins. This creates a timestamped record of how many pieces
drop-off &mdash; before any work begins. This creates a timestamped record of how many pieces
arrived, their condition, and who received them, which protects the shop if a customer later
disputes pre-existing damage or a missing piece.
</p>
@@ -513,10 +513,10 @@
On the intake form you will see:
<ul class="mt-1">
<li>A <strong>Job Summary</strong> card showing expected part count (from line items), due date, and any special instructions.</li>
<li><strong>Actual Part Count</strong> enter the number of pieces physically received. If this differs from the expected count, a warning appears prompting you to note the discrepancy.</li>
<li><strong>Condition Notes</strong> describe the condition of the parts (existing scratches, rust, missing hardware, special handling requirements, etc.).</li>
<li><strong>Advance to In Preparation</strong> toggle on to automatically move the job status to <em>In Preparation</em> at the same time. Leave off if the customer is dropping parts off early and work hasn't started yet.</li>
<li><strong>Before Photos</strong> upload photos documenting the condition at drop-off. Photos are saved as "Before" type on the job and appear in the Photos section of Job Details.</li>
<li><strong>Actual Part Count</strong> &mdash; enter the number of pieces physically received. If this differs from the expected count, a warning appears prompting you to note the discrepancy.</li>
<li><strong>Condition Notes</strong> &mdash; describe the condition of the parts (existing scratches, rust, missing hardware, special handling requirements, etc.).</li>
<li><strong>Advance to In Preparation</strong> &mdash; toggle on to automatically move the job status to <em>In Preparation</em> at the same time. Leave off if the customer is dropping parts off early and work hasn't started yet.</li>
<li><strong>Before Photos</strong> &mdash; upload photos documenting the condition at drop-off. Photos are saved as "Before" type on the job and appear in the Photos section of Job Details.</li>
</ul>
</li>
<li class="mb-2">Click <strong>Complete Intake</strong>.</li>
@@ -533,7 +533,7 @@
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
The intake form is optimised for use on a tablet at the front desk the layout is
The intake form is optimised for use on a tablet at the front desk &mdash; the layout is
touch-friendly and the photo upload works directly from a tablet camera.
</div>
</div>
@@ -547,7 +547,7 @@
<strong>Shop Mobile</strong> (<a href="/Jobs/ShopMobile">/Jobs/ShopMobile</a>) is a
phone and tablet-optimised view of all active jobs, designed for workers on the shop floor.
Unlike the Shop Display (which is a passive TV view) and the full desktop UI, Shop Mobile is
built for one-handed use large touch targets, no sidebar, and instant status advancement
built for one-handed use &mdash; large touch targets, no sidebar, and instant status advancement
with a single tap.
</p>
@@ -559,8 +559,8 @@
<li>Assigned worker, sandblasting/masking flags, and due date</li>
<li>Line items (up to 3 shown; tap the details button for the full list)</li>
<li>Powder colours</li>
<li>Special instructions (amber callout hard to miss)</li>
<li>An <strong>intake indicator</strong> a green box icon means parts were checked in; an amber box icon means intake is still pending (tap it to go directly to the intake form)</li>
<li>Special instructions (amber callout &mdash; hard to miss)</li>
<li>An <strong>intake indicator</strong> &mdash; a green box icon means parts were checked in; an amber box icon means intake is still pending (tap it to go directly to the intake form)</li>
<li>An <strong>overdue badge</strong> when the due date has passed</li>
</ul>
@@ -598,7 +598,7 @@
<i class="bi bi-person-gear text-primary me-2"></i>Changing the Customer
</h2>
<p>
The customer on a job can be changed at any time from the Job Details page no need to
The customer on a job can be changed at any time from the Job Details page &mdash; no need to
delete and re-create the job. This is useful when:
</p>
<ul class="mb-3">
@@ -610,16 +610,16 @@
<h3 class="h6 fw-semibold mt-3 mb-2">How to change the customer</h3>
<ol class="mb-3">
<li class="mb-2">Open the job from <strong>Operations &rsaquo; Jobs</strong> and go to its Details page.</li>
<li class="mb-2">Find the <strong>Customer</strong> field in the job header it appears as a dropdown showing the current customer.</li>
<li class="mb-2">Find the <strong>Customer</strong> field in the job header &mdash; it appears as a dropdown showing the current customer.</li>
<li class="mb-2">Select a different customer from the dropdown.</li>
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert.</li>
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> &mdash; click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert.</li>
</ol>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div>
The customer can also be changed on the <strong>Edit Job</strong> page using the Customer
dropdown there. Any invoices or deposits already linked to the job are not automatically
moved update those separately if needed.
moved &mdash; update those separately if needed.
</div>
</div>
</section>
@@ -629,24 +629,24 @@
<i class="bi bi-qr-code text-primary me-2"></i>Work Order QR Codes
</h2>
<p>
Every printed job work order includes two tiers of QR codes one for <strong>viewing</strong>
Every printed job work order includes two tiers of QR codes &mdash; one for <strong>viewing</strong>
the job and a separate set for <strong>acting</strong> on it. This gives shop workers everything
they need from a printed sheet without touching the desktop app.
All QR codes require a logged-in account.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-eye me-1"></i>Top QR View Job</h3>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-eye me-1"></i>Top QR &mdash; View Job</h3>
<p>
Located in the work order header, next to the job number. Scan it with your phone to open the
full <strong>Job Details</strong> page items, catalog product images, powder specs, coatings,
full <strong>Job Details</strong> page &mdash; items, catalog product images, powder specs, coatings,
prep services, and special instructions. Use it to verify you're working the right job or to
see catalog item images on your phone without hunting through the app.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-arrow-right-circle me-1"></i>Bottom QR Update Status</h3>
<h3 class="h6 fw-semibold mt-3 mb-2"><i class="bi bi-arrow-right-circle me-1"></i>Bottom QR &mdash; Update Status</h3>
<p>
Scan to open a mobile-friendly status bump page for this job. Tap the button to advance to the
next stage (or put the job on hold). The status change is recorded in history with your name
next stage (or put the job on hold). The status change is recorded in history with your name &mdash;
no anonymous bumps.
</p>
@@ -686,7 +686,7 @@
<h2 class="h5 fw-semibold mb-3">Blank Work Order</h2>
<p>
The Blank Work Order feature lets you instantly print a pre-formatted paper work order to hand to a
customer at drop-off before a digital job record has been created. It uses your company logo,
customer at drop-off &mdash; before a digital job record has been created. It uses your company logo,
address, and customizable terms so it looks professional right out of the box.
</p>
@@ -694,7 +694,7 @@
<ol>
<li>Go to <strong>Jobs</strong> in the sidebar.</li>
<li>Click the <strong><i class="bi bi-printer"></i> Blank Work Order</strong> button in the top-right toolbar (next to the Jobs Board button).</li>
<li>A PDF opens in a new browser tab print it or save it to PDF.</li>
<li>A PDF opens in a new browser tab &mdash; print it or save it to PDF.</li>
</ol>
<p>You can also navigate directly to <a href="/WorkOrder/Blank">/WorkOrder/Blank</a> to open the PDF at any time.</p>
@@ -714,8 +714,8 @@
Go to <strong>Company Settings → PDF Templates → Work Order</strong> to customize:
</p>
<ul>
<li><strong>Accent Color</strong> the color used for table headers, the title bar, and section labels. Defaults to dark gray.</li>
<li><strong>Terms &amp; Conditions</strong> up to 2,000 characters of text printed in italic below the notes box. Use this for your shop's standard policies, liability disclaimer, or payment terms.</li>
<li><strong>Accent Color</strong> &mdash; the color used for table headers, the title bar, and section labels. Defaults to dark gray.</li>
<li><strong>Terms &amp; Conditions</strong> &mdash; up to 2,000 characters of text printed in italic below the notes box. Use this for your shop's standard policies, liability disclaimer, or payment terms.</li>
</ul>
<p>Click <strong>Save</strong> to apply changes, or <strong>Preview</strong> to open the PDF instantly without saving.</p>
@@ -27,7 +27,7 @@
<p>
The PO workflow is designed to eliminate double data entry. When you receive a PO, stock levels
update automatically for any linked inventory items. When you are ready to pay, you can convert
the received PO directly into a vendor bill in Accounts Payable all the line items, quantities,
the received PO directly into a vendor bill in Accounts Payable &mdash; all the line items, quantities,
and prices carry over without retyping.
</p>
<p>
@@ -46,7 +46,7 @@
<li class="mb-2">Select the <strong>Vendor</strong> you are ordering from. The vendor's contact information and default payment terms are pulled in automatically.</li>
<li class="mb-2">Set the <strong>Order Date</strong> (defaults to today) and an optional <strong>Expected Delivery Date</strong>.</li>
<li class="mb-2">
Add line items click <strong>Add Line</strong> for each product you are ordering:
Add line items &mdash; click <strong>Add Line</strong> for each product you are ordering:
<ul class="mt-1">
<li>Start typing an item name in the search field to look up items from your inventory. If the item exists, select it to link the PO line directly to your stock record.</li>
<li>If the item is not in your inventory yet (a new product), enter a description manually. You can create the inventory item later after receiving the goods.</li>
@@ -54,7 +54,7 @@
</ul>
</li>
<li class="mb-2">Enter a <strong>Shipping Cost</strong> if the vendor charges for delivery. This is added to the PO total.</li>
<li class="mb-2">Add any <strong>Notes for the Vendor</strong> these appear on the printed PO document.</li>
<li class="mb-2">Add any <strong>Notes for the Vendor</strong> &mdash; these appear on the printed PO document.</li>
<li class="mb-2">Add any internal <strong>Notes</strong> for your own team (not shown to the vendor).</li>
<li class="mb-2">Click <strong>Save PO</strong>. The PO is saved as a Draft.</li>
</ol>
@@ -126,7 +126,7 @@
</h2>
<p>
After a PO has been received, you can convert it into a vendor bill in Accounts Payable with
one click no need to re-enter any of the line item details.
one click &mdash; no need to re-enter any of the line item details.
</p>
<ol class="mb-3">
<li class="mb-2">Open the received PO and click <strong>Create Bill</strong>.</li>
@@ -138,7 +138,7 @@
<li>The vendor's payment terms and a calculated due date</li>
</ul>
</li>
<li class="mb-2">Review the bill confirm the due date, check that expense accounts are correct, and add any notes.</li>
<li class="mb-2">Review the bill &mdash; confirm the due date, check that expense accounts are correct, and add any notes.</li>
<li class="mb-2">Click <strong>Save Bill</strong>. The bill is saved as a Draft in Accounts Payable.</li>
<li class="mb-2">When you have verified the bill against the vendor's paper invoice, click <strong>Mark as Open</strong> to post it to your AP ledger. See the <a asp-controller="Help" asp-action="AccountsPayable">Accounts Payable help page</a> for details.</li>
</ol>
+40 -40
View File
@@ -22,10 +22,10 @@
<p>
Quotes let you provide customers and prospects with a formal price estimate before work begins.
The quoting engine calculates material costs, labor, equipment time, overhead, and profit margin
automatically based on the surface area and complexity you enter no spreadsheet required.
automatically based on the surface area and complexity you enter &mdash; no spreadsheet required.
</p>
<p>
Quotes can be created for existing customers or for <strong>prospects</strong> people or
Quotes can be created for existing customers or for <strong>prospects</strong> &mdash; people or
businesses who have not yet become customers. Prospect quotes let you capture all contact details
and pricing in one place so nothing is lost while you are waiting for a decision. If the prospect
accepts, you can convert them to a customer and the quote to a job in just a few clicks.
@@ -47,16 +47,16 @@
top of the page. Your selection is remembered automatically for next time.
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Quick Quote</strong> shows only the essentials: customer picker (or walk-in info) and
<li class="mb-1"><strong>Quick Quote</strong> &mdash; shows only the essentials: customer picker (or walk-in info) and
the item wizard. Dates, notes, tags, oven settings, discounts, and photos are hidden. Use this for
fast phone or counter estimates where you just need a price.</li>
<li class="mb-1"><strong>Full Quote</strong> shows the complete form with all fields. Use this for formal
<li class="mb-1"><strong>Full Quote</strong> &mdash; shows the complete form with all fields. Use this for formal
quotes where you want to capture notes, set an expiry date, apply a discount, or add photos.</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
Switching to Quick Quote does not change how the quote saves all pricing calculations and the item
Switching to Quick Quote does not change how the quote saves &mdash; all pricing calculations and the item
wizard work exactly the same. Hidden fields use their default values (no rush fee, no discount, company
default tax rate).
</div>
@@ -68,13 +68,13 @@
<li class="mb-2">
Choose whether this quote is for an existing <strong>Customer</strong> or a <strong>Prospect</strong>:
<ul class="mt-1">
<li><strong>Customer</strong> select from your existing customer list. The customer's pricing tier discount is applied automatically.</li>
<li><strong>Prospect</strong> enter their first name, last name, company name (optional), email, and phone. These details are stored on the quote and can be used to create a customer record later.</li>
<li><strong>Customer</strong> &mdash; select from your existing customer list. The customer's pricing tier discount is applied automatically.</li>
<li><strong>Prospect</strong> &mdash; enter their first name, last name, company name (optional), email, and phone. These details are stored on the quote and can be used to create a customer record later.</li>
</ul>
</li>
<li class="mb-2">Set the <strong>Quote Date</strong> (defaults to today) and the <strong>Expiry Date</strong> (defaults to the system's configured validity period).</li>
<li class="mb-2">Add a <strong>Subject</strong> or description to identify the work being quoted.</li>
<li class="mb-2">Add one or more <strong>Line Items</strong> see the Quote Items section below for item types.</li>
<li class="mb-2">Add one or more <strong>Line Items</strong> &mdash; see the Quote Items section below for item types.</li>
<li class="mb-2">Add any <strong>Notes</strong> for the customer (these appear on the printed quote).</li>
<li class="mb-2">Add any internal <strong>Notes</strong> that are for your team only.</li>
<li class="mb-2">Click <strong>Save Quote</strong>. The quote is saved as a Draft.</li>
@@ -125,7 +125,7 @@
<div class="card-body small">
Enter a free-text description and type a price manually. Use this for one-off work,
repairs, or services that do not fit the standard surface-area calculation model.
No automatic pricing calculation you set the price directly.
No automatic pricing calculation &mdash; you set the price directly.
</div>
</div>
</div>
@@ -154,10 +154,10 @@
<p>
If an image has been uploaded for a catalog item, a small thumbnail appears to the left of its
name in the list. <strong>Hover over the thumbnail</strong> to see a larger preview near your
cursor useful for quickly confirming you have the right part without opening the full item record.
cursor &mdash; useful for quickly confirming you have the right part without opening the full item record.
</p>
<p>
Images are managed on the <a href="/CatalogItems">Catalog Items</a> page open any item, click
Images are managed on the <a href="/CatalogItems">Catalog Items</a> page &mdash; open any item, click
<strong>Edit</strong>, and use the <strong>Item Image</strong> section to upload a photo
(jpg, jpeg, png, gif, or webp; max 10 MB). A 200&times;200 thumbnail is generated automatically.
</p>
@@ -167,7 +167,7 @@
For Calculated and AI Photo items, after entering the surface area you proceed to the coatings
step. Select one or more powder coatings from your inventory. The wizard shows how much powder
will be needed per coat based on the coverage rate and your surface area. You then choose any
prep services sandblasting, masking, and/or cleaning that will be performed before coating.
prep services &mdash; sandblasting, masking, and/or cleaning &mdash; that will be performed before coating.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Save to Product Catalog</h3>
@@ -182,13 +182,13 @@
<li>Choose a <strong>Category</strong> from your existing catalog categories.</li>
<li>Optionally add or edit a <strong>Description</strong> and flag whether the item typically requires sandblasting or masking.</li>
<li>Click <strong>Save to Catalog &amp; Add</strong> to save the item to the catalog and add it to the quote simultaneously.</li>
<li>Click <strong>Skip Add to Quote Only</strong> to add the item to this quote without saving it to the catalog.</li>
<li>Click <strong>Skip &mdash; Add to Quote Only</strong> to add the item to this quote without saving it to the catalog.</li>
</ul>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-3" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
<strong>Catalog item prices are final.</strong> When a catalog item is added to a quote or job,
the price you entered is used exactly as-is no markup, no prep service charges, and no
the price you entered is used exactly as-is &mdash; no markup, no prep service charges, and no
complexity adjustments are added on top. Make sure the price you save already includes your
labor, materials, and margin.
</div>
@@ -196,7 +196,7 @@
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-bookmark-star-fill flex-shrink-0 mt-1"></i>
<div>
The catalog save happens <strong>immediately</strong> the item is added to your catalog even
The catalog save happens <strong>immediately</strong> &mdash; the item is added to your catalog even
if you later abandon the quote. This is intentional: once you take the time to describe and
price a part, it's worth keeping for next time. Manage saved items at
<a href="/CatalogItems">Catalog Items</a>.
@@ -208,7 +208,7 @@
<div>
The live pricing calculator at the bottom of the quote form updates the subtotal, discounts,
tax, and grand total every time you add or change a line item. There is no need to manually
recalculate the total shown when you save is the total the customer will see.
recalculate &mdash; the total shown when you save is the total the customer will see.
</div>
</div>
</section>
@@ -292,7 +292,7 @@
</p>
<ul class="mb-3">
<li class="mb-1">The customer must have <strong>SMS Opt-In</strong> enabled and a <strong>Mobile Phone</strong> number on their record.</li>
<li class="mb-1">If you already sent the quote via email, the same approval link is reused both the email link and SMS link remain valid simultaneously.</li>
<li class="mb-1">If you already sent the quote via email, the same approval link is reused &mdash; both the email link and SMS link remain valid simultaneously.</li>
<li class="mb-1">For prospect quotes, the SMS goes to the <strong>Prospect Phone</strong> field on the quote.</li>
</ul>
<p>
@@ -369,7 +369,7 @@
</h2>
<p>
When you send a quote by email, the customer receives a link to a self-service approval portal
at <strong>/QuoteApproval</strong>. No login is required the link is unique to that quote.
at <strong>/QuoteApproval</strong>. No login is required &mdash; the link is unique to that quote.
</p>
<p>From the approval portal the customer can:</p>
<ul class="mb-3">
@@ -410,7 +410,7 @@
</h2>
<p>
The <strong>AI Quick Quote</strong> widget lets you get an instant rough estimate from a verbal
description perfect for phone calls and walk-in customers when you don't have time to open the
description &mdash; perfect for phone calls and walk-in customers when you don't have time to open the
full quoting wizard. Look for the dark-blue floating button in the bottom-right corner of any page,
just above the AI Help button.
</p>
@@ -419,13 +419,13 @@
<ol class="mb-3">
<li class="mb-2">Click the <strong>AI Quick Quote</strong> floating button (bottom-right, dark blue with a lightning bolt icon).</li>
<li class="mb-2">
Type a description of the work for example:<br>
Type a description of the work &mdash; for example:<br>
<em>"4 motorcycle wheels in Alien Silver with a black base coat, need sandblasting"</em>
</li>
<li class="mb-2">Set the <strong>Quantity</strong> and <strong>Number of Coats</strong>.</li>
<li class="mb-2">Click <strong>Get Estimate</strong>. The AI analyses your description and returns a price estimate, estimated minutes, surface area, complexity rating, and a confidence score in just a few seconds.</li>
<li class="mb-2">
The panel also shows <strong>powder stock status</strong> for any color names detected in your description a green check means you have it in stock, red means you don't, and a grey question mark means the system couldn't match it.
The panel also shows <strong>powder stock status</strong> for any color names detected in your description &mdash; a green check means you have it in stock, red means you don't, and a grey question mark means the system couldn't match it.
</li>
<li class="mb-2">If the estimate looks right, enter an optional <strong>Customer Reference</strong> (e.g., the caller's name) and click <strong>Save as Draft Quote</strong>. You land directly on the new quote's Details page where you can adjust anything and assign the real customer.</li>
</ol>
@@ -433,13 +433,13 @@
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
Quick quotes are saved under a <strong>"Walk-In / Phone"</strong> customer so nothing blocks you from saving immediately. Once you have the customer's information, reassign the quote using the Customer dropdown on the Quote Details page see <em>Changing the Customer</em> below.
Quick quotes are saved under a <strong>"Walk-In / Phone"</strong> customer so nothing blocks you from saving immediately. Once you have the customer's information, reassign the quote using the Customer dropdown on the Quote Details page &mdash; see <em>Changing the Customer</em> below.
</div>
</div>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
The Quick Quote gives a rough estimate only it uses your shop's operating costs and the AI's
The Quick Quote gives a rough estimate only &mdash; it uses your shop's operating costs and the AI's
interpretation of your description. For formal quotes that will be sent to a customer, always
open the quote and verify the details using the full item wizard before sending.
</div>
@@ -451,7 +451,7 @@
<i class="bi bi-person-gear text-primary me-2"></i>Changing the Customer
</h2>
<p>
The customer on a quote can be changed at any time from the Quote Details page no need to
The customer on a quote can be changed at any time from the Quote Details page &mdash; no need to
delete and re-create the quote. This is particularly useful when:
</p>
<ul class="mb-3">
@@ -463,9 +463,9 @@
<h3 class="h6 fw-semibold mt-3 mb-2">How to change the customer</h3>
<ol class="mb-3">
<li class="mb-2">Open the quote from <strong>Operations &rsaquo; Quotes</strong> and go to its Details page.</li>
<li class="mb-2">Find the <strong>Customer</strong> field in the quote header it appears as a dropdown showing the current customer.</li>
<li class="mb-2">Find the <strong>Customer</strong> field in the quote header &mdash; it appears as a dropdown showing the current customer.</li>
<li class="mb-2">Select a different customer from the dropdown.</li>
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert to the original.</li>
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> &mdash; click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert to the original.</li>
</ol>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
@@ -487,22 +487,22 @@
whether your rates are covering costs. The breakdown includes:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Material Costs</strong> powder and consumables cost based on surface area and the configured cost-per-sq-ft rate.</li>
<li class="mb-1"><strong>Shop Supplies</strong> a small percentage of material and labor costs covering miscellaneous shop consumables (tape, abrasives, etc.).</li>
<li class="mb-1"><strong>Labor Costs</strong> base labor calculated from estimated job minutes. Sandblasting prep is charged at 1.5× the standard labor rate; masking at 0.5×. Additional coats beyond the first are charged at the configured additional coat labor percentage.</li>
<li class="mb-1"><strong>Equipment Costs</strong> hourly rates for the curing oven, sandblaster, and coating booth, applied for the estimated time each piece of equipment is in use.</li>
<li class="mb-1"><strong>Complexity Adjustment</strong> a percentage added based on the item's complexity rating (Simple, Moderate, Complex, or Extreme). Simple items have no adjustment; Extreme items can add 25% or more.</li>
<li class="mb-1"><strong>General Markup</strong> your configured profit percentage applied to the cost subtotal. The exact formula depends on the <em>Pricing Mode</em> set in Company Settings: <em>Markup on Materials</em> adds the percentage on top of costs; <em>Target Margin on Total Cost</em> back-calculates price from a gross-margin target.</li>
<li class="mb-1"><strong>Rush Charge</strong> applied automatically to jobs with Rush or Urgent priority, either as a percentage or a fixed amount depending on your settings.</li>
<li class="mb-1"><strong>Pricing Tier Discount</strong> if the customer has a pricing tier assigned (e.g., Preferred Shop 10% off), the discount is shown as a line item reduction. See <em>Hide Discount from Customer</em> below to control whether this line appears on the customer-facing PDF.</li>
<li class="mb-1"><strong>Tax</strong> the configured tax rate applied to the final subtotal. Zero for tax-exempt customers.</li>
<li class="mb-1"><strong>Grand Total</strong> the amount the customer will be invoiced.</li>
<li class="mb-1"><strong>Material Costs</strong> &mdash; powder and consumables cost based on surface area and the configured cost-per-sq-ft rate.</li>
<li class="mb-1"><strong>Shop Supplies</strong> &mdash; a small percentage of material and labor costs covering miscellaneous shop consumables (tape, abrasives, etc.).</li>
<li class="mb-1"><strong>Labor Costs</strong> &mdash; base labor calculated from estimated job minutes. Sandblasting prep is charged at 1.5× the standard labor rate; masking at 0.5×. Additional coats beyond the first are charged at the configured additional coat labor percentage.</li>
<li class="mb-1"><strong>Equipment Costs</strong> &mdash; hourly rates for the curing oven, sandblaster, and coating booth, applied for the estimated time each piece of equipment is in use.</li>
<li class="mb-1"><strong>Complexity Adjustment</strong> &mdash; a percentage added based on the item's complexity rating (Simple, Moderate, Complex, or Extreme). Simple items have no adjustment; Extreme items can add 25% or more.</li>
<li class="mb-1"><strong>General Markup</strong> &mdash; your configured profit percentage applied to the cost subtotal. The exact formula depends on the <em>Pricing Mode</em> set in Company Settings: <em>Markup on Materials</em> adds the percentage on top of costs; <em>Target Margin on Total Cost</em> back-calculates price from a gross-margin target.</li>
<li class="mb-1"><strong>Rush Charge</strong> &mdash; applied automatically to jobs with Rush or Urgent priority, either as a percentage or a fixed amount depending on your settings.</li>
<li class="mb-1"><strong>Pricing Tier Discount</strong> &mdash; if the customer has a pricing tier assigned (e.g., Preferred Shop &mdash; 10% off), the discount is shown as a line item reduction. See <em>Hide Discount from Customer</em> below to control whether this line appears on the customer-facing PDF.</li>
<li class="mb-1"><strong>Tax</strong> &mdash; the configured tax rate applied to the final subtotal. Zero for tax-exempt customers.</li>
<li class="mb-1"><strong>Grand Total</strong> &mdash; the amount the customer will be invoiced.</li>
</ul>
<h3 class="h6 fw-semibold mt-4 mb-2">Per-Item Cost Breakdown</h3>
<p>
Each line item on the Quote Details page can be expanded to show a cost breakdown for that
individual item click the row to open it. The per-item breakdown shows how material, labor,
individual item &mdash; click the row to open it. The per-item breakdown shows how material, labor,
equipment, complexity, and markup were calculated for that specific piece. This is useful for
spotting underpriced items or understanding where costs are concentrated across a multi-item quote.
</p>
@@ -514,7 +514,7 @@
</p>
<ul class="mb-3">
<li class="mb-1">The pricing tier discount line is <strong>not shown</strong> on the customer-facing quote PDF or on the online approval portal.</li>
<li class="mb-1">The discount is <strong>still applied</strong> to the total the customer pays the discounted price, they just don&rsquo;t see the discount as a separate line item.</li>
<li class="mb-1">The discount is <strong>still applied</strong> to the total &mdash; the customer pays the discounted price, they just don&rsquo;t see the discount as a separate line item.</li>
<li class="mb-1">The full breakdown (including the discount) remains visible to your staff on the internal Quote Details page.</li>
</ul>
<p>
@@ -525,8 +525,8 @@
<div class="alert alert-permanent alert-secondary d-flex gap-2 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div>
All rates used in the breakdown labor, equipment, markup, shop supplies, complexity multipliers,
rush charge, and tax are configured in <strong>Settings &rsaquo; Company Settings &rsaquo; Operating Costs</strong>.
All rates used in the breakdown &mdash; labor, equipment, markup, shop supplies, complexity multipliers,
rush charge, and tax &mdash; are configured in <strong>Settings &rsaquo; Company Settings &rsaquo; Operating Costs</strong>.
See the <a asp-controller="Help" asp-action="Settings">Settings help page</a> for details on Pricing Mode and all rate settings.
</div>
</div>
+48 -48
View File
@@ -21,7 +21,7 @@
</h2>
<p>
The Reports section gives you financial summaries, operational insights, and AI-powered analysis
of your business. Reports are organized on the landing page by category Financial, Operations,
of your business. Reports are organized on the landing page by category &mdash; Financial, Operations,
and AI-Powered. Most reports are read-only views of live data and support PDF export.
</p>
<p>
@@ -44,7 +44,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">KPI Dashboard</h3>
<p>
A high-level scorecard showing your most important metrics at a glance open jobs, pending
A high-level scorecard showing your most important metrics at a glance &mdash; open jobs, pending
quotes, outstanding invoices, and recent revenue. Good for a quick daily check-in.
</p>
@@ -53,15 +53,15 @@
Detailed charts and KPI cards for the selected date range:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Total Invoiced</strong> total value of all invoices created in the period.</li>
<li class="mb-1"><strong>Total Collected</strong> total payments actually received.</li>
<li class="mb-1"><strong>Outstanding AR</strong> current total of all unpaid invoices (always reflects today's balance, not filtered by date).</li>
<li class="mb-1"><strong>Overdue Balance</strong> portion of AR where the due date has passed.</li>
<li class="mb-1"><strong>Monthly Invoiced vs. Collected chart</strong> side-by-side bar chart by month; shows if collections are lagging behind invoicing.</li>
<li class="mb-1"><strong>Total Invoiced</strong> &mdash; total value of all invoices created in the period.</li>
<li class="mb-1"><strong>Total Collected</strong> &mdash; total payments actually received.</li>
<li class="mb-1"><strong>Outstanding AR</strong> &mdash; current total of all unpaid invoices (always reflects today's balance, not filtered by date).</li>
<li class="mb-1"><strong>Overdue Balance</strong> &mdash; portion of AR where the due date has passed.</li>
<li class="mb-1"><strong>Monthly Invoiced vs. Collected chart</strong> &mdash; side-by-side bar chart by month; shows if collections are lagging behind invoicing.</li>
</ul>
<p>
The <strong>AI Financial Summary</strong> button in this view generates a plain-English narrative
of your financial performance using AI useful for a quick briefing before a meeting. See the
of your financial performance using AI &mdash; useful for a quick briefing before a meeting. See the
<a href="#ai-reports">AI-Powered Reports</a> section below.
</p>
@@ -80,13 +80,13 @@
<h3 class="h6 fw-semibold mt-3 mb-2">AR Aging</h3>
<p>
Breaks down all outstanding invoices by how long they have been overdue:
<strong>Current</strong> (not yet due), <strong>130 days</strong>,
<strong>3160 days</strong>, <strong>6190 days</strong>, and <strong>90+ days</strong>.
Focus collection efforts on the 6190 and 90+ columns first.
<strong>Current</strong> (not yet due), <strong>1&ndash;30 days</strong>,
<strong>31&ndash;60 days</strong>, <strong>61&ndash;90 days</strong>, and <strong>90+ days</strong>.
Focus collection efforts on the 61&ndash;90 and 90+ columns first.
</p>
<p>
The AR Aging page also has an <strong>AI AR Follow-Up</strong> button that drafts a
collection email for any overdue invoice the tone automatically scales with how overdue
collection email for any overdue invoice &mdash; the tone automatically scales with how overdue
the invoice is (gentle reminder at 7 days, firmer at 30+).
</p>
@@ -98,21 +98,21 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Sales Tax Report</h3>
<p>
An invoice-basis Sales Tax Liability report shows what you collected in tax during the
An invoice-basis Sales Tax Liability report &mdash; shows what you collected in tax during the
period and breaks it down so you can file accurately. Key figures:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Total Tax Billed</strong> sum of all tax charged on invoices in the period.</li>
<li class="mb-1"><strong>Taxable Sales</strong> subtotals of invoices where tax was charged.</li>
<li class="mb-1"><strong>Non-Taxable Sales</strong> subtotals of tax-exempt invoices (e.g. tax-exempt customers).</li>
<li class="mb-1"><strong>Effective Tax Rate</strong> overall average rate across all taxable invoices.</li>
<li class="mb-1"><strong>By Tax Account</strong> breakdown by GL account (useful if you have multiple tax jurisdictions or rates).</li>
<li class="mb-1"><strong>By Month</strong> month-by-month chart and table of taxable sales and tax billed.</li>
<li class="mb-1"><strong>Invoice Detail</strong> every invoice in the period with its tax %, tax amount, and tax account. Non-taxable invoices appear shaded grey so they are easy to distinguish.</li>
<li class="mb-1"><strong>Total Tax Billed</strong> &mdash; sum of all tax charged on invoices in the period.</li>
<li class="mb-1"><strong>Taxable Sales</strong> &mdash; subtotals of invoices where tax was charged.</li>
<li class="mb-1"><strong>Non-Taxable Sales</strong> &mdash; subtotals of tax-exempt invoices (e.g. tax-exempt customers).</li>
<li class="mb-1"><strong>Effective Tax Rate</strong> &mdash; overall average rate across all taxable invoices.</li>
<li class="mb-1"><strong>By Tax Account</strong> &mdash; breakdown by GL account (useful if you have multiple tax jurisdictions or rates).</li>
<li class="mb-1"><strong>By Month</strong> &mdash; month-by-month chart and table of taxable sales and tax billed.</li>
<li class="mb-1"><strong>Invoice Detail</strong> &mdash; every invoice in the period with its tax %, tax amount, and tax account. Non-taxable invoices appear shaded grey so they are easy to distinguish.</li>
</ul>
<p>
This report supports both <strong>PDF export</strong> and <strong>CSV export</strong>.
The CSV is formatted for handing to your accountant or importing into tax-filing software
The CSV is formatted for handing to your accountant or importing into tax-filing software &mdash;
one row per invoice with all relevant columns. Use the quick preset buttons (This Month,
Last Month, YTD, Last Year) to jump to common filing periods without manually entering dates.
</p>
@@ -149,10 +149,10 @@
period. Key metrics include:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Job counts by status</strong> how many jobs are in each stage. A high count in one status can indicate a bottleneck.</li>
<li class="mb-1"><strong>Open, Completed, On Hold, and Overdue counts</strong> KPI cards for the overall health of your job queue.</li>
<li class="mb-1"><strong>Job counts by priority</strong> how many jobs are at each level (Low/Normal/High/Urgent/Rush).</li>
<li class="mb-1"><strong>Average completion time</strong> average days from job creation to Completed status. Use as a baseline for quoting lead times.</li>
<li class="mb-1"><strong>Job counts by status</strong> &mdash; how many jobs are in each stage. A high count in one status can indicate a bottleneck.</li>
<li class="mb-1"><strong>Open, Completed, On Hold, and Overdue counts</strong> &mdash; KPI cards for the overall health of your job queue.</li>
<li class="mb-1"><strong>Job counts by priority</strong> &mdash; how many jobs are at each level (Low/Normal/High/Urgent/Rush).</li>
<li class="mb-1"><strong>Average completion time</strong> &mdash; average days from job creation to Completed status. Use as a baseline for quoting lead times.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Customer Overview</h3>
@@ -164,7 +164,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Job Cycle Time Report</h3>
<p>
Shows how long jobs spend in each status on average. Use this to identify where jobs tend
to slow down for example, if jobs spend an unusually long time in Sandblasting, that
to slow down &mdash; for example, if jobs spend an unusually long time in Sandblasting, that
stage may need more capacity.
</p>
@@ -177,17 +177,17 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Inventory Snapshot</h3>
<p>Key inventory metrics including:</p>
<ul class="mb-3">
<li class="mb-1"><strong>Total inventory value</strong> on-hand quantity × unit cost for all items.</li>
<li class="mb-1"><strong>Items below reorder point</strong> items currently flagged as low stock.</li>
<li class="mb-1"><strong>Recent transaction summary</strong> log of recent stock movements.</li>
<li class="mb-1"><strong>Total inventory value</strong> &mdash; on-hand quantity × unit cost for all items.</li>
<li class="mb-1"><strong>Items below reorder point</strong> &mdash; items currently flagged as low stock.</li>
<li class="mb-1"><strong>Recent transaction summary</strong> &mdash; log of recent stock movements.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Equipment &amp; Maintenance Report</h3>
<p>Equipment health overview for maintenance supervisors:</p>
<ul class="mb-3">
<li class="mb-1"><strong>Equipment status distribution</strong> how many machines are Operational, Needs Maintenance, Under Maintenance, etc.</li>
<li class="mb-1"><strong>Upcoming maintenance tasks</strong> scheduled tasks due soon.</li>
<li class="mb-1"><strong>Overdue maintenance tasks</strong> tasks past their scheduled date that are not yet complete.</li>
<li class="mb-1"><strong>Equipment status distribution</strong> &mdash; how many machines are Operational, Needs Maintenance, Under Maintenance, etc.</li>
<li class="mb-1"><strong>Upcoming maintenance tasks</strong> &mdash; scheduled tasks due soon.</li>
<li class="mb-1"><strong>Overdue maintenance tasks</strong> &mdash; tasks past their scheduled date that are not yet complete.</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -211,7 +211,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">AI Financial Summary</h3>
<p>
Generates a plain-English narrative of your financial performance revenue trends, collection
Generates a plain-English narrative of your financial performance &mdash; revenue trends, collection
rate, outstanding AR, and notable patterns. Accessible from the <strong>Full Analytics</strong>
view via the "Generate AI Summary" button. Useful for summarizing performance before a meeting
or for stakeholders who prefer prose over tables.
@@ -227,8 +227,8 @@
<li class="mb-1">Active job pipeline (potential future revenue)</li>
</ul>
<p>
The forecast includes an <strong>outlook badge</strong> Strong, Moderate, Tight, or
Concerning based on the projected net cash position at each interval.
The forecast includes an <strong>outlook badge</strong> &mdash; Strong, Moderate, Tight, or
Concerning &mdash; based on the projected net cash position at each interval.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Anomaly Detection</h3>
@@ -236,10 +236,10 @@
Scans the last 90 days of bills for unusual patterns and flags them for review:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Duplicate bills</strong> same vendor, similar amount, close dates.</li>
<li class="mb-1"><strong>Amount spikes</strong> a vendor bill significantly higher than their usual amounts.</li>
<li class="mb-1"><strong>Unusual vendors</strong> vendors you have not used before or rarely use.</li>
<li class="mb-1"><strong>Account overruns</strong> expense accounts with spending well above their typical level.</li>
<li class="mb-1"><strong>Duplicate bills</strong> &mdash; same vendor, similar amount, close dates.</li>
<li class="mb-1"><strong>Amount spikes</strong> &mdash; a vendor bill significantly higher than their usual amounts.</li>
<li class="mb-1"><strong>Unusual vendors</strong> &mdash; vendors you have not used before or rarely use.</li>
<li class="mb-1"><strong>Account overruns</strong> &mdash; expense accounts with spending well above their typical level.</li>
</ul>
<p>Flags are sorted by severity. Review and dismiss any that are expected or already explained.</p>
@@ -247,7 +247,7 @@
<p>
Drafts a collection email for overdue invoices directly from the <strong>AR Aging</strong> report.
Click the envelope icon next to any overdue invoice to generate a draft. The tone scales with
how many days overdue the invoice is a gentle nudge at 7 days, a firmer reminder at 30+.
how many days overdue the invoice is &mdash; a gentle nudge at 7 days, a firmer reminder at 30+.
You can edit the draft before sending.
</p>
@@ -258,13 +258,13 @@
each open AR customer and assign a risk level:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>High</strong> customer has a history of late payment and is already overdue; prioritize a phone call today.</li>
<li class="mb-1"><strong>Medium</strong> overdue but reasonable history, or current but spotty past performance.</li>
<li class="mb-1"><strong>Low</strong> typically pays on time; no immediate follow-up needed.</li>
<li class="mb-1"><strong>High</strong> &mdash; customer has a history of late payment and is already overdue; prioritize a phone call today.</li>
<li class="mb-1"><strong>Medium</strong> &mdash; overdue but reasonable history, or current but spotty past performance.</li>
<li class="mb-1"><strong>Low</strong> &mdash; typically pays on time; no immediate follow-up needed.</li>
</ul>
<p>
Each prediction includes an estimated number of additional days to collection and a one-sentence
explanation of the scoring. Use this to triage your collection calls start with High-risk
explanation of the scoring. Use this to triage your collection calls &mdash; start with High-risk
customers first.
</p>
@@ -283,7 +283,7 @@
<p>
Each answer includes <strong>supporting facts</strong> pulled directly from your data so you can
verify the figures, and a <strong>follow-up suggestion</strong> for what to ask next. Claude
will not invent numbers if the data is not available in the snapshot, it says so. The page
will not invent numbers &mdash; if the data is not available in the snapshot, it says so. The page
also shows clickable example chips and remembers your last 5 questions during the session.
</p>
@@ -292,13 +292,13 @@
Inside <strong>Bank Reconciliation</strong> (<a asp-controller="BankReconciliations" asp-action="Index">/BankReconciliations</a>),
the <strong>Reconcile</strong> working view includes an <strong>AI Auto-Match</strong> panel.
Click <strong>"Suggest Matches"</strong> and Claude analyzes all uncleared transactions against
your statement ending balance, then suggests which items to mark as cleared sorted by
your statement ending balance, then suggests which items to mark as cleared &mdash; sorted by
confidence score with a one-sentence reason for each.
</p>
<p>
Click <strong>"Apply All Suggestions"</strong> to accept them in bulk; the checkboxes are marked
and persisted automatically. Review the highlighted rows (shown in blue) before applying if you
want to verify each one individually. Auto-match does not complete the reconciliation you
want to verify each one individually. Auto-match does not complete the reconciliation &mdash; you
still click "Complete Reconciliation" yourself once the difference reaches $0.00.
</p>
@@ -337,7 +337,7 @@
Total, Amount Paid, Balance Due, Tax Account.
</p>
<p>
All PDF and CSV exports respect the same date range you have selected in the report what
All PDF and CSV exports respect the same date range you have selected in the report &mdash; what
you see on screen is exactly what gets exported.
</p>
</section>
@@ -37,7 +37,7 @@
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
Take care when changing pricing rates (labor, equipment, overhead, margins) in Settings.
Existing quotes and invoices are not retroactively recalculated only new quotes created
Existing quotes and invoices are not retroactively recalculated &mdash; only new quotes created
after the change will use the updated rates. If you need to reprice an existing quote,
open it and recalculate manually.
</div>
@@ -50,15 +50,15 @@
</h2>
<p>
Set your company's basic contact information. This data appears on all customer-facing documents
quotes, invoices, and PDFs so it is important to keep it accurate and professional.
&mdash; quotes, invoices, and PDFs &mdash; so it is important to keep it accurate and professional.
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Company Name</strong> appears in the page header and on all printed documents.</li>
<li class="mb-1"><strong>Address</strong> your shop's street address, city, state, and ZIP code. Printed on quotes and invoices.</li>
<li class="mb-1"><strong>Phone</strong> your main business phone number.</li>
<li class="mb-1"><strong>Email</strong> your business email address. Used as the reply-to address for notifications sent to customers.</li>
<li class="mb-1"><strong>Website</strong> optional. Shown on printed documents if entered.</li>
<li class="mb-1"><strong>Company Logo</strong> upload a logo image (PNG or JPG recommended). The logo appears in the top-left corner of the sidebar and at the top of all printed quotes, invoices, and PDFs.</li>
<li class="mb-1"><strong>Company Name</strong> &mdash; appears in the page header and on all printed documents.</li>
<li class="mb-1"><strong>Address</strong> &mdash; your shop's street address, city, state, and ZIP code. Printed on quotes and invoices.</li>
<li class="mb-1"><strong>Phone</strong> &mdash; your main business phone number.</li>
<li class="mb-1"><strong>Email</strong> &mdash; your business email address. Used as the reply-to address for notifications sent to customers.</li>
<li class="mb-1"><strong>Website</strong> &mdash; optional. Shown on printed documents if entered.</li>
<li class="mb-1"><strong>Company Logo</strong> &mdash; upload a logo image (PNG or JPG recommended). The logo appears in the top-left corner of the sidebar and at the top of all printed quotes, invoices, and PDFs.</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
@@ -77,28 +77,28 @@
<p>
Pricing Configuration defines the rates used by the quoting engine to automatically calculate
the cost of every quote. You can find these settings under <strong>Settings &rsaquo; Company Settings &rsaquo; Operating Costs</strong>.
These are your shop's default rates individual quotes reflect these settings at the time they are created.
These are your shop's default rates &mdash; individual quotes reflect these settings at the time they are created.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Labor</h3>
<ul class="mb-3">
<li class="mb-1"><strong>Standard Labor Rate ($/hr)</strong> the hourly labor cost applied to all coating work. Sandblasting prep is calculated at 1.5× this rate; masking at 0.5× these multipliers are built into the pricing engine and are not separately configurable.</li>
<li class="mb-1"><strong>Additional Coat Labor (%)</strong> the percentage of the base labor cost charged for each coat beyond the first. For example, at 30%, a two-coat job charges 100% labor for the first coat and 30% for the second.</li>
<li class="mb-1"><strong>Per-Role Labor Rates</strong> you can optionally set a different hourly rate for each shop worker role (Coater, Sandblaster, Masker, etc.). When set, the role-specific rate overrides the standard rate for that type of work. If no role rate is configured, the standard rate is used as the fallback.</li>
<li class="mb-1"><strong>Standard Labor Rate ($/hr)</strong> &mdash; the hourly labor cost applied to all coating work. Sandblasting prep is calculated at 1.5× this rate; masking at 0.5× &mdash; these multipliers are built into the pricing engine and are not separately configurable.</li>
<li class="mb-1"><strong>Additional Coat Labor (%)</strong> &mdash; the percentage of the base labor cost charged for each coat beyond the first. For example, at 30%, a two-coat job charges 100% labor for the first coat and 30% for the second.</li>
<li class="mb-1"><strong>Per-Role Labor Rates</strong> &mdash; you can optionally set a different hourly rate for each shop worker role (Coater, Sandblaster, Masker, etc.). When set, the role-specific rate overrides the standard rate for that type of work. If no role rate is configured, the standard rate is used as the fallback.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Equipment Hourly Rates</h3>
<ul class="mb-3">
<li class="mb-1"><strong>Default Oven Rate ($/hr)</strong> the cost of running your curing oven per hour. Used when no specific named oven is selected on a quote.</li>
<li class="mb-1"><strong>Sandblaster Rate ($/hr)</strong> the cost of running the sandblasting equipment per hour.</li>
<li class="mb-1"><strong>Coating Booth Rate ($/hr)</strong> the cost of running the spray booth per hour.</li>
<li class="mb-1"><strong>Named Ovens</strong> if your shop has multiple ovens with different capacities or costs, you can add them individually (e.g., "Large Oven $18/hr, 200 sq ft"). When a named oven is selected on a quote the correct rate is applied automatically.</li>
<li class="mb-1"><strong>Default Oven Rate ($/hr)</strong> &mdash; the cost of running your curing oven per hour. Used when no specific named oven is selected on a quote.</li>
<li class="mb-1"><strong>Sandblaster Rate ($/hr)</strong> &mdash; the cost of running the sandblasting equipment per hour.</li>
<li class="mb-1"><strong>Coating Booth Rate ($/hr)</strong> &mdash; the cost of running the spray booth per hour.</li>
<li class="mb-1"><strong>Named Ovens</strong> &mdash; if your shop has multiple ovens with different capacities or costs, you can add them individually (e.g., "Large Oven &mdash; $18/hr, 200 sq ft"). When a named oven is selected on a quote the correct rate is applied automatically.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Material &amp; Shop Supplies</h3>
<ul class="mb-3">
<li class="mb-1"><strong>Powder Coating Cost per Sq Ft ($/sq ft)</strong> the baseline material cost per square foot of surface area, representing your average powder and consumables cost.</li>
<li class="mb-1"><strong>Shop Supplies Rate (%)</strong> a percentage applied to material and labor costs to cover miscellaneous shop supplies (abrasives, tape, fasteners, etc.) that are not tracked per-job.</li>
<li class="mb-1"><strong>Powder Coating Cost per Sq Ft ($/sq ft)</strong> &mdash; the baseline material cost per square foot of surface area, representing your average powder and consumables cost.</li>
<li class="mb-1"><strong>Shop Supplies Rate (%)</strong> &mdash; a percentage applied to material and labor costs to cover miscellaneous shop supplies (abrasives, tape, fasteners, etc.) that are not tracked per-job.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2" id="facility-overhead">Facility Overhead (Rent &amp; Utilities)</h3>
@@ -108,9 +108,9 @@
transparent in the pricing breakdown and ensures accurate job costing.
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Monthly Rent ($)</strong> your monthly shop lease or mortgage payment for the production facility.</li>
<li class="mb-1"><strong>Monthly Utilities ($)</strong> monthly gas, electricity, and water costs for the shop. Do not include costs already captured in your oven/equipment hourly rates.</li>
<li class="mb-1"><strong>Monthly Billable Hours</strong> the number of productive labor hours your shop operates per month (default: 160 roughly one full-time worker for a month). This is used to convert the combined rent + utilities into a per-hour overhead rate.</li>
<li class="mb-1"><strong>Monthly Rent ($)</strong> &mdash; your monthly shop lease or mortgage payment for the production facility.</li>
<li class="mb-1"><strong>Monthly Utilities ($)</strong> &mdash; monthly gas, electricity, and water costs for the shop. Do not include costs already captured in your oven/equipment hourly rates.</li>
<li class="mb-1"><strong>Monthly Billable Hours</strong> &mdash; the number of productive labor hours your shop operates per month (default: 160 &mdash; roughly one full-time worker for a month). This is used to convert the combined rent + utilities into a per-hour overhead rate.</li>
</ul>
<p>
<strong>How it is applied:</strong> The system calculates a per-hour overhead rate as
@@ -131,15 +131,15 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Pricing Mode, Markup, Minimums &amp; Rush Charges</h3>
<ul class="mb-3">
<li class="mb-1">
<strong>Pricing Mode</strong> controls how the General Markup % is applied:
<strong>Pricing Mode</strong> &mdash; controls how the General Markup % is applied:
<ul class="mt-1">
<li><em>Markup on Materials</em> (default) markup is added on top of costs: <code>price = cost &times; (1 + markup%)</code>. A 25% markup on $100 = $125.</li>
<li><em>Target Margin on Total Cost</em> markup % is treated as a gross-margin target: <code>price = cost &divide; (1 &minus; margin%)</code>. A 25% margin on $100 = $133.33. Use this mode if you think in terms of gross margin rather than markup.</li>
<li><em>Markup on Materials</em> (default) &mdash; markup is added on top of costs: <code>price = cost &times; (1 + markup%)</code>. A 25% markup on $100 = $125.</li>
<li><em>Target Margin on Total Cost</em> &mdash; markup % is treated as a gross-margin target: <code>price = cost &divide; (1 &minus; margin%)</code>. A 25% margin on $100 = $133.33. Use this mode if you think in terms of gross margin rather than markup.</li>
</ul>
</li>
<li class="mb-1"><strong>General Markup (%)</strong> your profit percentage. Its exact effect depends on the Pricing Mode selected above.</li>
<li class="mb-1"><strong>Shop Minimum Charge ($)</strong> a floor price for any job. If the calculated total falls below this amount the minimum charge is used instead. Set to 0 to disable.</li>
<li class="mb-1"><strong>Rush Charge</strong> an automatic surcharge applied to jobs with Rush or Urgent priority. Choose between a percentage of the quote total or a fixed dollar amount.</li>
<li class="mb-1"><strong>General Markup (%)</strong> &mdash; your profit percentage. Its exact effect depends on the Pricing Mode selected above.</li>
<li class="mb-1"><strong>Shop Minimum Charge ($)</strong> &mdash; a floor price for any job. If the calculated total falls below this amount the minimum charge is used instead. Set to 0 to disable.</li>
<li class="mb-1"><strong>Rush Charge</strong> &mdash; an automatic surcharge applied to jobs with Rush or Urgent priority. Choose between a percentage of the quote total or a fixed dollar amount.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Part Complexity Multipliers</h3>
@@ -149,16 +149,16 @@
and prep time involved with intricate parts.
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Simple (%)</strong> flat panels, basic shapes. Default: 0% (no adjustment).</li>
<li class="mb-1"><strong>Moderate (%)</strong> standard brackets, moderate curves. Default: 5%.</li>
<li class="mb-1"><strong>Complex (%)</strong> intricate assemblies, deep recesses. Default: 15%.</li>
<li class="mb-1"><strong>Extreme (%)</strong> highly ornate, heavy prep required. Default: 25%.</li>
<li class="mb-1"><strong>Simple (%)</strong> &mdash; flat panels, basic shapes. Default: 0% (no adjustment).</li>
<li class="mb-1"><strong>Moderate (%)</strong> &mdash; standard brackets, moderate curves. Default: 5%.</li>
<li class="mb-1"><strong>Complex (%)</strong> &mdash; intricate assemblies, deep recesses. Default: 15%.</li>
<li class="mb-1"><strong>Extreme (%)</strong> &mdash; highly ornate, heavy prep required. Default: 25%.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Tax &amp; Oven Defaults</h3>
<ul class="mb-3">
<li class="mb-1"><strong>Default Tax Rate (%)</strong> the sales tax percentage applied to quote and invoice totals. Set to 0 if you do not charge tax or handle it separately. Individual customers marked as Tax Exempt will always default to 0% regardless of this setting.</li>
<li class="mb-1"><strong>Default Oven Cycle (minutes)</strong> the default cure time used when scheduling oven batches. Individual named ovens can have their own default cycle time.</li>
<li class="mb-1"><strong>Default Tax Rate (%)</strong> &mdash; the sales tax percentage applied to quote and invoice totals. Set to 0 if you do not charge tax or handle it separately. Individual customers marked as Tax Exempt will always default to 0% regardless of this setting.</li>
<li class="mb-1"><strong>Default Oven Cycle (minutes)</strong> &mdash; the default cure time used when scheduling oven batches. Individual named ovens can have their own default cycle time.</li>
</ul>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
@@ -179,7 +179,7 @@
Pricing Tiers let you offer volume discounts to commercial customers who send you regular work.
You define each tier once in Settings, then assign a tier to each qualifying customer on their
customer record. The quoting engine applies the discount automatically when generating a quote
for that customer no manual adjustment needed.
for that customer &mdash; no manual adjustment needed.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">Creating a Pricing Tier</h3>
@@ -223,15 +223,15 @@
Typical accounts for a powder coating shop include:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Accounts Receivable</strong> money customers owe you (created automatically from invoices).</li>
<li class="mb-1"><strong>Accounts Payable</strong> money you owe vendors (created automatically from bills).</li>
<li class="mb-1"><strong>Cost of Goods Sold</strong> direct material costs: powder, primer, consumables used in jobs.</li>
<li class="mb-1"><strong>Shop Supplies</strong> indirect shop materials not tied to a specific job.</li>
<li class="mb-1"><strong>Equipment Maintenance</strong> service and repair costs for shop machinery.</li>
<li class="mb-1"><strong>Utilities</strong> gas, electricity, and water.</li>
<li class="mb-1"><strong>Rent / Occupancy</strong> monthly rent or lease payments for the shop premises.</li>
<li class="mb-1"><strong>Operating Expenses</strong> general overhead not covered by other categories.</li>
<li class="mb-1"><strong>Revenue / Sales Income</strong> income from completed jobs (created automatically from paid invoices).</li>
<li class="mb-1"><strong>Accounts Receivable</strong> &mdash; money customers owe you (created automatically from invoices).</li>
<li class="mb-1"><strong>Accounts Payable</strong> &mdash; money you owe vendors (created automatically from bills).</li>
<li class="mb-1"><strong>Cost of Goods Sold</strong> &mdash; direct material costs: powder, primer, consumables used in jobs.</li>
<li class="mb-1"><strong>Shop Supplies</strong> &mdash; indirect shop materials not tied to a specific job.</li>
<li class="mb-1"><strong>Equipment Maintenance</strong> &mdash; service and repair costs for shop machinery.</li>
<li class="mb-1"><strong>Utilities</strong> &mdash; gas, electricity, and water.</li>
<li class="mb-1"><strong>Rent / Occupancy</strong> &mdash; monthly rent or lease payments for the shop premises.</li>
<li class="mb-1"><strong>Operating Expenses</strong> &mdash; general overhead not covered by other categories.</li>
<li class="mb-1"><strong>Revenue / Sales Income</strong> &mdash; income from completed jobs (created automatically from paid invoices).</li>
</ul>
<p>
The chart of accounts is typically configured once during initial setup. You can add new accounts
@@ -262,7 +262,7 @@
<p>
The bell icon (<i class="bi bi-bell-fill"></i>) in the top-right header appears for all logged-in
users. A badge shows the count of unread notifications. Click it to open the dropdown showing your
20 most recent notifications both read and unread.
20 most recent notifications &mdash; both read and unread.
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Unread</strong> notifications have a purple left border, a light indigo background, and a blue dot.</li>
@@ -282,7 +282,7 @@
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
Staff-recorded payments and deposits do <strong>not</strong> trigger bell notifications those
Staff-recorded payments and deposits do <strong>not</strong> trigger bell notifications &mdash; those
are internal actions the staff member is already performing and does not need to be notified about.
</div>
</div>
@@ -298,7 +298,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Platform Announcements</h3>
<p>
The Powder Coating Logix platform team can send announcements to your company. These are delivered
directly to your notification bell not as page banners. Announcements may cover scheduled
directly to your notification bell &mdash; not as page banners. Announcements may cover scheduled
maintenance, new features, or policy updates. They appear as <strong>Announcement</strong> type
items in your bell dropdown and can be cleared with "Mark all read."
</p>
@@ -316,11 +316,11 @@
Each toggle controls whether that event type sends a customer-facing or internal notification email:
</p>
<ul class="mb-3">
<li class="mb-1"><strong>New Job</strong> emails your team when a new job is created.</li>
<li class="mb-1"><strong>New Quote</strong> emails your team when a new quote is created.</li>
<li class="mb-1"><strong>Job Status Change</strong> notifies the customer when a job's status changes.</li>
<li class="mb-1"><strong>Quote Approval</strong> notifies the customer when a quote is approved.</li>
<li class="mb-1"><strong>Payment Received</strong> sends the customer a payment confirmation email when a payment is recorded against their invoice.</li>
<li class="mb-1"><strong>New Job</strong> &mdash; emails your team when a new job is created.</li>
<li class="mb-1"><strong>New Quote</strong> &mdash; emails your team when a new quote is created.</li>
<li class="mb-1"><strong>Job Status Change</strong> &mdash; notifies the customer when a job's status changes.</li>
<li class="mb-1"><strong>Quote Approval</strong> &mdash; notifies the customer when a quote is approved.</li>
<li class="mb-1"><strong>Payment Received</strong> &mdash; sends the customer a payment confirmation email when a payment is recorded against their invoice.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Alert Thresholds</h3>
@@ -329,9 +329,9 @@
in the UI (for example, flagging a quote as expiring soon in the quotes list).
</p>
<ul class="mb-3">
<li class="mb-1"><strong>Quote Expiry Warning (days before)</strong> quotes expiring within this many days are flagged in the quotes list.</li>
<li class="mb-1"><strong>Due Date Warning (days before)</strong> invoices coming due within this many days are highlighted.</li>
<li class="mb-1"><strong>Maintenance Alert (days ahead)</strong> upcoming maintenance tasks scheduled within this many days are highlighted.</li>
<li class="mb-1"><strong>Quote Expiry Warning (days before)</strong> &mdash; quotes expiring within this many days are flagged in the quotes list.</li>
<li class="mb-1"><strong>Due Date Warning (days before)</strong> &mdash; invoices coming due within this many days are highlighted.</li>
<li class="mb-1"><strong>Maintenance Alert (days ahead)</strong> &mdash; upcoming maintenance tasks scheduled within this many days are highlighted.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Automated Payment Reminders</h3>
@@ -341,11 +341,11 @@
</p>
<ul class="mb-3">
<li class="mb-2">
<strong>Enable Payment Reminders</strong> master on/off toggle. When off, no automated
<strong>Enable Payment Reminders</strong> &mdash; master on/off toggle. When off, no automated
reminder emails are sent regardless of the day settings below.
</li>
<li class="mb-2">
<strong>Reminder Days</strong> a comma-separated list of day milestones (days past the invoice due date)
<strong>Reminder Days</strong> &mdash; a comma-separated list of day milestones (days past the invoice due date)
at which a reminder email is sent. For example, <code>7,14,30</code> sends one reminder at
7 days overdue, another at 14 days, and a final one at 30 days. You can use any positive
whole numbers (e.g., <code>3,10,21,45</code>).
@@ -364,7 +364,7 @@
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
<div>
Payment reminders are sent once per threshold per invoice if you have <code>7,14,30</code>
Payment reminders are sent once per threshold per invoice &mdash; if you have <code>7,14,30</code>
configured, a single invoice will receive at most three reminder emails over the course of its
collection cycle, one at each milestone. An invoice that gets paid between milestones will not
receive any further reminders (its status changes to Paid and it is excluded from future checks).
@@ -378,13 +378,13 @@
</p>
<ul class="mb-3">
<li class="mb-2">
<strong>Enable SMS Notifications</strong> master company toggle. The first time you enable it
<strong>Enable SMS Notifications</strong> &mdash; master company toggle. The first time you enable it
you must agree to the SMS terms of service, which covers your obligation to obtain prior written
customer consent before texting them (required by FCC/TCPA regulations). Once agreed, you can
toggle SMS on and off freely without re-agreeing unless the terms are updated.
</li>
<li class="mb-2">
<strong>Per-customer opt-in</strong> even with SMS enabled here, a customer will only receive
<strong>Per-customer opt-in</strong> &mdash; even with SMS enabled here, a customer will only receive
texts if their record has a <strong>Mobile Phone</strong> number and the <strong>SMS Opt-In</strong>
box checked. You are responsible for obtaining each customer's consent before enabling this on
their record.
@@ -393,12 +393,12 @@
<h4 class="h6 fw-semibold mt-3 mb-2" style="font-size:.85rem;">What events send an SMS</h4>
<ul class="mb-3">
<li class="mb-1">
<strong>Quote Sent</strong> sends the customer a link to review and approve or decline their
<strong>Quote Sent</strong> &mdash; sends the customer a link to review and approve or decline their
quote directly from their phone. Click <strong>Send Quote via SMS</strong> on any quote's Details
page. If an email was already sent for the same quote, the existing approval link is reused so both
delivery methods work simultaneously.
</li>
<li class="mb-1"><strong>Job Completed</strong> notifies the customer their job is done and ready for pickup.</li>
<li class="mb-1"><strong>Job Completed</strong> &mdash; notifies the customer their job is done and ready for pickup.</li>
</ul>
<p class="small text-muted mb-3">
Both SMS message templates can be customised at
@@ -413,7 +413,7 @@
</p>
<p>
When a <strong>Shop Floor</strong> worker marks a job complete the SMS is sent automatically using
the template no compose step.
the template &mdash; no compose step.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
@@ -421,7 +421,7 @@
Every outbound SMS automatically includes opt-out instructions ("Reply STOP to opt out"). If a
customer replies STOP, they are immediately opted out and will receive no further messages.
If they change their mind, they can reply <strong>START</strong>, <strong>YES</strong>, or <strong>UNSTOP</strong>
to re-subscribe automatically no action needed from your staff. You can also manually re-enable
to re-subscribe automatically &mdash; no action needed from your staff. You can also manually re-enable
SMS on their customer record.
</div>
</div>
@@ -438,10 +438,10 @@
</p>
<p>For each named oven you can set:</p>
<ul class="mb-3">
<li class="mb-1"><strong>Oven Name</strong> e.g., "Main Oven", "Small Parts Oven".</li>
<li class="mb-1"><strong>Operating Cost ($/hr)</strong> the hourly cost to run this specific oven.</li>
<li class="mb-1"><strong>Max Load Capacity (sq ft)</strong> the maximum total surface area that fits in one batch. The Oven Scheduler uses this to show remaining capacity as you add jobs.</li>
<li class="mb-1"><strong>Default Cycle Time (minutes)</strong> the default cure duration for this oven, used when scheduling batches.</li>
<li class="mb-1"><strong>Oven Name</strong> &mdash; e.g., "Main Oven", "Small Parts Oven".</li>
<li class="mb-1"><strong>Operating Cost ($/hr)</strong> &mdash; the hourly cost to run this specific oven.</li>
<li class="mb-1"><strong>Max Load Capacity (sq ft)</strong> &mdash; the maximum total surface area that fits in one batch. The Oven Scheduler uses this to show remaining capacity as you add jobs.</li>
<li class="mb-1"><strong>Default Cycle Time (minutes)</strong> &mdash; the default cure duration for this oven, used when scheduling batches.</li>
</ul>
<p>
When a named oven is selected on a quote or job, its specific cost rate is used instead of
@@ -456,14 +456,14 @@
</h2>
<p>
The <strong>Data Lookups</strong> tab in Company Settings lets you customize the dropdown
options that appear throughout the application job statuses, job priorities, quote statuses,
options that appear throughout the application &mdash; job statuses, job priorities, quote statuses,
appointment types, and inventory categories. You can rename labels, change colours, reorder
items, and add new ones to match your shop's terminology.
</p>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-3" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
<strong>Status codes drive workflow logic</strong> the underlying code value (e.g.
<strong>Status codes drive workflow logic</strong> &mdash; the underlying code value (e.g.
<code>IN_PROGRESS</code>, <code>COMPLETED</code>) is what the system uses internally.
You can safely rename the display label, but do not change the code of a built-in
status unless you understand the impact on automation and reporting.
@@ -475,7 +475,7 @@
<li class="mb-2">Go to <strong>Settings &rsaquo; Company Settings &rsaquo; Data Lookups</strong>.</li>
<li class="mb-2">Select the sub-tab for the lookup type (e.g., Job Statuses, Job Priorities).</li>
<li class="mb-2">Click <strong>Add</strong> to open the form.</li>
<li class="mb-2">Type a <strong>Display Name</strong> the Code field auto-fills as you type
<li class="mb-2">Type a <strong>Display Name</strong> &mdash; the Code field auto-fills as you type
(e.g., "In Progress" → <code>IN_PROGRESS</code>). You can override it if needed.</li>
<li class="mb-2">Choose a color and fill in any additional fields.</li>
<li class="mb-2">Click <strong>Save</strong>.</li>
@@ -483,16 +483,16 @@
<h3 class="h6 fw-semibold mt-3 mb-2">About the Code Field</h3>
<p>
Each lookup item has a <strong>Code</strong> a short uppercase identifier used internally
Each lookup item has a <strong>Code</strong> &mdash; a short uppercase identifier used internally
(e.g., <code>QUALITY_CHECK</code>, <code>URGENT</code>). When adding a new item:
</p>
<ul class="mb-3">
<li class="mb-1">The Code auto-derives from the Display Name as you type spaces become underscores, everything is uppercased.</li>
<li class="mb-1">The Code auto-derives from the Display Name as you type &mdash; spaces become underscores, everything is uppercased.</li>
<li class="mb-1">You can edit the Code directly; it will stay uppercase and only allow letters, numbers, and underscores.</li>
<li class="mb-1">Once saved, the Code cannot be changed it is locked to prevent breaking existing records that reference it.</li>
<li class="mb-1">Once saved, the Code cannot be changed &mdash; it is locked to prevent breaking existing records that reference it.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Inventory Categories Is Coating Flag</h3>
<h3 class="h6 fw-semibold mt-3 mb-2">Inventory Categories &mdash; Is Coating Flag</h3>
<p>
The <strong>Inventory Categories</strong> sub-tab includes an important flag: <strong>Is Coating</strong>.
Only inventory items whose category has this flag enabled will appear in the powder color dropdown
@@ -509,14 +509,14 @@
<p>
The <strong>AI Profile</strong> tab in Company Settings lets you describe your shop's
specialties, typical work, and pricing preferences in plain text. This context is automatically
included in the system prompt sent to AI for photo quoting resulting in more accurate and
included in the system prompt sent to AI for photo quoting &mdash; resulting in more accurate and
relevant estimates.
</p>
<p>Examples of useful AI Profile content:</p>
<ul class="mb-3">
<li class="mb-1">"We specialize in automotive parts and wheels. Most items are steel or aluminum."</li>
<li class="mb-1">"Our minimum charge is $75. We charge a 20% premium for intricate wrought iron."</li>
<li class="mb-1">"We rarely do sandblasting in-house assume parts arrive pre-blasted."</li>
<li class="mb-1">"We rarely do sandblasting in-house &mdash; assume parts arrive pre-blasted."</li>
</ul>
<p>
The AI Profile is optional but recommended. The more specific you are, the better the AI
@@ -549,7 +549,7 @@
</thead>
<tbody>
<tr><td><span class="badge bg-danger">Company Admin</span></td><td>Full company access including settings, users, and billing. All permissions granted automatically.</td></tr>
<tr><td><span class="badge bg-warning text-dark">Manager</span></td><td>Jobs, quotes, invoices, customers, inventory, vendors, reports no settings or user management.</td></tr>
<tr><td><span class="badge bg-warning text-dark">Manager</span></td><td>Jobs, quotes, invoices, customers, inventory, vendors, reports &mdash; no settings or user management.</td></tr>
<tr><td><span class="badge bg-success">Accountant</span></td><td>Financial focus: bills &amp; AP, invoices, bank reconciliations, chart of accounts, vendors, purchase orders, and reports. No job management or settings access.</td></tr>
<tr><td><span class="badge bg-primary">Worker</span></td><td>Create and edit jobs and quotes; no settings, billing, or user management.</td></tr>
<tr><td><span class="badge bg-secondary">Viewer</span></td><td>Read-only access to most data.</td></tr>
@@ -565,7 +565,7 @@
<h3 class="h6 fw-semibold mt-3 mb-2">Fine-Grained Permissions</h3>
<p>
Below the role dropdown, each user has individual permission checkboxes. These let you grant
specific capabilities independently of the role for example, giving a Worker access to view
specific capabilities independently of the role &mdash; for example, giving a Worker access to view
reports without making them a Manager. Company Admins always have all permissions and the
checkboxes are locked.
</p>
@@ -616,32 +616,32 @@
Company Administrators can export a complete copy of their company's data at any time from
<a href="/AccountDataExport">Settings &rsaquo; Download Your Data</a> (or navigate directly to
<code>/AccountDataExport</code>). This feature is available even when a subscription has
expired your data is never locked away.
expired &mdash; your data is never locked away.
</p>
<h3 class="h6 fw-semibold mt-3 mb-2">What Can Be Exported</h3>
<p>Choose any combination of the following data types:</p>
<ul class="mb-3">
<li class="mb-1"><strong>Customers</strong> all customer records including contact info, type, and credit settings.</li>
<li class="mb-1"><strong>Jobs</strong> all job records with status, priority, assigned worker, and dates.</li>
<li class="mb-1"><strong>Quotes</strong> all quote records with status and pricing.</li>
<li class="mb-1"><strong>Invoices</strong> all invoice records with amounts paid and status.</li>
<li class="mb-1"><strong>Inventory</strong> all inventory items with quantities and costs.</li>
<li class="mb-1"><strong>Equipment</strong> all equipment records and statuses.</li>
<li class="mb-1"><strong>Vendors</strong> all vendor/supplier records.</li>
<li class="mb-1"><strong>Shop Workers</strong> all shop worker records.</li>
<li class="mb-1"><strong>Users / Logins</strong> all user accounts and their roles.</li>
<li class="mb-1"><strong>Customers</strong> &mdash; all customer records including contact info, type, and credit settings.</li>
<li class="mb-1"><strong>Jobs</strong> &mdash; all job records with status, priority, assigned worker, and dates.</li>
<li class="mb-1"><strong>Quotes</strong> &mdash; all quote records with status and pricing.</li>
<li class="mb-1"><strong>Invoices</strong> &mdash; all invoice records with amounts paid and status.</li>
<li class="mb-1"><strong>Inventory</strong> &mdash; all inventory items with quantities and costs.</li>
<li class="mb-1"><strong>Equipment</strong> &mdash; all equipment records and statuses.</li>
<li class="mb-1"><strong>Vendors</strong> &mdash; all vendor/supplier records.</li>
<li class="mb-1"><strong>Shop Workers</strong> &mdash; all shop worker records.</li>
<li class="mb-1"><strong>Users / Logins</strong> &mdash; all user accounts and their roles.</li>
</ul>
<h3 class="h6 fw-semibold mt-3 mb-2">Export Formats</h3>
<ul class="mb-3">
<li class="mb-1"><strong>Excel (.xlsx)</strong> all selected data types in a single workbook, one tab per data type. Best for viewing in Excel or Google Sheets.</li>
<li class="mb-1"><strong>CSV (.zip)</strong> one CSV file per selected data type, bundled into a single ZIP archive. Best for importing into other software.</li>
<li class="mb-1"><strong>Excel (.xlsx)</strong> &mdash; all selected data types in a single workbook, one tab per data type. Best for viewing in Excel or Google Sheets.</li>
<li class="mb-1"><strong>CSV (.zip)</strong> &mdash; one CSV file per selected data type, bundled into a single ZIP archive. Best for importing into other software.</li>
</ul>
<p>
The export is generated on-demand and delivered directly to your browser. Nothing is stored
on the server each export is a fresh snapshot of your current data.
on the server &mdash; each export is a fresh snapshot of your current data.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -665,20 +665,20 @@
</p>
<ul class="mb-3">
<li class="mb-2">
<strong>Default Quote Validity Period (days)</strong> the number of days after creation before
<strong>Default Quote Validity Period (days)</strong> &mdash; the number of days after creation before
a quote expires. For example, a value of 30 means all new quotes expire 30 days after their
quote date. Expired quotes are flagged in the quote list and customers cannot act on them without
you creating a revised quote. Adjust this based on how stable your pricing is shops with
you creating a revised quote. Adjust this based on how stable your pricing is &mdash; shops with
volatile powder costs may want a shorter validity period (e.g., 14 days).
</li>
<li class="mb-2">
<strong>Default Payment Terms</strong> the payment terms applied to new customer records
<strong>Default Payment Terms</strong> &mdash; the payment terms applied to new customer records
and new invoices (e.g., "Net 30", "Net 15", "Due on Receipt"). This determines when invoices
become due and affects the AR Aging report. The default can be overridden on each customer
record and on individual invoices.
</li>
<li class="mb-2">
<strong>Currency</strong> the currency symbol and formatting used throughout the system for
<strong>Currency</strong> &mdash; the currency symbol and formatting used throughout the system for
all monetary values (e.g., USD, CAD, AUD, GBP). This is a display setting only and does not
perform any currency conversion. Set this once during initial setup to match your local currency.
</li>
@@ -20,7 +20,7 @@
<i class="bi bi-info-circle text-primary me-2"></i>Overview
</h2>
<p>
Your profile page is where you manage your personal account settings the information that
Your profile page is where you manage your personal account settings &mdash; the information that
identifies you in the system, your login credentials, your profile photo, and how the
application looks on your screen.
</p>
@@ -51,10 +51,10 @@
<li class="mb-1">
Update any of the following fields:
<ul class="mt-1">
<li><strong>First Name</strong> and <strong>Last Name</strong> your display name across the system.</li>
<li><strong>Email Address</strong> used for login and system notifications. Must be a valid email format and unique in the system.</li>
<li><strong>Phone Number</strong> optional; your direct contact number.</li>
<li><strong>Job Title</strong> optional; displayed on your profile card.</li>
<li><strong>First Name</strong> and <strong>Last Name</strong> &mdash; your display name across the system.</li>
<li><strong>Email Address</strong> &mdash; used for login and system notifications. Must be a valid email format and unique in the system.</li>
<li><strong>Phone Number</strong> &mdash; optional; your direct contact number.</li>
<li><strong>Job Title</strong> &mdash; optional; displayed on your profile card.</li>
</ul>
</li>
<li class="mb-1">Click <strong>Save Changes</strong>.</li>
@@ -89,12 +89,12 @@
<p>Your new password must meet all of the following requirements:</p>
<ul class="list-unstyled mb-3">
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least <strong>8 characters</strong> long</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>uppercase letter</strong> (AZ)</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>lowercase letter</strong> (az)</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>digit</strong> (09)</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>uppercase letter</strong> (A&ndash;Z)</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>lowercase letter</strong> (a&ndash;z)</li>
<li class="mb-1"><i class="bi bi-check-circle-fill text-success me-2"></i>At least one <strong>digit</strong> (0&ndash;9)</li>
</ul>
<p>
Example of a strong password: <code>ShopFloor2025!</code> long, mixed case, has a number,
Example of a strong password: <code>ShopFloor2025!</code> &mdash; long, mixed case, has a number,
and easy for you to remember but hard to guess.
</p>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -158,7 +158,7 @@
<li class="mb-1">Install an authenticator app on your phone if you do not have one already.</li>
<li class="mb-1">Scan the QR code shown on the setup page with your authenticator app.</li>
<li class="mb-1">Enter the 6-digit code from the app to confirm setup.</li>
<li class="mb-1">Save your <strong>recovery codes</strong> in a safe place you will need them if you lose access to your phone.</li>
<li class="mb-1">Save your <strong>recovery codes</strong> in a safe place &mdash; you will need them if you lose access to your phone.</li>
</ol>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
@@ -174,8 +174,8 @@
<i class="bi bi-fingerprint text-primary me-2"></i>Passkeys &amp; Biometric Login
</h2>
<p>
Passkeys let you sign in using your device's built-in biometrics Face ID or Touch ID on iPhone and Mac,
fingerprint or face unlock on Android, or Windows Hello on a PC without ever typing your password.
Passkeys let you sign in using your device's built-in biometrics &mdash; Face ID or Touch ID on iPhone and Mac,
fingerprint or face unlock on Android, or Windows Hello on a PC &mdash; without ever typing your password.
This is especially useful for shop floor workers who may have dirty or gloved hands.
</p>
@@ -197,10 +197,10 @@
<ol class="mb-3">
<li class="mb-1">Open the login page.</li>
<li class="mb-1">
Click the <strong>Use Face ID / Biometric</strong> button (the label matches your device
Click the <strong>Use Face ID / Biometric</strong> button (the label matches your device &mdash;
"Use Windows Hello", "Use Touch ID", etc.).
</li>
<li class="mb-1">Follow the device prompt. You are signed in immediately no password required.</li>
<li class="mb-1">Follow the device prompt. You are signed in immediately &mdash; no password required.</li>
</ol>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
@@ -215,13 +215,13 @@
<p>
Go to <a href="/Passkey/Manage">Passkeys &amp; Biometrics</a> (user menu → Passkeys &amp; Biometrics)
to see all devices you have enrolled. Each entry shows the device name, the date it was added,
and when it was last used. Click <strong>Remove</strong> to revoke a passkey from a specific device
and when it was last used. Click <strong>Remove</strong> to revoke a passkey from a specific device &mdash;
useful if you lose a phone or change devices.
</p>
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
Removing a passkey does not log you out it just means that device will require a password
Removing a passkey does not log you out &mdash; it just means that device will require a password
on the next login. If you lose a device, remove its passkey here as soon as possible.
</div>
</div>
@@ -236,14 +236,14 @@
Your choices are saved to your account and follow you across devices when you log in.
</p>
<h5 class="fw-semibold mt-3 mb-2">Theme Light or Dark Mode</h5>
<h5 class="fw-semibold mt-3 mb-2">Theme &mdash; Light or Dark Mode</h5>
<p>
Choose between <strong>Light</strong> mode (white backgrounds, dark text) and
<strong>Dark</strong> mode (dark backgrounds, light text).
</p>
<ul class="mb-3">
<li><strong>Light</strong> easier to read in bright environments like a well-lit office.</li>
<li><strong>Dark</strong> easier on the eyes in low-light conditions, such as a dimly lit shop floor or late-night work.</li>
<li><strong>Light</strong> &mdash; easier to read in bright environments like a well-lit office.</li>
<li><strong>Dark</strong> &mdash; easier on the eyes in low-light conditions, such as a dimly lit shop floor or late-night work.</li>
</ul>
<p>
To change the theme, open the Appearance tab, select your preferred option, and click
@@ -253,7 +253,7 @@
<h5 class="fw-semibold mt-3 mb-2">Sidebar Color</h5>
<p>
You can also choose the color scheme for the left navigation sidebar. A range of color
options is available from a clean white sidebar to darker branded colors. Pick whichever
options is available &mdash; from a clean white sidebar to darker branded colors. Pick whichever
suits your taste or matches your shop's brand.
</p>
<p>
+18 -18
View File
@@ -20,7 +20,7 @@
<i class="bi bi-info-circle text-primary me-2"></i>Overview
</h2>
<p>
Vendors are the companies you buy from powder suppliers, primer manufacturers, consumables
Vendors are the companies you buy from &mdash; powder suppliers, primer manufacturers, consumables
distributors, equipment service providers, and anyone else who sends you a bill. Maintaining
accurate vendor records lets you track what you owe, reorder stock quickly, and keep purchase
history organized.
@@ -42,12 +42,12 @@
<li class="mb-2">
Fill in the vendor details:
<ul class="mt-1">
<li><strong>Vendor Name</strong> the company name as it appears on their invoices.</li>
<li><strong>Contact Name</strong> your sales rep or accounts contact.</li>
<li><strong>Email &amp; Phone</strong> for placing orders and following up on deliveries.</li>
<li><strong>Address</strong> the vendor's billing address.</li>
<li><strong>Website</strong> optional, handy for quick product lookups.</li>
<li><strong>Account Number</strong> your account number with this vendor, if applicable.</li>
<li><strong>Vendor Name</strong> &mdash; the company name as it appears on their invoices.</li>
<li><strong>Contact Name</strong> &mdash; your sales rep or accounts contact.</li>
<li><strong>Email &amp; Phone</strong> &mdash; for placing orders and following up on deliveries.</li>
<li><strong>Address</strong> &mdash; the vendor's billing address.</li>
<li><strong>Website</strong> &mdash; optional, handy for quick product lookups.</li>
<li><strong>Account Number</strong> &mdash; your account number with this vendor, if applicable.</li>
</ul>
</li>
<li class="mb-2">Set the <strong>Payment Terms</strong> (see below).</li>
@@ -64,7 +64,7 @@
</h2>
<p>
The <strong>Default Expense Account</strong> field records which accounting category purchases from
this vendor typically fall under for example, <em>Cost of Goods Sold</em>, <em>Shop Supplies</em>,
this vendor typically fall under &mdash; for example, <em>Cost of Goods Sold</em>, <em>Shop Supplies</em>,
or <em>Equipment Maintenance</em>.
</p>
<p>
@@ -87,11 +87,11 @@
examples include:
</p>
<ul class="mb-3">
<li><strong>Net 15</strong> payment due within 15 days of the invoice date.</li>
<li><strong>Net 30</strong> payment due within 30 days (the most common).</li>
<li><strong>Net 60</strong> payment due within 60 days.</li>
<li><strong>Due on Receipt</strong> pay immediately on delivery.</li>
<li><strong>2/10 Net 30</strong> 2% discount if paid within 10 days, otherwise full amount due in 30.</li>
<li><strong>Net 15</strong> &mdash; payment due within 15 days of the invoice date.</li>
<li><strong>Net 30</strong> &mdash; payment due within 30 days (the most common).</li>
<li><strong>Net 60</strong> &mdash; payment due within 60 days.</li>
<li><strong>Due on Receipt</strong> &mdash; pay immediately on delivery.</li>
<li><strong>2/10 Net 30</strong> &mdash; 2% discount if paid within 10 days, otherwise full amount due in 30.</li>
</ul>
<p>
When you enter terms here, they are automatically used as the default on any bills generated from
@@ -114,7 +114,7 @@
restocking an inventory item.
</p>
<p>
You can mark more than one vendor as preferred it is a flag, not an exclusive designation. Use
You can mark more than one vendor as preferred &mdash; it is a flag, not an exclusive designation. Use
it however makes sense for your workflow.
</p>
</section>
@@ -127,17 +127,17 @@
Open a vendor's Details page by clicking their name in the vendor list. Here you will find:
</p>
<ul>
<li><strong>Contact information</strong> all the details you entered, in one place.</li>
<li><strong>Contact information</strong> &mdash; all the details you entered, in one place.</li>
<li>
<strong>Inventory Items tab</strong> a list of every inventory item linked to this vendor.
<strong>Inventory Items tab</strong> &mdash; a list of every inventory item linked to this vendor.
This tells you at a glance what you buy from them. Click any item to open it.
</li>
<li>
<strong>Purchase History tab</strong> a log of all purchase orders and transactions recorded
<strong>Purchase History tab</strong> &mdash; a log of all purchase orders and transactions recorded
against this vendor, with dates and amounts.
</li>
<li>
<strong>Notes</strong> any notes you or your colleagues have saved.
<strong>Notes</strong> &mdash; any notes you or your colleagues have saved.
</li>
</ul>
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
@@ -44,10 +44,10 @@
that we know are not yet fully conformant include:
</p>
<ul>
<li class="mb-1"><strong>Complex charts and graphs</strong> (in Reports) visual data is not yet accompanied by tabular data alternatives. We plan to add data table equivalents in a future release.</li>
<li class="mb-1"><strong>Drag-and-drop interfaces</strong> (Job Priority Board, Oven Scheduler) these currently lack keyboard-accessible equivalents. Workarounds exist via the standard list views.</li>
<li class="mb-1"><strong>PDF exports</strong> generated PDFs (invoices, work orders) are not currently tagged for screen reader accessibility.</li>
<li class="mb-1"><strong>Third-party components</strong> the Stripe payment element and some calendar widgets are third-party and may not fully conform to WCAG 2.1 AA.</li>
<li class="mb-1"><strong>Complex charts and graphs</strong> (in Reports) &mdash; visual data is not yet accompanied by tabular data alternatives. We plan to add data table equivalents in a future release.</li>
<li class="mb-1"><strong>Drag-and-drop interfaces</strong> (Job Priority Board, Oven Scheduler) &mdash; these currently lack keyboard-accessible equivalents. Workarounds exist via the standard list views.</li>
<li class="mb-1"><strong>PDF exports</strong> &mdash; generated PDFs (invoices, work orders) are not currently tagged for screen reader accessibility.</li>
<li class="mb-1"><strong>Third-party components</strong> &mdash; the Stripe payment element and some calendar widgets are third-party and may not fully conform to WCAG 2.1 AA.</li>
</ul>
</section>
@@ -73,13 +73,13 @@
content in a different format, please contact us:
</p>
<address class="ps-3">
<strong>Powder Coating Logix Accessibility</strong><br />
<strong>Powder Coating Logix &mdash; Accessibility</strong><br />
Email: <a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a><br />
Subject line: <em>Accessibility Request</em>
</address>
<p>
We aim to respond to accessibility requests within <strong>2 business days</strong>.
Please describe the barrier you encountered and what you were trying to do this helps us
Please describe the barrier you encountered and what you were trying to do &mdash; this helps us
prioritize fixes and provide an immediate workaround where possible.
</p>
</section>
@@ -96,7 +96,7 @@
<section class="mb-5">
<h2 class="h5 fw-bold border-start border-primary border-3 ps-3 mb-3">Ongoing Improvement</h2>
<p>
Accessibility is not a one-time checkbox it is an ongoing commitment. We review and improve
Accessibility is not a one-time checkbox &mdash; it is an ongoing commitment. We review and improve
accessibility with each release. If you notice a regression or have a suggestion, please let us know.
</p>
</section>
@@ -1,4 +1,4 @@
@{
@{
ViewData["Title"] = "Welcome";
Layout = null; // Use custom layout for login page
}
@@ -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">&bull;</span>
<a href="/Identity/Account/ForgotPassword">
<i class="bi bi-question-circle me-1"></i>Forgot password?
</a>
@@ -45,8 +45,8 @@
<h6 class="fw-semibold mt-3">1.1 Information You Provide</h6>
<ul>
<li><strong>Account &amp; Registration:</strong> Name, company name, email address, phone number, billing address, and password when you create an account.</li>
<li><strong>Billing Information:</strong> Payment card details (processed and stored by Stripe we do not store raw card numbers).</li>
<li><strong>Business Data:</strong> All content you enter into the Service customer records, job details, quotes, invoices, inventory, equipment records, photos, and any other business information.</li>
<li><strong>Billing Information:</strong> Payment card details (processed and stored by Stripe &mdash; we do not store raw card numbers).</li>
<li><strong>Business Data:</strong> All content you enter into the Service &mdash; customer records, job details, quotes, invoices, inventory, equipment records, photos, and any other business information.</li>
<li><strong>Communications:</strong> Messages you send to our support team, survey responses, and feedback.</li>
</ul>
@@ -172,10 +172,10 @@
The Service relies on the following key third-party providers, each with their own privacy policies:
</p>
<ul>
<li><strong>Stripe</strong> Payment processing: <a href="https://stripe.com/privacy" target="_blank" rel="noopener noreferrer">stripe.com/privacy</a></li>
<li><strong>SendGrid (Twilio)</strong> Email delivery: <a href="https://www.twilio.com/en-us/legal/privacy" target="_blank" rel="noopener noreferrer">twilio.com/legal/privacy</a></li>
<li><strong>Microsoft Azure</strong> Cloud infrastructure: <a href="https://privacy.microsoft.com/en-us/privacystatement" target="_blank" rel="noopener noreferrer">privacy.microsoft.com</a></li>
<li><strong>Anthropic</strong> AI features: <a href="https://www.anthropic.com/legal/privacy" target="_blank" rel="noopener noreferrer">anthropic.com/legal/privacy</a></li>
<li><strong>Stripe</strong> &mdash; Payment processing: <a href="https://stripe.com/privacy" target="_blank" rel="noopener noreferrer">stripe.com/privacy</a></li>
<li><strong>SendGrid (Twilio)</strong> &mdash; Email delivery: <a href="https://www.twilio.com/en-us/legal/privacy" target="_blank" rel="noopener noreferrer">twilio.com/legal/privacy</a></li>
<li><strong>Microsoft Azure</strong> &mdash; Cloud infrastructure: <a href="https://privacy.microsoft.com/en-us/privacystatement" target="_blank" rel="noopener noreferrer">privacy.microsoft.com</a></li>
<li><strong>Anthropic</strong> &mdash; AI features: <a href="https://www.anthropic.com/legal/privacy" target="_blank" rel="noopener noreferrer">anthropic.com/legal/privacy</a></li>
</ul>
</section>
@@ -201,7 +201,7 @@
</p>
</section>
<!-- 11 CCPA -->
<!-- 11 &mdash; CCPA -->
<section id="p-ccpa" class="mb-5">
<h2 class="h5 fw-bold border-start border-primary border-3 ps-3 mb-3">11. California Privacy Rights (CCPA)</h2>
<p>
@@ -219,7 +219,7 @@
<li><strong>Know</strong> what personal information we collect, use, and disclose about you;</li>
<li><strong>Delete</strong> personal information we have collected from you (subject to certain exceptions);</li>
<li><strong>Correct</strong> inaccurate personal information we hold about you;</li>
<li><strong>Opt out</strong> of the sale or sharing of personal information (not applicable we do not sell);</li>
<li><strong>Opt out</strong> of the sale or sharing of personal information (not applicable &mdash; we do not sell);</li>
<li><strong>Non-discrimination</strong> for exercising any of your CCPA rights.</li>
</ul>
<h6 class="fw-semibold mt-3">How to Submit a Request</h6>
@@ -256,7 +256,7 @@
If you have any questions, concerns, or requests regarding this Privacy Policy, please contact us:
</p>
<address class="ps-3">
<strong>Powder Coating Logix Privacy</strong><br />
<strong>Powder Coating Logix &mdash; Privacy</strong><br />
Email: <a href="mailto:privacy@powdercoatinglogix.com">privacy@powdercoatinglogix.com</a><br />
Support: <a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a><br />
Website: <a href="http://www.powdercoatinglogix.com" target="_blank" rel="noopener noreferrer">www.powdercoatinglogix.com</a>
@@ -20,7 +20,7 @@
<ul class="small mb-0">
<li class="mb-1"><strong>In transit:</strong> All communication between your browser and our servers is encrypted using TLS 1.2 or higher. HTTP is automatically redirected to HTTPS.</li>
<li class="mb-1"><strong>At rest:</strong> Databases and file storage are encrypted using AES-256 via Microsoft Azure's native encryption.</li>
<li><strong>Passwords:</strong> Stored as bcrypt hashes we never store or see your plain-text password.</li>
<li><strong>Passwords:</strong> Stored as bcrypt hashes &mdash; we never store or see your plain-text password.</li>
</ul>
</div>
</div>
@@ -30,7 +30,7 @@
<div class="card-body">
<h5 class="card-title fw-semibold"><i class="bi bi-building text-primary me-2"></i>Infrastructure</h5>
<ul class="small mb-0">
<li class="mb-1"><strong>Hosted on Microsoft Azure</strong> enterprise-grade cloud infrastructure with SOC 2 Type II, ISO 27001, and FedRAMP certifications.</li>
<li class="mb-1"><strong>Hosted on Microsoft Azure</strong> &mdash; enterprise-grade cloud infrastructure with SOC 2 Type II, ISO 27001, and FedRAMP certifications.</li>
<li class="mb-1"><strong>Geo-redundant storage</strong> ensures your data is replicated across multiple data centers.</li>
<li><strong>Daily automated backups</strong> with 7-day retention.</li>
</ul>
@@ -42,7 +42,7 @@
<div class="card-body">
<h5 class="card-title fw-semibold"><i class="bi bi-people-fill text-primary me-2"></i>Access Controls</h5>
<ul class="small mb-0">
<li class="mb-1"><strong>Role-based permissions:</strong> Each user in your account has a role that limits what they can see and do Admins, Managers, Employees, Shop Floor, and Read-Only.</li>
<li class="mb-1"><strong>Role-based permissions:</strong> Each user in your account has a role that limits what they can see and do &mdash; Admins, Managers, Employees, Shop Floor, and Read-Only.</li>
<li class="mb-1"><strong>Multi-tenancy isolation:</strong> Your data is logically isolated from all other customers at the database level via row-level filtering.</li>
<li><strong>Internal access:</strong> Platform engineers access production data only when required for support, under the principle of least privilege.</li>
</ul>
@@ -54,7 +54,7 @@
<div class="card-body">
<h5 class="card-title fw-semibold"><i class="bi bi-credit-card-fill text-primary me-2"></i>Payment Security</h5>
<ul class="small mb-0">
<li class="mb-1"><strong>Stripe handles all payment processing.</strong> We never see, store, or transmit your card numbers Stripe's PCI DSS Level 1 certified infrastructure handles that directly.</li>
<li class="mb-1"><strong>Stripe handles all payment processing.</strong> We never see, store, or transmit your card numbers &mdash; Stripe's PCI DSS Level 1 certified infrastructure handles that directly.</li>
<li class="mb-1"><strong>Stripe Connect</strong> is used for online invoice payments, meaning funds flow directly to your Stripe account.</li>
<li>Stripe's security details: <a href="https://stripe.com/docs/security" target="_blank" rel="noopener noreferrer">stripe.com/docs/security</a></li>
</ul>
@@ -107,10 +107,10 @@
We rely on the following vendors for core functionality, each of which maintains their own security programs:
</p>
<ul>
<li><strong>Microsoft Azure</strong> ISO 27001, SOC 2, FedRAMP</li>
<li><strong>Stripe</strong> PCI DSS Level 1</li>
<li><strong>SendGrid / Twilio</strong> ISO 27001, SOC 2</li>
<li><strong>Anthropic</strong> Enterprise AI security program</li>
<li><strong>Microsoft Azure</strong> &mdash; ISO 27001, SOC 2, FedRAMP</li>
<li><strong>Stripe</strong> &mdash; PCI DSS Level 1</li>
<li><strong>SendGrid / Twilio</strong> &mdash; ISO 27001, SOC 2</li>
<li><strong>Anthropic</strong> &mdash; Enterprise AI security program</li>
</ul>
</section>
@@ -119,7 +119,7 @@
<p>Security is a shared responsibility. You can help protect your account by:</p>
<ul>
<li>Using a strong, unique password and enabling 2FA;</li>
<li>Not sharing login credentials between users each person should have their own account;</li>
<li>Not sharing login credentials between users &mdash; each person should have their own account;</li>
<li>Assigning the minimum role necessary for each user's job;</li>
<li>Revoking access promptly when an employee leaves;</li>
<li>Reporting suspicious activity immediately.</li>
@@ -54,12 +54,12 @@
<td>&le; 7 hours 18 minutes</td>
</tr>
<tr>
<td>98.0% 98.9%</td>
<td>7h 19m 14h 24m</td>
<td>98.0% &ndash; 98.9%</td>
<td>7h 19m &ndash; 14h 24m</td>
</tr>
<tr>
<td>&lt; 98.0%</td>
<td>&gt; 14h 24m credit eligible (see Section 6)</td>
<td>&gt; 14h 24m &mdash; credit eligible (see Section 6)</td>
</tr>
</tbody>
</table>
@@ -94,7 +94,7 @@
expected to exceed 30 minutes.
</p>
<ul>
<li><strong>Preferred window:</strong> Sundays 2:00 AM 5:00 AM Eastern Time</li>
<li><strong>Preferred window:</strong> Sundays 2:00 AM &ndash; 5:00 AM Eastern Time</li>
<li><strong>Emergency maintenance:</strong> May occur without advance notice when required to address critical security vulnerabilities or prevent data loss. We will notify users as quickly as possible.</li>
</ul>
<p>All scheduled maintenance time is excluded from the downtime calculation in Section 1.</p>
@@ -104,7 +104,7 @@
<h2 class="h5 fw-bold border-start border-primary border-3 ps-3 mb-3">4. Support Response Times</h2>
<p>
Support is available by email at <a href="mailto:support@powdercoatinglogix.com">support@powdercoatinglogix.com</a>.
Response times are measured in <strong>business hours</strong> (MondayFriday, 9 AM5 PM Eastern, excluding
Response times are measured in <strong>business hours</strong> (Monday&ndash;Friday, 9 AM&ndash;5 PM Eastern, excluding
US federal holidays).
</p>
<div class="table-responsive">
@@ -147,7 +147,7 @@
</div>
<p class="small text-muted">
"First Response" means an acknowledgment that we received and are investigating your request.
"Target Resolution" is a goal, not a guarantee complex issues may take longer.
"Target Resolution" is a goal, not a guarantee &mdash; complex issues may take longer.
</p>
</section>
@@ -180,8 +180,8 @@
</tr>
</thead>
<tbody>
<tr><td>98.0% 98.9%</td><td>10% of monthly fee</td></tr>
<tr><td>95.0% 97.9%</td><td>25% of monthly fee</td></tr>
<tr><td>98.0% &ndash; 98.9%</td><td>10% of monthly fee</td></tr>
<tr><td>95.0% &ndash; 97.9%</td><td>25% of monthly fee</td></tr>
<tr><td>&lt; 95.0%</td><td>50% of monthly fee</td></tr>
</tbody>
</table>
@@ -198,7 +198,7 @@
<p>The uptime commitment and service credits do not apply to downtime caused by:</p>
<ul>
<li>Scheduled maintenance windows (Section 3);</li>
<li>Events beyond our reasonable control (force majeure natural disasters, power outages, internet backbone failures);</li>
<li>Events beyond our reasonable control (force majeure &mdash; natural disasters, power outages, internet backbone failures);</li>
<li>Third-party service failures (Stripe, SendGrid, Twilio, Azure infrastructure outages outside our control);</li>
<li>Your actions or inactions, including misconfiguration, unauthorized access by your users, or exceeding plan limits;</li>
<li>Beta features or features explicitly labeled as experimental;</li>
@@ -306,8 +306,8 @@
</p>
<p class="mb-2">
<strong>13.2 Cap on Liability.</strong> OUR TOTAL CUMULATIVE LIABILITY TO YOU FOR ALL CLAIMS
ARISING OUT OF OR RELATING TO THIS AGREEMENT OR THE SERVICE WHETHER IN CONTRACT, TORT
(INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE SHALL NOT EXCEED THE GREATER OF:
ARISING OUT OF OR RELATING TO THIS AGREEMENT OR THE SERVICE &mdash; WHETHER IN CONTRACT, TORT
(INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE &mdash; SHALL NOT EXCEED THE GREATER OF:
(A) THE TOTAL FEES ACTUALLY PAID BY YOU TO US IN THE <strong>THREE (3) MONTHS</strong> IMMEDIATELY
PRECEDING THE EVENT GIVING RISE TO THE CLAIM, OR (B) ONE HUNDRED U.S. DOLLARS ($100.00).
</p>
@@ -153,7 +153,7 @@ else
{
<div class="card-footer d-flex justify-content-between align-items-center">
<small class="text-muted">
Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
Showing @((pageNumber - 1) * pageSize + 1)&ndash;@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
@@ -4,7 +4,7 @@
ViewData["Title"] = "Add Inventory Item";
ViewData["PageIcon"] = "bi-box-seam";
ViewData["PageHelpTitle"] = "Add Inventory Item";
ViewData["PageHelpContent"] = "Add a new material to inventory powder coatings, consumables, or other shop supplies. Select a category first to auto-generate a SKU. Use AI Lookup to fill in manufacturer details from a part number. Set Reorder Point and Reorder Quantity so the system can alert you when stock runs low.";
ViewData["PageHelpContent"] = "Add a new material to inventory &mdash; powder coatings, consumables, or other shop supplies. Select a category first to auto-generate a SKU. Use AI Lookup to fill in manufacturer details from a part number. Set Reorder Point and Reorder Quantity so the system can alert you when stock runs low.";
}
<div class="row justify-content-center">
@@ -29,7 +29,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="Name and SKU are required. Category drives how the item is filtered and used in quotes choosing a Powder Coating category shows the Coating Specifications section. SKU is auto-generated from the category prefix but you can edit it. Description is optional free text for internal notes.">
data-bs-content="Name and SKU are required. Category drives how the item is filtered and used in quotes &mdash; choosing a Powder Coating category shows the Coating Specifications section. SKU is auto-generated from the category prefix but you can edit it. Description is optional free text for internal notes.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -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="Product Details"
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 6070%). Both values are used to calculate Powder Needed on quotes and jobs.">
data-bs-content="Manufacturer, part number, color name, color code, and finish describe the physical product. Use Lookup to auto-fill these fields &mdash; it checks the product catalog first, then falls back to AI. Coverage is how many sq ft one pound coats at 1 mil thickness (typical: 30). Transfer Efficiency is what percentage of the powder actually sticks (typical: 60&ndash;70%). Both values are used to calculate Powder Needed on quotes and jobs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -129,7 +129,7 @@
<div class="col-md-6">
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://" />
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://&hellip;" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
<i class="bi bi-file-earmark-pdf"></i>
@@ -140,7 +140,7 @@
<div class="col-md-6">
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://" />
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://&hellip;" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
<i class="bi bi-file-earmark-text"></i>
@@ -193,7 +193,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Transfer Efficiency"
data-bs-content="The percentage of powder that actually adheres to the part rather than being lost as overspray. Electrostatic spray guns typically achieve 6070%. A lower efficiency means you need to order more powder per job. The system uses this value in the Powder Needed calculation on quotes.">
data-bs-content="The percentage of powder that actually adheres to the part rather than being lost as overspray. Electrostatic spray guns typically achieve 60&ndash;70%. A lower efficiency means you need to order more powder per job. The system uses this value in the Powder Needed calculation on quotes.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -213,7 +213,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Coating Specifications"
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet they tell the oven operator the correct bake profile. Requires Clear Coat flags powders that need a clear top coat for durability or finish. Color Families tag this powder for filtering and matching in the quote wizard (e.g., a teal powder would get both Green and Blue).">
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet &mdash; they tell the oven operator the correct bake profile. Requires Clear Coat flags powders that need a clear top coat for durability or finish. Color Families tag this powder for filtering and matching in the quote wizard (e.g., a teal powder would get both Green and Blue).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -267,7 +267,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Stock Information"
data-bs-content="Quantity on Hand is your current starting stock. Reorder Point is the threshold at which the system shows a Low Stock alert when quantity drops to this level it's time to reorder. Reorder Quantity is how much to order in one batch. Location is the shelf or bin label so staff can find the material quickly.">
data-bs-content="Quantity on Hand is your current starting stock. Reorder Point is the threshold at which the system shows a Low Stock alert &mdash; when quantity drops to this level it's time to reorder. Reorder Quantity is how much to order in one batch. Location is the shelf or bin label so staff can find the material quickly.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -293,7 +293,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Reorder Point"
data-bs-content="When Quantity on Hand falls at or below this number a Low Stock warning appears on the item and in the Inventory summary. Set it high enough to cover your lead time for example if delivery takes a week and you use 2 lb/day, set the reorder point to at least 14 lbs.">
data-bs-content="When Quantity on Hand falls at or below this number a Low Stock warning appears on the item and in the Inventory summary. Set it high enough to cover your lead time &mdash; for example if delivery takes a week and you use 2 lb/day, set the reorder point to at least 14 lbs.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -306,7 +306,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Reorder Quantity"
data-bs-content="The standard quantity to order when restocking typically a full case or pallet quantity from your supplier. This is informational and appears as a suggested order amount when the item is flagged as low stock.">
data-bs-content="The standard quantity to order when restocking &mdash; typically a full case or pallet quantity from your supplier. This is informational and appears as a suggested order amount when the item is flagged as low stock.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -325,7 +325,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Pricing &amp; Vendor"
data-bs-content="Unit Cost is what you pay per unit (lb, each, etc.) this is used to calculate total stock value and feeds into job cost calculations. Primary Vendor links to your supplier record for quick reference and purchase ordering.">
data-bs-content="Unit Cost is what you pay per unit (lb, each, etc.) &mdash; this is used to calculate total stock value and feeds into job cost calculations. Primary Vendor links to your supplier record for quick reference and purchase ordering.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -343,7 +343,7 @@
<select asp-for="PrimaryVendorId" class="form-select" id="field-vendor" asp-items="@ViewBag.Vendors"
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="__new__">+ Add New Vendor&hellip;</option>
</select>
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
</div>
@@ -359,7 +359,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Financial Accounts"
data-bs-content="Inventory Account is the asset account where the value of this stock sits on the balance sheet (e.g., 1200 Inventory Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company defaults set in your Chart of Accounts.">
data-bs-content="Inventory Account is the asset account where the value of this stock sits on the balance sheet (e.g., 1200 Inventory &mdash; Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company defaults set in your Chart of Accounts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -370,7 +370,7 @@
<select asp-for="InventoryAccountId" class="form-select" asp-items="ViewBag.InventoryAccounts"
data-quick-add-url="/Accounts/Create?preSubType=4" data-quick-add-title="Add Inventory Account">
<option value="">(Default inventory account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Asset account where inventory value is tracked (e.g., 1200 Inventory - Powder).</small>
</div>
@@ -379,7 +379,7 @@
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
<option value="">(Default COGS account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Expense account debited when this material is consumed on a job.</small>
</div>
@@ -4,7 +4,7 @@
ViewData["Title"] = $"{Model.Name}";
ViewData["PageIcon"] = "bi-box-seam";
ViewData["PageHelpTitle"] = "Inventory Item";
ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item.";
ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds &mdash; a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item.";
}
@section Styles {
@@ -293,14 +293,14 @@
{
<div class="col-md-6">
<label class="text-muted small mb-1">Inventory Account</label>
<p class="mb-0">@(Model.InventoryAccountName ?? "")</p>
<p class="mb-0">@(Model.InventoryAccountName ?? "&mdash;")</p>
</div>
}
@if (Model.CogsAccountId.HasValue)
{
<div class="col-md-6">
<label class="text-muted small mb-1">COGS Account</label>
<p class="mb-0">@(Model.CogsAccountName ?? "")</p>
<p class="mb-0">@(Model.CogsAccountName ?? "&mdash;")</p>
</div>
}
</div>
@@ -375,7 +375,7 @@
</div>
<div class="col-6">
<label class="text-muted small mb-1">Location</label>
<p class="mb-0">@(Model.Location ?? "")</p>
<p class="mb-0">@(Model.Location ?? "&mdash;")</p>
</div>
<div class="col-6">
<label class="text-muted small mb-1">Reorder Point</label>
@@ -526,15 +526,15 @@
<div class="mb-3">
<label for="adjReason" class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
<select class="form-select" id="adjReason" name="reason" required>
<option value=""> Select a reason </option>
<option value="">&mdash; Select a reason &mdash;</option>
<optgroup label="Adding Stock">
<option value="Received from purchase order">Received from purchase order</option>
<option value="Physical count found extra">Physical count found extra</option>
<option value="Physical count &mdash; found extra">Physical count &mdash; found extra</option>
<option value="Returned unused product">Returned unused product</option>
<option value="Transfer in from another location">Transfer in from another location</option>
</optgroup>
<optgroup label="Removing Stock">
<option value="Physical count shortage found">Physical count shortage found</option>
<option value="Physical count &mdash; shortage found">Physical count &mdash; shortage found</option>
<option value="Damaged / unusable product">Damaged / unusable product</option>
<option value="Waste or spillage">Waste or spillage</option>
<option value="Transfer out to another location">Transfer out to another location</option>
@@ -551,7 +551,7 @@
<div class="mb-1">
<label for="adjNotes" class="form-label fw-semibold">Notes <span class="text-muted fw-normal">(optional)</span></label>
<textarea class="form-control" id="adjNotes" name="notes" rows="2"
placeholder="Any additional details about this adjustment" maxlength="500"></textarea>
placeholder="Any additional details about this adjustment&hellip;" maxlength="500"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -697,7 +697,7 @@
if (!confirm('This will reduce stock below zero. Continue?')) { e.preventDefault(); return; }
}
document.getElementById('adjSubmitBtn').disabled = true;
document.getElementById('adjSubmitBtn').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving';
document.getElementById('adjSubmitBtn').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving&hellip;';
});
qtyInput.addEventListener('input', function () { qtyInput.classList.remove('is-invalid'); });
@@ -815,7 +815,7 @@
var from = (page - 1) * pageSize + 1;
var to = Math.min(page * pageSize, total);
rangeEl.textContent = 'Showing ' + from + '' + to + ' of ' + total + ' photos';
rangeEl.textContent = 'Showing ' + from + '&ndash;' + to + ' of ' + total + ' photos';
paginationEl.innerHTML = '';
if (page > 1) addPageBtn(paginationEl, '', page - 1);
@@ -4,7 +4,7 @@
ViewData["Title"] = "Edit Inventory Item";
ViewData["PageIcon"] = "bi-box-seam";
ViewData["PageHelpTitle"] = "Edit Inventory Item";
ViewData["PageHelpContent"] = "Update any field on this item. Changes to Coverage or Transfer Efficiency will affect the Powder Needed calculation on future quotes and jobs. Changing Unit Cost does not retroactively update historical job costs it applies going forward. Use AI Lookup to refresh manufacturer details from a part number.";
ViewData["PageHelpContent"] = "Update any field on this item. Changes to Coverage or Transfer Efficiency will affect the Powder Needed calculation on future quotes and jobs. Changing Unit Cost does not retroactively update historical job costs &mdash; it applies going forward. Use AI Lookup to refresh manufacturer details from a part number.";
}
<div class="row justify-content-center">
@@ -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="Name and SKU are required. Category determines how the item is used in quotes Powder Coating items show the Coating Specifications section. Inactive items are hidden from pickers but their historical data is preserved.">
data-bs-content="Name and SKU are required. Category determines how the item is used in quotes &mdash; Powder Coating items show the Coating Specifications section. Inactive items are hidden from pickers but their historical data is preserved.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -87,7 +87,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Product Details"
data-bs-content="Manufacturer, part number, color name, and finish describe the physical product. Coverage is how many sq ft one pound coats (typical: 30). Transfer Efficiency is what percentage sticks to the part (typical: 6070%). Both values affect the Powder Needed calculation on quotes and jobs. Use AI Lookup to auto-fill fields from a part number.">
data-bs-content="Manufacturer, part number, color name, and finish describe the physical product. Coverage is how many sq ft one pound coats (typical: 30). Transfer Efficiency is what percentage sticks to the part (typical: 60&ndash;70%). Both values affect the Powder Needed calculation on quotes and jobs. Use AI Lookup to auto-fill fields from a part number.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -131,7 +131,7 @@
<div class="col-md-6">
<label asp-for="SdsUrl" class="form-label">Safety Data Sheet (SDS)</label>
<div class="input-group">
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://" />
<input asp-for="SdsUrl" class="form-control" id="field-sdsurl" placeholder="https://&hellip;" />
<a id="field-sdsurl-link" href="@Model.SdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.SdsUrl) ? "d-none" : "")" title="Open SDS">
<i class="bi bi-file-earmark-pdf"></i>
@@ -142,7 +142,7 @@
<div class="col-md-6">
<label asp-for="TdsUrl" class="form-label">Technical Data Sheet (TDS)</label>
<div class="input-group">
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://" />
<input asp-for="TdsUrl" class="form-control" id="field-tdsurl" placeholder="https://&hellip;" />
<a id="field-tdsurl-link" href="@Model.TdsUrl" target="_blank"
class="btn btn-outline-secondary @(string.IsNullOrWhiteSpace(Model.TdsUrl) ? "d-none" : "")" title="Open TDS">
<i class="bi bi-file-earmark-text"></i>
@@ -195,7 +195,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Transfer Efficiency"
data-bs-content="The percentage of powder that adheres to the part vs. lost as overspray. Electrostatic guns typically achieve 6070%. A lower value means more powder is needed per job. The system uses this in the Powder Needed calculation on quotes.">
data-bs-content="The percentage of powder that adheres to the part vs. lost as overspray. Electrostatic guns typically achieve 60&ndash;70%. A lower value means more powder is needed per job. The system uses this in the Powder Needed calculation on quotes.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -215,7 +215,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Coating Specifications"
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet and guide the oven operator. Requires Clear Coat flags powders that need a clear top coat for durability or gloss. Color Families tag this powder for filtering click chips to add or remove families (e.g., teal = Green + Blue).">
data-bs-content="Cure Temperature and Cure Time come from the manufacturer's tech data sheet and guide the oven operator. Requires Clear Coat flags powders that need a clear top coat for durability or gloss. Color Families tag this powder for filtering &mdash; click chips to add or remove families (e.g., teal = Green + Blue).">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -321,7 +321,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Pricing &amp; Vendor"
data-bs-content="Unit Cost is what you pay per unit used to calculate total stock value and feeds into job cost calculations. Changing this updates the displayed value going forward but does not change historical job costs. Primary Vendor links to your supplier for quick reference.">
data-bs-content="Unit Cost is what you pay per unit &mdash; used to calculate total stock value and feeds into job cost calculations. Changing this updates the displayed value going forward but does not change historical job costs. Primary Vendor links to your supplier for quick reference.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -339,7 +339,7 @@
<select asp-for="PrimaryVendorId" class="form-select" id="field-vendor" asp-items="@ViewBag.Vendors"
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="__new__">+ Add New Vendor&hellip;</option>
</select>
<span asp-validation-for="PrimaryVendorId" class="text-danger"></span>
</div>
@@ -360,7 +360,7 @@
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Financial Accounts"
data-bs-content="Inventory Account is the asset account where stock value sits on the balance sheet (e.g., 1200 Inventory Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company-wide defaults from your Chart of Accounts.">
data-bs-content="Inventory Account is the asset account where stock value sits on the balance sheet (e.g., 1200 Inventory &mdash; Powder). COGS Account is debited when this material is consumed on a job (e.g., 5000 Cost of Goods Sold). Leave blank to use the company-wide defaults from your Chart of Accounts.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@@ -371,7 +371,7 @@
<select asp-for="InventoryAccountId" class="form-select" asp-items="ViewBag.InventoryAccounts"
data-quick-add-url="/Accounts/Create?preSubType=4" data-quick-add-title="Add Inventory Account">
<option value="">(Default inventory account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Asset account where inventory value is tracked (e.g., 1200 Inventory - Powder).</small>
</div>
@@ -380,7 +380,7 @@
<select asp-for="CogsAccountId" class="form-select" asp-items="ViewBag.CogsAccounts"
data-quick-add-url="/Accounts/Create?preSubType=40" data-quick-add-title="Add COGS Account">
<option value="">(Default COGS account)</option>
<option value="__new__">+ Add New Account</option>
<option value="__new__">+ Add New Account&hellip;</option>
</select>
<small class="form-text text-muted">Expense account debited when this material is consumed on a job.</small>
</div>
@@ -4,7 +4,7 @@
ViewData["Title"] = "Inventory";
ViewData["PageIcon"] = "bi-box-seam";
ViewData["PageHelpTitle"] = "Inventory";
ViewData["PageHelpContent"] = "Track powder coatings, consumables, and other shop materials. Items show as Low Stock when quantity falls at or below the Reorder Point the Low Stock count at the top is your reorder alert. Click any row to view full details or edit. Use the search box and category filter to narrow the list. Low Stock filter shows only items needing attention.";
ViewData["PageHelpContent"] = "Track powder coatings, consumables, and other shop materials. Items show as Low Stock when quantity falls at or below the Reorder Point &mdash; the Low Stock count at the top is your reorder alert. Click any row to view full details or edit. Use the search box and category filter to narrow the list. Low Stock filter shows only items needing attention.";
var lowStockCount = (int)(ViewBag.StatsLowStockCount ?? 0);
var activeCount = (int)(ViewBag.StatsActiveCount ?? 0);
var totalValue = (decimal)(ViewBag.StatsTotalValue ?? 0m);
@@ -131,7 +131,7 @@
<i class="bi bi-funnel-fill me-2"></i>
@if (lowStockOnly)
{
<span>Showing <strong>@Model.TotalCount</strong> low stock item@(Model.TotalCount == 1 ? "" : "s") at or below reorder point</span>
<span>Showing <strong>@Model.TotalCount</strong> low stock item@(Model.TotalCount == 1 ? "" : "s") &mdash; at or below reorder point</span>
}
else
{
@@ -247,7 +247,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>
@@ -257,7 +257,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td>

Some files were not shown because too many files have changed in this diff Show More