- Outstanding Pickups card on Customer Details shows jobs awaiting pickup with age badges
- Customer Notes log: inline add/delete notes with important flag, AJAX-backed
- Clone Job action on Jobs controller; Repeat Last Job button on Customer Details quick actions
- Preferred Powders per customer: typeahead inventory search, AJAX add/remove
- CustomerPreferredPowder entity + migration; unit tests for CRM stats/timeline logic
- Fix EF Core concurrency bug: parallel Task.WhenAll FindAsync replaced with sequential awaits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edit GET now falls back to job.ProjectName for invoices created before the
column was added. Details view shows Project Name alongside Customer PO.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stores ProjectName on the Invoice entity (previously only inherited from the
linked job at display time). Pre-fills from the job when creating from a job.
Migration: AddInvoiceProjectName.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ProjectName (nvarchar 100, nullable) to Quote and Job entities;
migration AddProjectNameToQuotesAndJobs applied
- Add ProjectName to all relevant DTOs: QuoteDto/Create/Update,
JobDto/List/Create/Update, InvoiceDto (mapped from Job.ProjectName
via AutoMapper so the invoice PDF picks it up without a separate column)
- Form field added after Customer PO in Quote Create/Edit and Job Create/Edit
- CreateJobFromQuote copies ProjectName from quote to job automatically
- Details views (Quote and Job) display Project when set
- Printable quote PDF: Project row in the quote details block
- Work order: Project row in customer/job info section
- Invoice PDF: Project shown in the Job Reference block alongside Job # and PO #
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuotesController — ConvertToCustomer POST was wrongly setting the quote
status to 'Converted' (which means a job exists) and redirecting to the
customer page with no job created. The quote then disappeared from the
default list filter and the user had no way to create the job without
hunting for it. Fix: leave the quote at 'Approved' after customer
creation and redirect back to the quote details page with a toast
prompting the next step. 'Converted' status is now set exclusively by
CreateJobFromQuote when a job actually exists.
NotificationService — add tenant reply-to email address as a visible
line in the email footer so customers who ignore or whose mail client
doesn't honour the Reply-To header still have a clear address to contact.
Also adds Warning-level logging when no reply-to is configured for a
company so future routing issues are diagnosable from app logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs a Warning when no Reply-To email is configured for a company
(so the logs show why replies land at the platform sender address)
and a Debug entry when one is set, making future send issues
diagnosable without needing the SendGrid Activity API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the CFM=0→zero test (CFM no longer in the formula path).
Update expected values to match the new nozzle-primary tables and
corrected TierDefaults CFM/nozzle pairings. Add WetBlasting and
RustAndScale substrate coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the inaccurate CFM-based formula with nozzle-primary tables
sourced from industry standard abrasive blast cleaning references:
- Pressure pot: midpoints averaged from two reference tables
(#4 nozzle: 115 sqft/hr, #5: 175 sqft/hr, etc.)
- Siphon cabinet: dedicated siphon cabinet reference table
(#4 nozzle: 125 sqft/hr, #3: 75 sqft/hr, etc.)
- SiphonPot: 80% of pressure pot rate (open gravity feed, no enclosure)
- WetBlasting: 60% of pressure pot rate (water-media reduces velocity)
CFM is removed from the rate formula entirely — nozzle size determines
throughput and CFM draw, so CFM is a consequence of nozzle choice, not
an independent variable. Override field still bypasses formula for shops
that have measured their own throughput.
Also corrects TierDefaults nozzle/CFM pairings which were mismatched
(e.g. Small tier had 40 CFM assigned to a #5 nozzle that needs 150 CFM).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>
Adds CustomerPO to JobListDto (maps by convention), then builds a
Bootstrap tooltip per row with description · PO: xxx, skipping blank
fields. Rows with neither get no tooltip. Helps identify jobs at a
glance without opening the details page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
type="email" triggers jQuery Validate's email rule which rejects commas,
blocking multi-address input despite the multiple attribute. Switching to
type="text" defers validation to the server-side SplitEmails/MailAddress
logic in the DTO which already handles comma-separated lists correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously FieldsJson was serialized as an escaped string in the export
file, which was fragile and unreadable. Now parsed into a JsonElement and
embedded as a proper JSON array under the key "fields". Import reads it
back with GetRawText() to reconstruct the stored string. This prevents
the null/empty fields bug caused by manually-edited or round-tripped files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
System.Text.Json defaults to PascalCase for anonymous types, producing
"Name"/"OutputMode" etc., while the import used TryGetProperty("name")
causing every template to fail with "no name". Adding CamelCase naming
policy aligns the export format with what the import expects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ManualUnitPrice holds the per-item formula result. The previous code
incorrectly treated it as the batch total and divided by Quantity,
causing the unit price to shrink as quantity increased. Now follows
the same pattern as every other ManualUnitPrice path in this method.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export: GET /CompanySettings/ExportCustomItemTemplates downloads all
company templates as an indented JSON backup (strips internal IDs/paths)
- Import: POST /CompanySettings/ImportCustomItemTemplates restores from
that file; runs full field + formula validation, skips name duplicates,
returns per-item results (imported / skipped / errors)
- Unsaved-changes guard: cfModal now intercepts backdrop/ESC/X when the
form is dirty and prompts before discarding work
- Export and Import buttons added to the Custom Formulas card header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NCalc2 -> Antlr4 -> NETStandard.Library transitive dependency chain requires
System.Runtime.InteropServices >= 4.3.0, but the resolved version was 4.1.0.
Explicit pin in Application.csproj resolves the Jenkins publish failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes: Community Formula Library, Custom Formula Templates, Employee Timeclock,
Formula Library ratings, Job Profitability report, Quote Revision History,
flat-rate coat wizard UX improvements, customer import dedup fixes, inventory
incoming powder fixes, Custom Powder Order line item, and various bug fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes help: expand Item Types section to all 7 types (was showing only 3)
- Quotes help: update Coatings section to document Flat-Rate coat spec feature
- Quotes help: add Color Name tip to Custom Powder Order section
- HelpKnowledgeBase: update item types list to all 7 types with accurate descriptions
- HelpKnowledgeBase: add Color Name / blur-autofill note to Custom Powder Order entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 — 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>
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings
- Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer
- Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header)
- Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5
- Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable
- Help docs and AI knowledge base updated for all three features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reorganises the Add/Edit Formula Template modal into four logical rows
so that on mobile the sections stack in the natural authoring order:
1. Name / Description | Output Mode / Rate
2. Fields (left) | Formula + Test (right)
3. Diagram | AI Generator
4. Notes + Active (full width)
Previously, Fields appeared after the Formula on mobile because the left
column (containing Formula) stacked before the right column (containing
Fields). Also compacted Default Rate and Rate Label into a 2-column
mini-row so they sit side by side on all screen sizes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Custom Formulas and Timeclock tabs were completely missing from the mobile
dropdown selector, making them unreachable on phones; also adds AI Profile
and Online Payments which were similarly absent
- Formula library header: flex-column on mobile so title and button stack
cleanly instead of colliding
- Filter bar: icon-only button gets a visible label on mobile; added col-12
so it renders full-width correctly at xs
- Import modal: add modal-dialog-scrollable so body scrolls on small screens;
wrap field table in table-responsive to prevent horizontal overflow
- Settings card header: flex-column on mobile + flex-wrap on button group
so the three buttons don't overflow off the right edge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Normalize IF/Abs/Pow/etc. to lowercase before evaluation so AI-generated
or manually typed uppercase function names no longer cause "Function not
found" errors
- Add NormalizeAndValidate() which normalizes then does a parse-only check
on save — invalid formulas are rejected with a clear error before storing
- Update AI system prompt to list all functions in lowercase and explicitly
call out case-sensitivity; add if() to the supported function list
- Add collapsible NCalc quick-reference panel in the formula editor showing
all operators, functions (lowercase), built-in variables, and an example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Companies can now share their custom formula templates to a platform-wide
community library. Other tenants can browse, preview, and import formulas
as independent local copies. Includes attribution (source company name),
"Inspired by" lineage for re-contributed formulas, import counts, own-formula
badge, cascade diagram nullification, and AI assistant + help docs updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bg-purple (formula, AI badges) was only defined in company-settings-lookups.css
which is not loaded on Quote/Job pages. The badge element rendered with no
background, appearing as a blank space in front of the item description.
Moved all 13 custom color utilities (purple, pink, cyan, teal, indigo, lime,
brown, gray, orange, yellow, green, blue, red) to site.css so they are
available globally. company-settings-lookups.css retains its definitions for
now (harmless duplication; can be cleaned up later).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The UNIQUE index on (CompanyId, Email) uses HasFilter([Email] IS NOT NULL),
so NULL allows multiple rows but empty string '' does not — every blank-email
customer after the first was hitting a duplicate-key violation at save time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New logic:
Tier 1 - email present: email match -> skip (unchanged)
Tier 2 - email absent + phone present: name + phone composite -> skip
Tier 3 - email and phone absent: name + city/state/zip composite -> warn, import anyway
Tier 2 requires BOTH name and phone to match so two people sharing an
office line don't falsely collide. Tier 3 warns but imports because
location data is too imprecise to hard-skip on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tier 1 (email): existing behavior, now uses HashSet instead of O(n²) .Any()
Tier 2 (phone): when email is absent, deduplicate by normalised phone number
(last 10 digits of MobilePhone then Phone) against both DB and within-batch
Tier 3 (name): when both email and phone are absent, warn but still import
Fixes customers with no email being silently skipped or left undetected as
duplicates. NormalizePhone strips formatting so (423) 331-9834 and
423-331-9834 match correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. Wrong timing — inventory items with IsIncoming=true were auto-created during
quote save (in QuotePricingAssemblyService). Now deferred to quote approval so
inventory only reflects powders the shop is actually going to process.
2. Duplicate records — same powder on multiple items in one quote created multiple
inventory records. Now grouped by PowderCatalogItemId: one record per unique
catalog powder, all matching coats linked to the same record.
3. Wrong category — category resolution used first IsCoating=true by DisplayOrder,
which could land items in Cerakote or other unintended categories. Now prefers
CategoryCode==POWDER explicitly, with DisplayOrder fallback.
Changes:
- QuoteItemCoat: add PowderCatalogItemId int? — persists catalog reference at quote
save time so the approval path knows what to create
- QuotePricingAssemblyService.BuildQuoteItemCoatsAsync: store PowderCatalogItemId
on coat instead of calling CreateIncomingInventoryItemAsync immediately
- QuotePricingAssemblyService.CreateIncomingInventoryItemAsync: signature changed
from (coatDto, companyId) to (catalogItemId, companyId); category lookup prefers
POWDER code; no longer clears PowderCostPerLb on the DTO
- QuotePricingAssemblyService.EnsureIncomingInventoryForApprovedQuoteAsync: new
public method called at approval — loads pending coats, groups by catalog ID,
creates one inventory item per group, links all coats in each group
- IQuotePricingAssemblyService: exposes EnsureIncomingInventoryForApprovedQuoteAsync
- QuotesController.ApproveQuote: calls EnsureIncomingInventory after save
- QuotesController.ChangeQuoteStatus: calls EnsureIncomingInventory on Approved
- QuoteApprovalController: injects IQuotePricingAssemblyService; calls
EnsureIncomingInventory in ApproveInternal (customer-facing portal path)
- InventoryController.CreateIncomingFromCatalog: same category fix (prefers POWDER)
- Migration: AddPowderCatalogItemIdToCoat (nullable int on QuoteItemCoats)
- Tests: updated AddAsIncoming test to verify deferred behavior; new deduplication test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ClockEntryType enum (Work/Break/Lunch) on EmployeeClockEntry; default 0 = Work
so all existing entries are unaffected
- Migration AddClockEntryType applied
- Break and Lunch buttons on clock status card (only when AllowMultiplePunchesPerDay
is enabled); GoOnBreak closes current Work segment and opens Break/Lunch segment
- Return to Work button when on break/lunch; closes break segment, opens new Work
- Status badges on clock card and Who'\''s In grid: Working / On Break / At Lunch
- Break/Lunch hours excluded from all day totals, week totals, metrics, and CSV
- Manager: Manual Entry modal to create a time entry for any company employee
- Attendance report defaults to current ISO week; Week/Month mode toggle with
auto-submitting dropdowns (last 12 weeks or months); period label shown inline
- Attendance CSV: Type column added; day/week totals blank on Break/Lunch rows;
filename uses period label
- Week subtotal rows suppressed in single-week view (shown in month view only)
- Help article and AI knowledge base updated for all new features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration was scaffolded in the background while the dev server held the PDB lock
and silently failed to write the file. Re-ran and applied now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings tab (Company Settings > Timeclock):
- Enable/disable timeclock toggle (hides nav link and attendance report when off)
- Allow multiple clock-ins per day toggle
- Auto clock-out after X hours (auto-closes forgotten open entries on next punch)
- Kiosk devices table: lists activated tablets with name, activated date, last seen;
Deactivate button removes that device's access immediately
Multi-kiosk support (replaces single TimeclockKioskToken on Company):
- New TimeclockKioskDevice entity (one row per tablet, unique token, DeviceName, LastSeenAt)
- KioskActivate GET shows a form for optional device name before activating
- KioskDeactivate POST accepts device ID, deletes specific row (not all devices)
- Kiosk validation (Kiosk, KioskEmployees, KioskPunch) queries device table with
ignoreQueryFilters since no user is logged in on kiosk requests
- LastSeenAt updated on each Kiosk page load
Enforcement:
- ClockIn and KioskPunch both auto-close stale entries if AutoClockOutHours is set
- ClockIn and KioskPunch both block second same-day punch if AllowMultiplePunches=false
- TimeclockEnabled=false hides nav link (SubscriptionMiddleware sets Items key) and
returns Forbid on kiosk punch
- Migration: AddTimeclockSettings (adds 3 columns to Companies, new TimeclockKioskDevices table)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merging the kiosk PIN script and the role-permissions script into a single
@section Scripts block fixes the InvalidOperationException thrown when Razor
encounters two sections with the same name in the same view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New EmployeeClockEntry entity (facility-level attendance, separate from job time entries)
- KioskPin added to ApplicationUser; TimeclockKioskToken added to Company
- TimeclockController: clock in/out, who's in, 14-day history, manager edit/delete,
tablet kiosk with device-cookie auth, PIN management via Users edit page
- Kiosk UI: employee tile grid + 4-digit PIN pad + auto-detect clock-in vs clock-out
- Attendance report at /Reports/Attendance with weekly subtotal rows
- Payroll CSV export at /Reports/AttendanceCsv (flat, one row per segment)
- AllowCustomFormulas wired through PlatformSubscriptionController + subscription views
- Fix soft-delete bug on CustomItemTemplate (missing HasQueryFilter in OnModelCreating)
- Help article (Help/Timeclock.cshtml) and AI knowledge base updated
- Migrations: AddEmployeeTimeclock, AddTimeclockKioskToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both PricingBreakdown constructions in QuotesController.Details were
missing FacilityOverheadCost and FacilityOverheadRatePerHour mappings,
so the view condition (FacilityOverheadCost > 0) was always false even
though the overhead was correctly stored on the Quote entity and included
in the total. The quote-to-job conversion block already had them; now all
three are consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DataPurgeController was deleting Jobs without first clearing the nullable
Appointments.JobId FK, causing FK_Appointments_Jobs_JobId violations.
Fix nulls out the FK on any linked appointments before the DELETE runs.
Also applies migration AddAllowCustomFormulas (AllowCustomFormulas column
on SubscriptionPlanConfigs for custom formula pricing feature gating).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JobImportDto was missing Description despite Job entity having the field.
Downloadable template now includes a Description column; the importer maps
it directly to Job.Description with a fallback chain of Description ->
SpecialInstructions -> "Imported job".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
The #settingsTabs <button> elements had no explicit background-color,
letting browser UA button styling (white) bleed through in dark mode.
Added transparent overrides so the dark body background shows instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>