885f362a29
Replace PSD2 bank integration as the default with file-based bank import (CSV/XML), which better suits Swedish sole traders and small companies. Enable Banking is now an opt-in extension. - Phase 1: Extract generic transaction ingestion service (ingest.ts) with dedup, auto-categorization, and OCR-based invoice matching - Phase 2: Bank file parser library supporting Nordea, SEB, Swedbank, Handelsbanken CSV formats and ISO 20022 camt.053 XML - Phase 3: Database migration adding import_source, reference columns and bank_file_imports tracking table - Phase 4: Import wizard UI (5-step flow) and API routes for parse/execute - Phase 5: Move Enable Banking to extensions/enable-banking/ with commented-out loader entry for opt-in activation - Phase 6: 104 new tests (ingestion + all parser formats), fixing Nordea detection overlap and camt.053 XML tag collision bugs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
237 lines
8.2 KiB
TypeScript
237 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Progress } from '@/components/ui/progress'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import {
|
|
Upload,
|
|
FileText,
|
|
AlertCircle,
|
|
CheckCircle,
|
|
Building2,
|
|
HelpCircle,
|
|
} from 'lucide-react'
|
|
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
|
|
|
|
const FORMAT_NAMES: Record<string, string> = {
|
|
nordea: 'Nordea',
|
|
seb: 'SEB',
|
|
swedbank: 'Swedbank',
|
|
handelsbanken: 'Handelsbanken',
|
|
generic_csv: 'CSV (manuell mappning)',
|
|
camt053: 'ISO 20022 camt.053',
|
|
}
|
|
|
|
interface BankFileUploadStepProps {
|
|
onFileSelect: (file: File, formatOverride?: BankFileFormatId) => void
|
|
isLoading: boolean
|
|
error: string | null
|
|
detectedFormat?: string | null
|
|
detectedFormatName?: string | null
|
|
}
|
|
|
|
export default function BankFileUploadStep({
|
|
onFileSelect,
|
|
isLoading,
|
|
error,
|
|
detectedFormat,
|
|
detectedFormatName,
|
|
}: BankFileUploadStepProps) {
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
const [formatOverride, setFormatOverride] = useState<BankFileFormatId | undefined>(undefined)
|
|
|
|
const acceptedExtensions = '.csv,.txt,.xml'
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(true)
|
|
}, [])
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
}, [])
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
setIsDragging(false)
|
|
|
|
const files = e.dataTransfer.files
|
|
if (files.length > 0) {
|
|
const file = files[0]
|
|
const ext = file.name.toLowerCase()
|
|
if (ext.endsWith('.csv') || ext.endsWith('.txt') || ext.endsWith('.xml')) {
|
|
setSelectedFile(file)
|
|
onFileSelect(file, formatOverride)
|
|
}
|
|
}
|
|
}, [onFileSelect, formatOverride])
|
|
|
|
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = e.target.files
|
|
if (files && files.length > 0) {
|
|
setSelectedFile(files[0])
|
|
onFileSelect(files[0], formatOverride)
|
|
}
|
|
}, [onFileSelect, formatOverride])
|
|
|
|
const handleFormatChange = (value: string) => {
|
|
const format = value === 'auto' ? undefined : value as BankFileFormatId
|
|
setFormatOverride(format)
|
|
if (selectedFile) {
|
|
onFileSelect(selectedFile, format)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Upload className="h-5 w-5" />
|
|
Ladda upp kontoutdrag
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Exportera transaktioner som CSV eller XML från din internetbank och ladda upp filen.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Format override */}
|
|
<div className="flex items-center gap-3">
|
|
<label className="text-sm font-medium whitespace-nowrap">Bank/format:</label>
|
|
<Select
|
|
value={formatOverride || 'auto'}
|
|
onValueChange={handleFormatChange}
|
|
>
|
|
<SelectTrigger className="w-64">
|
|
<SelectValue placeholder="Automatisk identifiering" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">Automatisk identifiering</SelectItem>
|
|
<SelectItem value="nordea">Nordea</SelectItem>
|
|
<SelectItem value="seb">SEB</SelectItem>
|
|
<SelectItem value="swedbank">Swedbank</SelectItem>
|
|
<SelectItem value="handelsbanken">Handelsbanken</SelectItem>
|
|
<SelectItem value="camt053">ISO 20022 camt.053 (XML)</SelectItem>
|
|
<SelectItem value="generic_csv">Annan CSV (manuell mappning)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Drop zone */}
|
|
<div
|
|
className={`
|
|
relative border-2 border-dashed rounded-lg p-8 text-center transition-colors
|
|
${isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
|
${error ? 'border-destructive bg-destructive/5' : ''}
|
|
${isLoading ? 'pointer-events-none opacity-50' : 'cursor-pointer hover:border-primary/50'}
|
|
`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => document.getElementById('bank-file-input')?.click()}
|
|
>
|
|
<input
|
|
id="bank-file-input"
|
|
type="file"
|
|
accept={acceptedExtensions}
|
|
className="hidden"
|
|
onChange={handleFileInput}
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
{isLoading ? (
|
|
<div className="space-y-4">
|
|
<FileText className="mx-auto h-12 w-12 text-muted-foreground animate-pulse" />
|
|
<p className="text-muted-foreground">Analyserar fil...</p>
|
|
<Progress value={33} className="w-48 mx-auto" />
|
|
</div>
|
|
) : selectedFile && detectedFormat ? (
|
|
<div className="space-y-4">
|
|
<CheckCircle className="mx-auto h-12 w-12 text-green-600" />
|
|
<div>
|
|
<p className="font-medium">{selectedFile.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{(selectedFile.size / 1024).toFixed(1)} KB
|
|
</p>
|
|
<Badge variant="secondary" className="mt-2">
|
|
<Building2 className="mr-1 h-3 w-3" />
|
|
{detectedFormatName || FORMAT_NAMES[detectedFormat] || detectedFormat}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-medium">Dra och släpp bankfil här</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
CSV, TXT eller XML (max 10 MB)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Error display */}
|
|
{error && (
|
|
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg flex gap-3">
|
|
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-destructive">Kunde inte läsa filen</p>
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bank export instructions */}
|
|
<Card className="bg-muted/50">
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<HelpCircle className="h-4 w-4" />
|
|
Så exporterar du från din bank
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-sm space-y-3">
|
|
<div>
|
|
<p className="font-medium">Nordea</p>
|
|
<p className="text-muted-foreground">
|
|
Logga in → Konton → Välj konto → Transaktioner → Exportera (CSV)
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">SEB</p>
|
|
<p className="text-muted-foreground">
|
|
Logga in → Konton → Kontoutdrag → Hämta som fil (CSV)
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Swedbank</p>
|
|
<p className="text-muted-foreground">
|
|
Logga in → Konton → Transaktioner → Exportera kontoutdrag (CSV)
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Handelsbanken</p>
|
|
<p className="text-muted-foreground">
|
|
Logga in → Konton → Transaktioner → Ladda ner (CSV)
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|