Commit Graph

7 Commits

Author SHA1 Message Date
spouliot 5e3b0b9ddf Inline add-stock prompt when label scan finds existing inventory item
When a scanned label matches an item already in the tenant's inventory,
the scanner now opens an inline modal asking the user to add stock to the
existing item rather than navigating away or creating a duplicate.

- InventoryController.AddStock: new POST endpoint that creates a Purchase
  transaction, updates QuantityOnHand, and optionally updates UnitCost /
  LastPurchasePrice when a new cost is provided. Returns new balance as JSON.
- InventoryController.ScanLabel: extends the duplicate-detection response
  to include existingQuantityOnHand and existingUnitOfMeasure so the modal
  can display current stock level.
- _LabelScanModal.cshtml: adds #addStockModal with quantity (+ UOM label),
  optional unit cost (pre-filled from scan), optional notes, Add Stock CTA,
  and an escape hatch to create a new entry instead.
- inventory-label-scan.js: when scan returns existingInventoryId the JS
  opens addStockModal instead of a warning banner. Submitting POSTs to
  /Inventory/AddStock and shows the updated balance in a success bar with
  a link to the item. The 'new entry instead' path hides the modal and
  pre-fills the create form with a softer duplicate warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 19:59:43 -04:00
spouliot 3aeec4ffb2 Warn on label scan when product already exists in tenant inventory
After resolving manufacturer + SKU from the scan, ScanLabel now queries the
tenant's InventoryItems: first by ManufacturerPartNumber exact match (most
precise), then by ColorName + Manufacturer fuzzy match as fallback.

If a match is found, the response includes existingInventoryId and
existingInventoryName. The JS fillFromScan() shows a warning banner with a
direct link to the existing item instead of the normal success message. Form
fields are still pre-filled so the user can proceed to add a new entry (e.g.
a different lot or bag size) if that was the intent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 19:49:13 -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 1cb7a8ca4a Phases 3 & 4: Complete data access architecture migration
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers,
routing all data access through IUnitOfWork. Added IPlainRepository<T> for
the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote)
that intentionally don't extend BaseEntity and therefore can't use the
constrained IRepository<T>. Added permanent-exception comments to the 18
controllers that legitimately retain direct DbContext access (Identity infra,
cross-tenant platform ops, bulk streaming exports).

Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup
gate that reflects over every Controller subclass and throws at boot if any
non-exempt controller injects ApplicationDbContext. The app cannot start with
a violation.

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