- InventoryController: extract RecordInventoryUsageAsync helper; both LogUsage
(scan page) and LogMaterial (jobs modal, moved from JobsController) call it —
no more duplicate save/GL logic across two controllers
- Log Material modal: replace radio buttons with prominent toggle buttons so the
active mode (Amount Used vs Amount Remaining) is always visually obvious; add
always-visible preview line showing exactly what will be logged before saving
- Edit Usage modal: add quantity field (pre-populated from existing transaction)
with delta adjustment to InventoryItem.QuantityOnHand on save; include
completed/terminal jobs in the dropdown so entries can be corrected after a
job is marked done
- Scan page job picker: include jobs completed within the last 7 days (marked
with '(completed)') so usage can be logged after a job is finished
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subscription expiry (SubscriptionExpiryBackgroundService):
- Trials with no grace period now go directly Active -> Expired instead
of briefly entering GracePeriod for a day, which was causing repeated
'Grace Period Started' admin notification emails
- Remove redundant isTrial variable (query already filters to non-Stripe
companies, so all processed companies are trials by definition)
- Save per-company inside the loop so a single SaveChangesAsync failure
no longer discards all other companies' status changes and notification
log entries (which was the other cause of repeated emails)
HTML entities in page titles (33 views):
- Replace – / — with plain ' - ' in ViewData["Title"] C#
strings; Razor HTML-encodes these when rendering @ViewData["Title"],
causing browsers to display the literal text '–' instead of a dash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Vendor Create: reject duplicate company names (case-insensitive) before
saving; works for both the standalone form and the inline quick-add modal
- _Pagination: define changePageSize() JS function (was called but never
existed, breaking page size dropdown on every paginated list)
- Inventory Label: show bin/location on printed QR code labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an 'All Colors' dropdown to the inventory filter bar populated from
the ColorFamilies values already stored on inventory items. Selecting a
family (e.g. 'Red') returns only items tagged with that family.
Also refactors the 16-branch if/else filter builder into a single
composable predicate, making future filter additions trivial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
frame-src was missing 'self' so the Label iframe was blocked by CSP.
bootstrap.Modal.getOrCreate does not exist; correct method is getOrCreateInstance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vendors can now be tagged with one or more inventory categories (Powder,
Chemical, etc.) via checkboxes on the Create/Edit form. The inventory
Create/Edit vendor dropdown automatically filters to matching vendors when
a category is selected; falls back to all vendors if none are tagged.
Includes migration AddVendorCategories (VendorInventoryCategories join table).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Opens the same iframe modal used on the Details page; the iframe src is
set dynamically so a single shared modal handles every row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an embed mode to the Label view (hides standalone nav controls) and
an iframe-based modal on Inventory Details. The modal footer Print button
calls contentWindow.print() so the print dialog opens without spawning a
new window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Razor's @() expression auto-encodes &, turning — into &mdash; which
rendered as literal text in the browser. Wrapped all such expressions in
@Html.Raw() so the em-dash entity is passed through unescaped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manufacturer product pages are often not on secure connections; http:// is the
safer default to avoid connection failures on non-SSL sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If a stored URL is missing http:// or https:// the browser treats it as relative
and appends it to the app URL. Guard in three places:
- inventory-catalog-lookup.js syncLinkButton: ensureAbsoluteUrl() prepends https://
- inventory-label-scan.js syncLink: same guard for scan-filled URL fields
- Details.cshtml SafeUrl() Razor helper on SpecPageUrl, SdsUrl, TdsUrl links
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inventory: location filter dropdown + Print Bin page (line #, name, color, SKU)
- Fix: Prismatic Powders QR scan now extracts manufacturer/SKU/color from URL path
and uses full LookupAsync pipeline instead of relying on page fetch alone
- Fix: iOS Safari 'Login / data Zero KB' download -- add OnRejected HTML response to rate limiter
- Fix: mobile session logout -- ConfigureApplicationCookie with 30-day MaxAge persistent cookie
- Help: new 'Location Filtering & Bin Print' section in Inventory help article
- Help: HelpKnowledgeBase updated with bin filter and print bin details
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII
tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun
.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.
Build clean; 225 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show 'Add Your First X' and onboarding copy only when the list is truly
empty. When a search or filter is active with no results, show 'Add X'
and 'No X match your search/filters' instead.
Affected: Customers (table + mobile views), Equipment, Inventory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CSS fix: change blanket .table-responsive hide to only trigger when
a .mobile-card-view sibling exists (.mobile-card-view ~ .table-responsive
and :has() rule) — auto-fixes 60+ forms/reports/detail/help pages that
were showing blank on mobile by making their tables scroll instead
- Add mobile card views to remaining list pages:
JobsPriority (overdue jobs, main board, maintenance sections)
NotificationLogs (email/SMS log entries)
AiUsageReport (per-company AI usage breakdown)
GiftCertificates/BulkResult (batch certificate list)
Inventory/SamplePanels (Need to Order + On Wall tabs)
BannedIps (active bans + lifted/expired bans)
OnboardingProgress (per-company activation funnel)
ReleaseNotes/Manage (versioned changelog entries)
StorageMigration/Results (file migration status list)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).
Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sweeps em dashes, en dashes, multiplication signs, ellipses, and curly quotes
to their HTML entity equivalents (— – × … ‘ ’)
in all .cshtml files, skipping <script> blocks. Prevents encoding corruption
from AI tools and Windows encoding mismatches that caused recurring symbol bugs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now toggle between 'Amount Used' and 'Remaining Weight' on the
QR scan page. In remaining-weight mode, usage is calculated as
(current stock - remaining) before submit — no controller changes needed.
Includes live hint showing calculated usage and new balance as they type,
with validation preventing negative usage or remaining > current stock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Remove parseInt() from time entry worker select — GUIDs were destroyed
to NaN → sent as null → FindByIdAsync(null) threw 500
- Ledger pencil: also show for Adjustment rows (no PO) so scan-without-job
entries get an edit button, not just JobUsage rows
- InventoryController: always write JobUsage type for scan-based logs;
accept Adjustment in edit endpoints; promote Adjustment→JobUsage when
a job is assigned via edit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Previously these enrichments only ran in the label scanner path (ScanLabel).
The AI Lookup button and AiAugmentFromUrl went through separate code that
returned raw LookupAsync / LookupByUrlAsync results with no TDS fallback
and no SDS/TDS URL propagation to the form.
- InventoryController.ApplyTdsCureFallbackAsync: new private helper that
checks whether cure temp or cure time is still null after the primary
lookup, and if a TDS URL was returned calls FetchTdsCureSpecsAsync to
fill the gap. Mutates the result in place so callers just return it.
- AiLookup: calls ApplyTdsCureFallbackAsync after LookupAsync succeeds.
- AiAugmentFromUrl: calls ApplyTdsCureFallbackAsync after LookupByUrlAsync.
- ScanLabel: replaced the inline TDS fallback block with a call to the
same helper (merges catalog TDS URL into aiResult first so the helper
sees the best available URL).
- _InventoryColorFamilyScripts.cshtml: added fillDocUrl() helper that fills
field-sdsurl / field-tdsurl inputs and shows their open-link buttons when
the AI lookup returns sdsUrl / tdsUrl. These fields existed in the form
but were never populated by the AI Lookup button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
QR scanning:
- BarcodeDetector now snapshots to canvas before detect() instead of passing
live video element — more reliable across Chrome versions
- Uses BarcodeDetector.getSupportedFormats() to detect all formats the browser
supports rather than hardcoding ['qr_code'], catching data_matrix etc.
- jsQR fallback unchanged (attemptBoth inversion)
Processing overlay:
- Added #scan-processing overlay div to _LabelScanModal with spinner + message
- Camera/scanning UI blanks immediately when QR is found or Scan Text tapped;
overlay message differs per path ("QR code found..." vs "Reading label with AI...")
- Overlay hides on error (modal stays open); modal close triggers hideProcessing()
Camera permission:
- localStorage flag (scannerCameraGranted) set on every successful getUserMedia
- preWarmCamera() checks flag first, bypassing navigator.permissions.query which
can return 'prompt' for localhost even when Chrome has 'Allow' internally;
proactive getUserMedia on page load succeeds silently when permission is granted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>