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 ( - + { + if (!o) { + setUploadedFiles([]) + setShowUploadZone(false) + } + onOpenChange(o) + }}> Granska bokföring @@ -164,6 +203,39 @@ export default function QuickReviewDialog({
+ {/* 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'}