'use client' import { useState, useCallback, useEffect } from 'react' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { useToast } from '@/components/ui/use-toast' import { cn } from '@/lib/utils' import { ConfirmationDialog } from '@/components/ui/confirmation-dialog' import Link from 'next/link' import { FallbackPrompt } from '@/components/ui/fallback-prompt' import { ArrowLeft, ArrowRight, Loader2, AlertCircle, CheckCircle, Building2, Users, Truck, FileText, Database, ExternalLink, Info, RotateCcw, AlertTriangle, ChevronDown, ChevronRight, Calendar, XCircle, BookOpen, } from 'lucide-react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' type ArcimProvider = 'fortnox' | 'visma' | 'briox' | 'bokio' | 'bjornlunden' const ARCIM_PROVIDERS: { id: ArcimProvider; name: string; authType: 'oauth' | 'token' }[] = [ { id: 'fortnox', name: 'Fortnox', authType: 'oauth' }, { id: 'visma', name: 'Visma eEkonomi', authType: 'oauth' }, { id: 'bokio', name: 'Bokio', authType: 'token' }, { id: 'bjornlunden', name: 'Björn Lundén', authType: 'token' }, { id: 'briox', name: 'Briox', authType: 'token' }, ] interface MigrationResults { companyInfo?: { imported: boolean } customers?: { total: number; imported: number; skipped: number } suppliers?: { total: number; imported: number; skipped: number } salesInvoices?: { total: number; imported: number; skipped: number } supplierInvoices?: { total: number; imported: number; skipped: number } } import AccountMappingStep from '@/components/import/AccountMappingStep' import type { AccountMapping, ImportResult, ParsedSIEFile } from '@/lib/import/types' import type { BASAccount } from '@/types' // ── Types ──────────────────────────────────────────────────────── type WizardStep = 'provider' | 'connect' | 'preview' | 'mapping' | 'options' | 'migrating' | 'result' const STEPS: WizardStep[] = ['provider', 'connect', 'preview', 'mapping', 'options', 'migrating', 'result'] const STEP_LABELS: Record = { provider: 'Välj system', connect: 'Anslut', preview: 'Förhandsgranskning', mapping: 'Kontomappning', options: 'Alternativ', migrating: 'Migrerar', result: 'Resultat', } const MONTH_NAMES = [ 'Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December', ] interface MigrationOptions { importCompanyInfo: boolean importSIEData: boolean importCustomers: boolean importSuppliers: boolean importSalesInvoices: boolean importSupplierInvoices: boolean voucherSeries: string } const DEFAULT_OPTIONS: MigrationOptions = { importCompanyInfo: true, importSIEData: true, importCustomers: true, importSuppliers: true, importSalesInvoices: true, importSupplierInvoices: true, voucherSeries: 'B', } interface PreviewData { consent: { id: string provider: ArcimProvider status: number companyName?: string } companyInfo: { company_name: string | null org_number: string | null vat_number: string | null fiscal_year_start_month: number address_line1: string | null postal_code: string | null city: string | null phone: string | null email: string | null } | null sieAvailable: boolean sieStats: { accountCount: number transactionCount: number fiscalYears: number[] } | null } interface SIEData { parsed: ParsedSIEFile mappings: AccountMapping[] mappingStats: { total: number; mapped: number; unmapped: number } rawContent: string[] basAccounts: BASAccount[] } // ── Provider selection step ────────────────────────────────────── const COMING_SOON_PROVIDERS = new Set(['visma', 'bjornlunden', 'briox']) const PROVIDER_LOGOS: Record = { fortnox: '/logos/fortnox.svg', visma: '/logos/visma.jpeg', bokio: '/logos/bokio.png', bjornlunden: '/logos/bjornlunden.png', briox: '/logos/Briox_logo.png', } function ProviderStep({ onSelect }: { onSelect: (provider: ArcimProvider) => void }) { return (
Välj ditt nuvarande bokföringssystem Vi hämtar bokföringsdata via SIE och kunder, leverantörer och fakturor via API:et.
{ARCIM_PROVIDERS.map((provider) => { const comingSoon = COMING_SOON_PROVIDERS.has(provider.id) return ( ) })}
) } // ── Connect step (OAuth redirect or token input) ──────────────── function ConnectStep({ provider, authType, isLoading, error, authUrl, consentId, onTokenSubmit, onBack, }: { provider: ArcimProvider authType: 'oauth' | 'token' | null isLoading: boolean error: string | null authUrl: string | null consentId: string | null onTokenSubmit: (apiToken: string, companyId: string) => void onBack: () => void }) { const providerName = ARCIM_PROVIDERS.find(p => p.id === provider)?.name ?? provider const [apiToken, setApiToken] = useState('') const [companyId, setCompanyId] = useState('') // BL uses server-side client credentials — only needs company ID, no API key const isClientCredentials = provider === 'bjornlunden' const needsApiToken = !isClientCredentials const needsCompanyId = provider === 'bokio' || provider === 'bjornlunden' const tokenDescription = isClientCredentials ? `Ange ditt företags-ID (GUID) från Björn Lundén. gnubok ansluter automatiskt via sin integrationspartner-åtkomst.` : `Ange din API-nyckel från ${providerName} för att ge gnubok tillgång att läsa din bokföringsdata.` const tokenHelpText = isClientCredentials ? `Hittas i Björn Lundén under Inställningar \u2192 Företagsinformation (GUID-format).` : provider === 'bokio' ? `Du hittar din API-nyckel i ${providerName} under Inställningar \u2192 Integrationer \u2192 API. Ditt företags-ID är det GUID som syns i URL:en när du är inloggad, t.ex. https://app.bokio.se/ditt-företags-id/settings-r/private-integrations.` : `Du hittar din applikationstoken i ${providerName} under Administration \u2192 Integrationer.` const canSubmit = isClientCredentials ? !!companyId : !!(apiToken && (!needsCompanyId || companyId)) return (
Anslut till {providerName} {authType === 'token' ? tokenDescription : `Logga in i ${providerName} för att ge gnubok tillgång att läsa din bokföringsdata.` } {isLoading && (

Förbereder anslutning...

)} {error && ( <>

Anslutning misslyckades

{error}

{provider === 'fortnox' && (

Obs: Fortnox kräver ett aktivt integrationstillägg (tillkostnadsbelagd tilläggstjänst) för att kunna använda integrationer. Kontrollera att detta är aktiverat i ditt Fortnox-konto.

)}
)} {/* OAuth flow */} {authType === 'oauth' && authUrl && !isLoading && (

Klicka nedan för att logga in i {providerName} i ett nytt fönster. När du är klar skickas du tillbaka hit automatiskt.

)} {/* Token-based flow */} {authType === 'token' && consentId && !isLoading && (

{tokenHelpText}

{needsApiToken && (
setApiToken(e.target.value)} />
)} {needsCompanyId && (
setCompanyId(e.target.value)} />
)}
)}
) } // ── Preview step ──────────────────────────────────────────────── function PreviewStep({ preview, isLoading, error, onContinue, onBack, }: { preview: PreviewData | null isLoading: boolean error: string | null onContinue: () => void onBack: () => void }) { const providerName = preview ? ARCIM_PROVIDERS.find(p => p.id === preview.consent.provider)?.name ?? preview.consent.provider : '' return (
Anslutet till {providerName} {isLoading && (

Hämtar bokföringsdata...

)} {error && ( <>

{error}

)} {/* SIE stats summary */} {preview?.sieAvailable && preview.sieStats && (

Hittade {preview.sieStats.accountCount} konton och {preview.sieStats.transactionCount} verifikationer

{preview.sieStats.fiscalYears.length === 1 ? `Räkenskapsår ${preview.sieStats.fiscalYears[0]}` : `${preview.sieStats.fiscalYears.length} räkenskapsår: ${preview.sieStats.fiscalYears.join(', ')}` }

)} {preview && !preview.sieAvailable && !isLoading && (

SIE-hämtning inte tillgänglig

SIE-hämtning är inte tillgänglig för denna leverantör ännu. Du kan importera SIE-filen manuellt via SIE-importen.

)}
) } function InfoItem({ label, value }: { label: string; value: string | null }) { return (

{label}

{value || '—'}

) } // ── Mapping step (wraps AccountMappingStep) ───────────────────── function MappingStep({ sieData, isLoading, error, onMappingChange, onContinue, onBack, }: { sieData: SIEData | null isLoading: boolean error: string | null onMappingChange: (sourceAccount: string, targetAccount: string, targetName: string) => void onContinue: () => void onBack: () => void }) { if (isLoading) { return (

Analyserar bokföringsdata och förbereder kontomappning...

) } if (error) { return (

Kunde inte ladda SIE-data

{error}

) } if (!sieData) return null return ( ) } // ── Options step ──────────────────────────────────────────────── function OptionsStep({ options, sieAvailable, onChange, onStart, onBack, }: { options: MigrationOptions sieAvailable: boolean onChange: (options: MigrationOptions) => void onStart: () => void onBack: () => void }) { const [showConfirm, setShowConfirm] = useState(false) const toggleOption = (key: keyof MigrationOptions) => { onChange({ ...options, [key]: !options[key] }) } const selectedItems: string[] = [] if (options.importCompanyInfo) selectedItems.push('Företagsinformation') if (sieAvailable && options.importSIEData) selectedItems.push('Bokföringsdata (SIE)') if (options.importCustomers) selectedItems.push('Kunder') if (options.importSuppliers) selectedItems.push('Leverantörer') if (options.importSalesInvoices) selectedItems.push('Kundfakturor') if (options.importSupplierInvoices) selectedItems.push('Leverantörsfakturor') return (
Vad vill du importera? Bokföringsdata importeras via SIE-fil. Kunder, leverantörer och fakturor hämtas via API:et. } label="Företagsinformation" description="Namn, organisationsnummer, adress" checked={options.importCompanyInfo} onChange={() => toggleOption('importCompanyInfo')} /> {sieAvailable && ( <> } label="Bokföringsdata (SIE)" description="Kontoplan, ingående balanser och verifikationer" checked={options.importSIEData} onChange={() => toggleOption('importSIEData')} /> {options.importSIEData && (

Verifikationsserie

Serie för importerade verifikationer

onChange({ ...options, voucherSeries: e.target.value.toUpperCase() || 'B' })} maxLength={2} />
)} )} } label="Kunder" description="Kund-register med kontaktuppgifter" checked={options.importCustomers} onChange={() => toggleOption('importCustomers')} /> } label="Leverantörer" description="Leverantör-register med bankuppgifter" checked={options.importSuppliers} onChange={() => toggleOption('importSuppliers')} /> } label="Kundfakturor (öppna)" description="Obetalda kundfakturor" checked={options.importSalesInvoices} onChange={() => toggleOption('importSalesInvoices')} /> } label="Leverantörsfakturor (öppna)" description="Obetalda leverantörsfakturor" checked={options.importSupplierInvoices} onChange={() => toggleOption('importSupplierInvoices')} />
{ setShowConfirm(false) onStart() }} isSubmitting={false} title="Starta migrering" warningText="Bokföringsdata, kunder, leverantörer och fakturor importeras till gnubok. Se till att ingen annan import pågår." confirmLabel="Starta migrering" >

Följande importeras:

    {selectedItems.map((item) => (
  • {item}
  • ))}
) } function OptionRow({ icon, label, description, checked, onChange, }: { icon: React.ReactNode label: string description: string checked: boolean onChange: () => void }) { return (
{icon}

{label}

{description}

e.stopPropagation()} />
) } // ── Migrating step (progress) ─────────────────────────────────── function MigratingStep({ currentStep, progress }: { currentStep: string; progress: number }) { return ( Migrering pågår Vänta medan vi hämtar och importerar din bokföringsdata. Det kan ta några minuter.
{progress}%

{currentStep}

) } // ── Result step ───────────────────────────────────────────────── /** Format a fiscal year label from ISO dates, e.g. "2024-01-01" → "2024" or "2024/2025" */ function formatFiscalYearLabel(start: string, end: string): string { const startYear = start.slice(0, 4) const endYear = end.slice(0, 4) return startYear === endYear ? startYear : `${startYear}/${endYear}` } /** Determine the overall status icon and color for a single FY import */ function getFYStatus(r: ImportResult): { icon: 'success' | 'warning' | 'error'; label: string } { if (r.errors.length > 0 && r.journalEntriesCreated === 0) { return { icon: 'error', label: 'Misslyckades' } } if (r.errors.length > 0 || (r.details?.skippedVouchers && r.details.skippedVouchers.total > 0)) { return { icon: 'warning', label: 'Delvis importerad' } } return { icon: 'success', label: 'Importerad' } } const StatusIcon = ({ status }: { status: 'success' | 'warning' | 'error' }) => { if (status === 'error') return if (status === 'warning') return return } /** Expandable per-fiscal-year detail card */ function FiscalYearResult({ result, index }: { result: ImportResult; index: number }) { const [expanded, setExpanded] = useState(false) const status = getFYStatus(result) const d = result.details const fyLabel = d?.fiscalYear ? formatFiscalYearLabel(d.fiscalYear.start, d.fiscalYear.end) : `Räkenskapsår ${index + 1}` return (
{/* Header — always visible */} {/* Expanded details */} {expanded && (
{/* Errors — shown prominently */} {result.errors.length > 0 && (

{result.errors.length === 1 ? '1 fel vid import' : `${result.errors.length} fel vid import`}

{result.errors.map((e, i) => (

{e}

))}
)} {/* Opening balance adjustment */} {d?.openingBalance && (

Ingående balanser justerade

{d.openingBalance.explanation === 'unallocated_result' && ( <> Differens på {Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK bokförd på konto {d.openingBalance.bookedToAccount}. Detta beror troligen på att föregående års resultat inte allokerats till eget kapital i källsystemet — vanligt vid byte av bokföringsprogram. )} {d.openingBalance.explanation === 'excluded_accounts' && ( <> Exkluderade systemkonton (t.ex. Fortnox 0099) hade ingående saldon. Differensen ({Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK) bokförd på konto {d.openingBalance.bookedToAccount}. )} {d.openingBalance.explanation === 'rounding' && ( <> Avrundningsdifferens ({Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK) bokförd på konto {d.openingBalance.bookedToAccount}. )} {!d.openingBalance.explanation && ( <> Differens på {Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK bokförd på konto {d.openingBalance.bookedToAccount}. )}

)} {/* Skipped vouchers breakdown */} {d?.skippedVouchers && d.skippedVouchers.total > 0 && (

{d.skippedVouchers.total} verifikationer hoppades över

Ofullständiga verifikationer i källsystemet som inte kan importeras. Saldon har justerats automatiskt via omföringsverifikation.

{d.skippedVouchers.unbalanced > 0 && (
Obalanserade {d.skippedVouchers.unbalanced}
)} {d.skippedVouchers.unmapped > 0 && (
Ej mappade konton {d.skippedVouchers.unmapped}
)} {d.skippedVouchers.singleLine > 0 && (
Enradsverifikationer {d.skippedVouchers.singleLine}
)} {d.skippedVouchers.empty > 0 && (
Tomma {d.skippedVouchers.empty}
)}
)} {/* Migration adjustment info */} {d?.migrationAdjustment?.created && (

Omföringsverifikation skapad

{d.migrationAdjustment.accountsAdjusted} konton justerade för att saldon ska matcha källsystemet. Verifikationen kompenserar för hoppade verifikationer så att dina balansräkning och resultaträkning stämmer.

)} {/* Retry info (only shown if retries happened) */} {d && d.retriedBatches > 0 && (

{d.retriedBatches} {d.retriedBatches === 1 ? 'batch' : 'batcher'} behövde omförsök {d.failedBatches > 0 && ( {' · '}{d.failedBatches} misslyckades trots omförsök )}

)}
)}
) } function ResultStep({ results, sieResults, error, onDone, onRetry, }: { results: MigrationResults | null sieResults: ImportResult[] error: string | null onDone: () => void onRetry: () => void }) { if (error) { return (

Migreringen misslyckades

{error}

) } const hasResults = results || sieResults.length > 0 if (!hasResults) return null // Compute combined SIE stats const totalJournalEntries = sieResults.reduce((sum, r) => sum + r.journalEntriesCreated, 0) const totalErrors = sieResults.reduce((sum, r) => sum + r.errors.length, 0) const totalSkipped = sieResults.reduce((sum, r) => (r.details?.skippedVouchers?.total || 0) + sum, 0) const allSieSucceeded = sieResults.length > 0 && sieResults.every(r => r.success) const anySieFailed = sieResults.some(r => r.errors.length > 0 && r.journalEntriesCreated === 0) // Overall status const overallIcon = anySieFailed ? 'error' as const : (!allSieSucceeded || totalErrors > 0) ? 'warning' as const : 'success' as const return (
{/* ── Header card with overall summary ── */} {anySieFailed ? 'Migrering delvis genomförd' : !allSieSucceeded ? 'Migrering klar med anmärkningar' : 'Migrering klar'} {totalJournalEntries > 0 && ( <> {totalJournalEntries.toLocaleString('sv-SE')} {' verifikationer importerade'} {sieResults.length > 1 && ` över ${sieResults.length} räkenskapsår`} {totalSkipped > 0 && ( {' · '}{totalSkipped} hoppade över )} )} {/* ── Per-fiscal-year SIE breakdown ── */} {sieResults.length > 0 && (

Bokföringsdata (SIE)

{sieResults.map((r, i) => ( ))}
)} {/* ── API import results (company info, customers, etc.) ── */} {results && (

Övriga data

{results.companyInfo && ( } label="Företagsinformation" status={results.companyInfo.imported ? 'success' : 'skipped'} statusText={results.companyInfo.imported ? 'Importerad' : 'Hoppades över'} /> )} {results.customers && ( } label="Kunder" status="success" statusText={`${results.customers.imported} importerade`} detail={results.customers.skipped > 0 ? `${results.customers.skipped} fanns redan` : undefined} /> )} {results.suppliers && ( } label="Leverantörer" status="success" statusText={`${results.suppliers.imported} importerade`} detail={results.suppliers.skipped > 0 ? `${results.suppliers.skipped} fanns redan` : undefined} /> )} {results.salesInvoices && ( } label="Kundfakturor" status="success" statusText={`${results.salesInvoices.imported} importerade`} detail={results.salesInvoices.skipped > 0 ? `${results.salesInvoices.skipped} hoppades över` : undefined} /> )} {results.supplierInvoices && ( } label="Leverantörsfakturor" status="success" statusText={`${results.supplierInvoices.imported} importerade`} detail={results.supplierInvoices.skipped > 0 ? `${results.supplierInvoices.skipped} hoppades över` : undefined} /> )}
)} {/* ── Next steps ── */} Nästa steg
1

Granska importerade verifikationer

Kontrollera att bokföringen ser korrekt ut i huvudboken

2

Stäm av balansräkningen

Jämför ingående balanser och saldon mot ditt tidigare system

3

Kontrollera kunder och leverantörer

Verifiera kontaktuppgifter, organisationsnummer och bankinfo

) } /** Simple row for non-SIE entity results (customers, invoices, etc.) */ function EntityResultRow({ icon, label, status, statusText, detail, }: { icon: React.ReactNode label: string status: 'success' | 'skipped' statusText: string detail?: string }) { return (
{icon}

{label}

{statusText}

{detail &&

{detail}

}
) } // ── Main wizard ───────────────────────────────────────────────── export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps) { const { toast } = useToast() const [step, setStep] = useState('provider') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // Connection state const [selectedProvider, setSelectedProvider] = useState(null) const [consentId, setConsentId] = useState(null) const [authUrl, setAuthUrl] = useState(null) const [authType, setAuthType] = useState<'oauth' | 'token' | null>(null) // Preview state const [preview, setPreview] = useState(null) // SIE data state (held between mapping and execution steps) const [sieData, setSieData] = useState(null) // Options state const [migrationOptions, setMigrationOptions] = useState(DEFAULT_OPTIONS) // Migration state const [migrationStep, setMigrationStep] = useState('') const [migrationProgress, setMigrationProgress] = useState(0) const [migrationResults, setMigrationResults] = useState(null) const [sieImportResults, setSieImportResults] = useState([]) // Wizard progress — only user-interactive steps const userSteps = STEPS.filter(s => { if (s === 'migrating' || s === 'result') return false if (s === 'mapping' && !preview?.sieAvailable) return false return true }) const currentUserStepIndex = userSteps.indexOf(step) const isInteractiveStep = currentUserStepIndex !== -1 const progressPercent = isInteractiveStep ? ((currentUserStepIndex + 1) / userSteps.length) * 100 : 100 // ── Step handlers ────────────────────────────────────────────── const loadPreview = useCallback(async (cId: string) => { setStep('preview') setIsLoading(true) setError(null) try { const res = await fetch(`/api/extensions/ext/arcim-migration/preview?consentId=${cId}`) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } const data = await res.json() setPreview(data) setConsentId(cId) // If SIE is not available, disable SIE import by default if (!data.sieAvailable) { setMigrationOptions(prev => ({ ...prev, importSIEData: false })) } } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte hämta förhandsgranskning') } finally { setIsLoading(false) } }, []) const handleSelectProvider = useCallback(async (provider: ArcimProvider) => { setSelectedProvider(provider) setStep('connect') setIsLoading(true) setError(null) try { const res = await fetch('/api/extensions/ext/arcim-migration/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } const data = await res.json() setConsentId(data.consentId) setAuthType(data.authType) if (data.authType === 'oauth' && data.authUrl) { setAuthUrl(data.authUrl) } // Token-based providers stay on connect step for credential input } catch (err) { setError(err instanceof Error ? err.message : 'Anslutning misslyckades') } finally { setIsLoading(false) } }, []) // Handle token submission for token-based providers (Bokio, etc.) const handleTokenSubmit = useCallback(async (apiToken: string, companyId: string) => { if (!consentId || !selectedProvider) return setIsLoading(true) setError(null) try { const res = await fetch('/api/extensions/ext/arcim-migration/submit-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ consentId, provider: selectedProvider, apiToken, companyId: companyId || undefined, }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } // Token stored — consent is now accepted, proceed to preview await loadPreview(consentId) } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte ansluta') } finally { setIsLoading(false) } }, [consentId, selectedProvider, loadPreview]) // Handle OAuth callback via URL params const handleOAuthReturn = useCallback(async () => { // Check URL for migration callback params const url = new URL(window.location.href) const migrationStatus = url.searchParams.get('migration') const callbackConsentId = url.searchParams.get('consentId') if (migrationStatus === 'connected' && callbackConsentId) { // Clean URL url.searchParams.delete('migration') url.searchParams.delete('consentId') window.history.replaceState({}, '', url.pathname) await loadPreview(callbackConsentId) } else if (migrationStatus === 'error') { const callbackProvider = url.searchParams.get('provider') as ArcimProvider | null url.searchParams.delete('migration') url.searchParams.delete('provider') window.history.replaceState({}, '', url.pathname) setError('OAuth-anslutningen misslyckades. Försök igen.') if (callbackProvider) { setSelectedProvider(callbackProvider) setStep('connect') } else { setStep('provider') } } }, [loadPreview]) // Check for OAuth callback on mount useEffect(() => { handleOAuthReturn() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Load SIE data when entering mapping step const loadSIEData = useCallback(async () => { if (!consentId) return setStep('mapping') setIsLoading(true) setError(null) try { const res = await fetch(`/api/extensions/ext/arcim-migration/sie-data?consentId=${consentId}`) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } const data = await res.json() setSieData(data) // Auto-skip mapping step if all accounts are mapped if (data.mappingStats.unmapped === 0) { setStep('options') } } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte hämta SIE-data') } finally { setIsLoading(false) } }, [consentId]) const handlePreviewContinue = useCallback(() => { if (preview?.sieAvailable) { // Load SIE data for mapping step loadSIEData() } else { // Skip mapping step — no SIE available setStep('options') } }, [preview, loadSIEData]) const handleMappingChange = useCallback((sourceAccount: string, targetAccount: string, targetName: string) => { if (!sieData) return const updatedMappings = sieData.mappings.map(m => m.sourceAccount === sourceAccount ? { ...m, targetAccount, targetName, isOverride: true, matchType: 'manual' as const, confidence: 1 } : m ) setSieData(prev => prev ? { ...prev, mappings: updatedMappings, mappingStats: { ...prev.mappingStats, unmapped: updatedMappings.filter(m => !m.targetAccount).length, mapped: updatedMappings.filter(m => m.targetAccount).length, }, } : null) }, [sieData]) const handleStartMigration = useCallback(async () => { if (!consentId) return setStep('migrating') setMigrationStep('Startar migrering...') setMigrationProgress(5) setError(null) try { // ── Phase 1: SIE import ────────────────────────────────── if (migrationOptions.importSIEData && sieData && sieData.rawContent.length > 0) { setMigrationStep('Importerar bokföringsdata (SIE)...') setMigrationProgress(10) setSieImportResults([]) // Import all fiscal years' SIE content for (let i = 0; i < sieData.rawContent.length; i++) { const progress = 10 + Math.round((i / sieData.rawContent.length) * 40) setMigrationProgress(progress) setMigrationStep(`Importerar bokföringsdata (SIE) — fil ${i + 1} av ${sieData.rawContent.length}...`) const res = await fetch('/api/extensions/ext/arcim-migration/import-sie', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rawContent: sieData.rawContent[i], mappings: sieData.mappings, options: { createFiscalPeriod: true, importOpeningBalances: true, importTransactions: true, voucherSeries: migrationOptions.voucherSeries, }, }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `SIE import HTTP ${res.status}`) } const result = await res.json() as ImportResult setSieImportResults(prev => [...prev, result]) if (!result.success && result.errors.length > 0) { // Log but don't fail — continue with API import // Non-critical — continue with API import } } } // ── Phase 2: API import (customers, suppliers, invoices) ── const hasApiImport = migrationOptions.importCompanyInfo || migrationOptions.importCustomers || migrationOptions.importSuppliers || migrationOptions.importSalesInvoices || migrationOptions.importSupplierInvoices if (hasApiImport) { setMigrationStep('Importerar kunder, leverantörer och fakturor...') setMigrationProgress(55) const res = await fetch('/api/extensions/ext/arcim-migration/migrate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ consentId, importCompanyInfo: migrationOptions.importCompanyInfo, importCustomers: migrationOptions.importCustomers, importSuppliers: migrationOptions.importSuppliers, importSalesInvoices: migrationOptions.importSalesInvoices, importSupplierInvoices: migrationOptions.importSupplierInvoices, }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } const data = await res.json() setMigrationResults(data.results) } setMigrationProgress(100) setStep('result') toast({ title: 'Migrering klar', description: 'Din bokföringsdata har importerats.', }) } catch (err) { const msg = err instanceof Error ? err.message : 'Migrering misslyckades' setError(msg) setStep('result') } }, [consentId, migrationOptions, sieData, toast]) const handleDone = useCallback(() => { // Reset wizard setStep('provider') setSelectedProvider(null) setConsentId(null) setAuthUrl(null) setAuthType(null) setPreview(null) setSieData(null) setMigrationOptions(DEFAULT_OPTIONS) setMigrationResults(null) setSieImportResults([]) setError(null) }, []) // ── Render ───────────────────────────────────────────────────── return (
{/* Progress bar — only during interactive steps */} {step !== 'provider' && isInteractiveStep && (
Steg {currentUserStepIndex + 1}/{userSteps.length}: {STEP_LABELS[step]} {userSteps.map((s) => ( {STEP_LABELS[s]} ))}
)} {/* Step content */} {step === 'provider' && ( )} {step === 'connect' && selectedProvider && ( { setStep('provider') setError(null) }} /> )} {step === 'preview' && ( setStep('provider')} /> )} {step === 'mapping' && ( setStep('options')} onBack={() => setStep('preview')} /> )} {step === 'options' && ( preview?.sieAvailable ? setStep('mapping') : setStep('preview')} /> )} {step === 'migrating' && ( )} {step === 'result' && ( { setError(null) setStep('options') }} /> )}
) }