diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index b707be73..851835ce 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -322,6 +322,7 @@ function SIEImportWizard() { const [step, setStep] = useState('upload') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) + const [errorType, setErrorType] = useState<'duplicate' | 'duplicate_period' | 'validation' | 'parse' | undefined>() const [file, setFile] = useState(null) const [, setParsed] = useState(null) @@ -345,6 +346,7 @@ function SIEImportWizard() { const handleFileSelect = useCallback(async (selectedFile: File) => { setFile(selectedFile) setError(null) + setErrorType(undefined) setIsLoading(true) try { @@ -360,10 +362,13 @@ function SIEImportWizard() { if (!res.ok) { if (data.error === 'duplicate' || data.error === 'duplicate_period') { + setErrorType(data.error) setError(data.message) } else if (data.error === 'validation') { + setErrorType('validation') setError(`${data.message}: ${data.errors?.join(', ') || 'Unknown validation error'}`) } else { + setErrorType('parse') setError(data.error || 'Failed to parse file') } return @@ -450,30 +455,38 @@ function SIEImportWizard() { toast({ title: 'Konton skapade', description: `${data.created} nya konton har lagts till i din kontoplan` }) - if (file) { - const formData = new FormData() - formData.append('file', file) - - const parseRes = await fetch('/api/import/sie/parse', { method: 'POST', body: formData }) - const parseData = await parseRes.json() - - if (parseRes.ok) { - setMappings(parseData.mappings) - setPreview(parseData.preview) - - const accountsRes = await fetch('/api/bookkeeping/accounts') - if (accountsRes.ok) { - const accountsData = await accountsRes.json() - setBasAccounts(accountsData.data || []) - } + // Optimistically update mappings: mark created accounts as self-mapped + const createdSet = new Set(missingAccounts.map(a => a.number)) + setMappings(prev => prev.map(m => + !m.targetAccount && createdSet.has(m.sourceAccount) + ? { ...m, targetAccount: m.sourceAccount, targetName: m.sourceName, confidence: 1.0 } + : m + )) + setPreview(prev => { + if (!prev) return prev + const newMapped = prev.mappingStatus.mapped + createdSet.size + return { + ...prev, + mappingStatus: { + ...prev.mappingStatus, + mapped: newMapped, + unmapped: Math.max(0, prev.mappingStatus.unmapped - createdSet.size), + }, } + }) + + // Also refresh BAS accounts list + const accountsRes = await fetch('/api/bookkeeping/accounts') + if (accountsRes.ok) { + const accountsData = await accountsRes.json() + setBasAccounts(accountsData.data || []) } } catch (err) { toast({ title: 'Kunde inte skapa konton', description: err instanceof Error ? err.message : 'Försök igen.', variant: 'destructive' }) } finally { setIsCreatingAccounts(false) } - }, [missingAccounts, file, toast]) + }, [missingAccounts, toast]) const handleExecuteImport = useCallback(async (options: ImportExecuteOptions) => { if (!file) { setError('No file selected'); return } @@ -491,6 +504,10 @@ function SIEImportWizard() { const data = await res.json() if (!res.ok) { + if (data.error === 'duplicate') { + setError(data.message || 'Denna fil har redan importerats') + return + } if (data.result) { setImportResult(data.result) } else { setError(data.error || 'Import failed'); return } } else { setImportResult(data.result) @@ -513,7 +530,7 @@ function SIEImportWizard() { const handleNewImport = () => { setStep('upload'); setFile(null); setParsed(null); setMappings([]) - setPreview(null); setIssues([]); setImportResult(null); setError(null) + setPreview(null); setIssues([]); setImportResult(null); setError(null); setErrorType(undefined) setSieAccounts([]); setIsCreatingAccounts(false) } @@ -540,7 +557,7 @@ function SIEImportWizard() { - {step === 'upload' && } + {step === 'upload' && } {step === 'preview' && preview && ( void isLoading: boolean error: string | null + errorType?: 'duplicate' | 'duplicate_period' | 'validation' | 'parse' } -export default function SIEUploadStep({ onFileSelect, isLoading, error }: SIEUploadStepProps) { +export default function SIEUploadStep({ onFileSelect, isLoading, error, errorType }: SIEUploadStepProps) { const [isDragging, setIsDragging] = useState(false) const [selectedFile, setSelectedFile] = useState(null) const [loadingPhase, setLoadingPhase] = useState(0) @@ -161,7 +162,11 @@ export default function SIEUploadStep({ onFileSelect, isLoading, error }: SIEUpl
-

Kunde inte läsa filen

+

+ {errorType === 'duplicate' || errorType === 'duplicate_period' + ? 'Filen har redan importerats' + : 'Kunde inte läsa filen'} +

{error}

diff --git a/lib/import/sie-import.ts b/lib/import/sie-import.ts index 2120a598..db7ac3f4 100644 --- a/lib/import/sie-import.ts +++ b/lib/import/sie-import.ts @@ -1250,20 +1250,72 @@ export async function executeSIEImport( const accountMap = mappingsToMap(mappings) // Ensure all mapped target accounts exist in chart_of_accounts. - // The mapping contains every account referenced in the SIE file; accounts - // that were not seeded during onboarding need to be created here so that - // journal entry lines can link to them via account_id. - const seenTargets = new Set() - for (const mapping of mappings) { - if (mapping.targetAccount && !seenTargets.has(mapping.targetAccount)) { - seenTargets.add(mapping.targetAccount) - await ensureAccountExists( - supabase, - companyId, - userId, - mapping.targetAccount, - mapping.targetName - ) + // Uses a single batch query + batch insert instead of per-account round trips. + const targetAccounts = [...new Set( + mappings.filter(m => m.targetAccount).map(m => m.targetAccount!) + )] + + if (targetAccounts.length > 0) { + const { data: existing } = await supabase + .from('chart_of_accounts') + .select('account_number') + .eq('company_id', companyId) + .in('account_number', targetAccounts) + + const existingSet = new Set((existing || []).map(a => a.account_number)) + const missing = targetAccounts.filter(num => !existingSet.has(num)) + + if (missing.length > 0) { + const targetNameMap = new Map() + for (const m of mappings) { + if (m.targetAccount) targetNameMap.set(m.targetAccount, m.targetName || m.sourceName) + } + + const inserts = missing.map(num => { + const basRef = getBASReference(num) + if (basRef) { + return { + user_id: userId, + company_id: companyId, + account_number: num, + account_name: basRef.account_name, + account_class: basRef.account_class, + account_group: basRef.account_group, + account_type: basRef.account_type, + normal_balance: basRef.normal_balance, + sru_code: basRef.sru_code ?? computeSRUCode(num), + k2_excluded: basRef.k2_excluded, + plan_type: 'full_bas' as const, + is_active: true, + is_system_account: false, + } + } + const classNum = parseInt(num.charAt(0), 10) + const group = num.substring(0, 2) + const accountType = classNum === 1 ? 'asset' + : classNum === 2 ? (group === '21' ? 'untaxed_reserves' : (group === '20' ? 'equity' : 'liability')) + : classNum === 3 ? 'revenue' : 'expense' + return { + user_id: userId, + company_id: companyId, + account_number: num, + account_name: targetNameMap.get(num) || `Konto ${num}`, + account_class: classNum, + account_group: group, + account_type: accountType, + normal_balance: classNum <= 1 || classNum >= 4 ? 'debit' : 'credit', + sru_code: computeSRUCode(num), + plan_type: 'full_bas' as const, + is_active: true, + is_system_account: false, + } + }) + + const { error: insertError } = await supabase.from('chart_of_accounts').insert(inserts) + if (insertError && !insertError.message.includes('duplicate')) { + result.errors.push(`Failed to create accounts: ${insertError.message}`) + return result + } } }