Commit Graph

180 Commits

Author SHA1 Message Date
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 0afb474c3e Add Phase B: Inventory COGS auto-posting to GL on JobUsage transactions
When powder is consumed via a job (JobsController) or scan (InventoryController.LogUsage),
debit the item's CogsAccountId and credit its InventoryAccountId for the cost of the
quantity consumed (using AverageCost if available, else UnitCost). No-op when either
GL account is not configured on the InventoryItem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:39:23 -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 379b0de885 Refactor: centralize accounting helpers, status constants, and query deduplication
- AccountingDropdownHelper: wired into BillsController and ExpensesController,
  replacing 35-40 lines of duplicated DB queries per controller
- AppConstants.StatusCodes: added Job.* and Quote.* constants to replace all
  magic status strings across Jobs, Quotes, Appointments, OvenScheduler,
  AiQuickQuote, QuoteApproval, and AccountingDropdownHelper
- AccountingRules: extracted IsNormalDebitBalance into shared Infrastructure
  helper; removed duplicate private method from AccountBalanceService and
  LedgerService (~50 lines deleted)
- AccountDataExportController: extracted 9 Fetch*Async methods (superset of
  includes) so Add*Sheet and Build*Csv no longer duplicate DB queries; each
  entity is queried once regardless of whether XLSX or CSV format is requested
- BillsController.Create and ExpensesController.Create wrapped in
  ExecuteInTransactionAsync; blob uploads moved after commit to keep
  financial data atomic and prevent orphaned blobs from rolling back
- Number generators (Appointments, CreditMemo, OvenBatch) fixed from full-table
  GetAllAsync to prefix-filtered FindAsync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:42:39 -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 61866e1d1e Add carried-over jobs section to Daily Board and fix tip visibility
Non-terminal jobs scheduled for past dates now appear in a red 'Carried
Over' section at the top of today's board so they can't silently disappear.
Also added alert-permanent to the board tip so the layout doesn't auto-dismiss it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 15:34:39 -04:00
spouliot bc9de38da3 Fix Cash account subtype missing from debit-normal balance check
AccountSubType.Cash was not included in IsNormalDebitBalance in both
AccountBalanceService and LedgerService, causing Cash accounts to be
treated as credit-normal. Payments deposited to a Cash account were
debited in the wrong direction, producing a negative balance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:13:24 -04:00
spouliot 2694863d07 Fix health check URL to use production custom domain
Jenkins agent cannot resolve linuxpcl.azurewebsites.net due to DNS
restrictions on the build machine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:08:30 -04:00
spouliot 8646fa83c8 Fix PowerShell syntax error in zip creation step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:56:12 -04:00
spouliot 796d084ea6 Fix zip entry paths to use forward slashes for Linux compatibility
ZipFile.CreateFromDirectory on Windows produces backslash paths in zip
entries. Linux treats backslash as a literal filename character so
wwwroot\css\site.css is never found at wwwroot/css/site.css. Build the
zip manually with Replace backslash→forward slash on each entry name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:46:24 -04:00
spouliot 6d23c63912 Stop app before deploy to prevent rsync file lock failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:11:04 -04:00
spouliot 3803d16731 Fix prod Jenkins pipeline: add tests, restart, and health check stage
- Add Test stage (unit tests only) before migrations so bad code fails fast
- Add az webapp restart after async deploy to ensure new code is loaded
- Add Health Check stage that polls app for up to 3 minutes post-deploy
- Restore xcopy of wwwroot (needed for linux-x64 publish on Windows)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:44:51 -04:00
spouliot 29fd7163dc Merge dev into master: billing email, SMS consent, incoming powder, invoice/job fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:13:54 -04:00
spouliot 9a52e7fae5 Ad-hoc quote email, accounting improvements, AI lookup fix, and misc service updates
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file;
  QuotesController passes overrideEmail through to NotificationService
- Quotes/Details view: SMS consent display, email/SMS send button state based on consent
- Accounting module: AccountingDisplayHelpers for consistent ledger formatting;
  AccountsController + Accounts views improvements; AccountingEnums additions
- Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController
- InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path
  (LookupByUrlAsync already has it built in — was double-fetching)
- PdfService: quote/invoice PDF updates
- PricingCalculationService: minor pricing logic fix
- QuoteProfile: mapping updates for new quote fields
- ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:48:00 -04:00
spouliot 0d980e651a Add pricing breakdown and powder pre-fill to Job Details; surface voided invoice history
- Job Details: collapsible internal pricing breakdown card mirrors quote details breakdown
  (items subtotal, shop supplies, discount, rush fee, tax, total)
- Job Details: voided invoice history section shows previous invoices instead of hiding them
- Complete Job modal: pre-fills powder usage from QR-scanned / manually logged entries so
  staff don't double-log; consumes pre-logged credit per InventoryItemId before deducting net delta
- JobProfile: map ShopSuppliesAmount, ShopSuppliesPercent, IsRushJob, DiscountType/Value/Reason
  so the pricing breakdown has the data it needs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:47:44 -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 12f784f34c Add Unit Price column to quote PDF
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:41:44 -04:00
spouliot 90f93b6e2f Fall back to ProspectEmail for CustomerEmail on prospect quotes
Prospect quotes have no CustomerId so Customer is null — email is stored
in ProspectEmail directly on the quote. The send-button visibility check
was always seeing null and showing the 'no contact info' warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:31:12 -04:00
spouliot 135fd6f8d7 Clarify no-contact warning to say 'mobile number' not 'phone'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:06:24 -04:00
spouliot ff231d9dd2 Set quote status to Converted and show job number link on quote details
- CreateJobFromQuote now sets QuoteStatusId to CONVERTED after creating the job
- Added ConvertedToJobNumber to QuoteDto, populated in Details action
- 'View Job' button on Quote Details now shows the job number (e.g. 'View Job JOB-2505-0001')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:47:45 -04:00
spouliot e3c76ce7ce Fix missing customer contact fields on QuoteDto mapping
CustomerEmail, CustomerMobilePhone, CustomerNotifyBySms, and
CustomerNotifyByEmail were added to QuoteDto but never mapped in
QuoteProfile, causing the email/SMS visibility logic on Quote Details
to always see null and show the 'no contact info' warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:14:38 -04:00
spouliot 2cfe093780 Share Mark Complete modal as partial view; hide install button after PWA install
- 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>
2026-05-06 19:55:37 -04:00
spouliot bbedaedeaa Redirect board COMPLETED drop to Details page for full completion flow
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>
2026-05-06 17:54:13 -04:00
spouliot acbd9f60be Hide email controls when no email on file; show SMS hint for quote/job events
- 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>
2026-05-06 17:32:08 -04:00
spouliot d3863c713b Add QuoteApprovedByCustomer notification type; fix wrong type logged on approval
QuoteDeclinedByCustomer was used for both approve and decline responses,
so approval notifications showed the wrong type in the log. Added a distinct
QuoteApprovedByCustomer = 16 enum value, wired up the correct type in
NotificationService, added default templates in both the service fallback
dictionary and SeedData, and updated placeholder hints in CompanySettings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:13:34 -04:00
spouliot 4085ff7c73 Advance quote to Sent status when approval link sent via SMS
The SMS path was sending the message but never updating QuoteStatusId or
SentDate, leaving the quote in Draft. Now mirrors the email send path:
transitions Draft → Sent and stamps SentDate on first send only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:04:25 -04:00
spouliot 96ae3639ae Fix duplicate name on quote PDF for non-commercial customers
Non-commercial individuals have their full name stored in both CompanyName
and ContactFirstName/ContactLastName, causing the PDF to render the name
twice. Skip the company name line when it matches the assembled contact name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:01:37 -04:00
spouliot 8c30d5bf5a Restore IncludePrepCost pricing for catalog items with abnormal prep
The flag should default off (catalog DefaultPrice used as-is) but remain
functional so users can opt in when a job needs exceptional prep time
(e.g. 120-min outgassing vs. the typical 30 min baked into the catalog price).

Previous commit removed this entirely — restoring with correct default behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:47:48 -04:00
spouliot 9292f3169c Fix catalog item pricing and rogue margin-points text
Catalog DefaultPrice is always the base price — removed the IncludePrepCost
gate that was adding prep service labor on top of catalog items. PrepServices
on catalog items exist for scheduling purposes only, not pricing.

Also fixed Razor syntax bug in Details.cshtml where @(expr).ToString("F1")
rendered the raw decimal followed by the literal string ".ToString("F1")"
instead of the formatted value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:43:42 -04:00
spouliot 810d5a5dc1 Update idempotent EF migration script for prod deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:23:22 -04:00
spouliot 8a09a91234 Merge dev into master 2026-05-06 16:20:49 -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 2d8827ad5c Fix 'Oven (1 batch × 0 min)' display when OvenCycleMinutes is null
Quote.OvenCycleMinutes is nullable — null means 'use company default'.
The Details and DownloadPdf actions were converting null → 0 before passing
it to the view, so the pricing summary showed '× 0 min' even though the
oven cost was correctly calculated from DefaultOvenCycleMinutes.

Fix: resolve null against operatingCosts.DefaultOvenCycleMinutes in both
controller actions (DownloadPdf now loads operating costs for this). Added
a defensive > 0 guard in the view so the minutes clause is omitted entirely
if it still comes through as 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 15:35:38 -04:00
spouliot 4d10175ce3 Add oven batch cost to AI Quick Quote (1 batch, DefaultOvenCycleMinutes or 50 min)
Previously the quick quote omitted the oven charge entirely, so saved quotes
were under-priced relative to full quotes from the same items.

Pricing: CalculatePricing now calculates ovenBatchCost = (cycleMin/60) × OvenOperatingCostPerHour
using DefaultOvenCycleMinutes (fallback 50 min), then adds it to the total as a quote-level
charge matching how PricingCalculationService handles oven costs.

Save path: SaveQuickQuoteRequest gains OvenBatchCost + OvenCycleMinutes; the Quote record
now stores OvenBatchCost, OvenCycleMinutes, and Total = ItemsSubtotal + OvenBatchCost.

Display: results card shows a sub-line under the estimate price:
"incl. oven 1 batch 50 min: $12.00"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 15:20:10 -04:00
spouliot 9c4c20e8bd Fix false-positive 'source quote was modified' banner after job conversion
The banner fires when quote.UpdatedAt > job.QuoteSnapshotUpdatedAt. The
snapshot was captured before saving quote.ConvertedToJobId, so the EF
interceptor's automatic UpdatedAt stamp on that save always made the quote
appear newer than the snapshot — triggering the banner on every freshly
converted job even with no actual changes.

Fix: after saving ConvertedToJobId, re-stamp QuoteSnapshotUpdatedAt to the
quote's final UpdatedAt value and save the job once more. The snapshot now
includes the conversion write, so the comparison is equal (not "after") and
the banner stays hidden until the quote genuinely changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 14:46:39 -04:00
spouliot ecb285657a Fix sandblast-only toggle overflow and $0 AI quote pricing
Overflow: replaced Bootstrap form-check with an explicit flex row so the
two-line label (title + subtitle) never bleeds outside the card boundary.

$0 pricing: when sandblast-only was toggled on an AI item, manualUnitPrice
was cleared and isAiItem set to false. The pricing engine then returned $0
because no prep services with minutes were configured. Fix: preserve the AI
price when toggling sandblast-only, and keep isAiItem=true so the server
routes through the AI-price path (manualUnitPrice) rather than trying to
recalculate from prep labor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 14:29:03 -04:00
spouliot 0054b7d108 Fix NullReferenceException on Quote Details when quote total is zero
PricingBreakdown was only populated when quote.Total > 0, but the Details
view unconditionally dereferences PricingBreakdown.ItemsSubtotal. Sandblast-
only quotes can legitimately have a $0 total (no powder/oven costs), leaving
PricingBreakdown null and crashing the Details render.

Removed the Total > 0 guard from both Details action overloads — always
populate PricingBreakdown from the stored snapshot fields (all values are 0
for an unpriced or sandblast-only quote, which is safe for display).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 14:16:24 -04:00
spouliot 63a85b6ce9 Fix sandblast-only oven charge and wizard overflow; fix Jenkins test failures
Sandblast-only oven charge root cause:
renderCoatsList() is called on every step-3 render. When the sandblast-only
toggle was checked it cleared wz.data.coats to [], but renderCoatsList()
then saw an empty list and auto-called addCoatRow(), silently pushing a
Base Coat back into wz.data.coats. The server saw Coats.Any() = true and
included the item in the oven fraction calculation, producing an unexpected
oven batch charge. Fixed by bailing out of renderCoatsList() when
sandblastOnly is active, and added a matching safety net in
buildItemFromWizard() that forces coats:[] when sandblastOnly is true.

Also fixed sandblast-only toggle label overflow: subtitle span changed to
d-block so it wraps beneath the bold label instead of running inline.

Test fixes:
- DepositsController and GiftCertificatesController tests updated with the
  required ICompanyLogoService mock parameter added to the logo fix commit.
- Two PricingCalculationServiceTests updated to include a coat entry on
  each item, matching the service's updated requirement that only items
  with coating layers are considered for oven fraction calculation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:56:20 -04:00
spouliot 71caa93461 Fix unit test build failures after logo service and pricing changes
DepositsController and GiftCertificatesController gained a required
ICompanyLogoService constructor parameter in the PDF logo fix; their
test factories were not updated and failed to compile on Jenkins.
Added Mock.Of<ICompanyLogoService>() to both factory methods and the
missing using directive to DepositsControllerTests.

PricingCalculationService now only charges oven cost for items that
have explicit coating layers (Coats collection non-empty), because
sandblast/prep-only and labor items do not go in the oven. Two tests
that tested the old "all items count toward oven fraction" logic were
updated to include a single coat entry on each item, which restores
the expected oven fraction math without changing the tested behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:38:18 -04:00
spouliot 2e73cfab54 Miscellaneous UI and pricing updates from prior sessions
- PricingCalculationService: powder coverage and specific gravity math fixes
- Dashboard/Index: minor widget updates
- Jobs/Details, Jobs/Intake: shop floor and intake view improvements
- Quotes/Details: detail view updates
- GiftCertificates/Details: detail view update
- job-photos.js: photo gallery improvements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:27:37 -04:00
spouliot 74414c6c71 Add AI overload retry with model fallback and consolidate wizard errors
Anthropic returns overloaded_error (HTTP 529) during high-demand periods.
Previously this failed immediately with a generic error. Now the service
retries Sonnet once after 5s, then falls back to Haiku (a separate
capacity pool) after another 3s before giving up. If all three attempts
are overloaded the user sees a clear "high demand" message rather than a
generic error. Non-overload errors still log at Error level.

Also consolidated AI wizard error display in item-wizard.js: photo upload
failures were using browser alert() while analyze failures used the inline
red alert bar. All errors now go through aiShowError() so they always
appear consistently as the red bar below the Analyze button. Removed the
alert() fallback from aiShowError() itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:27:27 -04:00
spouliot a8fb56e8ec Fix company logo missing from PDFs and add AI photo save logging
When a tenant uploads a logo it is stored in Azure Blob Storage and
LogoData (the legacy DB byte[]) is cleared. All PDF controllers were
still reading the now-null LogoData, so logos never appeared on any
PDF after upload. Fixed by injecting ICompanyLogoService into all six
affected controllers (Quotes, Invoices, Deposits, GiftCertificates,
PurchaseOrders, CatalogItems) and loading the blob-stored logo first
before falling back to the legacy DB field.

Also added structured logging to the AI photo promotion path in
QuotesController Create/Edit POST so upload failures are visible in
production logs instead of silently swallowed.

Added onclick safety net to the Create and Edit quote submit buttons
so dynamically-injected hidden fields (AiPhotoTempIds) are written
before iOS Safari collects the form data on submit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:27:18 -04:00
spouliot ca4fb959aa Add Sales Tax Liability report with PDF and CSV export
Invoice-basis report showing taxable vs non-taxable sales, tax billed
by GL account, monthly trend table/chart, and full invoice detail grid.
Non-taxable invoice rows shaded grey for easy scanning. Quick-preset
date buttons (This Month, Last Month, YTD, Last Year) for common filing
periods. CSV export formatted for accountants and tax-filing software.
Gated behind AllowAccounting() like other financial reports.

- SalesTaxReportDto + 3 supporting DTOs in FinancialReportDtos.cs
- GetSalesTaxReportAsync on IFinancialReportService + implementation
- GenerateSalesTaxReportPdfAsync on IPdfService + QuestPDF implementation
- SalesTax / SalesTaxPdf / SalesTaxCsv actions in ReportsController
- Views/Reports/SalesTax.cshtml with Chart.js monthly trend chart
- Landing page card added to Finance section
- HelpKnowledgeBase and Help/Reports.cshtml updated with full docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:27:08 -04:00
spouliot 7e0699d5bd Add smart install prompt for supported browsers 2026-05-06 09:05:00 -04:00
spouliot f383339465 Store powder specific gravity and fix coverage math 2026-05-06 08:46:41 -04:00