'use client' import { useState, useEffect, useCallback } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { ArrowDownNarrowWide, ArrowUpNarrowWide, ChevronDown, ChevronRight, Paperclip, AlertTriangle, Loader2, BookOpen, X } from 'lucide-react' import { Input } from '@/components/ui/input' import { AccountNumber } from '@/components/ui/account-number' import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments' import CorrectionEntryDialog from '@/components/bookkeeping/CorrectionEntryDialog' import type { JournalEntry, JournalEntryLine } from '@/types' const NEEDS_ATTACHMENT = new Set([ 'manual', 'bank_transaction', 'supplier_invoice_registered', 'supplier_invoice_paid', 'supplier_invoice_cash_payment', 'import', ]) interface Props { periodId?: string } export default function JournalEntryList({ periodId }: Props) { const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(true) const [expandedId, setExpandedId] = useState(null) const [count, setCount] = useState(0) const [page, setPage] = useState(0) const [attachmentCounts, setAttachmentCounts] = useState>({}) const [showMissingOnly, setShowMissingOnly] = useState(false) const [correctionEntry, setCorrectionEntry] = useState(null) const [dateSortDir, setDateSortDir] = useState<'desc' | 'asc'>('desc') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [dateFromInput, setDateFromInput] = useState('') const [dateToInput, setDateToInput] = useState('') const pageSize = 20 const isValidDate = (v: string) => /^\d{4}-\d{2}-\d{2}$/.test(v) && !isNaN(Date.parse(v)) const fetchAttachmentCounts = useCallback(async (entryIds: string[]) => { if (entryIds.length === 0) return try { const res = await fetch( `/api/documents/counts?journal_entry_ids=${entryIds.join(',')}` ) const { data } = await res.json() setAttachmentCounts(data || {}) } catch { // Non-critical — silently ignore } }, []) async function fetchEntries() { setLoading(true) const params = new URLSearchParams({ limit: String(pageSize), offset: String(page * pageSize), sort_date: dateSortDir, }) if (periodId) params.set('period_id', periodId) if (dateFrom) params.set('date_from', dateFrom) if (dateTo) params.set('date_to', dateTo) const res = await fetch(`/api/bookkeeping/journal-entries?${params}`) if (!res.ok) { setLoading(false) return } const { data, count: total } = await res.json() const loadedEntries = data || [] setEntries(loadedEntries) setCount(total || 0) setLoading(false) // Fetch attachment counts for the loaded entries const ids = loadedEntries.map((e: JournalEntry) => e.id) fetchAttachmentCounts(ids) } useEffect(() => { fetchEntries() }, [periodId, page, dateSortDir, dateFrom, dateTo]) const handleAttachmentCountChange = useCallback((entryId: string, count: number) => { setAttachmentCounts((prev) => ({ ...prev, [entryId]: count })) }, []) const toggleExpand = (id: string) => { setExpandedId(expandedId === id ? null : id) } if (loading) { return (

Laddar verifikationer...

) } if (entries.length === 0) { return (

Inga verifikationer

Verifikationer skapas automatiskt vid fakturering och transaktionsbokföring, eller manuellt via fliken "Ny verifikation".

) } const filteredEntries = showMissingOnly ? entries.filter( (e) => NEEDS_ATTACHMENT.has(e.source_type) && !attachmentCounts[e.id] && e.status === 'posted' ) : entries return (
{/* Filters and sorting */}
{showMissingOnly && ( {filteredEntries.length} )}
setDateFromInput(e.target.value)} onBlur={() => { const v = dateFromInput.trim() const next = v === '' ? '' : isValidDate(v) ? v : dateFrom setDateFromInput(next) if (next !== dateFrom) { setDateFrom(next); setPage(0) } }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() }} className="h-8 w-[145px] text-xs" /> setDateToInput(e.target.value)} onBlur={() => { const v = dateToInput.trim() const next = v === '' ? '' : isValidDate(v) ? v : dateTo setDateToInput(next) if (next !== dateTo) { setDateTo(next); setPage(0) } }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() }} className="h-8 w-[145px] text-xs" /> {(dateFrom || dateTo) && ( )}
{filteredEntries.map((entry) => { const isExpanded = expandedId === entry.id const lines = (entry.lines || []) as JournalEntryLine[] return ( {isExpanded && ( {lines.length === 0 ? (

Inga kontorader hittades för denna verifikation.

) : ( <> {/* Mobile: stacked cards */}
{lines .sort((a, b) => a.sort_order - b.sort_order) .map((line) => (
{line.line_description && (

{line.line_description}

)}
{Number(line.debit_amount) > 0 && (

{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} D

)} {Number(line.credit_amount) > 0 && (

{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K

)}
))}
Summa
D: {lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K: {lines.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
{/* Desktop: table */}
{lines .sort((a, b) => a.sort_order - b.sort_order) .map((line) => ( ))}
Konto Beskrivning Debet Kredit
{line.line_description || ''} {Number(line.debit_amount) > 0 ? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2, }) : ''} {Number(line.credit_amount) > 0 ? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2, }) : ''}
Summa {lines .reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0) .toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {lines .reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0) .toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
)} handleAttachmentCountChange(entry.id, c)} /> {entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && (
)}
)}
) })}
{/* Correction dialog */} {correctionEntry && ( { if (!open) setCorrectionEntry(null) }} onCorrected={() => { setCorrectionEntry(null); fetchEntries() }} /> )} {/* Pagination */} {count > pageSize && (
Sida {page + 1} av {Math.ceil(count / pageSize)}
)}
) }