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
@@ -15,6 +15,7 @@ import { visibleWorklistTotal } from '@/lib/worklist/visible-total'
|
||||
import {
|
||||
ArrowLeftRight,
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
CalendarClock,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
@@ -52,6 +53,12 @@ interface AttGoraSectionProps {
|
||||
worklist: WorklistCounts
|
||||
suggestedMatches: SuggestedMatch[]
|
||||
expiringBankConnections?: ExpiringBankConnection[]
|
||||
/**
|
||||
* True while the setup checklist is open and the company has zero posted
|
||||
* journal entries. An empty ledger is not an achievement: the all-clear
|
||||
* state then says "nothing here yet" instead of a false "all caught up".
|
||||
*/
|
||||
emptyLedger?: boolean
|
||||
}
|
||||
|
||||
interface WorklistRowProps {
|
||||
@@ -99,6 +106,7 @@ export default function AttGoraSection({
|
||||
worklist,
|
||||
suggestedMatches,
|
||||
expiringBankConnections = [],
|
||||
emptyLedger = false,
|
||||
}: AttGoraSectionProps) {
|
||||
const t = useTranslations('dashboard')
|
||||
const { toast } = useToast()
|
||||
@@ -112,6 +120,9 @@ export default function AttGoraSection({
|
||||
const [matches, setMatches] = useState(suggestedMatches)
|
||||
const [leavingIds, setLeavingIds] = useState<Set<string>>(new Set())
|
||||
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
||||
// A confirmed match books a journal entry, so the server-derived
|
||||
// emptyLedger flag goes stale the moment one succeeds in this session.
|
||||
const [postedSinceLoad, setPostedSinceLoad] = useState(false)
|
||||
|
||||
async function refetchCounts() {
|
||||
try {
|
||||
@@ -158,6 +169,7 @@ export default function AttGoraSection({
|
||||
return
|
||||
}
|
||||
toast({ title: t('suggested_confirmed_toast') })
|
||||
setPostedSinceLoad(true)
|
||||
// Fade the row out, drop it, then re-sync every count from the source
|
||||
// of truth (the match also booked a transaction, so several numbers move).
|
||||
setLeavingIds((prev) => new Set(prev).add(match.transaction_id))
|
||||
@@ -206,18 +218,31 @@ export default function AttGoraSection({
|
||||
<div className="flex items-baseline justify-between border-b border-border px-1 pb-2.5">
|
||||
<h2 className="font-sans text-sm font-medium">{t('att_gora_title')}</h2>
|
||||
<p className="text-xs text-muted-foreground tabular-nums" role="status" aria-live="polite">
|
||||
{allClear ? t('all_done') : t('att_gora_left', { count: displayTotal })}
|
||||
{allClear
|
||||
? emptyLedger && !postedSinceLoad
|
||||
? t('att_gora_new_status')
|
||||
: t('all_done')
|
||||
: t('att_gora_left', { count: displayTotal })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{allClear ? (
|
||||
<EmptyState
|
||||
icon={CheckCircle2}
|
||||
title={t('att_gora_empty_title')}
|
||||
description={t('att_gora_empty_body')}
|
||||
className="py-10"
|
||||
/>
|
||||
emptyLedger && !postedSinceLoad ? (
|
||||
<EmptyState
|
||||
icon={BookOpen}
|
||||
title={t('att_gora_new_title')}
|
||||
description={t('att_gora_new_body')}
|
||||
className="py-10"
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={CheckCircle2}
|
||||
title={t('att_gora_empty_title')}
|
||||
description={t('att_gora_empty_body')}
|
||||
className="py-10"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="pb-2">
|
||||
{bokforRows && (
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ArrowRight } from 'lucide-react'
|
||||
import type { InitialSetupState, OnboardingProgress } from '@/types'
|
||||
import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types'
|
||||
import type { ResumeItem } from '@/lib/worklist/resume'
|
||||
import type { VatDeadlineLine } from '@/lib/onboarding/checklist'
|
||||
|
||||
interface DashboardContentProps {
|
||||
companyId: string
|
||||
@@ -48,6 +49,14 @@ interface DashboardContentProps {
|
||||
* full-screen onboarding takeover.
|
||||
*/
|
||||
agentBuilt?: boolean
|
||||
/** Personalized VAT-deadline line for the checklist's Skatteverket step. */
|
||||
vatLine?: VatDeadlineLine
|
||||
/**
|
||||
* True while the setup checklist is still open and the company has zero
|
||||
* posted journal entries: Att göra's all-clear then reads as "empty, get
|
||||
* started" instead of a false "all caught up".
|
||||
*/
|
||||
emptyLedger?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +77,8 @@ export default function DashboardContent({
|
||||
onboardingProgress,
|
||||
initialSetup,
|
||||
agentBuilt = true,
|
||||
vatLine = null,
|
||||
emptyLedger = false,
|
||||
}: DashboardContentProps) {
|
||||
const t = useTranslations('dashboard')
|
||||
const hasAi = useCapability(CAPABILITY.ai)
|
||||
@@ -123,14 +134,16 @@ export default function DashboardContent({
|
||||
hasBookkeepingImported={!!onboardingProgress?.hasSIEImport}
|
||||
hasBankConnected={!!onboardingProgress?.hasBankConnected}
|
||||
hasSkatteverketConnected={!!onboardingProgress?.hasSkatteverketConnected}
|
||||
hasInboxItems={!!onboardingProgress?.hasInboxItems}
|
||||
hasAgentBuilt={agentBuilt}
|
||||
vatLine={vatLine}
|
||||
/>
|
||||
|
||||
{/* Build-assistant hero: shown only until the company has a verified
|
||||
agent_profile, so existing/migrated users get a clear prompt instead
|
||||
of a full-screen onboarding takeover. While the stepped first-run
|
||||
checklist is visible it already carries the assistant as step 3, so
|
||||
the hero waits until that block is dismissed or completed. */}
|
||||
checklist is visible it already carries the assistant as its last
|
||||
step, so the hero waits until that block is dismissed or completed. */}
|
||||
{!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && (
|
||||
<section>
|
||||
{/* Non-payers keep seeing the hero (conversion surface) but it
|
||||
@@ -170,6 +183,7 @@ export default function DashboardContent({
|
||||
worklist={worklist}
|
||||
suggestedMatches={suggestedMatches}
|
||||
expiringBankConnections={expiringBankConnections}
|
||||
emptyLedger={emptyLedger}
|
||||
/>
|
||||
<ResumePane items={resumeItems} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user