feat: import system improvements, INK2 fix, and Swedish text corrections (#10)

- SIE parser: Windows-1252 and CP437 encoding detection and decoding
- Bank file parser: add Nordea Business (Företag) CSV format
- Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV
- INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years
- Dashboard: parallel Supabase queries, simplified dashboard page
- Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures
- Import wizard UI improvements across all steps
- Migration: add 'bas_range' match type to sie_account_mappings constraint
- Extensive new tests for SIE parser encoding and bank file parser

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-11 18:05:37 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent a1ca96453c
commit e8fb84b4fd
39 changed files with 1129 additions and 425 deletions
+4 -4
View File
@@ -204,7 +204,7 @@ export default function AccountMappingStep({
</div>
{/* Mapping table */}
<div className="border rounded-lg overflow-hidden">
<div className="border rounded-lg overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@@ -317,11 +317,11 @@ export default function AccountMappingStep({
</Card>
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack}>
Tillbaka
</Button>
<Button onClick={onContinue} disabled={!canContinue}>
<Button className="min-h-11" onClick={onContinue} disabled={!canContinue}>
{canContinue ? 'Fortsätt till granskning' : `${stats.unmapped} konton saknar mappning`}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
@@ -248,7 +248,7 @@ export default function BankFileColumnMappingStep({
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border max-h-64 overflow-y-auto">
<div className="rounded-md border max-h-64 overflow-x-auto overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
@@ -286,12 +286,12 @@ export default function BankFileColumnMappingStep({
)}
{/* Navigation */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack}>
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button onClick={handleConfirm} disabled={!isValid}>
<Button className="min-h-11" onClick={handleConfirm} disabled={!isValid}>
Fortsätt
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
+3 -2
View File
@@ -112,12 +112,13 @@ export default function BankFileConfirmStep({
</Card>
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack} disabled={isLoading}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack} disabled={isLoading}>
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button
className="min-h-11"
onClick={() => onExecute({
skip_duplicates: true,
auto_categorize: false,
+5 -5
View File
@@ -41,7 +41,7 @@ export default function BankFilePreviewStep({
return (
<div className="space-y-6">
{/* Summary cards */}
<div className="grid gap-4 md:grid-cols-4">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
@@ -129,7 +129,7 @@ export default function BankFilePreviewStep({
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border max-h-96 overflow-y-auto">
<div className="rounded-md border max-h-96 overflow-x-auto overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
@@ -201,12 +201,12 @@ export default function BankFilePreviewStep({
)}
{/* Navigation */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack}>
<ArrowLeft className="mr-2 h-4 w-4" />
Tillbaka
</Button>
<Button onClick={onContinue} disabled={hasIssues || transactions.length === 0}>
<Button className="min-h-11" onClick={onContinue} disabled={hasIssues || transactions.length === 0}>
Fortsätt
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
+11 -13
View File
@@ -30,7 +30,7 @@ export default function BankFileResultStep({
<CardTitle className="flex items-center gap-2">
{isSuccess ? (
<>
<CheckCircle className="h-6 w-6 text-muted-foreground" />
<CheckCircle className="h-6 w-6 text-success" />
Import genomförd
</>
) : (
@@ -97,21 +97,19 @@ export default function BankFileResultStep({
)}
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onNewImport}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onNewImport}>
<RotateCcw className="mr-2 h-4 w-4" />
Ny import
</Button>
<div className="flex gap-2">
{isSuccess && (
<Button asChild>
<Link href="/transactions">
Visa transaktioner
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
)}
</div>
{isSuccess && (
<Button className="min-h-11" asChild>
<Link href="/transactions">
Visa transaktioner
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
)}
</div>
</div>
)
+6 -3
View File
@@ -23,6 +23,7 @@ import type { BankFileFormatId } from '@/lib/import/bank-file/types'
const FORMAT_NAMES: Record<string, string> = {
nordea: 'Nordea',
nordea_business: 'Nordea Företag',
seb: 'SEB',
swedbank: 'Swedbank',
handelsbanken: 'Handelsbanken',
@@ -110,18 +111,19 @@ export default function BankFileUploadStep({
</CardHeader>
<CardContent className="space-y-4">
{/* Format override */}
<div className="flex items-center gap-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<label className="text-sm font-medium whitespace-nowrap">Bank/format:</label>
<Select
value={formatOverride || 'auto'}
onValueChange={handleFormatChange}
>
<SelectTrigger className="w-64">
<SelectTrigger className="w-full sm:w-64">
<SelectValue placeholder="Automatisk identifiering" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Automatisk identifiering</SelectItem>
<SelectItem value="nordea">Nordea</SelectItem>
<SelectItem value="nordea_business">Nordea Företag</SelectItem>
<SelectItem value="seb">SEB</SelectItem>
<SelectItem value="swedbank">Swedbank</SelectItem>
<SelectItem value="handelsbanken">Handelsbanken</SelectItem>
@@ -181,7 +183,8 @@ export default function BankFileUploadStep({
<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="font-medium hidden sm:block">Dra och släpp bankfil här</p>
<p className="font-medium sm:hidden">Tryck för att välja bankfil</p>
<p className="text-sm text-muted-foreground">
CSV, TXT eller XML (max 10 MB)
</p>
+6 -6
View File
@@ -51,7 +51,7 @@ export default function ImportResultStep({ result, onNewImport }: ImportResultSt
{/* Statistics */}
{result.success && (
<div className="grid gap-4 md:grid-cols-3">
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
@@ -183,21 +183,21 @@ export default function ImportResultStep({ result, onNewImport }: ImportResultSt
)}
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onNewImport}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onNewImport}>
<RotateCcw className="mr-2 h-4 w-4" />
Ny import
</Button>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
{result.success && (
<>
<Button variant="outline" asChild>
<Button variant="outline" className="min-h-11" asChild>
<Link href="/bookkeeping">
Visa bokföring
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button asChild>
<Button className="min-h-11" asChild>
<Link href="/reports">
Visa rapporter
<ExternalLink className="ml-2 h-4 w-4" />
+3 -3
View File
@@ -246,11 +246,11 @@ export default function ImportReviewStep({
</Card>
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack} disabled={isLoading}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack} disabled={isLoading}>
Tillbaka
</Button>
<Button onClick={handleExecute} disabled={isLoading}>
<Button className="min-h-11" onClick={handleExecute} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
+5 -5
View File
@@ -99,7 +99,7 @@ export default function SIEPreviewStep({
</Card>
{/* Statistics */}
<div className="grid gap-4 md:grid-cols-4">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-muted-foreground mb-1">
@@ -201,7 +201,7 @@ export default function SIEPreviewStep({
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-4">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div>
<p className="text-sm text-muted-foreground">Totalt</p>
<p className="font-medium">{preview.mappingStatus.total}</p>
@@ -303,11 +303,11 @@ export default function SIEPreviewStep({
)}
{/* Actions */}
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<Button variant="outline" className="min-h-11" onClick={onBack}>
Tillbaka
</Button>
<Button onClick={onContinue} disabled={hasBlockingErrors}>
<Button className="min-h-11" onClick={onContinue} disabled={hasBlockingErrors}>
{preview.mappingStatus.lowConfidence > 0 || preview.mappingStatus.unmapped > 0
? 'Granska mappningar'
: 'Fortsätt'}
+4 -2
View File
@@ -103,8 +103,10 @@ export default function SIEUploadStep({ onFileSelect, isLoading, error }: SIEUpl
<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 SIE-fil här</p>
<p className="text-sm text-muted-foreground">eller klicka för att välja fil</p>
<p className="font-medium hidden sm:block">Dra och släpp SIE-fil här</p>
<p className="font-medium sm:hidden">Tryck för att välja SIE-fil</p>
<p className="text-sm text-muted-foreground hidden sm:block">eller klicka för att välja fil</p>
<p className="text-sm text-muted-foreground sm:hidden">.sie eller .se-filer</p>
</div>
</div>
)}