'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { Check } from 'lucide-react' import { Badge } from '@/components/ui/badge' 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 thread (get the books in, connect the bank, connect * Skatteverket, build the assistant) on a hairline spine. * Only the step you are on argues its case: it carries the description and * the partner marks next to a filled action. Steps you have not reached yet * drop the pitch but keep a quiet outline action, so any step stays one * click away (connect Skatteverket before the bank, if that is your order). * Done steps collapse to their title. * 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 const stepCount = hasSkatteverket ? 4 : 3 return (

{t('title', { count: stepCount })}

    ( )} marks={ <> {t('step_books_sie')} } > {t('step_books_description')}{' '} {' '} {t('step_books_fresh_suffix')} ( )} marks={ hasBanking ? ( ) : undefined } > {t('step_bank_description')} {hasSkatteverket && ( ( )} marks={} > {t('step_skv_description')} )} ( )} > {t('step_assistant_description')}
) } /** * One numbered step on the thread: dot + hairline down to the next step. * `children` (the pitch) and `marks` (partner logos) render only while this * is the step the user is on. `action` renders on every step that is not * done: filled on the active step, outline on the ones further down, so no * step is ever unreachable just because it is not next in line. * The title row is a constant 36px so it matches the h-9 action button and * the dot (mt-1) centres in it; the spine then runs dot-bottom to dot-top * (top-8 to -bottom-1) without visible breaks. */ function Step({ number, done, active, title, badge, action, marks, last = false, children, }: { number: number done: boolean active: boolean title: string badge?: string action?: (variant: 'default' | 'outline') => React.ReactNode marks?: React.ReactNode last?: boolean children: React.ReactNode }) { const open = active && !done 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} ) }