diff --git a/app/(dashboard)/transactions/page.tsx b/app/(dashboard)/transactions/page.tsx
index 36c8274f..5bba42f0 100644
--- a/app/(dashboard)/transactions/page.tsx
+++ b/app/(dashboard)/transactions/page.tsx
@@ -185,7 +185,7 @@ export default function TransactionsPage() {
if (!response.ok) {
toast({ title: 'Fel', description: result.error || 'Kunde inte uppdatera transaktion', variant: 'destructive' })
setProcessingId(null)
- return false
+ return null
}
// Mark as exiting for animation, then update state
@@ -217,11 +217,11 @@ export default function TransactionsPage() {
setProcessingId(null)
}, 350)
- return true
+ return result.journal_entry_id || null
} catch {
toast({ title: 'Fel', description: 'Något gick fel vid bokföring', variant: 'destructive' })
setProcessingId(null)
- return false
+ return null
}
}
@@ -458,13 +458,14 @@ export default function TransactionsPage() {
category: TransactionCategory,
vatTreatment: VatTreatment | undefined,
accountOverride: string | undefined
- ) {
- const success = await handleCategorize(id, true, category, vatTreatment, accountOverride)
- if (success) {
+ ): Promise {
+ const journalEntryId = await handleCategorize(id, true, category, vatTreatment, accountOverride)
+ if (journalEntryId) {
setQuickReviewOpen(false)
setQuickReviewTransaction(null)
setQuickReviewCategory(null)
}
+ return journalEntryId
}
// Swipe view
diff --git a/app/api/transactions/[id]/categorize/route.ts b/app/api/transactions/[id]/categorize/route.ts
index db27329e..128151dc 100644
--- a/app/api/transactions/[id]/categorize/route.ts
+++ b/app/api/transactions/[id]/categorize/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
import { buildMappingResultFromCategory } from '@/lib/bookkeeping/category-mapping'
+import { getTemplateById, buildMappingResultFromTemplate } from '@/lib/bookkeeping/booking-templates'
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
import { saveUserMappingRule } from '@/lib/bookkeeping/mapping-engine'
import { validateBody } from '@/lib/api/validate'
@@ -153,18 +154,36 @@ export async function POST(
const fiscalYearStartMonth: number = settings?.fiscal_year_start_month ?? 1
// Determine the category to use
- const finalCategory: TransactionCategory = is_business
- ? (category || 'uncategorized')
- : 'private'
+ let finalCategory: TransactionCategory
+ if (body.template_id) {
+ const template = getTemplateById(body.template_id)
+ if (template) {
+ finalCategory = is_business ? template.fallback_category : 'private'
+ } else {
+ return NextResponse.json({ error: 'Invalid template_id' }, { status: 400 })
+ }
+ } else {
+ finalCategory = is_business ? (category || 'uncategorized') : 'private'
+ }
- // Build mapping result from category
- const mappingResult = buildMappingResultFromCategory(
- finalCategory,
- transaction as Transaction,
- is_business,
- entityType,
- body.vat_treatment
- )
+ // Build mapping result from template or category
+ let mappingResult
+ if (body.template_id) {
+ const template = getTemplateById(body.template_id)!
+ mappingResult = buildMappingResultFromTemplate(
+ template,
+ transaction as Transaction,
+ entityType
+ )
+ } else {
+ mappingResult = buildMappingResultFromCategory(
+ finalCategory,
+ transaction as Transaction,
+ is_business,
+ entityType,
+ body.vat_treatment
+ )
+ }
// Apply account override if provided (only for business transactions)
if (is_business && body.account_override) {
diff --git a/app/api/transactions/suggest-categories/route.ts b/app/api/transactions/suggest-categories/route.ts
index a4773834..5f2e219a 100644
--- a/app/api/transactions/suggest-categories/route.ts
+++ b/app/api/transactions/suggest-categories/route.ts
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
-import { getSuggestedCategories, mergeAiSuggestions, type SuggestedCategory } from '@/lib/transactions/category-suggestions'
-import type { Transaction, TransactionCategory } from '@/types'
+import { getSuggestedCategories, mergeAiSuggestions, getSuggestedTemplates, type SuggestedCategory, type SuggestedTemplate } from '@/lib/transactions/category-suggestions'
+import type { Transaction, TransactionCategory, EntityType } from '@/types'
/**
* POST /api/transactions/suggest-categories
@@ -78,8 +78,17 @@ export async function POST(request: Request) {
}
}
+ // Fetch entity type for template matching
+ const { data: settings } = await supabase
+ .from('company_settings')
+ .select('entity_type')
+ .eq('user_id', user.id)
+ .single()
+ const entityType = (settings?.entity_type as EntityType) || undefined
+
// Generate suggestions for each transaction
const suggestions: Record = {}
+ const template_suggestions: Record = {}
for (const tx of transactions) {
let result = getSuggestedCategories(
@@ -95,7 +104,8 @@ export async function POST(request: Request) {
}
suggestions[tx.id] = result
+ template_suggestions[tx.id] = getSuggestedTemplates(tx as Transaction, entityType)
}
- return NextResponse.json({ suggestions })
+ return NextResponse.json({ suggestions, template_suggestions })
}
diff --git a/components/import/BankFileUploadStep.tsx b/components/import/BankFileUploadStep.tsx
index d81550b2..5cc7b81e 100644
--- a/components/import/BankFileUploadStep.tsx
+++ b/components/import/BankFileUploadStep.tsx
@@ -27,6 +27,10 @@ const FORMAT_NAMES: Record = {
seb: 'SEB',
swedbank: 'Swedbank',
handelsbanken: 'Handelsbanken',
+ lansforsakringar: 'Länsförsäkringar',
+ ica_banken: 'ICA Banken',
+ skandia: 'Skandia',
+ lunar: 'Lunar',
generic_csv: 'CSV (manuell mappning)',
camt053: 'ISO 20022 camt.053',
}
@@ -122,6 +126,10 @@ export default function BankFileUploadStep({
SEB
Swedbank
Handelsbanken
+ Länsförsäkringar
+ ICA Banken
+ Skandia
+ Lunar
ISO 20022 camt.053 (XML)
Annan CSV (manuell mappning)
@@ -229,6 +237,30 @@ export default function BankFileUploadStep({
Logga in → Konton → Transaktioner → Ladda ner (CSV)
+
+
Länsförsäkringar
+
+ Logga in → Konton → Kontoutdrag → Exportera (CSV)
+
+
+
+
ICA Banken
+
+ Logga in → Konton → Transaktioner → Exportera till fil (CSV)
+
+
+
+
Skandia
+
+ Logga in → Konton → Transaktioner → Exportera (CSV)
+
+
+
+
Lunar
+
+ Logga in → Konto → Transaktioner → Exportera (CSV)
+
+
diff --git a/components/transactions/BatchCategorySelector.tsx b/components/transactions/BatchCategorySelector.tsx
index 9ffa5159..e7c52f30 100644
--- a/components/transactions/BatchCategorySelector.tsx
+++ b/components/transactions/BatchCategorySelector.tsx
@@ -4,6 +4,7 @@ import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Progress } from '@/components/ui/progress'
+import { Paperclip } from 'lucide-react'
import VatTreatmentSelect from './VatTreatmentSelect'
import { EXPENSE_CATEGORIES, INCOME_CATEGORIES } from './transaction-types'
import type { TransactionCategory, VatTreatment } from '@/types'
@@ -59,6 +60,14 @@ export default function BatchCategorySelector({
) : (
+ {/* Underlag reminder */}
+
+
+
+ Underlag behover bifogas separat for varje transaktion efter bokforing.
+
+
+
Momsbehandling
Promise
+ ) => Promise
}
export default function QuickReviewDialog({
@@ -38,11 +41,14 @@ export default function QuickReviewDialog({
defaultVat,
onConfirm,
}: QuickReviewDialogProps) {
+ const { toast } = useToast()
const [accountOverride, setAccountOverride] = useState(defaultAccount)
const [vatTreatment, setVatTreatment] = useState(defaultVat)
const [accounts, setAccounts] = useState([])
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState(null)
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [showUploadZone, setShowUploadZone] = useState(false)
// Handle account changes — clear VAT for liability/equity accounts (class 2)
const handleAccountChange = useCallback((account: string) => {
@@ -85,7 +91,34 @@ export default function QuickReviewDialog({
? accountOverride
: undefined
- await onConfirm(transaction.id, category, resolvedVat, override)
+ const journalEntryId = await onConfirm(transaction.id, category, resolvedVat, override)
+
+ // Link uploaded documents to the journal entry
+ if (journalEntryId && uploadedFiles.length > 0) {
+ const filesToLink = uploadedFiles.filter((f) => f.status === 'uploaded' && f.id)
+ let linkFailCount = 0
+ for (const file of filesToLink) {
+ try {
+ await fetch(`/api/documents/${file.id}/link`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ journal_entry_id: journalEntryId }),
+ })
+ } catch {
+ linkFailCount++
+ }
+ }
+ if (linkFailCount > 0) {
+ toast({
+ title: 'Underlag kunde inte bifogas',
+ description: `${linkFailCount} fil(er) kunde inte lankas till verifikationen.`,
+ variant: 'destructive',
+ })
+ }
+ }
+
+ setUploadedFiles([])
+ setShowUploadZone(false)
} catch {
setError('Ett fel uppstod vid bokföring.')
setIsProcessing(false)
@@ -93,7 +126,13 @@ export default function QuickReviewDialog({
}
return (
-
+ {/* Document upload */}
+
+
+ {showUploadZone && (
+
+
+
+ )}
+
+
{error && (
{error}
diff --git a/components/transactions/SwipeCategorizationView.tsx b/components/transactions/SwipeCategorizationView.tsx
index 7bc7508b..9db0c5d8 100644
--- a/components/transactions/SwipeCategorizationView.tsx
+++ b/components/transactions/SwipeCategorizationView.tsx
@@ -5,12 +5,15 @@ import { motion, useMotionValue, useTransform, AnimatePresence, type PanInfo } f
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
+import { useToast } from '@/components/ui/use-toast'
import VatTreatmentSelect from './VatTreatmentSelect'
import { formatCurrency, formatDate } from '@/lib/utils'
import { checkExpenseWarnings } from '@/lib/tax/expense-warnings'
import { getDefaultAccountForCategory, getDefaultVatTreatmentForCategory } from '@/lib/bookkeeping/category-mapping'
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
-import { X, ArrowLeft, ArrowRight, Building, AlertTriangle, Check, FileText, Link2, Receipt as ReceiptIcon, SkipForward } from 'lucide-react'
+import DocumentUploadZone from '@/components/bookkeeping/DocumentUploadZone'
+import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
+import { X, ArrowLeft, ArrowRight, Building, AlertTriangle, Check, FileText, Link2, Receipt as ReceiptIcon, SkipForward, Paperclip, ChevronDown, ChevronUp } from 'lucide-react'
import type { TransactionCategory, VatTreatment, BASAccount } from '@/types'
import type { SuggestedCategory } from '@/lib/transactions/category-suggestions'
import type { TransactionWithInvoice, CategorizeHandler, MatchInvoiceHandler } from './transaction-types'
@@ -34,6 +37,7 @@ export default function SwipeCategorizationView({
onMatchInvoice,
onClose,
}: SwipeCategorizationViewProps) {
+ const { toast } = useToast()
const [showAllCategories, setShowAllCategories] = useState(false)
const [currentIndex, setCurrentIndex] = useState(0)
const [showCategorySelect, setShowCategorySelect] = useState(false)
@@ -46,6 +50,8 @@ export default function SwipeCategorizationView({
const [accountOverride, setAccountOverride] = useState('')
const [vatTreatment, setVatTreatment] = useState
('standard_25')
const [accounts, setAccounts] = useState([])
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [showUploadZone, setShowUploadZone] = useState(false)
// Clear VAT treatment when switching to a liability/equity account (class 2)
useEffect(() => {
@@ -143,6 +149,11 @@ export default function SwipeCategorizationView({
[isProcessing, currentTransaction, handleCategorySelect, x, moveToNext]
)
+ const resetUploadState = useCallback(() => {
+ setUploadedFiles([])
+ setShowUploadZone(false)
+ }, [])
+
const handleReviewConfirm = async () => {
if (!pendingCategory) return
@@ -156,14 +167,39 @@ export default function SwipeCategorizationView({
? accountOverride
: undefined
- const success = await onCategorize(
+ const journalEntryId = await onCategorize(
currentTransaction.id,
true,
pendingCategory,
resolvedVat,
override
)
- if (success) {
+ if (journalEntryId) {
+ // Link uploaded documents to the journal entry
+ if (uploadedFiles.length > 0) {
+ const filesToLink = uploadedFiles.filter((f) => f.status === 'uploaded' && f.id)
+ let linkFailCount = 0
+ for (const file of filesToLink) {
+ try {
+ await fetch(`/api/documents/${file.id}/link`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ journal_entry_id: journalEntryId }),
+ })
+ } catch {
+ linkFailCount++
+ }
+ }
+ if (linkFailCount > 0) {
+ toast({
+ title: 'Underlag kunde inte bifogas',
+ description: `${linkFailCount} fil(er) kunde inte lankas till verifikationen.`,
+ variant: 'destructive',
+ })
+ }
+ }
+
+ resetUploadState()
setShowReviewStep(false)
setPendingCategory(null)
moveToNext()
@@ -204,8 +240,9 @@ export default function SwipeCategorizationView({
setShowCategorySelect(false)
setShowReviewStep(false)
setPendingCategory(null)
+ resetUploadState()
moveToNext()
- }, [moveToNext])
+ }, [moveToNext, resetUploadState])
if (!currentTransaction) {
return (
@@ -352,12 +389,45 @@ export default function SwipeCategorizationView({
/>
{isLiabilityAccount && (
- Ingen moms för skuld-/eget kapital-konton
+ Ingen moms for skuld-/eget kapital-konton
)}
+ {/* Document upload */}
+
+
+ {showUploadZone && (
+
+
+
+ )}
+
+
{error && (
{error}
@@ -373,7 +443,7 @@ export default function SwipeCategorizationView({
disabled={isProcessing || !accountOverride}
>
- {isProcessing ? 'Bokför...' : 'Bokför'}
+ {isProcessing ? 'Bokfor...' : 'Bokfor'}