feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup

- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-04 14:19:56 +01:00
co-authored by Claude Opus 4.6
parent a9bf24ce0b
commit 66a4027f1e
104 changed files with 4964 additions and 5501 deletions
@@ -2,7 +2,6 @@
export const ENABLED_EXTENSION_IDS: ReadonlySet<string> = new Set([
'enable-banking',
'ai-categorization',
'ai-chat',
'email',
])
@@ -1,13 +1,11 @@
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
import type { Extension } from '../types'
import { enableBankingExtension } from '@/extensions/general/enable-banking'
import { aiCategorizationExtension } from '@/extensions/general/ai-categorization'
import { aiChatExtension } from '@/extensions/general/ai-chat'
import { emailExtension } from '@/extensions/general/email'
export const FIRST_PARTY_EXTENSIONS: Extension[] = [
enableBankingExtension,
aiCategorizationExtension,
aiChatExtension,
emailExtension,
]
@@ -15,19 +15,6 @@ export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
"hasOwnData": true,
"subscriptionNotice": "Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera."
},
{
"slug": "ai-categorization",
"name": "AI-kategorisering",
"sector": "general",
"category": "operations",
"icon": "Sparkles",
"dataPattern": "core",
"description": "AI-drivna kategoriförslag för transaktioner",
"longDescription": "Använder AI för att automatiskt föreslå BAS-kontokategorier för dina banktransaktioner. Lär sig från dina tidigare bokföringsval.",
"readsCoreTables": [
"transactions"
]
},
{
"slug": "ai-chat",
"name": "AI-assistent",
@@ -5,6 +5,5 @@ import type { WorkspaceComponentProps } from '../workspace-registry'
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {
'general/enable-banking': dynamic(() => import('@/components/extensions/general/EnableBankingWorkspace')),
'general/ai-categorization': dynamic(() => import('@/components/extensions/general/AiCategorizationWorkspace')),
'general/ai-chat': dynamic(() => import('@/components/extensions/general/AiChatWorkspace')),
}
-80
View File
@@ -1,80 +0,0 @@
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
interface AccountTotal {
account_number: string
debit: number
credit: number
net: number
}
interface MonthlyTotal {
month: string
account_number: string
debit: number
credit: number
net: number
}
interface UseAccountTotalsOptions {
from: string
to: string
dateFrom?: string
dateTo?: string
groupBy?: 'month'
}
export function useAccountTotals(options: UseAccountTotalsOptions) {
const [totals, setTotals] = useState<AccountTotal[]>([])
const [monthly, setMonthly] = useState<MonthlyTotal[]>([])
const [isLoading, setIsLoading] = useState(true)
const mountedRef = useRef(true)
useEffect(() => {
mountedRef.current = true
return () => { mountedRef.current = false }
}, [])
const refresh = useCallback(async () => {
setIsLoading(true)
try {
const params = new URLSearchParams({
from: options.from,
to: options.to,
})
if (options.dateFrom) params.set('date_from', options.dateFrom)
if (options.dateTo) params.set('date_to', options.dateTo)
if (options.groupBy) params.set('group_by', options.groupBy)
const res = await fetch(`/api/bookkeeping/account-totals?${params}`)
if (res.ok) {
const json = await res.json()
if (mountedRef.current) {
setTotals(json.totals ?? [])
setMonthly(json.monthly ?? [])
}
}
} finally {
if (mountedRef.current) setIsLoading(false)
}
}, [options.from, options.to, options.dateFrom, options.dateTo, options.groupBy])
useEffect(() => {
refresh()
}, [refresh])
const totalDebit = totals.reduce((sum, t) => sum + t.debit, 0)
const totalCredit = totals.reduce((sum, t) => sum + t.credit, 0)
const totalNet = totals.reduce((sum, t) => sum + t.net, 0)
return {
totals,
monthly,
isLoading,
totalDebit: Math.round(totalDebit * 100) / 100,
totalCredit: Math.round(totalCredit * 100) / 100,
totalNet: Math.round(totalNet * 100) / 100,
refresh,
}
}
-85
View File
@@ -1,85 +0,0 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useExtensionData } from './use-extension-data'
interface MockMeta {
importedAt: string
source: 'csv' | 'json'
fileName: string
rowCount: number
}
interface UseMockDataResult<T> {
mockReport: T | null
isMockActive: boolean
isLoading: boolean
importedAt: string | null
meta: MockMeta | null
saveMockData: (report: T, meta: Omit<MockMeta, 'importedAt'>) => Promise<void>
clearMockData: () => Promise<void>
}
export function useMockData<T>(sector: string, slug: string): UseMockDataResult<T> {
const { getByKey, save, remove, isLoading } = useExtensionData(sector, slug)
const [mockReport, setMockReport] = useState<T | null>(null)
const [isMockActive, setIsMockActive] = useState(false)
const [meta, setMeta] = useState<MockMeta | null>(null)
// Read mock state from extension data on load
useEffect(() => {
if (isLoading) return
const enabledRecord = getByKey('mock:enabled')
const reportRecord = getByKey('mock:report')
const metaRecord = getByKey('mock:meta')
if (enabledRecord && (enabledRecord.value as { enabled?: boolean }).enabled && reportRecord) {
setIsMockActive(true)
setMockReport(reportRecord.value as T)
if (metaRecord) {
setMeta(metaRecord.value as unknown as MockMeta)
}
} else {
setIsMockActive(false)
setMockReport(null)
setMeta(null)
}
}, [isLoading, getByKey])
const saveMockData = useCallback(async (report: T, metaInput: Omit<MockMeta, 'importedAt'>) => {
const fullMeta: MockMeta = {
...metaInput,
importedAt: new Date().toISOString(),
}
await save('mock:enabled', { enabled: true })
await save('mock:report', report as unknown as Record<string, unknown>)
await save('mock:meta', fullMeta as unknown as Record<string, unknown>)
setIsMockActive(true)
setMockReport(report)
setMeta(fullMeta)
}, [save])
const clearMockData = useCallback(async () => {
await remove('mock:enabled')
await remove('mock:report')
await remove('mock:meta')
setIsMockActive(false)
setMockReport(null)
setMeta(null)
}, [remove])
return {
mockReport,
isMockActive,
isLoading,
importedAt: meta?.importedAt ?? null,
meta,
saveMockData,
clearMockData,
}
}