'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, } 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 DescribeResult { templates: TemplateMatch[] 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' function getExamplePrompts(transaction: TransactionWithInvoice): string[] { const desc = (transaction.description || '').toLowerCase() const isExpense = transaction.amount < 0 if (!isExpense) { return ['Konsultarvode', 'Forsaljning av varor', 'Aterbetalning'] } // Contextual suggestions based on description keywords 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 ['Tjansteresa', 'Hotell konferens', 'Flygbiljett'] } if (desc.includes('uber') || desc.includes('taxi') || desc.includes('bolt') || desc.includes('sj ')) { return ['Taxi till kund', 'Tjansteresa', 'Pendling'] } if (desc.includes('google') || desc.includes('meta') || desc.includes('facebook') || desc.includes('linkedin')) { return ['Online-annonsering', 'SaaS-prenumeration', 'Marknadsforingskampanj'] } if (desc.includes('amazon') || desc.includes('aws') || desc.includes('azure') || desc.includes('cloud')) { return ['Serverhosting', 'SaaS-prenumeration', 'Kontorsmaterial'] } // Generic expense suggestions return ['Kontorsmaterial', 'SaaS-prenumeration', 'Konsulttjanst', 'Reklam'] } 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 [selectedTemplateId, setSelectedTemplateId] = useState(null) function resetState() { setStep('describe') setDescription('') setIsSearching(false) setIsBooking(false) setIsBatchApplying(false) setDescribeResult(null) setSelectedTemplateId(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 soka mallar', variant: 'destructive', }) setIsSearching(false) return } setDescribeResult(result.data) setSelectedTemplateId(null) setStep('pick') } catch { toast({ title: 'Fel', description: 'Nagot gick fel vid sokning', variant: 'destructive', }) } setIsSearching(false) } async function handleBook() { if (!transaction || !selectedTemplateId || !describeResult) return setIsBooking(true) try { const response = await fetch(`/api/transactions/${transaction.id}/categorize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_business: true, template_id: selectedTemplateId, user_description: describeResult.user_description, }), }) const result = await response.json() if (!response.ok) { toast({ title: 'Fel', description: result.error || 'Kunde inte bokfora 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: 'Bokford', description: 'Transaktion bokford och verifikation skapad' }) onCategorized(transaction.id, result.journal_entry_id || null) handleOpenChange(false) } } catch { toast({ title: 'Fel', description: 'Nagot gick fel vid bokforing', variant: 'destructive', }) setIsBooking(false) } } async function handleBatchApply() { if (!describeResult || !selectedTemplateId) 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: selectedTemplateId, is_business: true, user_description: describeResult.user_description, }), }) const result = await response.json() if (!response.ok) { toast({ title: 'Fel', description: result.error || 'Kunde inte bokfora 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 bokforda`, }) } onBatchApplied?.(applied) handleOpenChange(false) } catch { toast({ title: 'Fel', description: 'Nagot gick fel vid batchbokforing', variant: 'destructive', }) setIsBatchApplying(false) } } function handleSkipBatch() { toast({ title: 'Bokford', description: 'Transaktion bokford och verifikation skapad' }) handleOpenChange(false) } if (!transaction) return null const isIncome = transaction.amount > 0 return ( {step === 'describe' && 'Beskriv transaktion'} {step === 'pick' && 'Valj mall'} {step === 'batch' && 'Bokfor liknande'} {step === 'describe' && 'Beskriv vad transaktionen galler sa hittar vi ratt bokforingsmall'} {step === 'pick' && 'Valj den mall som stammer bast'} {step === 'batch' && 'Transaktion bokford!'} {/* 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' && (