Commit Graph

9 Commits

Author SHA1 Message Date
spouliot 8acbc8605d Harden multi-tenant isolation across all user-facing controllers
Added explicit CompanyId == companyId predicates to every tenant-scoped
query in 22 controllers so cross-tenant data leakage is impossible even
if EF Core global query filters are bypassed or misconfigured.

Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true
for SuperAdmins with no CompanyId claim (break-glass accounts) and when
no HTTP context is present (background services, unit tests), resolving
225 unit test failures that stemmed from the global filter blocking all
in-memory test data.

New MultiTenantIsolationTests class (8 tests) verifies the explicit
predicate layer independently of the global query filters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:04:22 -04:00
spouliot 8caaa84eac Hide Start Intake button when kiosk not activated; relabel remote link
- Start Intake button only shows when company has an active kiosk token
- Remote Link button renamed to "Send Intake Link" for clarity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:00:09 -04:00
spouliot 2b89fcf483 Refactor dashboard queries to push filtering and aggregation into the database
DashboardReadService no longer loads full entity lists and filters in memory.
All job panels (today/overdue/in-progress) now execute targeted COUNT + capped
SELECT queries in SQL. AR aging buckets, powder order lines, bill totals, and
active-customer counts are all aggregated at the DB level. The SuperAdmin action
previously loaded every company row to compute plan distribution and alert lists;
it now delegates to a new GetSuperAdminDashboardDataAsync() that uses SQL GROUP BY
and projections instead.

DashboardIndexData record updated to carry pre-sliced counts and capped lists so
the controller only does lightweight DTO projection. DashboardPowderOrderLineData
replaces the deep Job→JobItem→Coat Include chains with a single flat coat query
projected in SQL. OnlineUserMiddleware switches its per-user throttle from a
static ConcurrentDictionary (grows forever) to IMemoryCache with a 60-second
sliding expiry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:00:43 -04:00
spouliot 8de9cd04b8 Add server-side dismiss persistence and SuperAdmin onboarding progress page
Progress widget dismiss now POSTs to Dashboard/DismissProgressWidget, writing
GuidedActivationDismissedAt to the DB so the widget stays hidden across devices
and cache clears (localStorage alone wasn't enough). BuildShopProgressWidgetAsync
suppresses the widget server-side when AllDone + dismissed.

New SuperAdmin page at /OnboardingProgress shows the activation funnel across
all tenant companies: wizard status, chosen path, milestone progress bar, key
dates (first job/quote, first invoice, workflow completed, widget dismissed),
and a status badge (Not Started / In Progress / Complete / Dismissed). Nav link
added under Users & Activity in the Platform Management sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 09:23:20 -04:00
spouliot 45441c1d07 Fix 'Customize your workflow' done signal not detecting deletions
The previous AnyAsync check used global query filters which hide
soft-deleted records. Deleting a lookup sets UpdatedAt on the record
(EF interceptor stamps Modified entities) but the IsDeleted filter
made it invisible to the query. Added ignoreQueryFilters: true with
an explicit CompanyId predicate so soft-deleted lookups are included —
any deletion or edit now correctly marks the step complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:25:30 -04:00
spouliot 64e9abceac Hide team invite step on progress widget for single-user plans
Injects ISubscriptionService into DashboardController and calls
GetUserCountAsync to check the plan's MaxUsers limit. When MaxUsers == 1
the "Bring your crew in" step is omitted from the progress widget entirely,
so solo-plan users aren't prompted to do something their subscription
doesn't allow. Plans with MaxUsers > 1 or unlimited (-1) show the step
as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:22:41 -04:00
spouliot 8aae30765f Onboarding overhaul: slim wizard, progress widget, guided activation UX
Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.

Dashboard progress widget (new): "Get the most out of your shop" checklist
appears for Company Admins after wizard completion. Tracks six post-setup
activation tasks with dynamic progress badge, motivating subtitle copy,
collapsed-state persistence via localStorage, and a full completion state
("Your shop is fully set up 🎉") that replaces the checklist at 100%.
The next recommended step is highlighted with a solid CTA button and a
subtle blue row tint. Completed steps show encouraging green subtext instead
of just "Done". Widget disappears from controller when AllDone would have
caused a silent vanish — now renders the completion state instead.

Guided activation (Daily Board): rewrote the BoardIntroStep callout to lead
with "This is your shop in real time" and a plain-English description of the
board's purpose. Added a separate InstructionText field to
GuidedActivationCalloutViewModel so the "Move this job to the next stage"
action prompt renders as a distinct bold line with an arrow icon rather than
being buried in the body copy. After the stage change, the confirmation
callout now reads "Nice — your workflow just updated" to reinforce what just
happened before prompting the invoice step.

All copy passes the "shop owner, not SaaS" test: no technical jargon,
benefit-driven descriptions, natural language throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:10:47 -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