'use client' import { useState, useEffect, useRef } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { formatCurrency, formatDate } from '@/lib/utils' import { CheckCircle, AlertCircle, ArrowRight, Loader2, Calendar, FileText, Database, Lock, } from 'lucide-react' import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes' import { useCanWrite } from '@/lib/hooks/use-can-write' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import ImportTheater from '@/components/import/ImportTheater' import type { ImportPreview, AccountMapping } from '@/lib/import/types' import type { TheaterModel } from '@/lib/import/theater-model' const SERIES_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') interface ImportReviewStepProps { preview: ImportPreview mappings: AccountMapping[] onExecute: (options: ImportExecuteOptions) => Promise onBack: () => void isLoading: boolean /** Client-parsed graph model for the import theater; null falls back to * the plain spinner takeover (parse failed, oversized file, or pending). */ theaterModel?: TheaterModel | null } export interface ImportExecuteOptions { createFiscalPeriod: boolean importOpeningBalances: boolean importTransactions: boolean updateAccountNames: boolean voucherSeries: string markImportedNoDocRequired: boolean } export default function ImportReviewStep({ preview, mappings, onExecute, onBack, isLoading, theaterModel = null, }: ImportReviewStepProps) { const { canWrite } = useCanWrite() const { company } = useCompany() const [options, setOptions] = useState({ createFiscalPeriod: true, importOpeningBalances: true, importTransactions: true, updateAccountNames: true, voucherSeries: 'B', markImportedNoDocRequired: false, }) const [defaultSeries, setDefaultSeries] = useState(null) const [existingSeries, setExistingSeries] = useState>(new Set()) const [seriesLoaded, setSeriesLoaded] = useState(false) const [elapsed, setElapsed] = useState(0) const intervalRef = useRef | null>(null) useEffect(() => { if (!company?.id) return setSeriesLoaded(false) const supabase = createClient() let cancelled = false ;(async () => { const [ { data: settingsData, error: settingsError }, { data: sequencesData, error: sequencesError }, ] = await Promise.all([ supabase .from('company_settings') .select('default_voucher_series') .eq('company_id', company.id) .maybeSingle(), supabase .from('voucher_sequences') .select('voucher_series') .eq('company_id', company.id), ]) if (cancelled) return if (settingsError) { console.error('Failed to load company settings for voucher series', settingsError) } if (sequencesError) { console.error('Failed to load voucher sequences', sequencesError) } const companyDefault = settingsData?.default_voucher_series || null const sequences = new Set((sequencesData || []).map((row) => row.voucher_series)) setDefaultSeries(companyDefault) setExistingSeries(sequences) const initial = companyDefault || (sequences.has('B') ? 'B' : Array.from(sequences).sort()[0]) || 'A' setOptions((prev) => ({ ...prev, voucherSeries: initial })) setSeriesLoaded(true) })() return () => { cancelled = true } }, [company?.id]) // Block browser close/refresh during import useUnsavedChanges(isLoading) // Elapsed time counter during import useEffect(() => { if (isLoading) { setElapsed(0) intervalRef.current = setInterval(() => setElapsed((s) => s + 1), 1000) } else { if (intervalRef.current) clearInterval(intervalRef.current) setElapsed(0) } return () => { if (intervalRef.current) clearInterval(intervalRef.current) } }, [isLoading]) const handleExecute = () => { onExecute(options) } const updateOption = ( key: K, value: ImportExecuteOptions[K] ) => { setOptions((prev) => ({ ...prev, [key]: value })) } // Calculate what will be imported const mappedCount = mappings.filter((m) => m.targetAccount).length const hasOpeningBalances = preview.openingBalanceTotal > 0 const hasTransactions = preview.voucherCount > 0 // An import whose fiscal year already ended in a prior calendar year is a // historical/migration import: the underlag live in the old system, so the // exemption is especially apt. Nudges (does not force) the toggle. const isHistoricalImport = (() => { if (!preview.fiscalYearEnd) return false const end = new Date(preview.fiscalYearEnd) const startOfThisYear = new Date(new Date().getFullYear(), 0, 1) return !isNaN(end.getTime()) && end < startOfThisYear })() // Identity-mapped accounts whose #KONTO name differs from the BAS default: // mirrors the filter in syncMappedAccounts, so the count matches what the // import would actually rename/create with a custom name. const customNameCount = mappings.filter( (m) => m.targetAccount && m.sourceAccount === m.targetAccount && m.sourceName?.trim() && m.sourceName.trim() !== m.targetName?.trim() ).length // Full-screen loading takeover during import execution. With a client-parsed // model the theater plays (the graph draws itself while the server writes); // without one, the plain spinner takeover remains the fallback. if (isLoading) { return (
{theaterModel ? (

Stäng inte sidan. Importen kan ta upp till några minuter beroende på antalet verifikationer.

) : (

Importerar bokföring...

{preview.voucherCount} verifikationer bearbetas

{elapsed}s

Stäng inte sidan. Importen kan ta upp till några minuter beroende på antalet verifikationer.

)}
) } return (
{/* Summary */} Redo att importera Granska inställningarna nedan och klicka på "Starta import" för att genomföra importen.

{preview.companyName || 'Okänt företag'}

{preview.orgNumber || 'Inget orgnr'}

Räkenskapsår

{preview.fiscalYearStart ? formatDate(preview.fiscalYearStart) : '?'}{' '} -{' '} {preview.fiscalYearEnd ? formatDate(preview.fiscalYearEnd) : '?'}

{mappedCount} konton mappade

{preview.voucherCount} verifikationer

{/* Import options */} Importinställningar Välj vad som ska importeras {/* Fiscal period */}

Skapar automatiskt räkenskapsåret om det inte redan finns

updateOption('createFiscalPeriod', checked)} />
{/* Opening balances */}

{hasOpeningBalances ? `Skapar verifikation för IB på ${formatCurrency(preview.openingBalanceTotal)}` : 'Inga ingående balanser i filen'}

updateOption('importOpeningBalances', checked)} disabled={!hasOpeningBalances} />
{/* Transactions */}

{hasTransactions ? `Importerar ${preview.voucherCount} verifikationer med ${preview.transactionLineCount} rader` : 'Inga verifikationer i filen (SIE1-format?)'}

updateOption('importTransactions', checked)} disabled={!hasTransactions} />
{/* Account names from file */}

{customNameCount > 0 ? `${customNameCount} ${customNameCount === 1 ? 'konto' : 'konton'} har egna namn i filen som skiljer sig från BAS-standard` : 'Kontonamnen i filen följer BAS-standard'}

updateOption('updateAccountNames', checked)} />
{/* Voucher series */} {options.importTransactions && hasTransactions && (

Använd en separat serie för att enkelt kunna skilja importerade från manuella verifikationer

)} {/* No-underlag exemption: keeps a multi-year migration from flooding "Att hantera: saknade underlag" with thousands of items. */}

Märker alla importerade verifikationer som att de inte behöver något separat underlag: underlagen finns kvar i ditt tidigare system. Annars hamnar de under "Att hantera: saknade underlag". Kan ändras per verifikation efteråt.

updateOption('markImportedNoDocRequired', checked)} disabled={!options.importTransactions || !hasTransactions} />
{/* Warnings */} {!preview.trialBalance.isBalanced && ( Observera

De ingående balanserna i filen balanserar inte helt. En justeringspost kommer att skapas automatiskt mot konto 2099 (Årets resultat).

)} {/* What happens next */} Vad händer när du importerar?

1. Räkenskapsåret skapas om det inte finns

2. En verifikation för ingående balanser skapas

3. Alla verifikationer importeras med nya verifikationsnummer

4. Kontomappningarna sparas för framtida importer

Importen kan inte ångras automatiskt, men du kan ta bort skapade verifikationer manuellt.

{/* Actions */}
) }