Commit Graph

10 Commits

Author SHA1 Message Date
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 f383339465 Store powder specific gravity and fix coverage math 2026-05-06 08:46:41 -04:00
spouliot 11a1b91be1 Add platform powder catalog management UI with full CRUD and AI lookup
- PowderCatalogController: Create, Edit, ToggleDiscontinued actions; searchable/filterable/sortable Index with pagination; AiLookup and AiAugmentFromUrl endpoints backed by IInventoryAiLookupService
- New views: Create, Edit, _Form partial (with AI-assisted field population), overhauled Index grid with completeness quality badges and responsive mobile cards
- New ViewModels: PowderCatalogIndexViewModel, PowderCatalogFormViewModel, PowderCatalogListItemViewModel
- AI lookup improvements: SpecificGravity field added to InventoryAiLookupResult; ApplyPowderFallbacks derives CoverageSqFtPerLb from specific gravity when docs omit it; DefaultTransferEfficiency (65%) applied everywhere transfer efficiency is null
- powder-catalog-ai-lookup.js: client-side AI lookup and URL augment wiring for the catalog form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 00:27:44 -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 4182286a31 Fall back to TDS sheet for cure specs when main lookup returns none
After the main AI lookup and catalog search, if CureTemperatureF or
CureTimeMinutes is still null but a TDS URL was found, fetch that page
and ask Claude to extract just the cure schedule.

- IInventoryAiLookupService.FetchTdsCureSpecsAsync: new interface method
- InventoryAiLookupService.FetchTdsCureSpecsAsync: fetches the TDS URL via
  the existing FetchPageAsync pipeline (JSON-LD + doc-link extraction, HTML
  stripping). If the page is a PDF or unreachable, returns Success=false
  silently so no error surfaces in the UI. Otherwise sends a small targeted
  prompt that asks only for cureTemperatureF and cureTimeMinutes and uses
  MaxTokens=256 so the call is fast and cheap.
- InventoryController.ScanLabel: after catalog lookup, computes the resolved
  cure values (catalog preferred over AI result). If either is null and a
  TDS URL exists, calls FetchTdsCureSpecsAsync and merges any newly found
  values back into aiResult before building the JSON response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:14:12 -04:00
spouliot 28b7b9f86b Fix QR detection (parallel loops), price extraction, and camera pre-warm
QR scanning:
- Run BarcodeDetector and jsQR in parallel — jsQR starts after JSQR_DELAY_MS
  (1.5 s) so both decode simultaneously. BarcodeDetector silently returns empty
  arrays for some QR variants; running jsQR in parallel via a separate rAF loop
  (rafId2) and its own off-screen canvas catches those cases. First decoder to
  find anything calls handleQrResult and sets qrFound = true; the other stops.

Price extraction (two bugs):
- ScanLabel: unitPrice was catalogMatch?.UnitPrice ?? 0m, ignoring aiResult
  .UnitCostPerLb entirely when no catalog match — changed to fall through to AI result
- AppendOffer: only read JSON-LD "price" field; Shopify AggregateOffer uses
  "lowPrice" instead — now checked as fallback so Prismatic Powders prices are found

Camera pre-warm:
- Reverted localStorage approach (caused getUserMedia to fire on every page load,
  showing Chrome's "Ask" prompt immediately before user clicked anything)
- Restored Permissions API gate: preWarmCamera only calls getUserMedia when
  navigator.permissions.query returns 'granted', never risks a page-load prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 19:45:22 -04:00
spouliot f881b7dd53 Fix label scanner: full field mapping, vision follow-up lookup, SDS/TDS extraction
- LookupByUrlAsync now maps all identity + spec fields from Claude response
  (manufacturer, SKU, colorName, description, sdsUrl, tdsUrl, unitCostPerLb, etc.)
  Previously only augmenting fields were mapped; Columbia QR path left 80% blank
- Vision scan follow-up: after ScanLabelAsync reads label text, automatically run
  LookupAsync using the extracted manufacturer + color/SKU to fill SDS/TDS URLs,
  product page, image, description, and any specs not printed on the bag;
  label values (cure schedule, SKU) remain authoritative and are never overwritten
- SDS/TDS URL extraction: added ExtractDocumentLinks() that scans anchor tags in
  raw HTML before tag-stripping, injects found URLs as [Structured Data] lines so
  Claude can read and echo them back in the JSON response; previously all hrefs
  were lost with the HTML stripping
- Added SdsUrl/TdsUrl to InventoryAiLookupResult, Claude system prompt JSON schema,
  LookupAsync mapping, and ScanLabel response (catalog match ?? aiResult fallback)
- SDS/TDS now also stored on auto-contributed catalog entries
- jsQR inversionAttempts: 'dontInvert' → 'attemptBoth' for better QR detection
  under varying label contrast and lighting conditions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:22:53 -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 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 63e12a9636 Initial commit 2026-04-23 21:38:24 -04:00