Files
accounted/components/import/BankFileConfirmStep.tsx
T
Jakob Wennberg 885f362a29 feat: add bank file import as core, move Enable Banking to extension
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>
2026-02-20 14:21:28 +01:00

179 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } 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 { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
ArrowLeft,
Loader2,
Play,
FileText,
AlertTriangle,
Link2,
Calendar,
} from 'lucide-react'
import { formatCurrency } from '@/lib/utils'
import type { BankFileParseResult } from '@/lib/import/bank-file/types'
interface BankFileConfirmStepProps {
parseResult: BankFileParseResult
onExecute: (options: { skip_duplicates: boolean; auto_categorize: boolean }) => void
onBack: () => void
isLoading: boolean
}
export default function BankFileConfirmStep({
parseResult,
onExecute,
onBack,
isLoading,
}: BankFileConfirmStepProps) {
const [skipDuplicates, setSkipDuplicates] = useState(true)
const [autoCategorize, setAutoCategorize] = useState(true)
const { transactions, stats, date_from, date_to, format_name } = parseResult
const refsCount = transactions.filter((t) => t.reference).length
return (
<div className="space-y-6">
{/* Summary */}
<Card>
<CardHeader>
<CardTitle>Bekräfta import</CardTitle>
<CardDescription>
Granska sammanfattningen och importera transaktionerna.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Stats grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<FileText className="h-4 w-4" />
<span className="text-xs">Transaktioner</span>
</div>
<p className="text-xl font-bold">{stats.parsed_rows}</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
<Calendar className="h-4 w-4" />
<span className="text-xs">Period</span>
</div>
<p className="text-sm font-medium">
{date_from} {date_to}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-green-600 mb-1">
<span className="text-xs">Inkomster</span>
</div>
<p className="text-xl font-bold text-green-600">
{formatCurrency(stats.total_income)}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-red-600 mb-1">
<span className="text-xs">Utgifter</span>
</div>
<p className="text-xl font-bold text-red-600">
{formatCurrency(stats.total_expenses)}
</p>
</div>
</div>
{/* Additional info */}
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">Format: {format_name}</Badge>
{refsCount > 0 && (
<Badge variant="outline" className="text-blue-600 border-blue-300">
<Link2 className="mr-1 h-3 w-3" />
{refsCount} med OCR/referens
</Badge>
)}
</div>
{/* Options */}
<div className="border rounded-lg p-4 space-y-4">
<h3 className="text-sm font-medium">Importinställningar</h3>
<div className="flex items-center space-x-3">
<Checkbox
id="skip-duplicates"
checked={skipDuplicates}
onCheckedChange={(checked) => setSkipDuplicates(checked === true)}
/>
<div>
<Label htmlFor="skip-duplicates" className="text-sm font-medium cursor-pointer">
Hoppa över dubletter
</Label>
<p className="text-xs text-muted-foreground">
Transaktioner som redan finns i systemet importeras inte igen
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Checkbox
id="auto-categorize"
checked={autoCategorize}
onCheckedChange={(checked) => setAutoCategorize(checked === true)}
/>
<div>
<Label htmlFor="auto-categorize" className="text-sm font-medium cursor-pointer">
Auto-kategorisera kända transaktioner
</Label>
<p className="text-xs text-muted-foreground">
Skapar automatiskt bokföringsposter för transaktioner med hög konfidens
</p>
</div>
</div>
</div>
{/* Warning note */}
<div className="flex gap-3 p-3 bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<AlertTriangle className="h-4 w-4 text-yellow-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground">
Importerade transaktioner som inte automatiskt kategoriseras visas som
&quot;okategoriserade&quot; transaktionssidan. Du kan kategorisera dem manuellt
efteråt.
</p>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack} disabled={isLoading}>
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button
onClick={() => onExecute({
skip_duplicates: skipDuplicates,
auto_categorize: autoCategorize,
})}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importerar...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Importera {stats.parsed_rows} transaktioner
</>
)}
</Button>
</div>
</div>
)
}