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
+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>