'use client' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog' import { useToast } from '@/components/ui/use-toast' import { formatCurrency, formatDate } from '@/lib/utils' import { ArrowUpRight, ArrowDownRight, Loader2, Search, ArrowLeft, Check, CheckCircle2, AlertTriangle, Sparkles, } from 'lucide-react' import JournalEntryPreview from './JournalEntryPreview' import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names' import type { TransactionWithInvoice } from './transaction-types' interface TemplateMatch { template_id: string name_sv: string name_en: string group: string debit_account: string credit_account: string confidence: number description_sv: string vat_rate: number vat_treatment: string | null deductibility: 'full' | 'non_deductible' | 'conditional' deductibility_note_sv: string | null special_rules_sv: string | null risk_level: string } interface AiSuggestion { debit_account: string credit_account: string vat_treatment: string | null category: string confidence: number reasoning: string warnings: string[] template_id: string | null } interface DescribeResult { templates: TemplateMatch[] ai_suggestion: AiSuggestion | null needs_more_detail: boolean user_description: string batch_candidate_count: number merchant_name: string | null } interface DescribeTransactionDialogProps { open: boolean onOpenChange: (open: boolean) => void transaction: TransactionWithInvoice | null onCategorized: (transactionId: string, journalEntryId: string | null) => void onBatchApplied?: (count: number) => void } type Step = 'describe' | 'pick' | 'batch' type Selection = { type: 'template'; templateId: string } | { type: 'ai' } function getExamplePrompts(transaction: TransactionWithInvoice): string[] { const desc = (transaction.description || '').toLowerCase() const isExpense = transaction.amount < 0 if (!isExpense) { return ['Konsultarvode', 'Försäljning av varor', 'Återbetalning'] } if (desc.includes('restaurang') || desc.includes('lunch') || desc.includes('middag') || desc.includes('mat')) { return ['Lunch med kund', 'Personalmiddag', 'Fika till kontoret'] } if (desc.includes('hotel') || desc.includes('hotell') || desc.includes('boende') || desc.includes('resa')) { return ['Tjänsteresa', 'Hotell konferens', 'Flygbiljett'] } if (desc.includes('uber') || desc.includes('taxi') || desc.includes('bolt') || desc.includes('sj ')) { return ['Taxi till kund', 'Tjänsteresa', 'Pendling'] } if (desc.includes('google') || desc.includes('meta') || desc.includes('facebook') || desc.includes('linkedin')) { return ['Online-annonsering', 'SaaS-prenumeration', 'Marknadsföringskampanj'] } if (desc.includes('amazon') || desc.includes('aws') || desc.includes('azure') || desc.includes('cloud')) { return ['Serverhosting', 'SaaS-prenumeration', 'Kontorsmaterial'] } return ['Kontorsmaterial', 'SaaS-prenumeration', 'Konsulttjänst', 'Reklam'] } function getVatRateFromTreatment(treatment: string | null): number { switch (treatment) { case 'standard_25': return 0.25 case 'reduced_12': return 0.12 case 'reduced_6': return 0.06 default: return 0 } } export default function DescribeTransactionDialog({ open, onOpenChange, transaction, onCategorized, onBatchApplied, }: DescribeTransactionDialogProps) { const { toast } = useToast() const [step, setStep] = useState('describe') const [description, setDescription] = useState('') const [isSearching, setIsSearching] = useState(false) const [isBooking, setIsBooking] = useState(false) const [isBatchApplying, setIsBatchApplying] = useState(false) const [describeResult, setDescribeResult] = useState(null) const [selection, setSelection] = useState(null) const selectedTemplateId = selection?.type === 'template' ? selection.templateId : null const isAiSelected = selection?.type === 'ai' function resetState() { setStep('describe') setDescription('') setIsSearching(false) setIsBooking(false) setIsBatchApplying(false) setDescribeResult(null) setSelection(null) } function handleOpenChange(isOpen: boolean) { if (!isOpen) { resetState() } onOpenChange(isOpen) } async function handleSearch() { if (!transaction || description.trim().length < 3) return setIsSearching(true) try { const response = await fetch(`/api/transactions/${transaction.id}/describe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: description.trim() }), }) const result = await response.json() if (!response.ok) { toast({ title: 'Fel', description: result.error || 'Kunde inte söka mallar', variant: 'destructive', }) setIsSearching(false) return } setDescribeResult(result.data) setSelection(null) setStep('pick') } catch { toast({ title: 'Fel', description: 'Något gick fel vid sökning', variant: 'destructive', }) } setIsSearching(false) } async function handleBook() { if (!transaction || !describeResult || !selection) return setIsBooking(true) try { // Build categorize request based on selection type let body: Record if (selection.type === 'template') { body = { is_business: true, template_id: selection.templateId, user_description: describeResult.user_description, } } else { // AI suggestion selected const ai = describeResult.ai_suggestion! if (ai.template_id) { // AI matched a template — use template-based booking body = { is_business: true, template_id: ai.template_id, user_description: describeResult.user_description, } } else { // AI category-based booking — category maps to the correct account body = { is_business: true, category: ai.category, vat_treatment: ai.vat_treatment || undefined, user_description: describeResult.user_description, } } } const response = await fetch(`/api/transactions/${transaction.id}/categorize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const result = await response.json() if (!response.ok) { toast({ title: 'Fel', description: result.error || 'Kunde inte bokföra transaktion', variant: 'destructive', }) setIsBooking(false) return } if (describeResult.batch_candidate_count > 0) { setStep('batch') setIsBooking(false) onCategorized(transaction.id, result.journal_entry_id || null) } else { toast({ title: 'Bokförd', description: 'Transaktion bokförd och verifikation skapad' }) onCategorized(transaction.id, result.journal_entry_id || null) handleOpenChange(false) } } catch { toast({ title: 'Fel', description: 'Något gick fel vid bokföring', variant: 'destructive', }) setIsBooking(false) } } async function handleBatchApply() { if (!describeResult) return // For batch apply, we need a template_id let templateId: string | null = null if (selection?.type === 'template') { templateId = selection.templateId } else if (selection?.type === 'ai' && describeResult.ai_suggestion?.template_id) { templateId = describeResult.ai_suggestion.template_id } if (!templateId) return setIsBatchApplying(true) try { const response = await fetch('/api/transactions/batch-describe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ merchant_name: describeResult.merchant_name, template_id: templateId, is_business: true, user_description: describeResult.user_description, }), }) const result = await response.json() if (!response.ok) { toast({ title: 'Fel', description: result.error || 'Kunde inte bokföra batch', variant: 'destructive', }) setIsBatchApplying(false) return } const applied = result.data?.applied || 0 const errors = result.data?.errors || [] if (errors.length > 0) { toast({ title: 'Delvis klart', description: `${applied} lyckades, ${errors.length} misslyckades`, variant: 'destructive', }) } else { toast({ title: 'Klart', description: `${applied} transaktioner bokförda`, }) } onBatchApplied?.(applied) handleOpenChange(false) } catch { toast({ title: 'Fel', description: 'Något gick fel vid batchbokföring', variant: 'destructive', }) setIsBatchApplying(false) } } function handleSkipBatch() { toast({ title: 'Bokförd', description: 'Transaktion bokförd och verifikation skapad' }) handleOpenChange(false) } if (!transaction) return null const isIncome = transaction.amount > 0 const aiSuggestion = describeResult?.ai_suggestion // Check if AI agrees with top template const topTemplate = describeResult?.templates[0] const aiAgreesWithTop = aiSuggestion && topTemplate && aiSuggestion.debit_account === topTemplate.debit_account // Determine if batch apply is available (requires a template_id) const canBatchApply = selection?.type === 'template' || (selection?.type === 'ai' && !!describeResult?.ai_suggestion?.template_id) return ( {step === 'describe' && 'Beskriv transaktion'} {step === 'pick' && 'Välj mall'} {step === 'batch' && 'Bokför liknande'} {step === 'describe' && 'Beskriv vad transaktionen gäller så hittar vi rätt bokföringsmall'} {step === 'pick' && 'Välj den mall som stämmer bäst'} {step === 'batch' && 'Transaktion bokförd!'} {/* Transaction summary - shown in describe and pick steps */} {(step === 'describe' || step === 'pick') && (
{isIncome ? ( ) : ( )}

{transaction.description}

{formatDate(transaction.date)}

{isIncome ? '+' : ''} {formatCurrency(transaction.amount, transaction.currency)}

)} {/* Step 1: Describe */} {step === 'describe' && (