An out-of-pocket purchase differs from any other receipt only in the
credit account, so the Underlag pane now asks one question for an
unmatched underlag (Företaget / Jag, privat / En anställd / Ingen ännu)
and books a privately paid receipt in place through POST
/api/expense-claims, replacing the "Andra sätt att bokföra" dropdown and
the deep link into the two-step wizard. The verifikat editor stays
reachable below as the escape hatch (BFL 5 kap 6-7 §).
The person owed surfaces in Att göra under a new Betala band, one row per
person (lib/worklist expense_payout, counted in the total and exposed to
agents through the attention resource). The Utlägg nav row is gated on
existing claims, the same hybrid gate as Körjournal, since the entry
point for a new utlägg is now the Underlag pane.
Claude-Session: https://claude.ai/code/session_01P8YsvPqjfGxGZUkGeBVUWQ
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The dashboard layout runs on every hard load, hard refresh, company switch
and the 16 router.refresh() sites, and loading.tsx cannot paint until it
resolves. It cost ~20 network calls in 4 sequential waves: a third
getUser() round trip to Supabase Auth (after the proxy's and the route
guard's), the company resolution, then 16 reads including four limit-1
probes whose only job is to decide whether to render the Webshop and
Körjournal nav rows, and an entitlements read that itself ran two waves.
- lib/auth/claims.ts: claimsPinned/userFromClaims extracted from
require-auth.ts (unchanged) so the dashboard request context shares the
exact pinning + mapping. getDashboardAuthContext verifies the JWT locally
and falls back to getUser() only when claims are missing, unpinned or
unverifiable: the proxy already performed the per-request revocation
check before the layout runs (same semantics approved for routes on
2026-07-23).
- Wave 1 (user-keyed, parallel with the company resolution): team
membership, profile, user preferences and the memberships join, which
now also supplies the active company's row and role, so the separate
companies and company_members reads are gone.
- Wave 2 (company-keyed): settings, agent profile, the switcher's settings
names, entitlements in ONE wave (getCompanyEntitlements takes the
team_id the join already carries and runs the grants read alongside
config + subscription), and get_dashboard_nav_flags().
- supabase/migrations/20260826120000_get_dashboard_nav_flags.sql:
SECURITY INVOKER, STABLE, EXECUTE for authenticated only; RLS applies
inside. lib/dashboard/nav-flags.ts wraps it with the pre-RPC four-probe
fallback on PGRST202/42883/42501 (self-hosted not yet migrated, deploy
ordering) and degrades to hidden rows on any other error.
- tests/pg/dashboard-nav-flags-rpc.pg.test.ts (6): fresh company, active vs
pending WooCommerce, active Shopify, mileage trips, RLS for a member of
another company, EXECUTE grants. Unit tests for the wrapper (RPC row,
single-object payload, each fallback code, other errors) and for the
entitlements teamId option.
~20 calls / 4 waves -> ~12 calls / 2 waves, 0 auth network calls.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>