'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { Check } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { useErrorToast } from '@/lib/hooks/use-error-toast' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { useCapability } from '@/contexts/CompanyContext' import { CAPABILITY } from '@/lib/entitlements/keys' import type { InitialSetupPath, InitialSetupState } from '@/types' interface NewUserChecklistProps { initialState: InitialSetupState className?: string hasBookkeepingImported?: boolean hasBankConnected?: boolean hasSkatteverketConnected?: boolean hasAgentBuilt?: boolean } /** * First-run getting-started block on Hem, in the founder-picked stepped * shape: a numbered three-step thread (get the books in, connect the bank, * build the assistant) with the partner marks on the steps that have them. * The persisted state machine is unchanged (path / completedAt / * dismissedAt via /api/onboarding/state); "Starta från början" records the * fresh path and simply checks off step one. */ export default function NewUserChecklist({ initialState, className, hasBookkeepingImported = false, hasBankConnected = false, hasSkatteverketConnected = false, hasAgentBuilt = false, }: NewUserChecklistProps) { const t = useTranslations('initial_setup') const router = useRouter() const showError = useErrorToast() const hasAi = useCapability(CAPABILITY.ai) const [state, setState] = useState(initialState) const [saving, setSaving] = useState(null) const hasMigration = ENABLED_EXTENSION_IDS.has('arcim-migration') const hasBanking = ENABLED_EXTENSION_IDS.has('enable-banking') const hasSkatteverket = ENABLED_EXTENSION_IDS.has('skatteverket') const persist = async ( body: Record, pending: InitialSetupPath | 'dismiss' | 'complete', ): Promise => { setSaving(pending) try { const response = await fetch('/api/onboarding/state', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!response.ok) { await showError(response, { context: 'settings' }) return null } const payload = await response.json() as { data: InitialSetupState } setState(payload.data) return payload.data } catch (error) { await showError(error, { context: 'settings' }) return null } finally { setSaving(null) } } const step1Done = hasBookkeepingImported || state.path === 'fresh' const step2Done = hasBankConnected // Companies built without the skatteverket extension skip that step. const step3Done = !hasSkatteverket || hasSkatteverketConnected const step4Done = hasAgentBuilt useEffect(() => { // The block retires itself once every step is done; Dölj remains the // manual way out. if (!state.completedAt && step1Done && step2Done && step3Done && step4Done && saving === null) { void persist({ completed: true }, 'complete') } // persist intentionally stays out: its identity follows the toast hook and // would retrigger this completion sync after every render. // eslint-disable-next-line react-hooks/exhaustive-deps }, [step1Done, step2Done, step3Done, step4Done, saving, state.completedAt]) if (state.dismissedAt || state.completedAt) return null const goMigration = async () => { const updated = await persist({ path: 'migration' }, 'migration') if (updated) router.push(hasMigration ? '/import?mode=migration' : '/import?mode=sie') } const goFresh = () => void persist({ path: 'fresh' }, 'fresh') const goBank = async () => { const updated = await persist({ path: state.path ?? 'bank' }, 'bank') if (updated) router.push(hasBanking ? '/import?mode=psd2' : '/import?mode=bank') } const activeStep = !step1Done ? 1 : !step2Done ? 2 : !step3Done ? 3 : 4 return (

{t('title', { count: hasSkatteverket ? 4 : 3 })}

{t('step_books_description')}{' '} {' '} {t('step_books_fresh_suffix')}

{t('step_books_sie')}

{t('step_bank_description')}

{hasBanking && ( )}
{hasSkatteverket && (

{t('step_skv_description')}

)}

{t('step_assistant_description')}

) } /** One numbered step on the thread: dot + hairline down to the next step. */ function Step({ number, done, active, title, badge, last = false, children, }: { number: number done: boolean active: boolean title: string badge?: string last?: boolean children: React.ReactNode }) { return (
{!last && (
) } /** Tiny partner mark: real logo on a white chip; `mono` darkens a white * source logo in light mode and lifts it in dark (Enable Banking). */ function LogoMark({ src, name, mono = false }: { src: string; name: string; mono?: boolean }) { return ( {/* eslint-disable-next-line @next/next/no-img-element */} {name} ) }