feat(onboarding): activation quick wins on the Hem checklist (#1461)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
ddbe9b1379
commit
44cff5e5e4
@@ -3,6 +3,7 @@ import DashboardContent from '@/components/dashboard/DashboardContent'
|
||||
import { getWorklistCounts, listSuggestedMatches } from '@/lib/worklist'
|
||||
import { listResumeItems } from '@/lib/worklist/resume'
|
||||
import { shouldShowOtherAccountHint } from '@/lib/company/other-account-hint'
|
||||
import { vatDeadlineLine } from '@/lib/onboarding/checklist'
|
||||
import type { OnboardingProgress } from '@/types'
|
||||
import {
|
||||
getDashboardAuthContext,
|
||||
@@ -44,6 +45,9 @@ export default async function DashboardPage() {
|
||||
{ data: bankConnections },
|
||||
{ count: sieImportCount },
|
||||
{ count: skatteverketTokenCount },
|
||||
{ count: inboxItemCount },
|
||||
{ count: postedEntryCount, error: postedEntryError },
|
||||
{ data: nextVatDeadline },
|
||||
{ data: profile },
|
||||
agentProfile,
|
||||
worklist,
|
||||
@@ -61,6 +65,27 @@ export default async function DashboardPage() {
|
||||
// carry the active company_id; either filter would work: we use user_id
|
||||
// because that's what the token-store reads/writes against.
|
||||
supabase.from('skatteverket_tokens').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
|
||||
// Any item ever received in the document inbox (email/WhatsApp/upload)
|
||||
// marks the receipts checklist step done: same "has ever done X" shape
|
||||
// as the other flags above.
|
||||
supabase.from('invoice_inbox_items').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
|
||||
// Posted entries distinguish "brand-new empty ledger" from "all caught
|
||||
// up" in the Att göra empty state (hits the partial posted/reversed index).
|
||||
supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('company_id', companyId).in('status', ['posted', 'reversed']),
|
||||
// Next upcoming momsdeklaration for the checklist's Skatteverket step.
|
||||
// Rows are system-generated per company settings; dismissed rows are
|
||||
// excluded everywhere deadlines are listed, so here too.
|
||||
supabase
|
||||
.from('deadlines')
|
||||
.select('due_date')
|
||||
.eq('company_id', companyId)
|
||||
.in('tax_deadline_type', ['moms_monthly', 'moms_quarterly', 'moms_yearly'])
|
||||
.eq('is_completed', false)
|
||||
.is('dismissed_at', null)
|
||||
.gte('due_date', now.toISOString().slice(0, 10))
|
||||
.order('due_date', { ascending: true })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
// First name for the greeting.
|
||||
supabase.from('profiles').select('full_name').eq('id', user.id).maybeSingle(),
|
||||
getResolvedDashboardAgentProfile(),
|
||||
@@ -98,8 +123,22 @@ export default async function DashboardPage() {
|
||||
hasBankConnected: (bankConnections?.length || 0) > 0 || (transactionCount || 0) > 0,
|
||||
hasSIEImport: (sieImportCount || 0) > 0,
|
||||
hasSkatteverketConnected: (skatteverketTokenCount || 0) > 0,
|
||||
hasInboxItems: (inboxItemCount || 0) > 0,
|
||||
}
|
||||
|
||||
const vatLine = vatDeadlineLine({
|
||||
vatRegistered: settings.vat_registered,
|
||||
momsPeriod: settings.moms_period ?? null,
|
||||
nextVatDueDate: nextVatDeadline?.due_date ?? null,
|
||||
})
|
||||
|
||||
// "Empty ledger" only matters while the setup checklist is still open; once
|
||||
// it is completed or dismissed the ordinary all-clear copy applies. A failed
|
||||
// count must NOT read as empty: that would tell a company with real
|
||||
// bookkeeping that its ledger is blank, so errors degrade to the normal copy.
|
||||
const setupOpen = !settings.initial_setup_completed_at && !settings.initial_setup_dismissed_at
|
||||
const emptyLedger = setupOpen && !postedEntryError && (postedEntryCount || 0) === 0
|
||||
|
||||
const nowMs = now.getTime()
|
||||
const expiringBankConnections = (bankConnections || [])
|
||||
.filter(conn => {
|
||||
@@ -135,6 +174,8 @@ export default async function DashboardPage() {
|
||||
completedAt: settings.initial_setup_completed_at ?? null,
|
||||
dismissedAt: settings.initial_setup_dismissed_at ?? null,
|
||||
}}
|
||||
vatLine={vatLine}
|
||||
emptyLedger={emptyLedger}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const getDashboardSettings = cache(async () => {
|
||||
|
||||
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')
|
||||
.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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user