Introduces per-company reusable NCalc2 pricing formula templates for complex
fabricated items (roof curbs, enclosures, welded frames). Templates support
two output modes — FixedRate (formula yields a dollar amount) and SurfaceAreaSqFt
(formula yields sq ft fed into the standard coating engine). Includes:
- CustomItemTemplate entity, migration (AddCustomItemTemplates), IUnitOfWork repo
- IsCustomFormulaItem / CustomItemTemplateId / FormulaFieldValuesJson flags on
QuoteItem, JobItem, CreateQuoteItemDto; mapped in all 3 JobItemAssemblyService
overloads and all existingItemsData JSON projections + pageMeta blocks
- ICustomFormulaAiService / CustomFormulaAiService: Claude-powered formula
generator (natural language + optional diagram image) and NCalc2 evaluator
- CompanySettings CRUD endpoints: GetCustomItemTemplates, Create/Update/Delete,
UploadTemplateDiagram, TemplateDiagram (blob serve), EvaluateFormula, GenerateFormulaFromAi
- Company Settings "Custom Formulas" tab + cfModal + company-settings-custom-formulas.js
- item-wizard.js: formula item type card, renderFormulaFields, wzFormulaRecalc
(live evaluate via POST), collectStep2 formula branch, buildCardHtml / emitHiddenFields
- Formula badge in Quotes/Details and Jobs/Details; AI badge gap fixed in Jobs/Details
- Help article (CustomFormulaTemplates.cshtml), Help Index card, HelpController action,
HelpKnowledgeBase entry; 225/225 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a PricingType enum to ReworkRecord (FixedPrice | PerItem), surfaces the
choice in the rework modal on Job Details, and wires the resulting unit/total
price display. Includes migration AddReworkPricingType, updated repository
query for rework history, and help article updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PatchItem: add case-insensitive JSON deserialization; add legacy fallback
that computes a live breakdown from job items when PricingBreakdownJson is null
- PatchItem: return itemsSubtotal, subtotalBeforeDiscount, subtotalAfterDiscount,
taxAmount in JSON response for immediate DOM updates
- GetCostingBreakdown: use job.FinalPrice as revenue (not invoice total) so
costing figures reflect inline edits before an invoice exists
- Details.cshtml: add data-pb attributes to visible pricing rows; add
job-final-price-display class to visible Total element
- Details.cshtml: wire afterSave callback to call costing.load() after each edit
- inline-item-edit.js: add afterSave hook in commit(); clean up debug logging
- Help docs: add Inline Price Editing sections to Jobs, Quotes, and Invoices
help articles; add inline editing + job costing revenue notes to AI knowledge base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jobs/PatchItem now returns the full breakdown (itemsSubtotal,
subtotalBeforeDiscount, subtotalAfterDiscount, taxAmount) so all rows
in the pricing card update live without a page refresh.
Added data-pb attributes to the matching spans in the pricing panel.
Updated window.inlineItemEdit.totals config for jobs to map each
response key to its DOM selector.
updateTotals in inline-item-edit.js is now fully generic — cfg.totals
keys must match server response property names directly, eliminating
the old hardcoded tax/taxAmount and balance/balanceDue mismatches.
Updated Quote and Invoice configs accordingly (tax→taxAmount,
balance→balanceDue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded #fff / #f8f9fa / #dee2e6 / #e8eeff colors with Bootstrap
CSS variables so the dropdown respects the active theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Allow description, quantity, and price to be edited inline on Quote,
Job, and Invoice details pages without re-opening the wizard. Coating
and prep service rows remain read-only by design. Invoice editing is
gated to Draft/Sent/Overdue statuses; totals update live in the DOM.
Remove receipt_email from Stripe PaymentIntent creation so customers
can use any email they choose at checkout — Stripe validates format
and sends the receipt to whatever the customer enters in the Payment
Element, eliminating the risk of a stored email mismatch blocking a
payment from processing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Job Details: hide desktop item tables on mobile (d-none d-lg-block) so only
the existing mobile-card layout shows on small screens — prevents 20-line rows
on a narrow phone display
- Invoices Create (ForJob path): load job item coats and derive ColorName from
coat colors when the item itself has no explicit color set; multiple coats join
as 'Color1 / Color2' — lets customers distinguish repeated items (e.g. multiple
caliper sets) on the invoice
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Appointment reminders: add AppointmentReminderBackgroundService (60s poll), ReminderSentAt
dedup stamp, NotifyAppointmentReminderAsync sends both customer email and creator staff email;
AppointmentReminderStaff notification type + default template added; DateTime.Now used instead
of UtcNow to match locally-stored ScheduledStartTime; ToLocalTime() double-conversion removed
- NoExtraLayerCharge not persisted: flag existed on CreateQuoteItemCoatDto and was used by
pricing engine but never written to JobItemCoat/QuoteItemCoat entities — every edit reset it
to false and re-applied the extra layer charge; added column to both entities (migration
AddNoExtraLayerChargeToCoats), both read DTOs, all 3 JobItemAssemblyService overloads,
JobItemCoatSeed inner class, and existingItemsData JSON in all 5 wizard views; fixed JS
template path that hard-coded noExtraLayerCharge: false
- Coat notes not visible: notes were rendered in desktop job details but missing from the wizard
item card summary and the mobile card view; both fixed
- Scroll position lost on item save: sessionStorage save/restore added to item-wizard.js owner
form submit handler; path-keyed so cross-page navigation does not restore stale position;
requestAnimationFrame used for reliable mobile scroll restoration
- Invoice Send dead button: #sendChannelModal was gated inside @if (isDraft) but the button
targeting it fires for Sent/Overdue invoices too when customer has both email and SMS; modal
moved outside the Draft guard
- InitialCreate migration added for fresh database installs; Baseline migration guarded with
IF OBJECT_ID check so it no-ops on fresh DBs; Razor scoping bug fixed in Customers/Index.cshtml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Powders already assigned to this job's coats appear under a 'This Job'
section header, then a divider, then 'All Inventory' — so the most
relevant choices are always one click away.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows manufacturer name as muted secondary text in each dropdown row
and includes it in the search filter, so users can find a powder by
brand when multiple items share a similar name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inventory lists grow over time; a plain <select> becomes unusable. The
new combobox filters as you type, supports keyboard navigation
(Arrow/Enter/Escape), and shows current stock on selection — matching
the pattern used by the powder picker in the item wizard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PC users were blocked to QR scan only for logging material usage. Now a
"Log Material" button opens an inline modal with:
- Inventory item dropdown (name + unit of measure, current stock shown on select)
- Entry method toggle: "Amount Used" or "Amount Remaining" (computes used = onHand - remaining)
- Reason: Job Usage or Waste/Spillage
- Notes field
Submits via AJAX to Jobs/LogMaterial (new POST action) which mirrors the
InventoryController.LogUsage flow — updates QuantityOnHand, creates InventoryTransaction,
posts GL entries (DR COGS / CR Inventory). QR scan button retained as icon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace mojibake box-drawing chars (U+2500 encoded as Windows-1252) with
plain ASCII dashes throughout all comments in Details.cshtml
- Fix intake button showing literal '✓' text: the entity was inside a
C# string so Razor HTML-encoded the '&'; switched to Html.Raw() so the
checkmark renders correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All � replacement characters replaced with correct HTML entities
(—, –, •, ×, …) and restored a
corrupted class attribute with missing double quotes on the Intake button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Carry OvenBatches/OvenCycleMinutes from Quote → Job entity (was missing fields; all job pricing recalcs hardcoded 1/null)
- Fix invoice creation from job always showing Quantity=1 (was using TotalPrice as UnitPrice with qty 1)
- Add IsAiItem to JobItem + migration; map in all 3 JobItemAssemblyService.CreateJobItem overloads so AI photo jobs no longer double-price on first edit after quote→job conversion
- Propagate IsAiItem through all existingItemsData JSON blocks in Jobs views (Edit, EditItems, Create) so the wizard preserves AI routing on re-edit
- Add PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem structural test + 3 behavioral IsAiItem tests to JobItemAssemblyServiceTests
- Consolidate item wizard partials (_ItemWizardModal, _SqFtCalculatorModal) and item-wizard.css into shared locations
- Document pricing flag propagation checklist in CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QuotePricingAssemblyService: catalog no-coat items now respect PowderCostOverride instead of always using DefaultPrice
- PricingCalculationService: removed duplicate catalog fast-path in CalculateQuoteTotalsAsync that bypassed CalculateQuoteItemPriceAsync (and thus ignored PowderCostOverride); all items now go through the single authoritative path
- Jobs/Details.cshtml: timeTracking.del() and timeTracking.save() now call costing.load() after success so the time entry row and costing breakdown stay in sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract _CompleteJobModal.cshtml partial; Details.cshtml uses PartialAsync
- Job board COMPLETED drop fetches partial via AJAX and shows modal in-place
- Add GET Jobs/CompleteJobModal action to load job data for the board modal
- install-app.js: persist installed state in localStorage; clears automatically when browser re-fires beforeinstallprompt after uninstall
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dragging a card to the Completed column on the job board previously called
MoveCard directly, skipping email/SMS notifications, CompletedDate, powder
deduction, and the completion modal entirely.
Now detects the COMPLETED status code on the target column, reverts the
visual drag, and navigates to the job Details page with #completeModal in
the hash. Details.cshtml auto-opens the Mark Complete modal on arrival,
so the user goes through the same code path as the Complete Job button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes Create/Edit: hide 'Send via email' checkbox when customer has no
email; show badge 'send via SMS from details' or 'SMS consent required'
when customer has a mobile number. JS responds to customer dropdown change.
- Quotes Details: hide 'Send Quote via Email' button and approval email
checkbox; hide SMS button when no mobile; show consent-required note.
- Jobs Details (Mark Complete modal): hide email checkbox; show
'SMS notification will be sent' badge or consent-required note.
- Jobs Index (status modal): hide email row when customer has no email.
- Jobs Edit: hide 'Notify customer if status changes' when no email.
- Invoices Details: hide Send/Re-send buttons when no email (vs. disabled).
DTOs: added CustomerEmail + CustomerNotifyByEmail to JobDto/JobListDto;
added CustomerNotifyByEmail/CustomerMobilePhone/CustomerNotifyBySms to
QuoteDto. Mapped in JobProfile and QuotesController customer blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove parseInt() from time entry worker select — GUIDs were destroyed
to NaN → sent as null → FindByIdAsync(null) threw 500
- Ledger pencil: also show for Adjustment rows (no PO) so scan-without-job
entries get an edit button, not just JobUsage rows
- InventoryController: always write JobUsage type for scan-based logs;
accept Adjustment in edit endpoints; promote Adjustment→JobUsage when
a job is assigned via edit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in
- CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version
- SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion)
- Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers
- Removed redundant Ready for Pickup SMS (Job Completed covers it)
- Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends
- Send SMS button on job details for ad-hoc messages (Admin/Manager only)
- SendJobSmsAsync auto-appends STOP opt-out language if missing
- Migrations: AddSmsGating, AddCompanySmsAgreement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a source quote is edited after a job was created from it, the job
details page now shows a warning banner with the date of the change and
a link to the quote. Two actions are offered:
- Re-sync from Quote: replaces all job items, coats, prep services, and
pricing from the current quote. Only available while the job is still
in a pre-production status (Pending, Quoted, Approved); hidden once
shop work has started (InPreparation or beyond).
- Dismiss: acknowledges the change without altering the job, clearing
the banner by advancing the stored snapshot timestamp.
Implemented via Job.QuoteSnapshotUpdatedAt (new nullable column), set at
quote→job conversion time. The banner fires when quote.UpdatedAt exceeds
this baseline. Migration: AddJobQuoteSnapshotUpdatedAt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.
Dashboard progress widget (new): "Get the most out of your shop" checklist
appears for Company Admins after wizard completion. Tracks six post-setup
activation tasks with dynamic progress badge, motivating subtitle copy,
collapsed-state persistence via localStorage, and a full completion state
("Your shop is fully set up 🎉") that replaces the checklist at 100%.
The next recommended step is highlighted with a solid CTA button and a
subtle blue row tint. Completed steps show encouraging green subtext instead
of just "Done". Widget disappears from controller when AllDone would have
caused a silent vanish — now renders the completion state instead.
Guided activation (Daily Board): rewrote the BoardIntroStep callout to lead
with "This is your shop in real time" and a plain-English description of the
board's purpose. Added a separate InstructionText field to
GuidedActivationCalloutViewModel so the "Move this job to the next stage"
action prompt renders as a distinct bold line with an arrow icon rather than
being buried in the body copy. After the stage change, the confirmation
callout now reads "Nice — your workflow just updated" to reinforce what just
happened before prompting the invoice step.
All copy passes the "shop owner, not SaaS" test: no technical jargon,
benefit-driven descriptions, natural language throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New AI Quick Quote floating button: staff type a verbal description to
get an instant price estimate for phone/walk-in customers; detected
color names are fuzzy-matched against inventory for stock status;
saves draft quote under a Walk-In / Phone customer with one click
- Inline customer change on Quote Details and Job Details: always-visible
native select with inline confirmation banner (no TomSelect dependency);
ChangeCustomer AJAX endpoints on QuotesController and JobsController
- Quote Edit page: customer dropdown is now editable (lock removed)
- Fix AutoMapper missing CatalogCategory -> UpdateCategoryDto mapping
that caused a crash on the catalog category Edit page
- Help docs and AI knowledge base updated for all three features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>