Commit Graph

36 Commits

Author SHA1 Message Date
spouliot 6721de91e4 Fix pricing consistency across Quote → Job → Invoice; add stage-flow tests
- Store complete PricingBreakdownJson snapshot on Job at every save point so
  the Details page reads stored data rather than re-running the pricing engine
- Add 7 missing fields to Quote entity (FacilityOverheadCost, tier/quote discounts,
  SubtotalAfterDiscount) and persist them via ApplyPricingSnapshot
- Fix OvenCostId-as-rate bug in JobsController (FK was passed as decimal $/hr)
- Fix hardcoded LaborCost * 0.4 multiplier in two JobItemAssemblyService overloads
- Fix FacilityOverheadCost dropped from invoices in both quote and direct-job paths
- Fix RushFee missing from direct-job invoices (read from PricingBreakdownJson)
- Fix Notes and CatalogItemId not copied to InvoiceItem
- Add 14 unit tests in PricingStageFlowTests covering all three pricing stages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:03:06 -04:00
spouliot 38748c2152 Add BatchId to GiftCertificate for persistent bulk batch tracking
BatchId (Guid?) is stamped on every certificate in a bulk run so the batch
is permanently addressable. BulkResult is now a bookmarkable GET by batchId
rather than TempData, so users can return to re-download at any time.
BatchDownloadPdf is a GET link (no form POST needed). Index shows a Batch
badge on bulk certs that links directly back to the batch result page.

Migration: AddGiftCertificateBatchId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:32:56 -04:00
spouliot 539c6c2559 Fix oven batch conversion, invoice quantity, AI photo pricing, and enforce pricing flag propagation
- 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>
2026-05-14 16:54:22 -04:00
spouliot d5ad9fa073 Add KioskIntakeOutput company setting and fix kiosk submission bugs
- New CompanyPreferences.KioskIntakeOutput setting ("Quote" default / "Job"): controls
  what the kiosk creates on submission; shown as a card-style radio toggle in
  Company Settings → Kiosk tab
- KioskSession.LinkedQuoteId added so quote-first sessions link back to the draft quote
- Migration AddKioskIntakeOutputSetting applies both schema changes
- ProcessSubmissionAsync branches on setting: creates Draft quote (quote-first) or
  Pending job (job-first); save order fixed (CompleteAsync before using DB-assigned Id as FK)
- Terms.cshtml pricing paragraph is now dynamic: "subject to formal quote" for Quote mode,
  "team member will reach out about pricing" for Job mode
- Customer Intakes list: "View Quote" button appears when LinkedQuoteId is set
- Notification label fixed: Remote sessions now say "Remote Intake", not "Walk-in Intake"
- Inactivity reset shortened to 45 s on intake steps
- Signature pad: hosted locally (no CDN), canvas resize deferred via requestAnimationFrame
- AI photo upload: client-side compression to ≤1200px + AbortController 120 s timeout
- Help article and AI knowledge base updated with kiosk feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:35:37 -04:00
spouliot 6a918c2afc Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor

Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:25:27 -04:00
spouliot 27bfd4db4d Close all GL entry gaps across the accounting surface
- Stripe payments/refunds/chargebacks now post DR/CR entries (PaymentController)
- Vendor credit void now reverses the posted GL lines (VendorCreditsController)
- Gift certificate issue/redeem/void post GL to account 2500 GC Liability;
  FinancialReportService Trial Balance + Balance Sheet include GC liability and
  breakage income; P&L shows deferred revenue deduction and breakage income line
- Customer deposits now post DR Checking / CR 2300 on record, reverse on delete;
  invoice auto-apply uses DR 2300 / CR AR (not a second bank debit); draft
  invoice delete reverses deposit-apply GL before the AR reversal
- Deposit.DepositAccountId column added; account 2300 seeded via migration
- InvoicesController.ApplyCredit now posts DR Sales Discounts / CR AR,
  consistent with CreditMemosController.Apply
- IssueRefund (cash/card) posts DR AR / CR Bank and sets Refund.DepositAccountId;
  refund modal gains a bank account selector hidden for store-credit path
- CancelRefund (cash/card) reverses the IssueRefund GL entries
- LedgerService GetAccountLedgerAsync + ComputePriorBalanceAsync now include
  Refunds, CreditMemoApplications, VendorCreditApplications, GC Liability (2500),
  and Customer Deposits (2300) so account ledger view and RecalculateAllAsync
  produce correct balances
- Three EF migrations applied: SeedSalesDiscountsAccount, AccountingGapsPhase2,
  AccountingDepositsGL
- Unit tests updated for new IAccountBalanceService constructor params (200/200)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 12:42:46 -04:00
spouliot 17da692dce Fix two production billing bugs: invoice missing oven cost, quote stuck in Draft after send
Bug 1 — Invoice total didn't match job total for direct jobs:
- Root cause: all three item-save paths in JobsController passed null for
  ovenCostId, so FinalPrice/ShopSuppliesAmount were stored without oven cost
  while the Details page recalculated live with OvenCostId and showed higher.
- Add OvenBatchCost stored field to Job entity (migration AddJobOvenBatchCost,
  default 0 for existing rows).
- Fix Create, Edit, and UpdateItems to pass job.OvenCostId and save OvenBatchCost.
- Fix InvoicesController.Create GET for direct jobs to use stored OvenBatchCost
  and ShopSuppliesAmount as separate labeled lines instead of recalculating
  shop supplies from scratch (which excluded the oven cost base).

Bug 2 — Quote status stayed Draft after "Send Quote via Email":
- ResendQuote advanced the approval token and sent the email but never
  updated the status. Added Draft → Sent advancement (same guard used by
  the SMS send path) so the status updates on successful email send.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 10:39:49 -04:00
spouliot feff0fa73d Add Accountant role and CanManageBills/CanManageAccounting permissions
- AppConstants: add Accountant to CompanyRoles; add CanManageBills and
  CanManageAccounting to Policies
- ApplicationUser: add CanManageBills and CanManageAccounting bool fields
- UserManagementDtos: expose new fields in all three DTOs
- ClaimsPrincipalFactory: emit ManageBills and ManageAccounting claims
- Program.cs: add CanManageBills and CanManageAccounting policies;
  update CanManageInvoices, CanViewReports, CanManagePurchaseOrders,
  and CanManageVendors to auto-pass for Accountant role
- BillsController: replace CanManageInventory with CanManageBills on
  all write actions (correct policy — bills are not inventory)
- BankReconciliationsController: replace CanManageJobs with
  CanManageAccounting on write actions
- CompanyUsersController: add Accountant to validCompanyRoles (both
  Create/Edit), legacyRole switch, and all permission assignment blocks
- Create/Edit views: add Accountant option to role dropdown; add
  CanManageBills and CanManageAccounting checkboxes; JS auto-checks
  financial permissions when Accountant role is selected
- Migration AddAccountantRolePermissions: adds columns + backfills
  CanManageBills=1 and CanManageAccounting=1 for all CompanyAdmin users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 19:42:53 -04:00
spouliot 4fd9c52aaf Phase G: Add Budgeting and Year-End Close
Budgeting:
- Budget + BudgetLine entities with Jan–Dec monthly columns per GL account
- BudgetsController: Index, Create, Edit, SetDefault, Copy, Delete
- Copy action rolls a budget forward to a new fiscal year
- Budget vs. Actual report (BudgetVsActual): compares monthly budget amounts to
  real P&L by calling GetProfitAndLossAsync once per month; variance shown as
  favorable/unfavorable; year + budget selectors in header
- Views: Budgets/Index, Create, Edit with inline annual totals via budget-edit.js
- Nav link + report card on Landing

Year-End Close:
- YearEndClose entity records each closed year + JE reference for audit trail
- AccountsController.YearEndClose GET (history + form) + CloseYear POST
- Close zeroes all Revenue and Expense/COGS account balances into Retained Earnings
  via IAccountBalanceService and posts a supporting JE dated Dec 31
- Idempotency: rejects attempt to close an already-closed year
- Pre-close checklist in view to guide the workflow
- Nav link under Finance

Migration AddBudgetsAndYearEndClose applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 13:01:56 -04:00
spouliot fde24b09c9 Phase F: Add Invoice Write-Off, Fixed Assets, Period Locking, and 1099 Tracking
- Invoice Write-Off: WriteOff POST action in InvoicesController posts bad-debt JE
  (DR bad debt expense / CR AR), reduces customer balance, marks invoice WrittenOff;
  write-off modal added to Invoice Details view with expense account selector
- Fixed Assets: FixedAsset + FixedAssetDepreciationEntry entities with straight-line
  depreciation; FixedAssetsController (Index/Create/Edit/Details/PostDepreciation/Delete);
  PostDepreciation auto-generates one JE per asset per period, skips already-posted,
  fully-depreciated, and disposed assets; full CRUD views + nav link
- Period Locking: Company.BookLockedThrough field; AccountingPeriodValidator static helper;
  lock check added to JE Post and Bill Create (blocks backdating into closed periods);
  SetPeriodLock action + date picker UI in Company Settings Accounting section
- 1099 Tracking: Is1099Vendor flag on Vendor entity + DTOs; checkbox in Create/Edit views;
  TaxReporting1099 report action + view lists payments by year, flags vendors >= $600;
  report card added to Reports Landing
- Migration AddFixedAssetsLockAnd1099 applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 12:19:32 -04:00
spouliot 42eff3357e Phase G: Add Recurring Transactions (BackgroundService + CRUD UI)
- RecurringTemplate entity with Frequency/IntervalCount/NextFireDate/EndDate/MaxOccurrences/TemplateData JSON
- RecurringFrequency + RecurringTemplateType enums
- RecurringTransactionService BackgroundService: hourly check, creates Draft bills or immediate expenses, advances NextFireDate, auto-deactivates on limits
- RecurringTemplatesController: Index/Create/Edit/ToggleActive/Delete/GenerateNow (on-demand fire)
- Three views + external JS for type-toggle and dynamic bill line items
- Finance sidebar nav: Recurring Transactions
- Migration: AddRecurringTemplates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 11:08:36 -04:00
spouliot d3a5d827f9 Phase F: Customer/Vendor Statements, Payment Terms Parser, Tax Rates
F1: GetCustomerStatementAsync/GetVendorStatementAsync on IFinancialReportService;
    StatementLineDto; CustomerStatementDto/VendorStatementDto; Statement action on
    CustomersController + VendorsController; Statement views + PDF download via
    StatementPdfHelper (QuestPDF); Statement button on Customer/Vendor Details pages.

F2: PaymentTermsParser static helper (CalculateDueDate, ParseEarlyPaymentDiscount);
    EarlyPaymentDiscountPercent/Days on Invoice entity; GetCustomerPaymentTerms AJAX
    endpoint on InvoicesController auto-populates Terms + due date on customer select;
    early payment discount notice on Invoice Create.

F3: TaxRate entity (Name/Rate/State/IsDefault/IsActive, tenant-filtered);
    IUnitOfWork.TaxRates + UnitOfWork + ApplicationDbContext; TaxRatesController
    (Index/Create/Edit/Delete/ToggleActive, CompanyAdminOnly); GetTaxRateForCustomer
    AJAX endpoint; Tax Rates in Settings gear menu.

Also fixes AddVendorCredits migration: VendorCreditApplications FKs changed from
CASCADE to NoAction to resolve SQL Server error 1785 (multiple cascade paths).
Migration: AddPaymentTermsAndTaxRates applied locally; 200/200 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 10:55:22 -04:00
spouliot 1229081436 Phase E: Add Bank Reconciliation
- IsCleared + ClearedDate added to Payment, BillPayment, Expense entities
- BankReconciliation entity (account, statement date, beginning/ending balance, status)
- BankReconciliationStatus enum (InProgress, Completed)
- Migration AddBankReconciliation: new BankReconciliations table + IsCleared/ClearedDate columns
- IUnitOfWork/UnitOfWork wired with BankReconciliations repo
- BankReconciliationsController: Index, Create, Reconcile, ToggleCleared (AJAX), Complete, Report
- Reconcile view: deposit/payment checkboxes with live running balance and difference via JS
- Complete is gated: only enabled when difference == $0.00
- Nav: Bank Reconciliation added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:10:38 -04:00
spouliot cf9dcfb4c1 Phase D: Add Vendor Credits (AP cycle completion)
- VendorCredit, VendorCreditLineItem, VendorCreditApplication entities
- VendorCreditStatus enum (Open, PartiallyApplied, Applied, Voided)
- Migration AddVendorCredits: three new tables
- IUnitOfWork/UnitOfWork wired with all three repositories
- VendorCreditsController: Index (status tabs), Create, Details, Post, Apply, Void
- Post action: DR AP, CR each expense line (reverses original expense)
- Apply action: links credit to bill, updates Bill.AmountPaid and bill status
- Views: Index (summary cards + table), Create (dynamic line grid), Details (apply panel)
- Nav: Vendor Credits added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:03:14 -04:00
spouliot a33687f7bd Phase C: Add Manual Journal Entries (double-entry GL)
- JournalEntry + JournalEntryLine entities with Draft/Posted/Reversed lifecycle
- JournalEntryStatus enum (Draft, Posted, Reversed)
- Migration AddJournalEntries: two new tables with self-referencing reversal FK
- IUnitOfWork/UnitOfWork wired with JournalEntries + JournalEntryLines repos
- ApplicationDbContext: DbSets, tenant query filters, reversal FK config
- LedgerService: JE lines added as 10th source in GetAccountLedgerAsync and ComputePriorBalanceAsync
- JournalEntriesController: Index (All/Draft/Posted tabs), Create, Details, Post, Reverse, Delete
- Views: Index, Create (dynamic balanced line grid with running debit/credit totals), Details
- journal-entry-create.js: dynamic line management with balance indicator
- Nav: Journal Entries added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:56:03 -04:00
spouliot 7e1676cfd7 Add Phase A accounting features: AP Aging, Trial Balance, Cash vs Accrual
- AP Aging report (GetApAgingAsync, controller actions, view, PDF export)
  mirrors AR Aging — groups open bills by vendor, buckets by days past due date
- Trial Balance report (GetTrialBalanceAsync, view, PDF export)
  uses Account.CurrentBalance, groups by AccountType, validates debits == credits
- Cash vs Accrual accounting method setting on Company entity
  switchable at any time — report-time only, no GL re-posting on change
  P&L cash: revenue = payments received; expenses = bills/expenses paid in period
  Balance Sheet cash: omits AR and AP lines (no receivables/payables concept)
  AccountingMethod badge shown on P&L and Balance Sheet views
- Migration A (AddAccountingMethod) applied, default = Accrual for all existing companies
- AP Aging and Trial Balance added to Reports Landing page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:34:54 -04:00
spouliot edd7389d7d Refactor: extract shared helpers, fix field drift, add assembly services
- IJobItemAssemblyService / IQuotePricingAssemblyService: centralize job item
  and quote pricing construction that was duplicated across create, rework copy,
  and quote-to-job conversion paths
- BlobFileHelper: single ValidateUpload/GetContentType/SanitizeFileName used by
  6 blob services (JobPhoto, QuotePhoto, ProfilePhoto, CompanyLogo, Equipment,
  Catalog) and BillsController + ExpensesController, removing 8 private copies
- PagedResult<T>.From(): static factory eliminates 6-line boilerplate in 11
  controllers (Appointments, Customers, Equipment, Inventory, Invoices, Jobs,
  Maintenance, CompanyUsers, PlatformUsers, Quotes, Vendors)
- AccountingDropdownHelper: single LoadAsync() call replaces duplicate
  vendor/account/job queries in BillsController and ExpensesController
- JobTemplateItem: add IsSalesItem + Sku fields with migration; propagate
  through JobTemplatesController snapshot copy and GetTemplatesJson projection,
  and JobsController template-application path
- Test assertions updated for standardized BlobFileHelper error messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:12:33 -04:00
spouliot 3278152d83 Fix invoice re-creation after void; add payment terms selector and shop supplies line
- Voided invoices no longer block creating a new invoice for the same job: voided invoice's
  JobId FK is cleared so the unique index slot is freed for the replacement
- Invoice Details view shows voided invoices as history rather than hiding them
- Payment terms: standardized SelectList (Due on Receipt, Net 15/30/45/60/90, 2% 10 Net 30,
  COD) with custom-term preservation; invoice-due-date.js auto-updates Due Date on term change
- Shop supplies on direct (no-quote) jobs: InvoicesController derives the shop supplies line
  from the company rate when the job has no source quote to read the pre-agreed amount from
- Job entity: ShopSuppliesAmount + ShopSuppliesPercent fields preserved through job lifecycle
- Migration: AddShopSuppliesAmountToJob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:47:34 -04:00
spouliot fc35fd123c Add IsIncoming inventory flag and catalog-to-incoming powder flow in item wizard
- InventoryItem.IsIncoming: marks powder ordered but not yet received; enables QR code
  printing on work orders while the shipment is in transit
- InventoryController.CreateIncomingFromCatalog: POST endpoint creates a 0-balance inventory
  record from a PowderCatalogItem and returns it in wizard-compatible shape
- item-wizard.js: custom coat tab now searches the platform powder catalog as a fallback;
  catalog results show an 'Add as Incoming Order' option; createIncomingFromCatalog POSTs
  to server and selects the new item without a page refresh
- QuoteItemCoatDto: CatalogItemId + AddAsIncoming fields so the wizard can signal server-side
  incoming-item creation during quote save
- Inventory Create/Edit/Index views: IsIncoming badge and field
- IInventoryAiLookupService: minor interface update
- Migration: AddInventoryIsIncoming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:47:19 -04:00
spouliot f40d58ac2e Add TCPA-compliant SMS consent tracking for prospect quotes
- Quote entity: ProspectSmsConsent (bool) + ProspectSmsConsentedAt (DateTime?) fields
- QuoteDtos: consent fields on Create/Update/Convert DTOs with TCPA guidance text
- Quote Create/Edit views: SMS consent checkbox shown when mobile number is entered
- Quote ConvertToCustomer view: staff must re-confirm consent carries over to customer record
- QuoteApproval: consent state exposed in ViewModel and ApprovalPage for transparency
- Consent timestamp cleared when prospect quote is linked to an existing customer
- Migration: AddProspectSmsConsent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:47:04 -04:00
spouliot fb979bc88d Add BillingEmail field for commercial customers; support comma-separated multi-email
- Customer entity + DTO: new BillingEmail field (accounting/invoicing address)
- Email fields now accept comma-separated lists; DTO validates each address individually
- NotificationService: SendToEmailListAsync helper fans out to all addresses in a list;
  NotifyQuoteSentAsync accepts optional overrideEmail so staff can send to an ad-hoc address
- Migration: AddCustomerBillingEmail
- Customer Create/Edit/Details views updated to show Billing Email field
- customer-billing-email.js: client-side helpers for billing email input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:46:53 -04:00
spouliot d8622b3187 Fix catalog item repricing on oven-only quote edits
QuoteItem was missing IncludePrepCost, so the Edit GET always deserialized
it as true (DTO default). On save, prep service labor was added on top of
the catalog base price, silently bumping prices whenever any quote field
(e.g. oven cycle minutes) was changed without touching items.

Migration defaults new column to false for catalog items and true for
non-catalog items (matching the wizard's historical defaults).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:04:45 -04:00
spouliot f383339465 Store powder specific gravity and fix coverage math 2026-05-06 08:46:41 -04:00
spouliot 20ae11be03 Commit remaining unstaged changes from this session
- Platform settings service: IPlatformSettingsService, PlatformSettingKeys,
  PlatformSettingsService, SubscriptionService, AppConstants,
  SubscriptionExpiryBackgroundService, SubscriptionMiddleware
- JobTimeEntry entity, DTOs, AutoMapper profile (ShopWorker → UserId migration)
- InventoryDtos: SourceTransactionId on PowderUsageLogDto
- InventoryTransactionRepository: include Job.Customer in ledger query
- InventoryAiLookupService: @graph unwrap + HTML price fallback
- ApplicationDbContextModelSnapshot: reflect migration changes
- launchSettings.json, publish profile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:20:30 -04:00
spouliot 1fc79b77fe Add platform powder catalog, catalog-first lookup, and label scanner
- Platform PowderCatalogItem table (IPlainRepository, no tenant filter) with
  full spec fields: cure temp/time, finish, color families, clear coat flag,
  coverage sq ft/lb, transfer efficiency, IsUserContributed
- Two EF migrations: AddPowderCatalogItem + AddPowderCatalogSpecFields
- PowderCatalogController (SuperAdminOnly): import from Prismatic JSON scrape,
  Lookup AJAX endpoint (catalog-first, ranked by SKU exact match), stats view
  with Tenant Contributed card
- Unified smart Lookup button on inventory Create/Edit: catalog hit fills all
  fields via catalogSnapshot pattern; AI augments cure/finish data from product
  URL if subscription enabled; catalog miss falls through to AI lookup
- In-browser label scanner (_LabelScanModal): getUserMedia live camera feed,
  jsQR auto-detects QR codes in rAF loop; "Scan Label Text" fallback sends
  captured frame to Claude vision via /Inventory/ScanLabel
- ScanLabel endpoint handles both QR URL path (LookupByUrlAsync) and vision
  path (ScanLabelAsync); auto-inserts unrecognized products as
  IsUserContributed=true; returns wasInCatalog/addedToCatalog flags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:36:25 -04:00
spouliot 6569d9c4ea Add SMS gating, TCPA terms agreement, and compose-before-send modal
- 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>
2026-05-01 22:29:39 -04:00
spouliot 90a06c6acd Add product image to powder inventory via AI lookup
When AI Lookup fetches a manufacturer product page, it now extracts the
og:image (Open Graph) meta tag before stripping HTML tags. The image URL
is returned in InventoryAiLookupResult.ImageUrl and automatically shown
as a preview on the Create/Edit form alongside the other filled fields.

The preview includes a Remove button to clear the image, and the Wrong
Match? button clears it along with the other AI-filled fields.

On the inventory Details page a product image card is rendered above the
Stock & Pricing card whenever ImageUrl is set. The field is nullable so
existing records and powders without an image are unaffected.

New field: InventoryItem.ImageUrl (nvarchar, nullable).
Migration: AddInventoryItemImageUrl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 18:15:55 -04:00
spouliot 9221fcc783 Add quote-changed banner with re-sync to job details
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>
2026-04-29 18:02:46 -04:00
spouliot 8aae30765f Onboarding overhaul: slim wizard, progress widget, guided activation UX
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>
2026-04-28 21:10:47 -04:00
spouliot a4b8ae611a Add passkey prompt dismissal and consolidate company admin navigation
- Add "Don't ask me again" to passkey enrollment prompt (PasskeyPromptDismissed
  field on ApplicationUser; DismissPrompt POST action; migration applied)
- Add Subscription & Features button to Companies/Index btn-group and
  Companies/Edit header for direct navigation to SubscriptionManagement/Manage
- Add Edit Company back-link on SubscriptionManagement/Manage
- Remove duplicate AI Features section from Companies/Edit (managed exclusively
  via Subscription & Features page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 10:34:50 -04:00
spouliot cb7bbc37bd Add three-layer feature gating for AI Catalog Price Check
Adds platform-level, plan-level (Enterprise only), and per-company
toggles for the AI Catalog Price Check feature. Includes:
- Company.AiCatalogPriceCheckEnabled per-company flag
- SubscriptionPlanConfig.AllowAiCatalogPriceCheck plan-level flag
- PlatformSetting 'AiCatalogPriceCheckEnabled' global kill switch
- IPlatformSettingsService.GetBoolAsync helper
- ISubscriptionService.CanUseAiCatalogPriceCheckAsync
- UI controls in Companies/Edit, PlatformSubscription/Edit+Index,
  and SubscriptionManagement/Manage
- Migration AddAiCatalogPriceCheckGating applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 08:29:51 -04:00
spouliot 54f444d981 Add AI Catalog Price Check feature
Claude reviews every active catalog item against the shop's own operating costs
and returns a per-item verdict (below-cost / thin-margin / high / ok) with a
suggested price range, cost floor, and assumptions.

- New entity: CatalogPriceCheckReport (JSON blob, archived per company)
- New service: IAiCatalogPriceCheckService / AiCatalogPriceCheckService
  batches items 25 at a time to stay within model context limits
- Two new controller actions: GET AiPriceCheck (view report) + POST RunAiPriceCheck
- AiPriceCheck view: summary cards (counts by verdict), color-coded item cards
  with Edit Price link, assumptions detail, and loading spinner on submit
- AI Price Check button added to catalog Index header
- Migration AddCatalogPriceCheckReport applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:41:56 -04:00
spouliot 0bb96a502a Add passkey / biometric login (WebAuthn FIDO2)
Shop floor workers can log in once with a password, enroll a passkey,
and use Face ID / Windows Hello / fingerprint for all future logins.

- UserPasskey entity + AddUserPasskeys migration (Fido2 v4.0.1)
- PasskeyController: RegisterOptions, Register, LoginOptions, Login,
  Manage, Remove endpoints
- Login page: platform-aware button (Face ID / Windows Hello / etc.)
  hidden automatically if browser doesn't support WebAuthn
- Post-login floating prompt to enroll on first use; session-dismissed
- Passkeys & Biometrics link in user dropdown menu
- Manage page: list registered devices, add new, remove individual
- passkey.js: targeted base64url conversion (only challenge + user.id
  + credential IDs) — fixes "Required parameters missing" error caused
  by blindly converting rp.id and other string fields to ArrayBuffers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:07:01 -04:00
spouliot 00bf8a4cd0 Add catalog item images with thumbnail preview in wizard
Each catalog item now supports one optional image (jpg/jpeg/png/gif/webp,
max 10 MB). Uploading generates a 200x200 JPEG thumbnail automatically via
SixLabors.ImageSharp. Images are stored in Azure Blob Storage under a new
catalogimages container, keyed by {companyId}/catalog/{itemId}/.

- CatalogItem entity: ImagePath + ThumbnailPath (nullable string fields)
- Migration: AddCatalogItemImages applied
- ICatalogImageService / CatalogImageService: upload, thumbnail generation,
  delete; old blobs replaced atomically on re-upload
- CatalogItemsController: Create/Edit accept optional IFormFile image;
  Image(id, thumbnail) action serves blobs with [Authorize] so wizard users
  can load thumbnails without CanManageProducts policy
- Catalog index (_CategoryNode): 40x40 thumbnail (or placeholder icon)
  left of each item name
- Details view: image card in right column with click-to-full-size link
- Create/Edit views: file picker with live preview; Edit shows current
  thumbnail with Remove checkbox
- Wizard (item-wizard.js): thumbnails in product list with hover preview
  that follows the cursor (showCatalogPreview / moveCatalogPreview);
  fixed Bootstrap d-flex !important bug that broke the filter box by
  moving flex layout to an inner wrapper div

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 09:33:59 -04:00
spouliot 4153acf3aa Add facility overhead (rent + utilities) to operating costs and pricing engine
Adds MonthlyRent, MonthlyUtilities, and MonthlyBillableHours to CompanyOperatingCosts so fixed shop occupancy costs are recovered on every quote. The pricing engine converts these into a per-hour rate and applies it as a transparent "Facility Overhead" line between oven batch cost and shop supplies. UI added in Company Settings Operating Costs tab and Setup Wizard Step 3; migration AddFacilityOverheadFields applied. Help docs and AI knowledge base updated to cover the new fields and the revised quote pricing calculation order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 19:35:00 -04:00
spouliot 63e12a9636 Initial commit 2026-04-23 21:38:24 -04:00