feat: enable PSD2 bank integration via settings
- Uncomment Enable Banking extension in loader (now registered at runtime) - Add subscriptionNotice field to ExtensionDefinition type - Show confirmation dialog when enabling extensions with subscription requirements - Fix Settings banking tab: toggle-aware visibility, URL-addressable tabs, BankSelector widget, correct API paths (/api/extensions/ext/enable-banking/*) - Replace inline bank connection cards with BankConnectionStatus component - Add actionable link to Settings from EnableBankingWorkspace - Update CLAUDE.md with latest architecture docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4163b46eae
commit
9d684d7e9f
@@ -34,38 +34,43 @@ app/
|
||||
(onboarding)/ 6-step setup wizard
|
||||
(dashboard)/ Authenticated routes (invoices, customers, transactions,
|
||||
bookkeeping, reports, suppliers, supplier-invoices,
|
||||
receipts, settings, calendar, help, import,
|
||||
receipts, deadlines, settings, help, import,
|
||||
extensions, e/[sector]/[slug])
|
||||
(public)/ Public invoice action links (no auth)
|
||||
api/ API routes organized by domain
|
||||
|
||||
components/
|
||||
ui/ shadcn/ui primitives (button, card, dialog, table, etc.)
|
||||
bookkeeping/ Chart of accounts manager, account combobox, add/edit dialogs
|
||||
calendar/ Calendar views, deadline cards, payment summary, todo widgets
|
||||
bookkeeping/ Chart of accounts manager, account combobox, add/edit dialogs,
|
||||
journal entry form/list/review, document upload, correction dialog
|
||||
chat/ ChatWidget, ChatPanel, ChatInput, ChatMessage
|
||||
customers/ CustomerForm
|
||||
dashboard/ DashboardContent, DashboardNav, FSkattWarningCard
|
||||
deadlines/ DeadlineCard, DeadlineFilters, DeadlineForm, DeadlineList,
|
||||
TaxTodoWidget, UpcomingDeadlinesWidget
|
||||
extensions/ Extension marketplace UI (ExtensionCard, SectorCard,
|
||||
ExtensionToggleButton, workspace components,
|
||||
per-sector subdirectories)
|
||||
import/ Bank file import workflow components
|
||||
per-sector subdirectories, shared reusable components)
|
||||
import/ Bank file import workflow components (SIE + bank file steps)
|
||||
invoices/ InvoiceReviewContent
|
||||
onboarding/ NewUserChecklist, setup step components
|
||||
reports/ Report views (including BankReconciliationView)
|
||||
reports/ Report views (BankReconciliationView, charts)
|
||||
settings/ CalendarFeedSettings
|
||||
suppliers/ Supplier-related components
|
||||
transactions/ Transaction list, categorization, booking components
|
||||
suppliers/ SupplierForm, SupplierInvoiceReviewContent
|
||||
transactions/ Transaction list, categorization, booking, swipe review,
|
||||
batch operations, invoice matching, VAT treatment
|
||||
|
||||
extensions/ Sector-based extension hierarchy
|
||||
general/ General-purpose extensions (all businesses)
|
||||
ai-categorization/ AI-powered transaction categorization
|
||||
ai-chat/ Claude-based chat assistant (LangChain RAG)
|
||||
calendar/ Payment calendar views, deadline cards, payment summary
|
||||
enable-banking/ PSD2 bank integration (opt-in, commented out in loader)
|
||||
example-logger/ Minimal reference extension (not loaded)
|
||||
invoice-inbox/ Supplier invoice intake via email/upload with AI extraction
|
||||
push-notifications/ Web push notification system
|
||||
receipt-ocr/ Receipt image OCR processing (includes components/pages)
|
||||
user-description-match/ User description matching for transaction categorization
|
||||
restaurant/ Restaurant & cafe sector
|
||||
food-cost/ Food cost percentage calculator
|
||||
earnings-per-liter/ Revenue per liter of alcohol
|
||||
@@ -100,6 +105,9 @@ lib/
|
||||
vat-entries.ts VAT line generation
|
||||
bas-reference.ts BAS account catalog (~180 accounts with metadata, SRU codes)
|
||||
account-descriptions.ts Human-readable account name lookup
|
||||
booking-templates.ts Booking template patterns for AI suggestions
|
||||
template-embeddings.ts AI embeddings for booking template matching
|
||||
client-account-names.ts Custom account name management
|
||||
validate-period-duration.ts Fiscal period duration validation (BFL 3 kap.)
|
||||
handlers/ Booking handler functions (supplier-invoice-handler.ts)
|
||||
core/
|
||||
@@ -111,6 +119,7 @@ lib/
|
||||
currency/ Riksbanken exchange rates
|
||||
deadlines/ Tax deadline tracking, status engine
|
||||
email/ Email service (Resend), invoice/reminder templates
|
||||
errors/ Error message utilities (get-error-message.ts)
|
||||
events/ Event bus (bus.ts, types.ts)
|
||||
extensions/ Extension system
|
||||
loader.ts FIRST_PARTY_EXTENSIONS array, static imports
|
||||
@@ -125,9 +134,13 @@ lib/
|
||||
icon-resolver.tsx Dynamic icon lookup for extensions
|
||||
use-account-totals.ts Hook for account balance queries
|
||||
use-extension-data.ts Hook for extension-specific data
|
||||
invoice-inbox-utils.ts Utilities for supplier invoice inbox
|
||||
use-mock-data.ts Mock data utilities for development
|
||||
hooks/ React hooks (use-unsaved-changes.ts)
|
||||
import/ SIE parser, SIE import orchestrator, bank file parser
|
||||
bank-file/ Bank file parser with format modules
|
||||
formats/ camt053, generic-csv, handelsbanken, nordea, seb, swedbank
|
||||
formats/ camt053, generic-csv, handelsbanken, nordea, seb, swedbank,
|
||||
ica-banken, lansforsakringar, lunar, skandia
|
||||
invoices/ VAT rules, invoice matching, PDF template, reminder processor
|
||||
reconciliation/ Bank reconciliation engine (4-pass matching algorithm)
|
||||
reports/ Financial reports (trial-balance, income-statement,
|
||||
@@ -137,9 +150,12 @@ lib/
|
||||
ar-ledger, ar-reconciliation, monthly-breakdown)
|
||||
supabase/ Client setup (client.ts = browser, server.ts = server/admin,
|
||||
fetch-all.ts = pagination helper, middleware.ts)
|
||||
tax/ Tax calculations, deadlines, Swedish holidays, expense warnings
|
||||
tax/ Tax calculations, deadlines, deadline generator,
|
||||
Swedish holidays, expense warnings
|
||||
transactions/ Transaction processing, category suggestions
|
||||
vat/ VAT utilities (VIES client for EU VAT validation)
|
||||
init.ts Extension loader (idempotent, called by API routes)
|
||||
logger.ts Centralized logging utility
|
||||
utils.ts Shared utility functions
|
||||
|
||||
types/index.ts Canonical type definitions (single source of truth)
|
||||
@@ -270,14 +286,16 @@ Key types (from `lib/extensions/types.ts`):
|
||||
### Currently Loaded Extensions (FIRST_PARTY_EXTENSIONS)
|
||||
|
||||
```
|
||||
receiptOcrExtension @/extensions/general/receipt-ocr
|
||||
aiCategorizationExtension @/extensions/general/ai-categorization
|
||||
pushNotificationsExtension @/extensions/general/push-notifications
|
||||
sruExportExtension @/extensions/sru-export
|
||||
neBilagaExtension @/extensions/ne-bilaga
|
||||
aiChatExtension @/extensions/general/ai-chat
|
||||
invoiceInboxExtension @/extensions/general/invoice-inbox
|
||||
# enableBankingExtension @/extensions/general/enable-banking (commented out, opt-in)
|
||||
receiptOcrExtension @/extensions/general/receipt-ocr
|
||||
aiCategorizationExtension @/extensions/general/ai-categorization
|
||||
pushNotificationsExtension @/extensions/general/push-notifications
|
||||
sruExportExtension @/extensions/sru-export
|
||||
neBilagaExtension @/extensions/ne-bilaga
|
||||
aiChatExtension @/extensions/general/ai-chat
|
||||
invoiceInboxExtension @/extensions/general/invoice-inbox
|
||||
calendarExtension @/extensions/general/calendar
|
||||
userDescriptionMatchExtension @/extensions/general/user-description-match
|
||||
# enableBankingExtension @/extensions/general/enable-banking (commented out, opt-in)
|
||||
```
|
||||
|
||||
### Extension Interface
|
||||
@@ -430,16 +448,35 @@ mockResult({ data: makeTransaction(), error: null })
|
||||
- `lib/api/__tests__/validate.test.ts` — Body/query validation helpers
|
||||
- `lib/bookkeeping/__tests__/engine.test.ts` — Balance validation
|
||||
- `lib/bookkeeping/__tests__/invoice-entries.test.ts` — Per-line VAT, mixed-rate invoices, credit notes
|
||||
- `lib/bookkeeping/__tests__/mapping-engine.test.ts` — Auto-categorization rule matching
|
||||
- `lib/bookkeeping/__tests__/category-mapping.test.ts` — Category-to-account mapping
|
||||
- `lib/bookkeeping/__tests__/booking-templates.test.ts` — Booking template patterns
|
||||
- `lib/bookkeeping/__tests__/template-embeddings.test.ts` — AI embedding matching
|
||||
- `lib/bookkeeping/__tests__/validate-period-duration.test.ts` — Fiscal period duration
|
||||
- `lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts` — Supplier booking handler
|
||||
- `lib/core/bookkeeping/__tests__/storno-service.test.ts` — Complex mock queues
|
||||
- `lib/core/bookkeeping/__tests__/period-service.test.ts` — Fiscal period management
|
||||
- `lib/core/bookkeeping/__tests__/year-end-service.test.ts` — Year-end closing
|
||||
- `lib/core/documents/__tests__/document-service.test.ts` — Storage mocking
|
||||
- `lib/core/tax/__tests__/tax-code-service.test.ts` — Tax code management
|
||||
- `lib/currency/__tests__/riksbanken.test.ts` — Exchange rate fetching
|
||||
- `lib/events/__tests__/bus.test.ts` — Event bus behavior
|
||||
- `lib/extensions/__tests__/registry.test.ts` — Extension registration
|
||||
- `lib/extensions/__tests__/loader.test.ts` — Extension loader
|
||||
- `lib/extensions/__tests__/toggle-check.test.ts` — Extension toggle logic
|
||||
- `lib/extensions/__tests__/validation.test.ts` — Extension data validation
|
||||
- `lib/extensions/__tests__/sectors.test.ts` — Sector registry
|
||||
- `lib/extensions/__tests__/context-factory.test.ts` — Extension context builder
|
||||
- `lib/extensions/__tests__/invoice-inbox-utils.test.ts` — Invoice inbox utilities
|
||||
- `lib/import/__tests__/sie-parser.test.ts` — SIE file parsing
|
||||
- `lib/import/__tests__/sie-import.test.ts` — SIE import orchestration
|
||||
- `lib/import/__tests__/account-mapper.test.ts` — SIE account mapping
|
||||
- `lib/import/bank-file/__tests__/parser.test.ts` — Bank file format parsing
|
||||
- `lib/reconciliation/__tests__/bank-reconciliation.test.ts` — Reconciliation matching algorithm
|
||||
- `lib/reports/__tests__/vat-declaration.test.ts` — VAT declaration report
|
||||
- `lib/tax/__tests__/deadline-config.test.ts` — Tax deadline configuration
|
||||
- `lib/transactions/__tests__/ingest.test.ts` — Transaction ingestion and dedup
|
||||
- `lib/vat/__tests__/vies-client.test.ts` — VIES VAT number validation
|
||||
|
||||
---
|
||||
|
||||
@@ -447,11 +484,11 @@ mockResult({ data: makeTransaction(), error: null })
|
||||
|
||||
### Location
|
||||
|
||||
`supabase/migrations/` — currently 39 files numbered `20240101000001` through `20240101000038` (note: two files share number `000033`).
|
||||
`supabase/migrations/` — currently 41 files numbered `20240101000001` through `20240101000041`.
|
||||
|
||||
### Naming Convention
|
||||
|
||||
`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000039_*.sql`
|
||||
`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000042_*.sql`
|
||||
|
||||
### Migration Rules
|
||||
|
||||
@@ -491,12 +528,14 @@ mockResult({ data: makeTransaction(), error: null })
|
||||
- **Migration 031 (`invoice_document_type`)** — Adds `document_type` column to `invoices` (CHECK: invoice/proforma/delivery_note, default 'invoice') and `converted_from_id` FK for tracking proforma-to-invoice conversions.
|
||||
- **Migration 032 (`add_accounting_method`)** — Adds `accounting_method` column to `company_settings` (CHECK: accrual/cash, default 'accrual') to support kontantmetoden vs faktureringsmetoden.
|
||||
- **Migration 033 (`ai_chat_schema`)** — AI chat conversation and message storage.
|
||||
- **Migration 033 (`invoice_inbox`)** — Invoice inbox table for supplier invoice intake. (Note: shares number with ai_chat_schema.)
|
||||
- **Migration 034 (`fix_extension_data_trigger`)** — Fixes extension data trigger.
|
||||
- **Migration 035 (`fix_push_notifications`)** — Push notifications schema fix.
|
||||
- **Migration 036 (`fix_enable_banking`)** — Enable Banking schema fix.
|
||||
- **Migration 037 (`extension_toggles`)** — Extension toggle table for per-user enable/disable.
|
||||
- **Migration 038 (`fix_match_documents_search_path`)** — Fixes search path for document matching function.
|
||||
- **Migration 039 (`invoice_inbox`)** — Invoice inbox table for supplier invoice intake via email/upload.
|
||||
- **Migration 040 (`booking_template_embeddings`)** — Booking templates with AI embeddings for suggestion matching.
|
||||
- **Migration 041 (`user_description_matching`)** — User description matching for transaction categorization.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@ export default async function ExtensionDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExtensionToggleButton sectorSlug={sectorSlug} extensionSlug={extensionSlug} />
|
||||
<ExtensionToggleButton
|
||||
sectorSlug={sectorSlug}
|
||||
extensionSlug={extensionSlug}
|
||||
subscriptionNotice={definition.subscriptionNotice}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
|
||||
@@ -8,18 +8,13 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import {
|
||||
Loader2,
|
||||
Building,
|
||||
CreditCard,
|
||||
User,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
LogOut,
|
||||
Bell,
|
||||
Calendar,
|
||||
@@ -27,11 +22,14 @@ import {
|
||||
Moon,
|
||||
Monitor,
|
||||
Palette,
|
||||
Landmark,
|
||||
} from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import type { CompanySettings, BankConnection } from '@/types'
|
||||
import { NotificationSettings } from '@/extensions/general/push-notifications/NotificationSettings'
|
||||
import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings'
|
||||
import { BankSelector, type Bank } from '@/extensions/general/enable-banking/components/BankSelector'
|
||||
import { BankConnectionStatus } from '@/extensions/general/enable-banking/components/BankConnectionStatus'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
@@ -46,9 +44,12 @@ export default function SettingsPage() {
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [hasBankingExtension, setHasBankingExtension] = useState(false)
|
||||
const [selectedBank, setSelectedBank] = useState<Bank | null>(null)
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const initialTab = searchParams.get('tab') || 'company'
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
@@ -106,18 +107,16 @@ export default function SettingsPage() {
|
||||
|
||||
setBankConnections(connections || [])
|
||||
|
||||
// Check if extension API is available (will return banks list if extension is loaded)
|
||||
// Check if Enable Banking extension is enabled via toggle API
|
||||
try {
|
||||
const bankingCheck = await fetch('/api/extensions/enable-banking/callback', {
|
||||
method: 'HEAD',
|
||||
})
|
||||
// The extension route existing means it's deployed - but we also need
|
||||
// to check if there are existing connections to show the tab
|
||||
setHasBankingExtension(
|
||||
(connections && connections.length > 0) || bankingCheck.status !== 404
|
||||
)
|
||||
const toggleRes = await fetch('/api/extensions/toggles/general/enable-banking')
|
||||
if (toggleRes.ok) {
|
||||
const { data } = await toggleRes.json()
|
||||
setHasBankingExtension(data?.enabled || (connections && connections.length > 0) || false)
|
||||
} else {
|
||||
setHasBankingExtension((connections && connections.length > 0) || false)
|
||||
}
|
||||
} catch {
|
||||
// If the extension routes don't exist, only show tab if there are existing connections
|
||||
setHasBankingExtension((connections && connections.length > 0) || false)
|
||||
}
|
||||
|
||||
@@ -182,7 +181,7 @@ export default function SettingsPage() {
|
||||
setIsConnecting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/extensions/enable-banking/callback', {
|
||||
const response = await fetch('/api/extensions/ext/enable-banking/connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ aspsp_name: bankName, aspsp_country: bankCountry }),
|
||||
@@ -210,7 +209,7 @@ export default function SettingsPage() {
|
||||
setIsSyncing(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/extensions/enable-banking/sync/cron', {
|
||||
const response = await fetch('/api/extensions/ext/enable-banking/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ connection_id: connectionId }),
|
||||
@@ -275,24 +274,6 @@ export default function SettingsPage() {
|
||||
|
||||
const activeConnections = bankConnections.filter((c) => c.status === 'active')
|
||||
|
||||
// Helper to calculate days until consent expires
|
||||
function getDaysUntilExpiry(expiresAt: string | null): number | null {
|
||||
if (!expiresAt) return null
|
||||
const expiryDate = new Date(expiresAt)
|
||||
const now = new Date()
|
||||
const diffTime = expiryDate.getTime() - now.getTime()
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
return Math.max(0, diffDays)
|
||||
}
|
||||
|
||||
function isConsentExpiringSoon(expiresAt: string | null): boolean {
|
||||
if (!expiresAt) return false
|
||||
const expiryDate = new Date(expiresAt)
|
||||
const warningDate = new Date()
|
||||
warningDate.setDate(warningDate.getDate() + 7)
|
||||
return expiryDate <= warningDate
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -302,7 +283,7 @@ export default function SettingsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="company" className="space-y-6">
|
||||
<Tabs defaultValue={initialTab} className="space-y-6">
|
||||
<TabsList className="flex-wrap h-auto gap-1">
|
||||
<TabsTrigger value="company">
|
||||
<Building className="mr-2 h-4 w-4" />
|
||||
@@ -554,66 +535,63 @@ export default function SettingsPage() {
|
||||
<CardTitle>Anslutna banker</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{activeConnections.map((connection) => {
|
||||
const daysUntilExpiry = getDaysUntilExpiry(connection.consent_expires)
|
||||
const isExpiring = isConsentExpiringSoon(connection.consent_expires)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={connection.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<CreditCard className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{connection.bank_name}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>
|
||||
Senast synkad: {formatDate(connection.last_synced_at || connection.created_at)}
|
||||
</span>
|
||||
{isExpiring && (
|
||||
<Badge variant="warning" className="flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{daysUntilExpiry} dagar kvar
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSyncTransactions(connection.id)}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDisconnectBank(connection.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{activeConnections.map((connection) => (
|
||||
<BankConnectionStatus
|
||||
key={connection.id}
|
||||
connection={connection}
|
||||
onSync={handleSyncTransactions}
|
||||
onDisconnect={handleDisconnectBank}
|
||||
isSyncing={isSyncing}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Connect new bank */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Anslut ny bank</CardTitle>
|
||||
<CardDescription>
|
||||
Välj din bank nedan för att koppla ditt konto via PSD2.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<BankSelector
|
||||
onSelect={setSelectedBank}
|
||||
isLoading={isConnecting}
|
||||
/>
|
||||
{selectedBank && (
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Landmark className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{selectedBank.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{selectedBank.country}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleConnectBank(selectedBank.name, selectedBank.country)}
|
||||
disabled={isConnecting}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Ansluter...
|
||||
</>
|
||||
) : (
|
||||
'Anslut bank'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Info about PSD2 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bankintegration (PSD2)</CardTitle>
|
||||
<CardTitle>Om bankintegration (PSD2)</CardTitle>
|
||||
<CardDescription>
|
||||
Automatisk import av transaktioner via PSD2 open banking.
|
||||
Samtycket gäller i 90 dagar och behöver sedan förnyas.
|
||||
|
||||
@@ -76,11 +76,14 @@ export default function DashboardNav({ companyName, entityType, enabledExtension
|
||||
const isOvrigtExpanded = isOnOvrigtPage || manualOvrigtExpanded
|
||||
// Auto-expand Tillägg when on an extension page, or when manually toggled (persisted)
|
||||
const isOnExtensionPage = pathname.startsWith('/e/')
|
||||
const [manualTillaggExpanded, setManualTillaggExpanded] = useState(() => {
|
||||
if (typeof window === 'undefined') return false
|
||||
return localStorage.getItem('tillagg-expanded') === 'true'
|
||||
})
|
||||
const [manualTillaggExpanded, setManualTillaggExpanded] = useState(false)
|
||||
const isTillaggExpanded = isOnExtensionPage || manualTillaggExpanded
|
||||
|
||||
// Restore persisted state after hydration to avoid SSR mismatch
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('tillagg-expanded') === 'true'
|
||||
if (stored) setManualTillaggExpanded(true)
|
||||
}, [])
|
||||
const [liveExtensions, setLiveExtensions] = useState(enabledExtensions ?? [])
|
||||
|
||||
const fetchExtensions = useCallback(async () => {
|
||||
|
||||
@@ -1,23 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { useExtensionToggle } from '@/lib/extensions/hooks'
|
||||
|
||||
export default function ExtensionToggleButton({
|
||||
sectorSlug,
|
||||
extensionSlug,
|
||||
subscriptionNotice,
|
||||
}: {
|
||||
sectorSlug: string
|
||||
extensionSlug: string
|
||||
subscriptionNotice?: string
|
||||
}) {
|
||||
const { enabled, isLoading, toggle } = useExtensionToggle(sectorSlug, extensionSlug)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
function handleToggle() {
|
||||
// Show confirmation dialog when enabling an extension with a subscription notice
|
||||
if (!enabled && subscriptionNotice) {
|
||||
setShowConfirm(true)
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
setShowConfirm(false)
|
||||
toggle()
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={toggle}
|
||||
disabled={isLoading}
|
||||
aria-label={enabled ? 'Inaktivera tillägg' : 'Aktivera tillägg'}
|
||||
/>
|
||||
<>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading}
|
||||
aria-label={enabled ? 'Inaktivera tillägg' : 'Aktivera tillägg'}
|
||||
/>
|
||||
|
||||
{subscriptionNotice && (
|
||||
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Extern prenumeration krävs</DialogTitle>
|
||||
<DialogDescription>{subscriptionNotice}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowConfirm(false)}>
|
||||
Avbryt
|
||||
</Button>
|
||||
<Button onClick={handleConfirm}>
|
||||
Jag förstår, aktivera
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
|
||||
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
|
||||
import { Landmark } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Landmark, Settings } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function EnableBankingWorkspace({ userId }: WorkspaceComponentProps) {
|
||||
return (
|
||||
<EmptyExtensionState
|
||||
title="Bankintegration (PSD2)"
|
||||
description="Koppla ditt bankkonto under Inst\u00e4llningar f\u00f6r att synka transaktioner automatiskt."
|
||||
icon={<Landmark className="h-12 w-12 text-muted-foreground/40 mb-4" />}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Landmark className="h-12 w-12 text-muted-foreground/40 mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground">Bankintegration (PSD2)</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-md">
|
||||
Koppla ditt bankkonto under Inställningar för att synka transaktioner automatiskt.
|
||||
</p>
|
||||
<Button asChild variant="outline" className="mt-4">
|
||||
<Link href="/settings?tab=banking">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Gå till bankinställningar
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,7 @@ import { intrastatExtension } from '@/extensions/export/intrastat'
|
||||
import { currencyReceivablesExtension } from '@/extensions/export/currency-receivables'
|
||||
import type { Extension } from './types'
|
||||
|
||||
// ── Enable Banking (PSD2) — opt-in extension ───────────────────────────
|
||||
// Uncomment the following line to enable automatic PSD2 bank transaction sync.
|
||||
// Requires ENABLE_BANKING_APP_ID and ENABLE_BANKING_PRIVATE_KEY env vars.
|
||||
//
|
||||
// import { enableBankingExtension } from '@/extensions/general/enable-banking'
|
||||
import { enableBankingExtension } from '@/extensions/general/enable-banking'
|
||||
|
||||
/**
|
||||
* Explicit list of first-party extensions.
|
||||
@@ -36,7 +32,7 @@ const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
invoiceInboxExtension,
|
||||
calendarExtension,
|
||||
userDescriptionMatchExtension,
|
||||
// enableBankingExtension, // Uncomment to activate PSD2 bank sync
|
||||
enableBankingExtension,
|
||||
|
||||
// ── Export sector ──────────────────────────────────────────
|
||||
euSalesListExtension,
|
||||
|
||||
@@ -117,6 +117,8 @@ export const SECTORS: Sector[] = [
|
||||
description: 'Automatisk banktransaktionssynk via PSD2',
|
||||
longDescription:
|
||||
'Koppla ditt bankkonto direkt och synka transaktioner automatiskt via säker PSD2-bankintegration. Stöder de flesta svenska banker.',
|
||||
subscriptionNotice:
|
||||
'Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface ExtensionDefinition {
|
||||
readsCoreTables?: string[]
|
||||
hasOwnData?: boolean
|
||||
quickAction?: QuickActionDefinition
|
||||
/** Notice shown when enabling — e.g. external subscription requirement */
|
||||
subscriptionNotice?: string
|
||||
}
|
||||
|
||||
/** Sector definition with its extensions */
|
||||
|
||||
@@ -166,8 +166,8 @@ function accountToCategory(account: string, amount: number): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Source priority for sorting — higher-quality sources rank first.
|
||||
* AI and mapping rules always beat history-based guesses.
|
||||
* Source priority for tiebreaking — used when two suggestions
|
||||
* have the same confidence score.
|
||||
*/
|
||||
const SOURCE_PRIORITY: Record<SuggestedCategory['source'], number> = {
|
||||
mapping_rule: 3,
|
||||
@@ -187,19 +187,28 @@ export function mergeAiSuggestions(
|
||||
aiSuggestions: { category: string; basAccount: string; confidence: number; reasoning: string }[],
|
||||
transactionAmount?: number
|
||||
): SuggestedCategory[] {
|
||||
const seen = new Set<string>(existing.map((s) => s.category))
|
||||
const merged = [...existing]
|
||||
|
||||
for (const ai of aiSuggestions) {
|
||||
if (seen.has(ai.category)) continue
|
||||
|
||||
// Skip suggestions that don't match transaction direction
|
||||
if (transactionAmount !== undefined) {
|
||||
if (transactionAmount > 0 && ai.category.startsWith('expense_')) continue
|
||||
if (transactionAmount < 0 && ai.category.startsWith('income_')) continue
|
||||
}
|
||||
|
||||
seen.add(ai.category)
|
||||
const existingIdx = merged.findIndex((s) => s.category === ai.category)
|
||||
if (existingIdx !== -1) {
|
||||
// Upgrade existing entry if the AI has higher confidence
|
||||
if (ai.confidence > merged[existingIdx].confidence) {
|
||||
merged[existingIdx] = {
|
||||
...merged[existingIdx],
|
||||
account: ai.basAccount || merged[existingIdx].account,
|
||||
confidence: ai.confidence,
|
||||
source: 'ai',
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
merged.push({
|
||||
category: ai.category as TransactionCategory,
|
||||
@@ -210,12 +219,12 @@ export function mergeAiSuggestions(
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by source priority first, then by confidence within the same tier
|
||||
// Sort by confidence first, then by source priority as tiebreaker
|
||||
return merged
|
||||
.sort((a, b) => {
|
||||
const priorityDiff = SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]
|
||||
if (priorityDiff !== 0) return priorityDiff
|
||||
return b.confidence - a.confidence
|
||||
const confidenceDiff = b.confidence - a.confidence
|
||||
if (confidenceDiff !== 0) return confidenceDiff
|
||||
return SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]
|
||||
})
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user