Quotes now reflect the current catalog price instead of a tenant's stale
typed-in cost, without disturbing accounting.
- InventoryItem gains CatalogReferencePrice + CatalogPriceUpdatedAt: the
QUOTING price (current replacement cost), kept separate from UnitCost/
AverageCost (the cost basis that drives valuation/COGS).
- The catalog sync (PowderCatalogUpsertService.PropagateToLinkedInventoryAsync,
run at the end of every upsert) refreshes linked inventory items with the
catalog's current price and product data (description, cure, SDS/TDS, color
families, coverage, SG, transfer eff, requires-clear-coat). It NEVER touches
cost, quantity, notes, image, location, or stock levels, and never nulls a
tenant value with a catalog null. EF persists only actual changes.
- CatalogReferencePrice is also set at link time (catalog receive, incoming-
from-catalog, identity match on create) so a freshly added powder quotes at
the current price immediately.
- Pricing now uses CatalogReferencePrice ?? UnitCost: the quote/job powder
pickers and PricingCalculationService (in-stock usage and powder-to-order
billing). Falls back to UnitCost for non-catalog/manual powders, so nothing
regresses. One current price for the whole quantity — no on-hand/to-order
split. Per-coat snapshot still locks the price at quote creation.
Tests: propagation updates reference price + specs but not cost/qty/notes/
image, and skips a $0 catalog price. Full suite 276 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes to the "Got It" powder receive flow:
1. Skip the modal when the powder is in the master catalog. Clicking "Got It"
now first calls ReceivePowderFromCatalog, which — if the powder resolves in
the catalog — creates a fully populated inventory record (specs, cure, SDS/
TDS, image, pricing) and marks the coat received, no modal. Only when the
powder isn't in the catalog does it fall back to the manual entry modal.
The catalog match/apply and the receive finalize (opening txn, mark received,
sibling-coat linking) are extracted into shared helpers used by both the
modal save and the auto-receive path.
2. Fix a crash re-receiving a previously-deleted powder. The unique index
IX_InventoryItems_CompanyId_SKU had no filter, so a soft-deleted item still
reserved its SKU; re-creating it generated the same SKU and violated the
constraint. The index is now filtered on IsDeleted = 0, matching the app's
soft-delete semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a quote uses a powder the company doesn't stock but the master catalog
does, receiving it ("Got it") created an inventory record with only the color
code/name and cost carried on the quote — cure schedule, SDS/TDS, sample image,
color families, etc. were left blank.
AddCustomPowderToInventory now matches the platform powder catalog by SKU (the
coat's color code, preferring the same manufacturer, then by color name) and
fills every blank spec/document field from it, links PowderCatalogItemId, and
falls back to standard coverage/efficiency defaults. Only gaps are filled, so
anything entered on the receive form is preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from reviewing the first full sync of the real catalog:
- Exclude non-powder / size-variant listings: physical swatch cards (-SW /
"SWATCH"), 4 oz testers (-04 / "Tester"), and 5 lb sample bags ("Sample (").
These are not standalone powder colors. Filtered before mapping, and a
cleanup step deletes any already synced (so they're removed, not flagged
discontinued). Sample detection keys off the "Sample (" name, not the bare
-S suffix, to avoid catching a real SKU ending in S (verified 0 collisions).
- Tighten RequiresClearCoat: was flagging ~53% of the catalog on any casual
"clear coat" mention. Now only genuine signals (partial-cure schedules, the
Illusion line, explicit "requires a clear" phrasing) trip it.
- Fix literal "—" in the sync success banner (TempData is HTML-encoded).
Tests cover the exclusion patterns and the tightened clear-coat detection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Products with no featured image return featured_image: [] (empty array) rather
than an object or null, which failed to bind to a single ColumbiaImage and
aborted the whole sync. Adds ColumbiaImageJsonConverter that reads the object
when present and yields null for any non-object form ([], false, ""), and drops
the unused GalleryImages property (we only use featured_image) to remove the
same risk. Regression tests cover both the empty-array and object cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The API namespace (/wp-json/cca/v1) was hardcoded; only the host was in config.
Adds a Columbia:ApiBasePath config key (default /wp-json/cca/v1) so an API
version bump is a config change, not a code change. The client now composes
the products URL from BaseUrl + ApiBasePath + /products. appsettings carries
the live key (private Gitea; Azure App Settings override in prod).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5 (part): compliance. PurgeColumbiaData (SuperAdmin) deletes every
catalog record whose Source is the Columbia Coatings API feed — regardless of
derived manufacturer, since PPG and KP Pigments products were served through
that feed — and nulls any inventory PowderCatalogItemId links across all
tenants. Tenant stock records are preserved (they keep their add-time snapshot,
losing only the live catalog link/badge), honoring the boundary that the
distributor's right-to-delete covers their catalog, not customers' purchased
stock. Adds a confirmed "Remove Columbia data" button to the catalog admin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5 (part): the inventory tie-in.
- Set InventoryItem.PowderCatalogItemId on the catalog-sourced create paths:
directly in CreateIncomingFromCatalog, and via a new FindCatalogMatchAsync
(Manufacturer + ManufacturerPartNumber) helper in Create.
- Inventory Details loads the linked catalog row (falling back to an identity
match for items created before linking) and shows a "Discontinued by
manufacturer — cannot reorder" badge + banner when it's discontinued.
Deliberately distinct from the shop's own Active/Inactive status: existing
stock can still be used and quoted, it just can't be reordered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4: automation. ColumbiaCatalogSyncBackgroundService wakes hourly and
runs a full sync only when ColumbiaSyncEnabled is on and ColumbiaSyncIntervalDays
has elapsed since the last successful run (tracked via the ColumbiaLastSyncedAt
setting). No-ops quietly when disabled or unconfigured. The hourly due-check is
negligible; the actual sync runs at most once per interval. Sync failures are
recorded on the result/settings, never thrown, so a bad run can't kill the loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3: SuperAdmin-triggered sync. Adds a SyncColumbia POST action that runs
a full catalog sync on demand (bypassing the schedule) and reports the result
via TempData. The catalog index header gains a "Sync Columbia" button (with a
syncing spinner) and a status line showing the scheduled-sync on/off state,
last-synced time, and last-run summary, read from the platform settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2: the mapping and sync core.
- ColumbiaCatalogMapper (pure/static, unit-tested): maps an API product to a
PowderCatalogItem. Derives manufacturer (PPG/KP Pigments/Columbia) from
taxonomy+SKU; flags additives into the Powder Additives category; takes base
price from the top-level price with variant fallback; captures variation /
tiered pricing as JSON; parses the free-text cure schedule into all curves
(three degree glyphs, @/at, multi-curve in order, partial-cure -> none) with
the first as the primary temp/time; strips HTML descriptions; joins color
groups; normalizes chemistry; flags clear-coat powders.
- PowderCatalogUpsertService (IPowderCatalogUpsertService): single upsert path
matching on (VendorName, SKU). Copies only feed-sourced fields and leaves
enrichment fields (specific gravity, coverage, transfer efficiency, finish)
untouched so syncs never wipe lazily-enriched TDS/AI data.
- ColumbiaCatalogSyncService (IColumbiaCatalogSyncService): pulls the full
catalog, maps + de-dupes, upserts, then reconciles discontinuations ONLY on a
complete pull (a partial pull throws and aborts before the sweep). Reactivates
reappearing items; records last-synced/last-result platform settings.
- 25 mapper unit tests covering the cure parser, manufacturer derivation,
simple/variable pricing, chemistry, color, and HTML cases from real records.
Full suite green (261 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds GetProductBySkuAsync (GET /products?sku=) and GetProductByIdAsync
(GET /products/{id}) for ad-hoc / on-demand refresh of a single record
without pulling the full catalog. Extracts the shared 429-retry send loop
into SendWithRetryAsync, which now also treats 404 as not-found (null) so
single lookups don't throw on a missing SKU/ID.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1b of the Columbia Coatings integration: the typed read client and
its configuration, ahead of the sync/mapper service.
- ColumbiaProductDtos: wire-shape models for GET /products. tiered_pricing
is captured as JsonElement because the API returns it as an object on
simple products but an empty array on variable ones — binding it raw
avoids a deserialization throw; the mapper interprets it.
- IColumbiaCoatingsApiClient / ColumbiaCoatingsApiClient: pages the catalog
via GET /products (NOT the export download_url, which is Cloudflare-blocked
for server clients). Sends X-API-Key from config, honors 429/Retry-After,
and THROWS on any page failure so a partial pull can never be mistaken for
the full catalog (protects the later discontinuation sweep).
- ColumbiaIntegrationConstants: single home for config keys, setting keys,
and the derived Source/manufacturer/category values.
- Config: Columbia:ApiKey (blank — secret supplied per environment) and
Columbia:BaseUrl in appsettings.
- SeedColumbiaSyncSettings migration: seeds SuperAdmin-managed platform
settings ColumbiaSyncEnabled (off by default), ColumbiaSyncIntervalDays
(7), and last-sync tracking, under a new "Integrations" group.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1a of the Columbia Coatings API integration. Adds the persisted
fields the sync/mapper will need, ahead of the client and sync service:
PowderCatalogItem:
- Category: our product category (e.g. "Powder Additives" for gram-sold
pigments) derived from vendor taxonomy at import, not stored raw.
- Source: provenance (e.g. "Columbia Coatings API"), kept separate from
VendorName (= derived manufacturer) so a distributor's right-to-delete
can purge by feed regardless of manufacturer.
- ChemistryType: resin chemistry (Polyester/TGIC/Epoxy/...), distinct
from Finish.
- MilThickness: recommended film build as vendor free text.
- CureScheduleText: raw cure schedule verbatim (formats vary widely).
- CureCurvesJson: all parsed cure curves, so alternate low-temp curves
are preserved for heat-sensitive substrates, not just the primary.
- FormulationChanges: vendor reformulation log; a signal cure specs may
have changed.
InventoryItem:
- PowderCatalogItemId: loose link to the catalog row (matches the
QuoteItemCoat pattern) so inventory detail can show manufacturer-level
status (e.g. discontinued/cannot reorder) and future change flags.
Nulled, never cascaded, when source catalog data is purged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Wrap Line Items and Payment History tables in table-responsive so they
scroll horizontally rather than overflowing the viewport. Expenses detail
page uses a definition list layout and was not affected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the desktop table in table-responsive to fix horizontal scrolling,
and adds a mobile-card-view section matching the pattern used on Invoices,
PurchaseOrders, and other list pages. Cards show type, number, vendor,
status, date, due date, amount, balance due, and memo/account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SuppressNotification to RecordPaymentDto and a checkbox to the
modal. When checked, the payment is fully recorded but NotifyPaymentReceivedAsync
is skipped — useful for historical imports or cases where the customer
should not receive an email.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bare /Invoices now redirects to statusGroup=unpaid (Draft, Sent, Overdue)
so the list is immediately actionable. Four pills — All, Unpaid, Partial,
Paid — mirror the Jobs page pattern with live badge counts. The existing
status dropdown and outstanding/thisMonth flags are preserved for
dashboard deep-links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
textContent treats — as a plain string; replaced with innerHTML
for static dash placeholders, and — JS escape where user input
is concatenated. Also removed a dead textContent line in timeclock-kiosk.js
that was immediately overwritten by innerHTML on the next line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Quotes Index stat strip (OPEN / APPROVED / TOTAL VALUE) summed every
non-deleted quote, while the default list hides Converted quotes. A quote
converted to a job (whose deletion is blocked by the linked job) therefore
stayed invisible in the list but kept inflating the cards -- e.g. a blank
list showing "1" and a non-zero total value.
GetIndexStatsAsync now excludes the Converted status so the cards reflect
the same population as the default list. Converted value is intentionally
dropped from the quote pipeline because it carries forward on the job
(counting it in both would double-count the same dollars).
Also adds an explicit CompanyId predicate to GetIndexStatsAsync (defense in
depth) -- it was the only Quote query in the typed repo relying solely on
the global tenant filter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Save button was type=submit, so HTML5 form validation silently blocked
the submit event and nothing happened on click. Switch to type=button
with an explicit click handler. Also replace AutoMapper Map() with
explicit property assignment so EF reliably detects the mutations, and
re-enable the button in showButtonSuccess() after a successful save.
Cherry-picked CompanySettings hunks from dev commit 0b839d0746 as a
targeted production patch off v2026.06.09.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReleaseNotesController had [Authorize] only on Index(), leaving the class
unprotected at declaration level — any future unannotated action would be
publicly accessible.
KioskController had no class-level auth, meaning PushSmsConsent() and
CancelSmsConsent() (staff-only POST actions) were reachable by anonymous
callers. [AllowAnonymous] on the existing tablet/intake actions still
overrides correctly, so the customer-facing kiosk flow is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When multiple jobs need the same powder, the 'Powder in Queue to be
Ordered' panel now collapses them into a single line (summed lbs) rather
than showing one row per coat. 'Mark as Ordered' marks all contributing
coats at once and injects each into the 'Awaiting Receipt' panel
individually so per-coat receiving still works unchanged.
- Add PowderOrderJobRefDto; PowderOrderLineDto gains CoatIds + Jobs lists
(scalar CoatId/JobId/etc. become computed accessors for backward compat)
- MapPowderOrderGroupsMerged: secondary GroupBy on (ColorName, ColorCode,
Finish, SKU) within vendor group for the 'needed' panel
- MapPowderOrderGroups kept per-coat for the 'awaiting receipt' panel
- MarkPowderOrdered accepts comma-separated coatIds, returns coats array
- Dashboard view: Customer column loops job refs for merged rows; JS posts
coatIds and iterates data.coats to populate awaiting-receipt panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Seed data fixes:
- Fix EF interceptor: no longer overwrites explicitly-set CreatedAt on Added
entities — root cause of all "same month" chart issues
- Customer seeder: generates 15 customers/month from Jan → current month;
keeps 10 commercial anchors in deterministic order for job seeder index map
- Invoice seeder: historical range bumped from 2→8 paid invoices/month so
P&L shows consistent profit (~$5,200 collected vs ~$4,200 monthly expenses)
- Month -1 bumped to 7 paid invoices to stay above expenses
- Jobs: set UpdatedAt to historical event date so analytics don't need null fallback
- Analytics (ReportsController): use CompletedDate ?? UpdatedAt ?? CreatedAt for
revenue chart grouping; fixes empty Revenue Trend charts on Overview/Revenue tabs
- SeedDataService: inject IAccountBalanceService; auto-recalculate account balances
after seeding; patch checking/savings opening balances unconditionally on reset
- Customer list: sort by CompanyName ?? ContactLastName so individuals and
commercial accounts interleave instead of appearing as two blocks
Invoice resend:
- ResendInvoice action now accepts sendEmail + sendSms parameters; SMS-only
resend no longer requires an email address on file
- Ensures PublicViewToken exists before SMS so the view link is always valid
- canResend in Details view now allows Paid invoices (removed != Paid guard)
- Resend button shows channel-choice modal when customer has both email + SMS,
direct SMS button when SMS only, or email button when email only
- New #resendChannelModal mirrors the Send channel modal but posts to ResendInvoice
- resendInvoice() JS updated to pass sendEmail/sendSms query params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: fingerprint-based removal failed on databases seeded with
older code (different emails/SKUs); plus Vendors, Named Ovens, and
Appointments had no removal path at all.
- Add ForceRemoveAll flag to RemoveSeedDataOptions: when true, all
removal blocks delete by CompanyId instead of fingerprint matching
- Customers block: ForceRemoveAll deletes all company customers
- Workers block: ForceRemoveAll deletes all users with CompanyRole=Worker
- New Vendors block (triggered by options.Vendors || ForceRemoveAll)
- New NamedOvens (OvenCost) block (triggered by options.NamedOvens || ForceRemoveAll)
- New Appointments block (triggered by options.Appointments || ForceRemoveAll)
- ResetDemoCompany: set ForceRemoveAll=true and enable all new flags so
every re-seedable table is wiped clean before re-seeding
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Appointments: add ~25 past appointments (last 90 days) with Completed,
Cancelled, No Show, and Rescheduled statuses; completed records carry
ActualStartTime/ActualEndTime with realistic variance; cancel/no-show
notes explain why; customer label falls back to ContactFirst/LastName
for residential customers
- Fix future appointment title for residential customers (was always using
CompanyName which is null for individuals)
- New SeedDataService.AiPredictions.cs: seeds 8 AiItemPrediction records
(varied complexity/confidence/tags/reasoning) and attaches them to the
first 8 eligible QuoteItems, marking those items IsAiItem=true; 3 of 8
have UserOverrodeEstimate=true for AI Accuracy report demo
- SeedDataService.cs: wire SeedAiPredictionsAsync after Invoices
- Remove.cs: collect QuoteItem.AiPredictionId FKs before deleting items,
then delete orphaned AiItemPrediction records after quotes are removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bills.cs: replace aceHardware/fastenal lookups with grainger/harbor/localSupply
to match Phase 1 vendor renames; update all vendor invoice number prefixes
- Bills.cs: add 3 AP aging-bucket bills (30-60, 61-90, 90+ days overdue) so all
four AP aging buckets are populated for report demos
- Invoices.cs: add 3 more overdue invoices (31-60, 61-90, 90+ day AR buckets)
alongside the existing 21-day overdue; total now 29 invoices
- New SeedDataService.PurchaseOrders.cs: 7 POs — 3 Received (historical), 2
Submitted (in-flight), 2 Draft (pending approval); links to inventory items
where available
- SeedDataService.cs: wire SeedPurchaseOrdersAsync after Vendors seeder
- Remove.cs: add PO + POItem cleanup inside Bills removal block (two-step ID
fetch to avoid nested LINQ translation issues)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 5 named shop workers seeded as ApplicationUser (Employee role):
Mike Sanders (Coater), Jake Wilson (Sandblaster), Sarah Brooks (Inspector),
Tyler Green (General), Chris Mason (Lead) — @pcldemo.com fingerprint domain
- Job time entries seeded for all in-progress and completed jobs;
Worker Productivity report will have data from day one
- Maintenance history seeded per equipment: 2 completed records + 1 upcoming
scheduled + 1 overdue record on Pressure Pot for overdue alert demo
- Equipment renamed to spec names: Main Batch Oven, Small Batch Oven, Powder
Coating Booth, Blast Cabinet, Pressure Pot Blaster, Air Compressor, Wash
Station, Forklift (replaced Overhead Conveyor which wasn't in spec)
- RemoveSeedDataOptions.Workers added; Remove.cs cleans up workers + time
entries on Demo Reset; SeedDataController resets workers in ResetDemoCompany
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Customers: 100 → 27 (15 commercial across auto/industrial/architectural/
fitness/marine/energy, including 2 tax-exempt govts; 12 individuals)
Quotes: 75 → 20; date range extended to 4-6 months (was 90 days);
status distribution adjusted proportionally (2 draft, 3 sent, 10 approved,
3 rejected, 2 expired)
Jobs: fixed 50-loop → per-customer 0-5 jobs (~32 total); jobIdx cycles
all 16 statuses globally so every status is visible; creation dates spread
across 1-5 months for in-progress/early jobs, 2-6 months for completed jobs
SeededCustomerEmails updated to match new 27-customer set (added
gnelson@email.com and carol.evans@email.com)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Bills and Expenses flags to RemoveSeedDataOptions
- RemoveSeedDataAsync: delete BillPayments + BillLineItems + Bills, then
Expenses for the company when those flags are set
- ResetDemoCompany action: enable Bills=true and Expenses=true so all
seeded AP data is cleared before re-seeding (was skipping on second reset)
- Fix apostrophe in success message (was ' in C# string, double-encoded
by Razor to literal ' on screen)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New ResetDemoCompany POST action wipes all seeded data (customers, jobs,
quotes, invoices, inventory, equipment, catalog, pricing tiers, operating
costs) from the DEMO company and immediately re-seeds with fresh records
dated relative to today. Seed data already used relative dates so every
reset produces a realistic, current-looking dataset.
View adds a red "Reset Demo Company" card at the top of the Seed Data page,
visible only when the DEMO company exists. Single button with confirm dialog;
shows exactly what will be wiped and what will be preserved (user accounts,
company settings, lookup tables).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Company Settings: switch save button from type=submit to type=button
to bypass HTML5 form validation blocking the submit event; replace
AutoMapper Map() with explicit property assignment so EF change
tracking reliably detects mutations; fix showButtonSuccess() never
re-enabling the button after a successful save
- Invoice PDF: move PAID stamp into the header row as a centered middle
column so it sits between the company and invoice blocks without
adding height to the document
- Purge script: use business-date fields instead of CreatedAt so
imported records (which all share today's CreatedAt) are correctly
filtered by actual transaction dates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove watermark overlay layer; PAID badge now sits centered between
the header row and the accent rule so it never obscures line items
- Add PrimaryContactEmail to company info block in header
- Remove ComposePaidStamp helper (no longer needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmailService: add RedirectIfNonProd() mirroring SmsService pattern;
reads SendGrid:DevRedirectEmail and redirects all outbound email in
non-production so real customers are never contacted on local/dev
- appsettings.json: set DevRedirectEmail to spouliot@scppowdercoating.com
- PdfService: revert Opacity() (not in QuestPDF 2024.12.3); use
Colors.Green.Lighten2 for stamp + border to achieve lighter look
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduce font 80→52, border 5→3, add 35% opacity so stamp no longer
obscures line items on dense invoices.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Suppress ColorName line if it matches the item Name (powders use
color name as their item name, causing it to show twice)
- Suppress Finish if already contained in the item Name
- Always show Manufacturer regardless of whether it is populated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Ready pill passed searchTerm=ReadyForPickup which did a text search —
"readyforpickup" (no spaces) never matched the display name "Ready for Pickup".
Converted to statusGroup=ready and added the corresponding controller case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jobs: use AllJobCount (global total) to distinguish truly-empty from
filter-returned-nothing; show Clear Filters button in the latter case.
Quotes: expand the filter-active check to include tagFilter and statusCode,
which were missing from the condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inline display:none!important on the results div blocked all CSS rules
from showing it, including the :not(:empty) trick. Switched to explicit
JS show/hide so the dropdown is reliably visible after typing 2+ chars.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>