* feat(mileage): surface Körjournal in the nav behind a settings toggle The /mileage page shipped hidden: the route works but no nav row points at it. Add company_settings.mileage_enabled (mirroring dimensions_enabled) with a switch in Fönster -> Bokföring, and show the Arbeta nav row when the toggle is on OR the company already has mileage_trips rows, the same hybrid gate as webshop orders, so trips created via API/MCP can never become invisible underlag. UI visibility only, never load-bearing for correctness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): move mileage_enabled migration after already-applied 20260812153208 origin/main merged in 20260812153208 which prod has already applied; a new file sorting before it risks an out-of-order db push abort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
import 'server-only'
|
|
|
|
import { cache } from 'react'
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { getActiveCompanyId } from '@/lib/company/context'
|
|
import { ensureSandboxAgentProfile } from '@/lib/sandbox/ensure-agent'
|
|
|
|
/**
|
|
* Request-local dashboard auth context. React cache shares the Supabase client
|
|
* and auth lookup between the dashboard layout and every nested server page.
|
|
*/
|
|
export const getDashboardAuthContext = cache(async () => {
|
|
const supabase = await createClient()
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser()
|
|
|
|
return { supabase, user }
|
|
})
|
|
|
|
/**
|
|
* Request-local active company resolution. Nested layouts and pages commonly
|
|
* need the same value, so resolving it once removes repeated preference and
|
|
* membership round trips without caching anything across requests.
|
|
*/
|
|
export const getDashboardCompanyId = cache(async () => {
|
|
const { supabase, user } = await getDashboardAuthContext()
|
|
return user ? getActiveCompanyId(supabase, user.id) : null
|
|
})
|
|
|
|
export const getDashboardSettings = cache(async () => {
|
|
const [{ supabase }, companyId] = await Promise.all([
|
|
getDashboardAuthContext(),
|
|
getDashboardCompanyId(),
|
|
])
|
|
if (!companyId) return { data: null, error: null }
|
|
|
|
return supabase
|
|
.from('company_settings')
|
|
.select('company_name, onboarding_complete, entity_type, pays_salaries, is_sandbox, dimensions_enabled, mileage_enabled, ore_rounding, initial_setup_path, initial_setup_completed_at, initial_setup_dismissed_at, vat_registered, moms_period')
|
|
.eq('company_id', companyId)
|
|
.maybeSingle()
|
|
})
|
|
|
|
const getDashboardAgentProfile = cache(async () => {
|
|
const [{ supabase }, companyId] = await Promise.all([
|
|
getDashboardAuthContext(),
|
|
getDashboardCompanyId(),
|
|
])
|
|
if (!companyId) return { data: null, error: null }
|
|
|
|
return supabase
|
|
.from('agent_profiles')
|
|
.select('display_name, avatar_id, verified_at')
|
|
.eq('company_id', companyId)
|
|
.maybeSingle()
|
|
})
|
|
|
|
export const getResolvedDashboardAgentProfile = cache(async () => {
|
|
const [{ supabase }, companyId, settingsResult, profileResult] = await Promise.all([
|
|
getDashboardAuthContext(),
|
|
getDashboardCompanyId(),
|
|
getDashboardSettings(),
|
|
getDashboardAgentProfile(),
|
|
])
|
|
|
|
let profile = profileResult.data
|
|
if (companyId && settingsResult.data?.is_sandbox === true && !profile?.verified_at) {
|
|
await ensureSandboxAgentProfile(supabase, companyId)
|
|
const refreshed = await supabase
|
|
.from('agent_profiles')
|
|
.select('display_name, avatar_id, verified_at')
|
|
.eq('company_id', companyId)
|
|
.maybeSingle()
|
|
profile = refreshed.data ?? profile
|
|
}
|
|
|
|
return profile
|
|
})
|