Commit Graph

21 Commits

Author SHA1 Message Date
spouliot 0498decfb0 Fix quote/job create page jumping to bottom on fresh load
The wizard scroll-restore saved scroll position on form submit but never
cleared it if the server redirected to a success page. Next fresh visit
to the same URL found the stale sessionStorage key and jumped down.

Fix: track whether the page unload was caused by our own form submit.
On pagehide for any other reason (nav link, success redirect), remove
the key so it never fires on a clean page load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:24:47 -04:00
spouliot 1e5510477a Allow fractional quantities (< 1) in item wizard
Catalog, calculated, generic, formula, and AI item types now accept
decimal quantities (e.g. 0.25 for a quarter of a catalog set). Sales/
merchandise items remain whole-number only.

- Input min changed from 1 to 0.01; step="0.01" added where missing
- All parseInt reads on quantity inputs changed to parseFloat so values
  like 0.25 aren't truncated to 0 before being stored in wz.data
- Server-side Quantity is already decimal on all relevant DTOs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:41:06 -04:00
spouliot ad986561c9 Fix AI quote blast rate: single formula path, correct client preview
Root cause: company-settings-lookups.js had its own baseByCfm/multiplier
tables that were completely different from ShopCapabilityCalculator.cs,
so the UI showed an inflated rate (e.g. 82 sqft/hr) while the AI prompt
received the server-computed rate (e.g. 9 sqft/hr).

- Add CompanySettingsController.DeriveBlastRate endpoint — thin GET that
  calls ShopCapabilityCalculator directly; now the single formula path
- Delete all client-side formula code (baseByCfm, multiplier tables,
  deriveBlastRate) — ~30 lines removed
- Modal live preview calls /CompanySettings/DeriveBlastRate with 250ms
  debounce instead of computing locally
- Blast setup table uses setup.derivedRate from GetBlastSetups (already
  server-computed) instead of recalculating client-side
- QuotesController.AiAnalyzeItem: when no blast setup is explicitly
  selected, fall back to the company's default blast setup so the
  configured rate is always used

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:57:46 -04:00
spouliot 0d5553f3b2 Fix dark mode hover colors in coat/powder dropdown menus
Replace all hardcoded light-mode colors (#f0f4ff, #e8eeff, #fff8e1, #fff)
with Bootstrap CSS variables (--bs-secondary-bg, --bs-primary-bg-subtle,
--bs-warning-bg-subtle, --bs-body-bg) so dropdown containers and hover
states render correctly in both light and dark mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:12:24 -04:00
spouliot 7e31846777 Fix flat-rate coat wizard UX, &mdash; literal, select caret overlap, and walkthrough modal sizing
- Flat-rate items now default coat type to Custom so Color Name field is immediately visible
- Catalog search blur copies typed text to Color Name when no catalog result was selected
- Item card shows 'No color specified' badge when coat has powder-to-order but no color name
- Color Name label marked required with '(shows on quote)' hint
- Coat name select min-width prevents text overlapping Bootstrap caret arrow
- Remove extra unbalanced </div> from renderSalesFields
- Fix literal &mdash; in quote simple-mode hint (textContent → innerHTML)
- Formula walkthrough modal fixed at 700px so all steps render at identical window size

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 23:01:33 -04:00
spouliot a7ad0e1de8 Add Custom Powder Order line item and fix CSV import FinalPrice crash
Custom powder/incoming powder material cost now flows into a separate
auto-generated 'Custom Powder Order' line item instead of rolling into
individual item prices, so users can add shipping charges before the
customer sees the total. A dashed yellow preview card in the wizard
shows the material cost and lets users edit the total (including shipping)
before saving. After first save the price is user-owned.

Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric
value (e.g. 'false' from a spreadsheet formula): the job CSV importer now
streams rows one at a time with a lenient decimal converter, treating bad
values as $0 with a per-row warning instead of aborting the entire import.

Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with
Custom Powder Order behavior and a new Data Import / Export section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:37:46 -04:00
spouliot 4650ba3d4d Fix custom formula wizard bugs and add field name validation
- Fix Add Field blanking inputs: cfFields was IIFE-scoped so inline oninput
  handlers couldn't reach it; expose cfUpdateField on window
- Fix ManualUnitPrice dropped in buildItemFromData: condition excluded
  isCustomFormulaItem, causing FixedRate items to reprice from scratch
- Fix formula card missing on job pages: load CustomFormulaTemplates in
  PopulateJobItemDropDownsAsync so Details, EditItems, and Edit all get it;
  add customFormulaTemplates + formulaEvalUrl to Details and EditItems pageMeta
- Add NCalc field name validation: client-side inline feedback (is-invalid +
  message on oninput) and pre-save sweep; server-side ValidateTemplateFields
  on Create and Update; rules: letter-start, letters/digits/underscores only,
  no duplicates, "rate" reserved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 10:28:41 -04:00
spouliot 1eba50cf0f Add Custom Formula Item Templates with AI generation and wizard integration
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>
2026-05-23 15:09:22 -04:00
spouliot 2bf8871892 Fix NoExtraLayerCharge persistence, appointment reminders, coat notes display, scroll restoration, and invoice Send dead-button
- 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>
2026-05-19 15:48:16 -04:00
spouliot 7239f55308 Fix tax-exempt customers always charged tax in quote preview
parseFloat('0') is falsy in JS, so '0 || pageMeta.taxPercent' was
falling through to the company default rate even when the TaxPercent
field was correctly set to 0 for a tax-exempt customer. Use an
explicit field presence check instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:05:07 -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 f1d7054b3e Fix AI quote reliability on mobile: compress photos + add fetch timeout
- Compress photos client-side before uploading (1200px max, JPEG 85%):
  full-res phone photos (5-15 MB) → ~150-250 KB, dramatically reducing
  upload time on slow mobile connections and Anthropic processing time
- Add 120s AbortController to both AiAnalyzeItem fetch calls so a stalled
  mobile connection produces a clear 'timed out' error instead of spinning forever
- After 30s show 'Still analyzing… this can take a minute on mobile' to
  reassure users the request is in progress
- Reset loading text on retry so the slow-connection hint doesn't persist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:22:32 -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 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 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 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 50b1794799 Add PWA manifest, fix AI multi-coat pricing, and improve catalog lookup
- PWA: manifest.json + minimal service worker so iOS/Android persist camera
  permission after "Add to Home Screen"; theme-color and apple meta tags in layout
- PWA icons: 192x192 and 512x512 from transparent PCL logo; updated pcl-logo.png
- AI pricing: apply AdditionalCoatLaborPercent per extra coat on AI items,
  matching the calculated-item path (was ignoring extra coats entirely)
- AI wizard: live price recalc when coats are added/removed; session-expiry
  errors now show a clear "refresh and sign in" message instead of raw HTTP status;
  smooth-scroll to follow-up/results sections on AI response
- Catalog lookup: exclude SKUs already in company inventory from results;
  pass currentId on edit so own entry still appears; vendor-scoped search
  with cross-vendor fallback; result count shown in multi-match modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:58:10 -04:00
spouliot ac3e4452b2 Fix catalog filter leaving items unclickable after filtering
Multi-word search now uses Array.every so word order doesn't matter.
Scroll is reset to top after each filter so visible items aren't hidden
off-screen. A forced layout reflow (offsetHeight read) ensures iOS Safari
re-registers filtered-in elements as interactive — without it, items that
transition from display:none back to visible remain unresponsive to taps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 13:19:41 -04:00
spouliot cad728ba66 Fix passkey login tracking, add email opt-out UI guards, and add Quick/Full quote mode toggle
- PasskeyController: set LastLoginDate on passkey sign-in so Company Health
  and audit pages show accurate last-login times (was always showing 'Never')
- Jobs/Index status modal: disable 'Notify customer' email toggle and show
  warning when customer has notifications turned off; CustomerNotifyByEmail
  added to JobListDto + JobProfile mapping + data-customer-notify attribute
- Quotes/Create: disable 'Send quote via email' checkbox with 'Notifications
  off' badge when selected customer has email opt-out; ViewBag.CustomerEmailOptOutIds
  added alongside existing CustomerTaxExemptIds pattern
- Quotes/Create: Quick Quote / Full Quote segmented toggle at top of form;
  hides non-essential fields (dates, notes, tags, oven, discount, photos) in
  Quick mode; selection persisted in localStorage
- InvoicesController Send action: improved error logging and user-facing
  warning when PDF generation or email dispatch fails after status is saved
- item-wizard.js: guard item restoration with try/catch; ensure writeHiddenFields
  always runs on form submit via capture-phase listener
- Help docs and AI knowledge base updated for all new features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 13:32:34 -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 63e12a9636 Initial commit 2026-04-23 21:38:24 -04:00