'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { createCompanyFromOnboarding } from '@/lib/company/actions' import { computeFiscalPeriod } from '@/lib/company/compute-fiscal-period' import { useToast } from '@/components/ui/use-toast' import { Building2 } from 'lucide-react' import { cn } from '@/lib/utils' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { getBranding } from '@/lib/branding/service' const branding = getBranding() 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 /** Pre-fill Step 2 org_number when the picker routed here via ?org_number=. */ initialOrgNumber?: string } export default function WelcomeOnboarding({ firstName, teamId, skipWelcome, hasExistingCompanies, initialOrgNumber, }: WelcomeOnboardingProps) { const router = useRouter() const { toast } = useToast() const [started, setStarted] = useState(skipWelcome ?? false) const [isSaving, setIsSaving] = useState(false) const [currentStep, setCurrentStep] = useState(1) const [settings, setSettings] = useState>( initialOrgNumber ? { org_number: initialOrgNumber } : {}, ) 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' const handleNext = async (stepData: Partial) => { // Reset org_number/company_name only on a genuine change (user going back // and picking a different entity type). First-time selection must not // wipe a pre-fill (e.g. ?org_number= deep-link from /select-company). if ( currentStep === 1 && stepData.entity_type && settings.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 }) let title = 'Fel' let description: string = result.error || 'Kunde inte skapa företag. Försök igen.' let backToStep2 = false if (result.error === 'org_number_exists') { title = 'Företaget finns redan' description = `Det här företaget finns redan i ${branding.appName.toLowerCase()}. Be en befintlig administratör att bjuda in dig.` backToStep2 = true } else if (result.error === 'org_number_invalid') { title = 'Ogiltigt organisationsnummer' description = 'Kontrollera att du angett ett giltigt 10- eller 12-siffrigt organisationsnummer.' backToStep2 = true } toast({ title, description, variant: 'destructive', }) // Back user up to step 2 so they can correct the org number. if (backToStep2) { setCurrentStep(2) } 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) } } 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 {branding.appName}

) } 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} /> )}
) }