From a0bdd2b5b49882668645bc9c8c363e24373146ce Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 20 May 2026 21:37:10 -0400 Subject: [PATCH] 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 --- .githooks/pre-commit | 27 +++ .../Views/AccountDataExport/Index.cshtml | 6 +- .../Views/AccountingExport/Index.cshtml | 6 +- .../Views/Accounts/Create.cshtml | 16 +- .../Views/Accounts/Edit.cshtml | 10 +- .../Views/Accounts/Index.cshtml | 8 +- .../Views/Accounts/Ledger.cshtml | 8 +- .../Views/Accounts/YearEndClose.cshtml | 6 +- .../Views/AiUsageReport/Index.cshtml | 24 +-- .../Views/Announcements/Index.cshtml | 4 +- .../Announcements/_AnnouncementForm.cshtml | 10 +- .../Views/Appointments/Create.cshtml | 6 +- .../Views/Appointments/Details.cshtml | 4 +- .../Views/Appointments/Edit.cshtml | 4 +- .../Views/AuditLog/Details.cshtml | 12 +- .../Views/AuditLog/Index.cshtml | 6 +- .../Views/BankReconciliations/Create.cshtml | 2 +- .../BankReconciliations/Reconcile.cshtml | 8 +- .../Views/BankReconciliations/Report.cshtml | 4 +- .../Views/BannedIps/Index.cshtml | 2 +- .../Views/Billing/Index.cshtml | 6 +- .../Views/Bills/Create.cshtml | 34 ++-- .../Views/Bills/Details.cshtml | 12 +- src/PowderCoating.Web/Views/Bills/Edit.cshtml | 18 +- .../Views/Bills/Index.cshtml | 6 +- .../Views/Bills/RecurringDetection.cshtml | 2 +- .../Views/Budgets/Edit.cshtml | 4 +- .../Views/Budgets/Index.cshtml | 2 +- .../Views/BugReport/Index.cshtml | 4 +- .../Views/BugReport/Submit.cshtml | 12 +- .../Views/CatalogCategories/Create.cshtml | 2 +- .../Views/CatalogCategories/Edit.cshtml | 2 +- .../Views/CatalogItems/AiPriceCheck.cshtml | 20 +- .../Views/CatalogItems/Create.cshtml | 14 +- .../Views/CatalogItems/Edit.cshtml | 12 +- .../Views/CatalogItems/Index.cshtml | 2 +- .../Views/Companies/Details.cshtml | 38 ++-- .../Views/Companies/Edit.cshtml | 8 +- .../Views/Companies/Index.cshtml | 8 +- .../Views/CompanyHealth/Index.cshtml | 8 +- .../CompanySettings/DeleteAccount.cshtml | 12 +- .../Views/CompanySettings/EditTemplate.cshtml | 6 +- .../Views/CompanySettings/Index.cshtml | 148 +++++++------- .../CompanySettings/_LookupModals.cshtml | 16 +- .../Views/CompanyUsers/Create.cshtml | 6 +- .../Views/CompanyUsers/Edit.cshtml | 8 +- .../Views/Contact/Index.cshtml | 4 +- .../Views/Contact/Submissions.cshtml | 2 +- .../Views/CreditMemos/Create.cshtml | 6 +- .../Views/CreditMemos/Details.cshtml | 6 +- .../Views/CreditMemos/Index.cshtml | 4 +- .../Views/Customers/Activity.cshtml | 8 +- .../Views/Customers/Create.cshtml | 10 +- .../Views/Customers/Details.cshtml | 6 +- .../Views/Customers/Edit.cshtml | 14 +- .../Views/Customers/Index.cshtml | 8 +- .../Views/Customers/Invoices.cshtml | 2 +- .../Views/Customers/Statement.cshtml | 4 +- .../Views/Dashboard/Index.cshtml | 54 ++--- .../Views/DashboardTips/Index.cshtml | 4 +- .../Views/DataExport/Index.cshtml | 14 +- .../Views/DataPurge/Index.cshtml | 26 +-- .../Views/Diagnostics/Index.cshtml | 14 +- .../Views/EmailBroadcast/Preview.cshtml | 4 +- .../EmailBroadcast/SelectCompanies.cshtml | 4 +- .../Views/Equipment/Create.cshtml | 2 +- .../Views/Equipment/Delete.cshtml | 8 +- .../Views/Equipment/Details.cshtml | 8 +- .../Views/Equipment/Index.cshtml | 4 +- .../Views/Expenses/Details.cshtml | 4 +- .../Views/FixedAssets/Create.cshtml | 8 +- .../Views/FixedAssets/Details.cshtml | 8 +- .../Views/FixedAssets/Edit.cshtml | 6 +- .../Views/GiftCertificates/Create.cshtml | 2 +- .../Views/GiftCertificates/Details.cshtml | 2 +- .../Views/Help/AccountsPayable.cshtml | 54 ++--- .../Views/Help/CustomerIntakeKiosk.cshtml | 38 ++-- .../Views/Help/Customers.cshtml | 40 ++-- .../Views/Help/Equipment.cshtml | 44 ++-- .../Views/Help/GettingStarted.cshtml | 42 ++-- src/PowderCoating.Web/Views/Help/Index.cshtml | 2 +- .../Views/Help/Inventory.cshtml | 98 ++++----- .../Views/Help/Invoices.cshtml | 48 ++--- src/PowderCoating.Web/Views/Help/Jobs.cshtml | 96 ++++----- .../Views/Help/PurchaseOrders.cshtml | 10 +- .../Views/Help/Quotes.cshtml | 80 ++++---- .../Views/Help/Reports.cshtml | 96 ++++----- .../Views/Help/Settings.cshtml | 190 +++++++++--------- .../Views/Help/UserProfile.cshtml | 40 ++-- .../Views/Help/Vendors.cshtml | 36 ++-- .../Views/Home/Accessibility.cshtml | 14 +- src/PowderCoating.Web/Views/Home/Index.cshtml | 4 +- .../Views/Home/Privacy.cshtml | 18 +- .../Views/Home/Security.cshtml | 18 +- .../Views/Home/ServiceLevelAgreement.cshtml | 18 +- .../Views/Home/TermsOfService.cshtml | 4 +- .../Views/InAppNotifications/Index.cshtml | 2 +- .../Views/Inventory/Create.cshtml | 30 +-- .../Views/Inventory/Details.cshtml | 20 +- .../Views/Inventory/Edit.cshtml | 24 +-- .../Views/Inventory/Index.cshtml | 8 +- .../Views/Inventory/Label.cshtml | 6 +- .../Views/Inventory/Ledger.cshtml | 22 +- .../Views/Inventory/SamplePanels.cshtml | 22 +- .../Views/Inventory/Scan.cshtml | 10 +- .../_InventoryColorFamilyScripts.cshtml | 12 +- .../Views/Inventory/_LabelScanModal.cshtml | 4 +- .../Views/Invoices/Create.cshtml | 16 +- .../Views/Invoices/Details.cshtml | 42 ++-- .../Views/Invoices/Edit.cshtml | 8 +- .../Views/Invoices/Index.cshtml | 2 +- .../Views/Invoices/OnlinePayments.cshtml | 10 +- .../Views/JobTemplates/Details.cshtml | 6 +- .../Views/JobTemplates/Edit.cshtml | 4 +- src/PowderCoating.Web/Views/Jobs/Board.cshtml | 6 +- .../Views/Jobs/Create.cshtml | 14 +- .../Views/Jobs/Details.cshtml | 8 +- src/PowderCoating.Web/Views/Jobs/Edit.cshtml | 12 +- .../Views/Jobs/EditItems.cshtml | 4 +- src/PowderCoating.Web/Views/Jobs/Index.cshtml | 2 +- .../Views/Jobs/Intake.cshtml | 6 +- .../Views/Jobs/ShopDisplay.cshtml | 12 +- .../Views/Jobs/ShopMobile.cshtml | 4 +- .../Views/Jobs/StatusBump.cshtml | 6 +- .../Views/Jobs/WorkOrder.cshtml | 12 +- .../Views/JobsPriority/Index.cshtml | 18 +- .../Views/JournalEntries/Details.cshtml | 2 +- .../Views/Kiosk/Activate.cshtml | 2 +- .../Views/Kiosk/Intake/Confirmation.cshtml | 2 +- .../Views/Kiosk/Intake/Job.cshtml | 2 +- .../Views/Kiosk/Intake/Terms.cshtml | 4 +- .../Views/Kiosk/Welcome.cshtml | 2 +- .../Views/Maintenance/Create.cshtml | 4 +- .../Views/Maintenance/Delete.cshtml | 4 +- .../Views/Maintenance/Details.cshtml | 2 +- .../Views/Maintenance/Edit.cshtml | 4 +- .../Views/Maintenance/Index.cshtml | 2 +- .../ManufacturerLookupPatterns/Create.cshtml | 14 +- .../ManufacturerLookupPatterns/Edit.cshtml | 14 +- .../ManufacturerLookupPatterns/Index.cshtml | 10 +- .../Views/NotificationLogs/Index.cshtml | 2 +- .../Views/OnboardingProgress/Index.cshtml | 10 +- .../Views/OvenScheduler/Index.cshtml | 40 ++-- .../Views/OvenScheduler/_BatchCard.cshtml | 2 +- .../Views/Passkey/EnrollPrompt.cshtml | 6 +- .../Views/Passkey/Manage.cshtml | 2 +- .../Views/Pay/Deposit.cshtml | 10 +- src/PowderCoating.Web/Views/Pay/Index.cshtml | 12 +- .../Views/Payment/Index.cshtml | 6 +- .../Views/Payment/Success.cshtml | 2 +- .../PlatformNotifications/Details.cshtml | 2 +- .../Views/PlatformNotifications/Index.cshtml | 2 +- .../Views/PlatformSubscription/Edit.cshtml | 4 +- .../Views/PlatformSubscription/Index.cshtml | 4 +- .../Views/PlatformUsers/Index.cshtml | 4 +- .../Views/PowderInsights/Index.cshtml | 20 +- .../PowderInsights/_JobPowderSummary.cshtml | 8 +- .../Views/PricingTiers/Create.cshtml | 6 +- .../Views/PricingTiers/Edit.cshtml | 8 +- .../Views/PricingTiers/Index.cshtml | 6 +- .../Views/Profile/Index.cshtml | 58 +++--- .../Views/PurchaseOrders/Create.cshtml | 14 +- .../Views/PurchaseOrders/Details.cshtml | 4 +- .../Views/PurchaseOrders/Edit.cshtml | 8 +- .../Views/PurchaseOrders/Index.cshtml | 6 +- .../Views/PurchaseOrders/Receive.cshtml | 12 +- .../SelectLowStockVendor.cshtml | 2 +- .../Views/QuoteApproval/Confirmation.cshtml | 2 +- .../Views/Quotes/Create.cshtml | 24 +-- .../Views/Quotes/Delete.cshtml | 4 +- .../Views/Quotes/Details.cshtml | 48 ++--- .../Views/Quotes/Edit.cshtml | 26 +-- .../Views/Quotes/Index.cshtml | 2 +- .../Views/RecurringTemplates/Create.cshtml | 14 +- .../Views/RecurringTemplates/Edit.cshtml | 14 +- .../Views/RecurringTemplates/Index.cshtml | 2 +- .../Views/Registration/Welcome.cshtml | 28 +-- .../Views/ReleaseNotes/Manage.cshtml | 4 +- .../Views/Reports/AnomalyDetection.cshtml | 6 +- .../Views/Reports/ApAging.cshtml | 24 +-- .../Views/Reports/ArAging.cshtml | 26 +-- .../Views/Reports/BalanceSheet.cshtml | 2 +- .../Views/Reports/BudgetVsActual.cshtml | 2 +- .../Views/Reports/CashFlowForecast.cshtml | 10 +- .../Views/Reports/CashFlowStatement.cshtml | 2 +- .../Views/Reports/CustomerRetention.cshtml | 12 +- .../Views/Reports/FinancialQuery.cshtml | 4 +- .../Views/Reports/Index.cshtml | 48 ++--- .../Views/Reports/InventoryTurnover.cshtml | 2 +- .../Views/Reports/InvoiceAgingDetail.cshtml | 14 +- .../Views/Reports/JobStatusAging.cshtml | 2 +- .../Views/Reports/Landing.cshtml | 16 +- .../Views/Reports/OperationsReport.cshtml | 2 +- .../Views/Reports/PowderConsumption.cshtml | 4 +- .../Views/Reports/PowderUsage.cshtml | 4 +- .../Views/Reports/ProfitAndLoss.cshtml | 20 +- .../Views/Reports/SalesAndIncome.cshtml | 10 +- .../Views/Reports/SalesByCustomer.cshtml | 2 +- .../Views/Reports/SalesTax.cshtml | 12 +- .../Views/Reports/TaxReporting1099.cshtml | 8 +- .../Views/Revenue/Index.cshtml | 8 +- .../Views/SeedData/Index.cshtml | 4 +- .../Views/SetupWizard/Complete.cshtml | 4 +- .../Views/SetupWizard/Step1.cshtml | 22 +- .../Views/SetupWizard/Step10.cshtml | 4 +- .../Views/SetupWizard/Step11.cshtml | 2 +- .../Views/SetupWizard/Step12.cshtml | 4 +- .../Views/SetupWizard/Step13.cshtml | 10 +- .../Views/SetupWizard/Step14.cshtml | 2 +- .../Views/SetupWizard/Step15.cshtml | 2 +- .../Views/SetupWizard/Step16.cshtml | 2 +- .../Views/SetupWizard/Step17.cshtml | 2 +- .../Views/SetupWizard/Step18.cshtml | 2 +- .../Views/SetupWizard/Step2.cshtml | 4 +- .../Views/SetupWizard/Step3.cshtml | 46 ++--- .../Views/SetupWizard/Step4.cshtml | 8 +- .../Views/SetupWizard/Step5.cshtml | 4 +- .../Views/SetupWizard/Step6.cshtml | 6 +- .../Views/SetupWizard/Step7.cshtml | 2 +- .../Views/SetupWizard/Step8.cshtml | 2 +- .../Views/SetupWizard/Step9.cshtml | 2 +- .../_QbMigrationWizardModal.cshtml | 6 +- .../Views/Shared/_AiHelpWidget.cshtml | 4 +- .../Views/Shared/_AiQuickQuoteWidget.cshtml | 6 +- .../Views/Shared/_KioskLayout.cshtml | 8 +- .../Views/Shared/_Layout.cshtml | 34 ++-- .../Views/Shared/_SectionHeader.cshtml | 2 +- .../Views/SmsAgreements/Index.cshtml | 20 +- .../Views/SmsConsentAudit/Index.cshtml | 12 +- .../Views/StorageMigration/Index.cshtml | 8 +- .../Views/StripeEvents/Details.cshtml | 6 +- .../Views/StripeEvents/Index.cshtml | 10 +- .../Views/SubscriptionManagement/Index.cshtml | 16 +- .../SubscriptionManagement/Manage.cshtml | 22 +- .../Views/SystemInfo/Index.cshtml | 4 +- .../Views/SystemLogs/Index.cshtml | 24 +-- .../Views/TaxRates/Index.cshtml | 4 +- .../Views/Tools/Index.cshtml | 2 +- .../Views/TwoFactorSetup/Setup.cshtml | 4 +- .../Views/Unsubscribe/EmailConfirm.cshtml | 2 +- .../Views/UsageQuota/Index.cshtml | 8 +- .../Views/UserActivity/Index.cshtml | 2 +- .../Views/UserActivity/Online.cshtml | 8 +- .../Views/VendorCredits/Create.cshtml | 6 +- .../Views/VendorCredits/Details.cshtml | 6 +- .../Views/VendorCredits/Index.cshtml | 2 +- .../Views/Vendors/Create.cshtml | 4 +- .../Views/Vendors/Details.cshtml | 4 +- .../Views/Vendors/Edit.cshtml | 4 +- .../Views/Vendors/Index.cshtml | 10 +- .../Views/Vendors/Statement.cshtml | 4 +- tools/Fix-Encoding.ps1 | 125 ++++++++++++ 252 files changed, 1785 insertions(+), 1633 deletions(-) create mode 100644 .githooks/pre-commit create mode 100644 tools/Fix-Encoding.ps1 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..bd722a6 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,27 @@ +#!/bin/sh +# Pre-commit hook: block commits containing corrupted Unicode in .cshtml files. +# +# All corruption variants start with the UTF-8 byte sequence for a-circumflex +# followed by euro-sign (bytes C3 A2 E2 82 AC), which is the first two chars +# of every known corruption pattern. Grep for that byte sequence in staged files. + +STAGED=$(git diff --cached --name-only | grep '\.cshtml$') +if [ -z "$STAGED" ]; then + exit 0 +fi + +# $'\xc3\xa2\xe2\x82\xac' = UTF-8 bytes for a-circumflex + euro-sign +CORRUPT=$(echo "$STAGED" | xargs grep -l $'\xc3\xa2\xe2\x82\xac' 2>/dev/null) + +if [ -n "$CORRUPT" ]; then + echo "" + echo "ERROR: Corrupted Unicode characters detected in staged .cshtml files:" + echo "$CORRUPT" | sed 's/^/ /' + echo "" + echo "Fix by running: .\\tools\\Fix-Encoding.ps1" + echo "Then re-stage the files and commit again." + echo "" + exit 1 +fi + +exit 0 diff --git a/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml b/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml index cfe22eb..1640c28 100644 --- a/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml +++ b/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml @@ -72,14 +72,14 @@
@@ -111,7 +111,7 @@ document.getElementById('exportForm').addEventListener('submit', function () { var btn = document.getElementById('exportBtn'); btn.disabled = true; - btn.innerHTML = 'Generating…'; + btn.innerHTML = 'Generating…'; // Re-enable after 10s in case browser blocks the download dialog setTimeout(function () { btn.disabled = false; diff --git a/src/PowderCoating.Web/Views/AccountingExport/Index.cshtml b/src/PowderCoating.Web/Views/AccountingExport/Index.cshtml index 6fa0b06..2472c1c 100644 --- a/src/PowderCoating.Web/Views/AccountingExport/Index.cshtml +++ b/src/PowderCoating.Web/Views/AccountingExport/Index.cshtml @@ -49,7 +49,7 @@
QuickBooks Desktop
-
IIF files — import directly via File > Utilities > Import
+
IIF files — import directly via File > Utilities > Import
customers.iif invoices_payments.iif @@ -146,7 +146,7 @@ }); } - // Init — mark CSV as selected by default + // Init — 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 = 'Generating…'; + btn.innerHTML = 'Generating…'; setTimeout(function() { btn.disabled = false; btn.innerHTML = 'Download Export Package'; }, 8000); }); diff --git a/src/PowderCoating.Web/Views/Accounts/Create.cshtml b/src/PowderCoating.Web/Views/Accounts/Create.cshtml index b819cf2..f59330b 100644 --- a/src/PowderCoating.Web/Views/Accounts/Create.cshtml +++ b/src/PowderCoating.Web/Views/Accounts/Create.cshtml @@ -1,11 +1,11 @@ -@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto +@model PowderCoating.Application.DTOs.Accounting.CreateAccountDto @using PowderCoating.Core.Enums @{ ViewData["Title"] = "New Account"; ViewData["PageIcon"] = "bi-journal-plus"; ViewData["PageHelpTitle"] = "New Account"; - ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first — it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses."; + ViewData["PageHelpContent"] = "Add a custom account to the Chart of Accounts. Select a Sub-Type first — it auto-sets the Account Type. Use conventional numbering: 1000s = Assets, 2000s = Liabilities, 3000s = Equity, 4000s = Revenue, 5000s = Cost of Goods, 6000s+ = Expenses."; bool isInline = ViewBag.Inline == true; } @@ -31,7 +31,7 @@ + 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).">
@@ -55,7 +55,7 @@
@@ -70,7 +70,7 @@ @@ -152,7 +152,7 @@ diff --git a/src/PowderCoating.Web/Views/CompanySettings/EditTemplate.cshtml b/src/PowderCoating.Web/Views/CompanySettings/EditTemplate.cshtml index 0b877da..cd7a059 100644 --- a/src/PowderCoating.Web/Views/CompanySettings/EditTemplate.cshtml +++ b/src/PowderCoating.Web/Views/CompanySettings/EditTemplate.cshtml @@ -1,6 +1,6 @@ @model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto @{ - ViewData["Title"] = $"Edit Template — {Model.DisplayName}"; + ViewData["Title"] = $"Edit Template — {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) { - +
@@ -131,7 +131,7 @@
+ title="@description — click to copy"> @placeholder Copied! diff --git a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml index 9480002..310aec8 100644 --- a/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml +++ b/src/PowderCoating.Web/Views/CompanySettings/Index.cshtml @@ -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 @@ + 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 <strong>Primary Contact Email</strong> is used as the reply-to address on all outgoing notifications.<br><br><a href='/Help/Settings#company-information' target='_blank'>Learn more →</a>"> @@ -165,39 +165,39 @@ + placeholder="Examples: • We specialise in automotive restoration — wheels, frames, suspension brackets, and roll cages are our bread and butter. • Our customers expect premium pricing. We rarely work on items over 20 sqft. • Most items come to us already stripped; sandblasting adds roughly 15 min per item on average. • We use a 2-stage cure cycle — pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)
- Plain language — write it as if briefing a new estimator on your shop. + Plain language — write it as if briefing a new estimator on your shop. @(Model.OperatingCosts?.AiContextProfile?.Length ?? 0)/2000
@@ -832,7 +832,7 @@ Save AI Profile @@ -843,9 +843,9 @@
How AI Learning Works
-

Layer 1 — Pricing config: Your operating costs (labor, equipment, markup) are always injected automatically.

-

Layer 2 — Your shop profile: The description you write here is added to every AI analysis, guiding estimates toward your typical work.

-

Layer 3 — Automatic learning: 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.

+

Layer 1 — Pricing config: Your operating costs (labor, equipment, markup) are always injected automatically.

+

Layer 2 — Your shop profile: The description you write here is added to every AI analysis, guiding estimates toward your typical work.

+

Layer 3 — Automatic learning: 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.

@@ -874,10 +874,10 @@
Used by the AI when estimating job complexity and throughput.
@@ -978,11 +978,11 @@
@@ -1046,7 +1046,7 @@ + data-bs-content="The prefix is combined with a date stamp and sequence number to form record IDs — for example prefix <strong>QT</strong> produces <em>QT-2603-0042</em>. Change the prefix to match your preferred numbering convention. Changing it only affects <strong>new</strong> records; existing numbers are not renamed."> @@ -1082,7 +1082,7 @@ + data-bs-content="Controls how jobs are created and flow through your shop. <strong>Require Customer PO</strong> enforces that a PO number is entered before a job can be saved — useful for commercial accounts. <strong>Allow Customer Approval</strong> enables the approval step in the job workflow — when a quote is approved, the job moves to an Approved status before work begins."> @@ -1141,7 +1141,7 @@ + data-bs-content="Controls which events send emails to your team and customers. Set the <strong>From Email Address</strong> 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."> @@ -1311,7 +1311,7 @@ + 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 <strong>{{placeholder}}</strong> tokens that are replaced with live data when the email is sent. Click <strong>Edit</strong> on any row to modify it; use <strong>Reset to Default</strong> to restore the original wording at any time.<br><br>Changes take effect immediately — the next triggered notification will use the updated template."> @@ -1371,7 +1371,7 @@ + data-bs-content="Controls how long records are kept. Most businesses set quote and job retention to <strong>7 years</strong> to satisfy tax and audit requirements. <strong>Deleted record retention</strong> is the grace period after a soft-delete before the record is permanently purged — useful if someone accidentally deletes something."> @@ -1433,7 +1433,7 @@ + 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. <strong>Status codes</strong> drive workflow logic and should not be changed unless you understand the impact."> @@ -1869,7 +1869,7 @@ -
Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.
+
Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.
@@ -2168,7 +2168,7 @@
- +
$ @@ -88,7 +88,7 @@ diff --git a/src/PowderCoating.Web/Views/Invoices/Create.cshtml b/src/PowderCoating.Web/Views/Invoices/Create.cshtml index fbc8ccc..042959c 100644 --- a/src/PowderCoating.Web/Views/Invoices/Create.cshtml +++ b/src/PowderCoating.Web/Views/Invoices/Create.cshtml @@ -5,7 +5,7 @@ ViewData["Title"] = "Create Invoice"; ViewData["PageIcon"] = "bi-receipt"; ViewData["PageHelpTitle"] = "Create Invoice"; - ViewData["PageHelpContent"] = "Invoices start as Drafts — you can freely edit them until you click Send. Once sent, the invoice is locked and the customer is emailed. Line items are pre-populated from the job's items but you can add, edit, or remove any line before sending. Partial payments are supported after sending."; + ViewData["PageHelpContent"] = "Invoices start as Drafts — you can freely edit them until you click Send. Once sent, the invoice is locked and the customer is emailed. Line items are pre-populated from the job's items but you can add, edit, or remove any line before sending. Partial payments are supported after sending."; var jobNumber = ViewBag.JobNumber as string; var customerName = ViewBag.CustomerName as string; var customers = ViewBag.Customers as List; @@ -130,7 +130,7 @@ + data-bs-content="Invoice Date is the date of issue — this is what appears on the printed invoice and determines when payment terms start counting. Due Date drives overdue status and A/R aging reports. Payment Terms is free text (e.g., 'Net 30') that prints on the invoice; it defaults from the customer's settings but you can override it here.">
@@ -143,7 +143,7 @@ + data-bs-content="The date the invoice is issued. This appears on the printed document and is the reference date for payment terms — e.g., Net 30 means payment is due 30 days after this date. Defaults to today.">
@@ -196,7 +196,7 @@ + data-bs-content="Each row is a billable line on the invoice. Pre-populated from the job's items. Qty × Unit Price = Total per line; you can override the Total directly too. Color is optional — it appears under the description on the printed invoice. Add manual lines for anything not in the job (e.g., pickup fee, rush charge)."> @@ -321,7 +321,7 @@ + data-bs-content="Customer Notes appear on the printed and emailed invoice — use these for payment instructions, thank-you messages, or job-specific reminders. Internal Notes are only visible to staff here in the app and never sent to the customer."> @@ -354,7 +354,7 @@ + data-bs-content="Subtotal = sum of all line item totals. Discount is a flat dollar amount deducted before tax — use it for customer-specific deals or courtesy adjustments. Tax % is applied to (Subtotal − Discount). Both default from the company settings but can be overridden per invoice."> @@ -584,7 +584,7 @@ onmousedown="event.preventDefault();merchComboSelect(this)" onmouseenter="this.style.background=document.documentElement.getAttribute('data-bs-theme')==='dark'?'#2c3a5a':'#f0f4ff'" onmouseleave="this.classList.contains('mc-active')?null:this.style.background=''"> - ${i.name}${i.sKU ? ' [' + i.sKU + ']' : ''} — ${formatCurrency(i.defaultPrice)} + ${i.name}${i.sKU ? ' [' + i.sKU + ']' : ''} — ${formatCurrency(i.defaultPrice)} ` ).join('') ).join(''); @@ -656,7 +656,7 @@ } function addGiftCertLineItem(btn) { - // Bootstrap teleports modals to — navigate relative to the button + // Bootstrap teleports modals to — navigate relative to the button const modalEl = btn ? btn.closest('.modal') : document.getElementById('gcModal'); const q = sel => modalEl ? modalEl.querySelector(sel) : document.querySelector(sel); diff --git a/src/PowderCoating.Web/Views/Invoices/Details.cshtml b/src/PowderCoating.Web/Views/Invoices/Details.cshtml index 30bd6e2..1b13309 100644 --- a/src/PowderCoating.Web/Views/Invoices/Details.cshtml +++ b/src/PowderCoating.Web/Views/Invoices/Details.cshtml @@ -17,8 +17,8 @@ var emailOptedOut = hasEmail && !Model.CustomerNotifyByEmail; var smsPhone = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone) ? Model.CustomerMobilePhone : Model.CustomerPhone; var hasSms = !string.IsNullOrWhiteSpace(smsPhone) && Model.CustomerNotifyBySms; - var showSendModal = hasEmail && !emailOptedOut && hasSms; // both channels — show choice modal - var directSendSms = !hasEmail && hasSms; // SMS only — skip modal + var showSendModal = hasEmail && !emailOptedOut && hasSms; // both channels — show choice modal + var directSendSms = !hasEmail && hasSms; // SMS only — skip modal var hasAvailableCredits = ViewBag.AvailableCreditMemos != null && ((IEnumerable)ViewBag.AvailableCreditMemos).Any(); var canIssueRefund = !isDraft && !isVoided && Model.AmountPaid > 0; var canApplyCredit = !isVoided && Model.BalanceDue > 0 && hasAvailableCredits; @@ -80,7 +80,7 @@
- @Model.CustomerName has no email address on file — you'll be prompted to enter one when sending. + @Model.CustomerName has no email address on file — you'll be prompted to enter one when sending. Add one in customer settings.
@@ -179,12 +179,12 @@

- @(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—") + @(Model.DueDate.HasValue ? Model.DueDate.Value.ToString("MMMM d, yyyy") : "—")

-

@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")

+

@(Model.SentDate.HasValue ? Model.SentDate.Value.ToString("MMMM d, yyyy") : "—")

@if (!string.IsNullOrWhiteSpace(Model.CustomerPO)) { @@ -350,7 +350,7 @@ - @(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—") + @(gcItem.Description.Contains("for ") ? gcItem.Description.Substring(gcItem.Description.IndexOf("for ") + 4).TrimEnd(')') : "—") @gcItem.TotalPrice.ToString("C") @@ -396,7 +396,7 @@ @p.PaymentDate.ToString("MM/dd/yyyy") @p.PaymentMethodDisplay - @(p.Reference ?? "—") + @(p.Reference ?? "—") @if (!string.IsNullOrEmpty(p.DepositAccountName)) { @@ -404,10 +404,10 @@ } else { - + } - @(p.RecordedByName ?? "—") + @(p.RecordedByName ?? "—") @p.Amount.ToString("C") @if (!isVoided) @@ -463,7 +463,7 @@ @r.RefundDate.ToString("MM/dd/yyyy") @r.RefundMethodDisplay @r.Reason - @(r.Reference ?? "—") + @(r.Reference ?? "—") @r.Status (@r.Amount.ToString("C")) @@ -575,7 +575,7 @@ + data-bs-content="Workflow: Edit (Draft only) → Send Invoice (locks it, emails customer) → Record Payment. Partial payments are supported — record multiple payments until fully paid. Void cancels the invoice and reverses the customer balance without deleting history. Delete is only available for Drafts."> @@ -601,7 +601,7 @@ } else if (showSendModal) { - @* Both email + SMS available — let staff choose *@ + @* Both email + SMS available — let staff choose *@ @@ -50,7 +50,7 @@ + data-bs-content="Core job information. Priority and due date are visible on the shop floor board and affect how work is sorted. Customer PO is the customer's own reference number for their purchase order — include it so it appears on invoices. Special Instructions go directly to the shop floor worker."> @@ -70,7 +70,7 @@ + data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team."> @@ -100,7 +100,7 @@ + data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list."> @@ -130,7 +130,7 @@ + data-bs-content="Free-text notes visible to the shop floor worker on the work order. Use this for masking requirements, handling notes, customer preferences, or anything that doesn't fit in the item-level notes — e.g., 'Keep brackets separated, customer allergic to zinc primer'."> @@ -201,7 +201,7 @@ + data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work."> diff --git a/src/PowderCoating.Web/Views/Jobs/Details.cshtml b/src/PowderCoating.Web/Views/Jobs/Details.cshtml index fbd776d..9334148 100644 --- a/src/PowderCoating.Web/Views/Jobs/Details.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Details.cshtml @@ -1,4 +1,4 @@ -@model PowderCoating.Application.DTOs.Job.JobDto +@model PowderCoating.Application.DTOs.Job.JobDto @{ ViewData["Title"] = $"Job {Model.JobNumber}"; @@ -2796,7 +2796,7 @@ document.getElementById('costingMargin').textContent = `${d.grossMargin}%`; document.getElementById('costingQuotedMargin').textContent = - d.quotedPrice > 0 ? `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})` : '—'; + d.quotedPrice > 0 ? `${d.quotedMargin}% (quoted ${fmt(d.quotedPrice)})` : '—'; // Powder detail lines const pBody = document.getElementById('powderLines'); @@ -2900,9 +2900,9 @@ } function updateTotals(total) { - const fmt = total > 0 ? total.toFixed(2) + ' hrs' : '—'; + const fmt = total > 0 ? total.toFixed(2) + ' hrs' : '—'; document.getElementById('totalHoursDisplay').textContent = fmt; - document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '—'; + document.getElementById('timeEntriesTotalHours').textContent = total > 0 ? total.toFixed(2) : '—'; } // -- Modal helpers ------------------------------------------------- diff --git a/src/PowderCoating.Web/Views/Jobs/Edit.cshtml b/src/PowderCoating.Web/Views/Jobs/Edit.cshtml index bb81cb2..b192c85 100644 --- a/src/PowderCoating.Web/Views/Jobs/Edit.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Edit.cshtml @@ -1,4 +1,4 @@ -@model PowderCoating.Application.DTOs.Job.UpdateJobDto +@model PowderCoating.Application.DTOs.Job.UpdateJobDto @using PowderCoating.Core.Entities @{ @@ -26,7 +26,7 @@ + data-bs-content="Core job information. Priority and due date are visible on the shop floor board and job list. Customer PO is the customer's own reference number — it appears on invoices. Special Instructions go directly to the shop floor worker on the work order."> @@ -45,7 +45,7 @@ + data-bs-content="Tracks where the job is in the workflow: Pending â†' Approved â†' Sandblasting â†' Cleaning â†' Coating â†' Curing â†' QualityCheck â†' Completed â†' ReadyForPickup â†' Delivered. Status changes trigger customer email notifications (if enabled). Use OnHold to pause work without losing progress."> @@ -57,7 +57,7 @@ + data-bs-content="Controls sort order on the shop floor board and job list. Rush and Urgent jobs are highlighted in red/orange. Normal is the default. Raise priority only when the customer has an actual deadline constraint — overuse of Rush dilutes its meaning for the shop floor team."> @@ -85,7 +85,7 @@ + data-bs-content="The customer's deadline — when the work must be ready for pickup or delivery. Overdue jobs (past due date and not yet completed) are highlighted in red on the job list."> @@ -170,7 +170,7 @@ + data-bs-content="Each item represents a physical piece being coated. Use the wizard to pick from the catalog, enter custom dimensions, or upload a photo for AI analysis. Each item gets its own coating specification — color, powder, finish, and cure details. You can add multiple coating passes per item for multi-color or primer+topcoat work."> diff --git a/src/PowderCoating.Web/Views/Jobs/EditItems.cshtml b/src/PowderCoating.Web/Views/Jobs/EditItems.cshtml index 62d0758..7bc84af 100644 --- a/src/PowderCoating.Web/Views/Jobs/EditItems.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/EditItems.cshtml @@ -1,8 +1,8 @@ -@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel +@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel @using PowderCoating.Core.Entities @{ - ViewData["Title"] = $"Edit Items — {Model.JobNumber}"; + ViewData["Title"] = $"Edit Items — {Model.JobNumber}"; ViewData["PageIcon"] = "bi-list-check"; } diff --git a/src/PowderCoating.Web/Views/Jobs/Index.cshtml b/src/PowderCoating.Web/Views/Jobs/Index.cshtml index 6be0d17..5553f09 100644 --- a/src/PowderCoating.Web/Views/Jobs/Index.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Index.cshtml @@ -81,7 +81,7 @@ diff --git a/src/PowderCoating.Web/Views/Jobs/Intake.cshtml b/src/PowderCoating.Web/Views/Jobs/Intake.cshtml index 67a7c0e..2fd937c 100644 --- a/src/PowderCoating.Web/Views/Jobs/Intake.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Intake.cshtml @@ -1,7 +1,7 @@ @model (PowderCoating.Application.DTOs.Job.JobDto Job, PowderCoating.Application.DTOs.Job.IntakeJobDto Form) @{ - ViewData["Title"] = $"Part Intake — {Model.Job.JobNumber}"; + ViewData["Title"] = $"Part Intake — {Model.Job.JobNumber}"; ViewData["PageIcon"] = "bi-box-seam"; var job = Model.Job; var form = Model.Form; @@ -122,7 +122,7 @@ {
- Count doesn't match expected — note the discrepancy in condition notes. + Count doesn't match expected — note the discrepancy in condition notes.
} @@ -246,7 +246,7 @@ const formData = new FormData(); formData.append('jobId', jobId); formData.append('photo', file); - formData.append('caption', 'Intake — before'); + formData.append('caption', 'Intake — before'); formData.append('photoType', '0'); // JobPhotoType.Before = 0 const token = document.querySelector('input[name="__RequestVerificationToken"]').value; diff --git a/src/PowderCoating.Web/Views/Jobs/ShopDisplay.cshtml b/src/PowderCoating.Web/Views/Jobs/ShopDisplay.cshtml index 6f508aa..50e46e9 100644 --- a/src/PowderCoating.Web/Views/Jobs/ShopDisplay.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/ShopDisplay.cshtml @@ -141,7 +141,7 @@ text-align: center; } - /* bg-info is a light cyan — use dark text so it's readable on TV */ + /* bg-info is a light cyan — use dark text so it's readable on TV */ .badge-xl.bg-info { color: #000 !important; } .worker-badge { @@ -457,7 +457,7 @@ @item.Description @if (item.Colors.Any()) { - + @string.Join(" / ", item.Colors) } @@ -626,7 +626,7 @@ }); diff --git a/src/PowderCoating.Web/Views/Jobs/ShopMobile.cshtml b/src/PowderCoating.Web/Views/Jobs/ShopMobile.cshtml index a03ffc5..1a55ace 100644 --- a/src/PowderCoating.Web/Views/Jobs/ShopMobile.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/ShopMobile.cshtml @@ -445,7 +445,7 @@ } else { - + } @@ -608,7 +608,7 @@ btn.disabled = false; } } catch (e) { - showToast('Network error — try again', 'danger'); + showToast('Network error — try again', 'danger'); btn.innerHTML = originalHtml; btn.disabled = false; } diff --git a/src/PowderCoating.Web/Views/Jobs/StatusBump.cshtml b/src/PowderCoating.Web/Views/Jobs/StatusBump.cshtml index 2a6996d..ecca943 100644 --- a/src/PowderCoating.Web/Views/Jobs/StatusBump.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/StatusBump.cshtml @@ -222,7 +222,7 @@ Due: @job.DueDate.Value.ToString("MMM d, yyyy") @if (job.DueDate < DateTime.Today && !isTerminal) { - — OVERDUE + — OVERDUE } } @@ -237,14 +237,14 @@ } else if (isOnHold) { - @* On hold — offer resume (next logical status after resume by advancing) *@ + @* On hold — offer resume (next logical status after resume by advancing) *@ @if (nextStatus != null) {
@Html.AntiForgeryToken()
} diff --git a/src/PowderCoating.Web/Views/Jobs/WorkOrder.cshtml b/src/PowderCoating.Web/Views/Jobs/WorkOrder.cshtml index 6347b79..4042f30 100644 --- a/src/PowderCoating.Web/Views/Jobs/WorkOrder.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/WorkOrder.cshtml @@ -483,10 +483,10 @@ @coat.Sequence @coat.CoatName - @(coat.ColorName ?? "—") - @(coat.ColorCode ?? "—") - @(coat.Finish ?? "—") - @(coat.VendorName ?? "—") + @(coat.ColorName ?? "—") + @(coat.ColorCode ?? "—") + @(coat.Finish ?? "—") + @(coat.VendorName ?? "—") @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @@ -501,7 +501,7 @@ } else { - + } @@ -622,7 +622,7 @@ } - @* Powder usage QRs — one per unique inventory item *@ + @* Powder usage QRs — one per unique inventory item *@ @if (hasPowderQrs) { @foreach (var pqr in powderQrCodes!) diff --git a/src/PowderCoating.Web/Views/JobsPriority/Index.cshtml b/src/PowderCoating.Web/Views/JobsPriority/Index.cshtml index 533a643..853c112 100644 --- a/src/PowderCoating.Web/Views/JobsPriority/Index.cshtml +++ b/src/PowderCoating.Web/Views/JobsPriority/Index.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @using PowderCoating.Application.DTOs.Job @using PowderCoating.Core.Entities @using PowderCoating.Core.Enums @@ -51,7 +51,7 @@ - @* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@ + @* -- Carried-Over (Overdue) Jobs ---------------------------------------- *@ @if (overdueJobs.Any()) {
@@ -59,7 +59,7 @@ } - @* ── Scheduled Maintenance for the Day ──────────────────────────────── *@ + @* -- Scheduled Maintenance for the Day -------------------------------- *@
@@ -561,7 +561,7 @@ - @(item.Equipment?.EquipmentName ?? "—") + @(item.Equipment?.EquipmentName ?? "—") @if (!string.IsNullOrEmpty(item.Equipment?.Location)) {
@item.Equipment.Location @@ -1110,14 +1110,14 @@ - +