'use client' import { useState } from 'react' import Link from 'next/link' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { cn, formatCurrency, formatDate } from '@/lib/utils' import { getCategoryDisplayName } from '@/lib/tax/expense-warnings' import { Search, ArrowUpRight, ArrowDownRight, ArrowLeftRight, Check, Landmark, Link2, FileText, Loader2, Trash2, } from 'lucide-react' import { TransactionAttachmentIndicator } from './TransactionAttachmentIndicator' import type { TransactionWithInvoice, HistoryFilter } from './transaction-types' import type { SkattekontoTransactionWithSuggestion, StoredSkattekontoTransaction, } from '@/types/skatteverket' type SourceFilter = 'all' | 'bank' | 'skatteverket' type HistoryRow = | { source: 'bank'; date: string; data: TransactionWithInvoice } | { source: 'skatteverket'; date: string; data: SkattekontoTransactionWithSuggestion } interface TransactionHistoryListProps { transactions: TransactionWithInvoice[] skvRows?: SkattekontoTransactionWithSuggestion[] onOpenMatchDialog: (transaction: TransactionWithInvoice) => void onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void onDelete?: (id: string) => void onSkvBokfor?: (row: StoredSkattekontoTransaction) => void onSkvMatch?: (row: StoredSkattekontoTransaction) => void hasMore?: boolean isLoadingMore?: boolean onLoadMore?: () => void } export default function TransactionHistoryList({ transactions, skvRows = [], onOpenMatchDialog, onOpenCategoryDialog, onDelete, onSkvBokfor, onSkvMatch, hasMore, isLoadingMore, onLoadMore, }: TransactionHistoryListProps) { const [searchTerm, setSearchTerm] = useState('') const [filter, setFilter] = useState('all') const [sourceFilter, setSourceFilter] = useState('all') // The bank/private filter doesn't apply to SKV rows — they have no // is_business flag. So when the filter is 'business' or 'private' we // implicitly hide SKV (it doesn't match either). Source filter narrows // further if the user picks one explicitly. const bankFiltered = transactions.filter((t) => { const matchesSearch = t.description.toLowerCase().includes(searchTerm.toLowerCase()) const matchesFilter = filter === 'all' || (filter === 'business' && t.is_business === true) || (filter === 'private' && t.is_business === false) return matchesSearch && matchesFilter }) const skvFiltered = skvRows.filter((r) => { if (filter !== 'all') return false return r.transaktionstext.toLowerCase().includes(searchTerm.toLowerCase()) }) const merged: HistoryRow[] = [] if (sourceFilter !== 'skatteverket') { for (const t of bankFiltered) { merged.push({ source: 'bank', date: t.date, data: t }) } } if (sourceFilter !== 'bank') { for (const r of skvFiltered) { merged.push({ source: 'skatteverket', date: r.transaktionsdatum, data: r }) } } merged.sort((a, b) => { if (a.date !== b.date) return b.date.localeCompare(a.date) return a.source === 'bank' ? -1 : 1 }) const showSourceFilter = skvRows.length > 0 && transactions.length > 0 const filtered = merged return (
{/* Search + filter pills */}
setSearchTerm(e.target.value)} className="pl-10" />
{(['all', 'business', 'private'] as const).map((f) => ( ))}
{showSourceFilter && (
Källa: {(['all', 'bank', 'skatteverket'] as const).map((s) => ( ))}
)} {/* Transaction list */} {filtered.length === 0 ? (

Inga transaktioner

{searchTerm ? 'Inga transaktioner matchar din sökning' : 'Inga transaktioner att visa med valt filter'}

) : (
{filtered.map((item) => item.source === 'bank' ? ( ) : ( ), )} {hasMore && onLoadMore && !searchTerm && (
)}
)}
) } function BankHistoryRow({ transaction, onOpenMatchDialog, onOpenCategoryDialog, onDelete, }: { transaction: TransactionWithInvoice onOpenMatchDialog: (transaction: TransactionWithInvoice) => void onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void onDelete?: (id: string) => void }) { return (
0 ? 'bg-success/10 text-success' : 'bg-destructive/10 text-destructive' }`} > {transaction.amount > 0 ? ( ) : ( )}

{transaction.description}

{formatDate(transaction.date)} {transaction.is_business !== null && !( transaction.is_business && transaction.category === 'uncategorized' && transaction.journal_entry_id ) && ( <> · {transaction.is_business ? getCategoryDisplayName(transaction.category) : 'Privat'} )} {transaction.invoice_id && ( <> · Kopplad till faktura )} {transaction.journal_entry_id ? ( <> · Bokförd ) : ( <> · )} {transaction.potential_invoice && !transaction.invoice_id && ( <> · )}
{!transaction.journal_entry_id && ( )} {!transaction.journal_entry_id && onDelete && ( )}

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

{transaction.currency !== 'SEK' && transaction.amount_sek && (

{formatCurrency(transaction.amount_sek)}

)}
) } function SkattekontoHistoryRow({ row, onBokfor, onMatch, }: { row: SkattekontoTransactionWithSuggestion onBokfor?: (row: StoredSkattekontoTransaction) => void onMatch?: (row: StoredSkattekontoTransaction) => void }) { const amount = Number(row.belopp_skatteverket) const isIncome = amount > 0 const isBooked = !!row.journal_entry_id return (
{isIncome ? ( ) : ( )}

{row.transaktionstext}

{formatDate(row.transaktionsdatum)} · Skatteverket {isBooked ? ( <> · Bokförd ) : row.match_suggestion ? ( <> · Möjlig dublett ) : ( <> · Ej bokförd )}
{!isBooked && onMatch && ( )} {!isBooked && !row.match_suggestion && onBokfor && ( )} {isBooked && ( )}

{isIncome ? '+' : ''} {formatCurrency(amount)}

) }