feat: arcim migration wizard UX, import fixes, Sentry setup (#22)

* feat: import system improvements, INK2 fix, and Swedish text corrections

- 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>

* feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding

Arcim migration wizard improvements:
- Progress bar now excludes non-interactive steps (migrating/result)
- Fix OAuth text to match target="_blank" behavior (new tab, not redirect)
- Display month names instead of "Månad X" in preview
- Fix Swedish typo "förifylla" in no-company-info message
- Replace native checkboxes with shadcn Switch in options step
- Add ConfirmationDialog before starting migration
- Show progress percentage during migration
- Add "Nästa steg" guidance and navigation links in result step
- Add "Försök igen" button in error state (returns to options)
- Add Bokio company ID help text (GUID from URL)
- Add Fortnox integration add-on hint on connection failure

Also includes: SIE import system improvements, INK2 fixes, Swedish text
corrections, Sentry error tracking setup, and arcim-migration extension
scaffolding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback

- Fix OAuth error recovery blank page (restore provider from URL params)
- Pass real userId to MigrationWizard instead of empty string
- Remove ~50 debug console.log statements from sie-import.ts
- Fix comment referencing account 3740 → 3741

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-13 16:24:54 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 82ad630448
commit 2ad8731dc9
44 changed files with 5108 additions and 122 deletions
+55 -3
View File
@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Progress } from '@/components/ui/progress'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import { ArrowLeftRight, FileText, ArrowLeft, Landmark, Loader2 } from 'lucide-react'
import { ArrowLeftRight, ArrowRightLeft, FileText, ArrowLeft, Landmark, Loader2 } from 'lucide-react'
import { createClient } from '@/lib/supabase/client'
import { BankSelector, type Bank } from '@/extensions/general/enable-banking/components/BankSelector'
import { BankConnectionStatus } from '@/extensions/general/enable-banking/components/BankConnectionStatus'
@@ -39,6 +39,12 @@ import type {
} from '@/lib/import/types'
import type { BASAccount } from '@/types'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import dynamic from 'next/dynamic'
const MigrationWizard = dynamic(
() => import('@/components/extensions/general/ArcimMigrationWorkspace'),
{ ssr: false, loading: () => <div className="flex items-center gap-3 text-muted-foreground p-6"><Loader2 className="h-5 w-5 animate-spin" />Laddar migreringsverktyg...</div> }
)
// ============================================================
// Bank File Import Wizard Steps
@@ -730,16 +736,36 @@ function PSD2ConnectWizard() {
// Import Page with Selection Cards
// ============================================================
type ImportMode = null | 'psd2' | 'bank' | 'sie'
type ImportMode = null | 'psd2' | 'bank' | 'sie' | 'migration'
export default function ImportPage() {
const [mode, setMode] = useState<ImportMode>(null)
const [userId, setUserId] = useState('')
// Fetch authenticated user ID for migration wizard
useEffect(() => {
const supabase = createClient()
supabase.auth.getUser().then(({ data: { user } }) => {
if (user) setUserId(user.id)
})
}, [])
// Auto-detect OAuth callback from migration extension
useEffect(() => {
if (new URLSearchParams(window.location.search).get('migration')) {
setMode('migration')
}
}, [])
// If extension isn't compiled in, we know synchronously it's unavailable
const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking')
const [hasBankingExtension, setHasBankingExtension] = useState<boolean | null>(
bankingCompiledIn ? null : false
)
// Migration extension: show card when compiled in (no DB toggle needed,
// since the extensions marketplace is not exposed in the UI)
const hasMigrationExtension = ENABLED_EXTENSION_IDS.has('arcim-migration')
useEffect(() => {
if (!bankingCompiledIn) return
fetch('/api/extensions/toggles/general/enable-banking')
@@ -761,7 +787,7 @@ export default function ImportPage() {
</div>
{mode === null && (
<div className={`grid gap-4 ${hasBankingExtension !== false ? 'md:grid-cols-3' : 'md:grid-cols-2'}`}>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{hasBankingExtension === null && (
<Card className="animate-pulse">
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
@@ -844,6 +870,31 @@ export default function ImportPage() {
</p>
</CardContent>
</Card>
{hasMigrationExtension === true && (
<Card
role="button"
tabIndex={0}
className="cursor-pointer hover:border-primary/50 active:scale-[0.98] transition-all"
onClick={() => setMode('migration')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('migration') } }}
>
<CardContent className="pt-6 pb-6 flex flex-col items-center text-center space-y-3">
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center">
<ArrowRightLeft className="h-6 w-6 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold">Migrera från annat system</h3>
<p className="text-sm text-muted-foreground mt-1">
Flytta bokföring, kunder, leverantörer och fakturor från Fortnox, Visma, Bokio, Björn Lundén eller Briox.
</p>
</div>
<p className="text-xs text-muted-foreground">
SIE-data, kunder, leverantörer, fakturor
</p>
</CardContent>
</Card>
)}
</div>
)}
@@ -857,6 +908,7 @@ export default function ImportPage() {
{mode === 'psd2' && <PSD2ConnectWizard />}
{mode === 'bank' && <BankFileImportWizard />}
{mode === 'sie' && <SIEImportWizard />}
{mode === 'migration' && <MigrationWizard userId={userId} />}
</div>
)
}
+2
View File
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import DashboardNav from '@/components/dashboard/DashboardNav'
import { RecaptIdentify } from '@/components/RecaptIdentify'
import { SentryIdentify } from '@/components/SentryIdentify'
import { SandboxBanner } from '@/components/dashboard/SandboxBanner'
import type { EntityType } from '@/types'
@@ -60,6 +61,7 @@ export default async function DashboardLayout({
{children}
</div>
</main>
<SentryIdentify userId={user.id} email={user.email} />
{!isSandbox && (
<RecaptIdentify
userId={user.id}
-1
View File
@@ -81,7 +81,6 @@ export default async function DashboardPage() {
const onboardingProgress: OnboardingProgress = {
hasCustomers: (customerCount || 0) > 0,
hasInvoices: (invoiceCount || 0) > 0,
hasReceipts: (receiptCount || 0) > 0,
hasBankConnected: (transactionCount || 0) > 0,
}
+24 -1
View File
@@ -40,7 +40,12 @@ export async function GET(
/**
* DELETE /api/import/sie/[id]
* Delete an import record (does not delete created journal entries)
* Delete an import record.
*
* Only failed or pending imports can be deleted. Completed imports have created
* journal entries that are part of räkenskapsinformation — deleting the metadata
* without reversing entries would leave orphaned bookkeeping data, and deleting
* both is prohibited under BFL 7 kap (7-year retention).
*/
export async function DELETE(
request: Request,
@@ -57,6 +62,24 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check current status before deleting
const { data: importRecord } = await supabase
.from('sie_imports')
.select('status')
.eq('id', id)
.eq('user_id', user.id)
.single()
if (!importRecord) {
return NextResponse.json({ error: 'Import not found' }, { status: 404 })
}
if (importRecord.status === 'completed') {
return NextResponse.json({
error: 'Slutförd import kan inte raderas. Importerade verifikationer ingår i räkenskapsinformationen (BFL 7 kap).',
}, { status: 403 })
}
const { error } = await supabase
.from('sie_imports')
.delete()
+13 -2
View File
@@ -7,7 +7,7 @@ import {
decodeBuffer,
calculateFileHash,
} from '@/lib/import/sie-parser'
import { suggestMappings, getMappingStats } from '@/lib/import/account-mapper'
import { suggestMappings, getMappingStats, isSystemAccount } from '@/lib/import/account-mapper'
import { generateImportPreview, checkDuplicateImport } from '@/lib/import/sie-import'
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
import type { SIEAccountMappingRecord } from '@/lib/import/types'
@@ -78,6 +78,15 @@ export async function POST(request: Request) {
}, { status: 400 })
}
// Separate source-system internal accounts (e.g. Fortnox 0099) from
// real bookkeeping accounts. System accounts have no BAS equivalent and
// should not appear in the mapping step.
const excludedSystemAccounts = parsed.accounts
.filter((a) => isSystemAccount(a.number))
.map((a) => ({ number: a.number, name: a.name }))
const bookkeepingAccounts = parsed.accounts
.filter((a) => !isSystemAccount(a.number))
// Fetch stored mappings from database
const { data: storedMappings } = await supabase
.from('sie_account_mappings')
@@ -88,13 +97,15 @@ export async function POST(request: Request) {
// the user's active chart (~40 accounts). Accounts that match will be
// auto-activated during the execute step.
const mappings = suggestMappings(
parsed.accounts,
bookkeepingAccounts,
BAS_REFERENCE,
(storedMappings as SIEAccountMappingRecord[]) || undefined
)
// Generate preview
const preview = generateImportPreview(parsed, mappings)
preview.excludedSystemAccounts = excludedSystemAccounts
preview.accountCount = bookkeepingAccounts.length
// Calculate file hash for storage
const fileHash = await calculateFileHash(content)
+37
View File
@@ -0,0 +1,37 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html lang="sv">
<body>
<div className="flex min-h-screen items-center justify-center p-8">
<div className="text-center space-y-4">
<h2 className="text-xl font-semibold">Något gick fel</h2>
<p className="text-muted-foreground">
Ett oväntat fel inträffade. Försök igen.
</p>
<button
onClick={reset}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
>
Försök igen
</button>
</div>
</div>
</body>
</html>
);
}
+27
View File
@@ -0,0 +1,27 @@
"use client";
import * as Sentry from "@sentry/nextjs";
export default function SentryExamplePage() {
return (
<div className="flex min-h-screen items-center justify-center p-8">
<div className="text-center space-y-4">
<h1 className="text-xl font-semibold">Sentry Test</h1>
<p className="text-muted-foreground text-sm">
Click the button to send a test error to Sentry.
</p>
<button
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
onClick={() => {
Sentry.captureException(
new Error("Sentry test error from gnubok")
);
alert("Test error sent to Sentry!");
}}
>
Throw Test Error
</button>
</div>
</div>
);
}