'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { createClient } from '@/lib/supabase/client' import { createCompanyFromOnboarding } from '@/lib/company/actions' import { computeFiscalPeriod } from '@/lib/company/compute-fiscal-period' import { useToast } from '@/components/ui/use-toast' import { Loader2, Building2 } from 'lucide-react' import { cn } from '@/lib/utils' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import type { CompanyLookupResult } from '@/lib/company-lookup/types' import type { CompanySettings, EntityType, MomsPeriod } from '@/types' import Step1EntityType from '@/components/onboarding/Step1EntityType' import Step2CompanyDetails from '@/components/onboarding/Step2CompanyDetails' import Step3TaxRegistration from '@/components/onboarding/Step3TaxRegistration' import Step4VatAccounting from '@/components/onboarding/Step4VatAccounting' const STEP_INFO = [ { title: 'Företagsform', subtitle: 'Välj din företagsform för att komma igång.' }, { title: 'Uppgifter', subtitle: 'Uppgifterna visas på fakturor och dokument.' }, { title: 'F-skatt & räkenskapsår', subtitle: 'Ange din skatteregistrering och räkenskapsår.' }, { title: 'Moms & bokföring', subtitle: 'Momsregistrering och bokföringsmetod.' }, ] function translatePeriodError(msg: string): string { if (msg.includes('end must be after')) return 'Slutdatumet måste vara efter startdatumet.' if (msg.includes('start must be the 1st')) return 'Startdatumet måste vara den 1:a i en månad.' if (msg.includes('end must be the last day')) return 'Slutdatumet måste vara sista dagen i en månad.' if (msg.includes('exceeds maximum 18 months')) return 'Räkenskapsåret får inte överstiga 18 månader (BFL 3 kap.).' return 'Ogiltigt räkenskapsår. Kontrollera datumen och försök igen.' } const LOG = '[welcome-onboarding]' function logError(message: string, extra?: Record) { console.error(LOG, message, extra ?? '') fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: `welcome-onboarding: ${message}`, extra }), }).catch(() => {}) } interface WelcomeOnboardingProps { firstName?: string | null teamId: string skipWelcome?: boolean hasExistingCompanies?: boolean } export default function WelcomeOnboarding({ firstName, teamId, skipWelcome, hasExistingCompanies }: WelcomeOnboardingProps) { const router = useRouter() const { toast } = useToast() const supabase = createClient() const [started, setStarted] = useState(skipWelcome ?? false) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) const [currentStep, setCurrentStep] = useState(1) const [settings, setSettings] = useState>({}) const ticEnabled = ENABLED_EXTENSION_IDS.has('tic') const [ticLookup, setTicLookup] = useState(null) const totalSteps = 4 const hour = new Date().getHours() const greeting = hour < 5 ? 'God natt' : hour < 10 ? 'Godmorgon' : hour < 14 ? 'Hej' : hour < 18 ? 'God eftermiddag' : 'God kväll' // Pre-fill the user's folkbokföringsadress from BankID/SPAR enrichment when // available. The row is persisted (not consumed here) so /select-company and // repeat /onboarding visits still see it; it's deleted only once a company // is successfully created (see createCompanyFromOnboarding). useEffect(() => { async function loadSparAddress() { const { data: { user } } = await supabase.auth.getUser() if (!user) { router.push('/login') return } try { const { data: enrichmentRow } = await supabase .from('extension_data') .select('value') .eq('user_id', user.id) .eq('extension_id', 'tic') .eq('key', 'bankid_enrichment') .maybeSingle() const spar = (enrichmentRow?.value as { spar?: Record } | null)?.spar if (spar) { setSettings((prev) => ({ ...prev, address_line1: spar.Folkbokforingsadress_SvenskAdress_Utdelningsadress1 || prev.address_line1, postal_code: spar.Folkbokforingsadress_SvenskAdress_PostNr || prev.postal_code, city: spar.Folkbokforingsadress_SvenskAdress_Postort || prev.city, })) } } catch (err) { console.warn(LOG, 'enrichment loading failed (non-blocking)', err) } setIsLoading(false) } loadSparAddress() }, [supabase, router]) const handleNext = async (stepData: Partial) => { if (currentStep === 1 && stepData.entity_type && stepData.entity_type !== settings.entity_type) { stepData = { ...stepData, org_number: '', company_name: '' } setTicLookup(null) } const mergedSettings = { ...settings, ...stepData } // Validate fiscal period at step 3 before advancing if (currentStep === 3) { const periodResult = computeFiscalPeriod(mergedSettings) if (periodResult.error) { toast({ title: 'Ogiltigt räkenskapsår', description: translatePeriodError(periodResult.error), variant: 'destructive', }) return } } // Steps 1-3: collect data client-side only, advance step if (currentStep < totalSteps) { setSettings(mergedSettings) setCurrentStep(currentStep + 1) return } // Step 4 (final): create everything via server action. // Going through a server action ensures that if the Next.js server is // unreachable, nothing touches Supabase — no ghost companies. const periodResult = computeFiscalPeriod(mergedSettings) if (periodResult.error) { toast({ title: 'Ogiltigt räkenskapsår', description: translatePeriodError(periodResult.error), variant: 'destructive', }) return } setIsSaving(true) try { const result = await createCompanyFromOnboarding({ teamId, settings: mergedSettings as Record, fiscalPeriod: { startDate: periodResult.startStr, endDate: periodResult.endStr, name: periodResult.periodName, }, }) if (result.error || !result.companyId) { logError('create company action failed', { error: result.error }) toast({ title: 'Fel', description: result.error || 'Kunde inte skapa företag. Försök igen.', variant: 'destructive', }) return } console.log(LOG, 'onboarding completed', result.companyId) toast({ title: 'Välkommen!', description: 'Ditt företag är nu redo.', }) router.push('/') } catch (err) { const message = err instanceof Error ? err.message : String(err) logError('create company action threw', { error: message }) toast({ title: 'Fel', description: 'Ett oväntat fel uppstod. Försök igen.', variant: 'destructive' }) } finally { setIsSaving(false) } } const handleBack = () => { if (currentStep > 1) { setCurrentStep(currentStep - 1) } } if (isLoading) { return (
) } const stepInfo = STEP_INFO[currentStep - 1] // Welcome screen — show before user clicks "Lägg till ditt första företag" if (!started) { return (

{greeting}

Välkommen till Gnubok

) } return (
{/* Greeting header */}

{greeting}{firstName ? `, ${firstName}` : ''}

{hasExistingCompanies ? 'Lägg till ett företag.' : 'Lägg till ditt första företag för att komma igång.'}

{/* Onboarding card */}
{/* Card header with step info */}
Nytt företag
{STEP_INFO.map((_, i) => { const num = i + 1 return (
currentStep && 'w-3 bg-white/[0.1]', )} /> ) })} {currentStep}/{totalSteps}

{stepInfo.title}

{stepInfo.subtitle}

{/* Form content */}
{currentStep === 1 && ( handleNext(data)} isSaving={isSaving} /> )} {currentStep === 2 && ( handleNext(data)} onBack={handleBack} isSaving={isSaving} /> )} {currentStep === 3 && ( handleNext(data)} onBack={handleBack} isSaving={isSaving} /> )} {currentStep === 4 && ( handleNext(data)} onBack={handleBack} isSaving={isSaving} /> )}
) }