* feat(onboarding): activation quick wins on the Hem checklist First implementation slice of the approved activation concept (artifact de543d57, dev_docs/onboarding_activation_analysis.md §9): - New "Kvitton och underlag" checklist step, gated on the invoice-inbox extension like the Skatteverket step; done once the company has ever received an inbox item (email/WhatsApp/upload). Non-AI companies route to billing, matching the assistant step. - Personalized VAT line in the Skatteverket step: the company's real next momsdeklaration due date from the deadlines table, with an explicit "välj momsperiod" prompt when vat_registered is set but moms_period is null (that state silently generates zero VAT deadlines). - Truthful Att göra empty state: while the setup checklist is open and no journal entry is posted, the all-clear reads "Bokföringen är tom än" instead of a false "Allt klart!". - Activation funnel events (onboarding_setup_step_started / _completed / _dismissed) via posthog-js, mirroring the existing guarded capture pattern; sandbox never renders the block so no extra gate is needed. Pure helpers live in lib/onboarding/checklist.ts with tests; step numbering now adapts to both optional extensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the receipts-signal and moms-period-guard decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(onboarding): review triage: error-safe emptyLedger, stale-state guard, copy - A failed posted-entries count no longer reads as an empty ledger. - Confirming a suggested match books an entry, so the empty-ledger copy retires for the rest of the session (postedSinceLoad). - 'Bokföringen är tom än så länge' reads naturally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.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, 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
|
|
})
|