fix: SIE import duplicate check, performance, and UX improvements (#162)
* fix: catch duplicate SIE import early with clear error message - Add duplicate check in execute route before doing any work (defense in depth) - Handle duplicate error from execute route in frontend - Show "Filen har redan importerats" heading instead of generic "Kunde inte läsa filen" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: catch duplicate SIE import early and batch account creation for performance - Add duplicate check in execute route before doing any work (defense in depth) - Handle duplicate error from execute route in frontend with clear Swedish message - Replace sequential ensureAccountExists loop (50-100 DB round trips) with single batch SELECT + batch INSERT — reduces import time from 3+ min to seconds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update mappings optimistically after creating missing accounts Previously re-parsed the SIE file after account creation, which could fail with a 409 duplicate error (leaving the "create accounts" card stuck). Now optimistically marks created accounts as self-mapped and updates the preview stats immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile P2 feedback — error handling and typed errorType prop - Check batch insert error in executeSIEImport account creation safety net - Replace brittle string-match error detection with typed errorType prop - Remove stale file dependency from handleCreateAccounts useCallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9dceb6285c
commit
d23cb4c859
@@ -322,6 +322,7 @@ function SIEImportWizard() {
|
||||
const [step, setStep] = useState<ImportWizardStep>('upload')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [errorType, setErrorType] = useState<'duplicate' | 'duplicate_period' | 'validation' | 'parse' | undefined>()
|
||||
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [, setParsed] = useState<ParsedSIEFile | null>(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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} />}
|
||||
{step === 'upload' && <SIEUploadStep onFileSelect={handleFileSelect} isLoading={isLoading} error={error} errorType={errorType} />}
|
||||
{step === 'preview' && preview && (
|
||||
<SIEPreviewStep preview={preview} issues={issues} missingAccounts={missingAccounts}
|
||||
onCreateAccounts={handleCreateAccounts} isCreatingAccounts={isCreatingAccounts}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NextResponse } from 'next/server'
|
||||
import { requireCompanyId } from '@/lib/company/context'
|
||||
import { parseSIEFile, detectEncoding, decodeBuffer } from '@/lib/import/sie-parser'
|
||||
import { suggestMappings } from '@/lib/import/account-mapper'
|
||||
import { executeSIEImport } from '@/lib/import/sie-import'
|
||||
import { executeSIEImport, checkDuplicateImport } from '@/lib/import/sie-import'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
import type { AccountMapping, SIEAccountMappingRecord } from '@/lib/import/types'
|
||||
@@ -56,6 +56,15 @@ export async function POST(request: Request) {
|
||||
// Parse the SIE file
|
||||
const parsed = parseSIEFile(content)
|
||||
|
||||
// Check for duplicate import before doing any work
|
||||
const duplicate = await checkDuplicateImport(supabase, companyId, content)
|
||||
if (duplicate) {
|
||||
return NextResponse.json({
|
||||
error: 'duplicate',
|
||||
message: `Denna fil har redan importerats ${duplicate.imported_at ? new Date(duplicate.imported_at).toLocaleDateString('sv-SE') : ''}`.trim(),
|
||||
}, { status: 409 })
|
||||
}
|
||||
|
||||
// Get mappings - either from request or generate new ones
|
||||
let mappings: AccountMapping[]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user