import { useState, useMemo, useEffect } from 'react'; import { Typography, Button, Space, DatePicker, Select, Modal, Form, Input, InputNumber, message, Tag, Tooltip, Dropdown, Skeleton, Empty, Descriptions, Table, Drawer, } from 'antd'; import { PlusOutlined, FilterOutlined, EyeOutlined, EditOutlined, CopyOutlined, DeleteOutlined, MoreOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import dayjs from 'dayjs'; import DataTable, { DataTableColumn } from '@/components/tables/DataTable'; import { useCompanyStore } from '@/stores/companyStore'; import { useActiveAccounts } from '@/api/queries/accountQueries'; import { useJournalEntryDrafts } from '@/api/queries/draftQueries'; import { formatCurrency } from '@/lib/formatters'; import { PageHeader } from '@/components/shared/PageHeader'; import { validateDoubleEntry } from '@/lib/accounting'; import type { TransactionLine, JournalEntryDraft, JournalEntryDraftStatus } from '@/types/accounting'; import { useCreateJournalEntryDraft, useUpdateJournalEntryDraft, useDiscardJournalEntryDraft } from '@/api/mutations/draftMutations'; import { usePeriodStore } from '@/stores/periodStore'; const { Text } = Typography; const { RangePicker } = DatePicker; // Display type for journal entry drafts interface DraftDisplay { id: string; transactionNumber: string; date: string; description: string; totalDebit: number; totalCredit: number; isReconciled: boolean; isVoided: boolean; status: JournalEntryDraftStatus; lines: JournalEntryDraft['lines']; postedAt?: string; postedBy?: string; } export default function Kassekladde() { const { activeCompany } = useCompanyStore(); const [isModalOpen, setIsModalOpen] = useState(false); const [editingDraft, setEditingDraft] = useState(null); const [dateFilter, setDateFilter] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null); const [accountFilter, setAccountFilter] = useState(null); const [statusFilter, setStatusFilter] = useState(null); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); const [detailDraft, setDetailDraft] = useState(null); const [form] = Form.useForm(); const [lines, setLines] = useState[]>([ { debit: 0, credit: 0 }, { debit: 0, credit: 0 }, ]); const { currentFiscalYear } = usePeriodStore(); // Mutation hooks const createDraftMutation = useCreateJournalEntryDraft(); const updateDraftMutation = useUpdateJournalEntryDraft(); const discardDraftMutation = useDiscardJournalEntryDraft(); // Fetch accounts and drafts from API const { data: accounts = [], isLoading: accountsLoading } = useActiveAccounts(activeCompany?.id); const { data: drafts = [], isLoading: draftsLoading } = useJournalEntryDrafts(activeCompany?.id); const isLoading = accountsLoading || draftsLoading; // Convert drafts to display format const displayData: DraftDisplay[] = useMemo(() => drafts.map(draft => ({ id: draft.id, transactionNumber: draft.voucherNumber || draft.name, date: draft.documentDate || draft.createdAt, description: draft.description || draft.name, lines: draft.lines || [], totalDebit: draft.lines?.reduce((sum, l) => sum + (l.debitAmount || 0), 0) ?? 0, totalCredit: draft.lines?.reduce((sum, l) => sum + (l.creditAmount || 0), 0) ?? 0, isReconciled: draft.status === 'posted', isVoided: draft.status === 'discarded', status: draft.status, postedAt: draft.status === 'posted' ? draft.updatedAt : undefined, postedBy: draft.createdBy, })), [drafts]); // Apply filters to display data const filteredData: DraftDisplay[] = useMemo(() => { let data = displayData; // Date filter if (dateFilter && dateFilter[0] && dateFilter[1]) { const startDate = dateFilter[0].startOf('day'); const endDate = dateFilter[1].endOf('day'); data = data.filter(d => { const dDate = dayjs(d.date); return (dDate.isAfter(startDate) || dDate.isSame(startDate, 'day')) && (dDate.isBefore(endDate) || dDate.isSame(endDate, 'day')); }); } // Account filter - filter drafts where any line references the selected account if (accountFilter) { data = data.filter(d => d.lines.some(l => l.accountId === accountFilter) ); } // Status filter if (statusFilter) { data = data.filter(d => d.status === statusFilter); } return data; }, [displayData, dateFilter, accountFilter, statusFilter]); // Pre-populate form when editing a draft useEffect(() => { if (editingDraft && isModalOpen) { form.setFieldsValue({ date: editingDraft.date ? dayjs(editingDraft.date) : dayjs(), description: editingDraft.description, }); // Populate lines from the draft if (editingDraft.lines && editingDraft.lines.length > 0) { setLines(editingDraft.lines.map(l => ({ accountId: l.accountId, debit: l.debitAmount || 0, credit: l.creditAmount || 0, description: l.description, vatCode: l.vatCode, }))); } else { setLines([{ debit: 0, credit: 0 }, { debit: 0, credit: 0 }]); } } }, [editingDraft, isModalOpen, form]); const columns: DataTableColumn[] = [ { dataIndex: 'transactionNumber', title: 'Bilagsnr.', width: 120, sortable: true, render: (value) => #{value as string}, }, { dataIndex: 'date', title: 'Dato', width: 100, sortable: true, columnType: 'date', }, { dataIndex: 'description', title: 'Beskrivelse', ellipsis: true, }, { dataIndex: 'totalDebit', title: 'Debet', width: 120, sortable: true, columnType: 'currency', }, { dataIndex: 'totalCredit', title: 'Kredit', width: 120, sortable: true, columnType: 'currency', }, { dataIndex: 'isReconciled', title: 'Status', width: 100, render: (value, record) => { if (record.isVoided) { return Annulleret; } return value ? ( Bogført ) : ( Kladde ); }, }, { dataIndex: 'id', title: '', width: 50, render: (_, record) => { const menuItems: MenuProps['items'] = [ { key: 'view', icon: , label: 'Vis detaljer', }, { key: 'edit', icon: , label: 'Rediger', disabled: record.isReconciled || record.isVoided, }, { key: 'copy', icon: , label: 'Kopier', }, { type: 'divider', }, { key: 'void', icon: , label: 'Annuller', danger: true, disabled: record.isVoided, }, ]; return ( handleAction(key, record), }} trigger={['click']} > } /> {/* Filters */} setDateFilter(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)} format="DD-MM-YYYY" /> {showAdvancedFilters && ( <> setStatusFilter(value ?? null)} options={[ { value: 'posted', label: 'Bogført' }, { value: 'draft', label: 'Kladde' }, { value: 'discarded', label: 'Annulleret' }, ]} /> )} {/* Data Table */} {filteredData.length === 0 ? ( ) : ( data={filteredData} columns={columns} exportFilename="kassekladde" rowSelection="multiple" onRowClick={(record) => handleAction('view', record)} rowClassName={(record) => record.isVoided ? 'voided-row' : '' } /> )} {/* Detail Drawer */} setDetailDraft(null)} width={600} > {detailDraft && ( <> #{detailDraft.transactionNumber} {detailDraft.date ? dayjs(detailDraft.date).format('DD-MM-YYYY') : '-'} {detailDraft.description} {getStatusLabel(detailDraft.status).label} {detailDraft.isReconciled && detailDraft.postedAt && ( {dayjs(detailDraft.postedAt).format('DD-MM-YYYY HH:mm')} )} {detailDraft.postedBy && ( {detailDraft.postedBy} )} {formatCurrency(detailDraft.totalDebit)} {formatCurrency(detailDraft.totalCredit)} Posteringslinjer ({ ...l, key: idx }))} columns={[ { title: 'Konto', dataIndex: 'accountId', key: 'account', render: (accountId: string) => getAccountName(accountId), }, { title: 'Debet', dataIndex: 'debitAmount', key: 'debit', align: 'right' as const, render: (v: number) => v ? formatCurrency(v) : '-', }, { title: 'Kredit', dataIndex: 'creditAmount', key: 'credit', align: 'right' as const, render: (v: number) => v ? formatCurrency(v) : '-', }, { title: 'Tekst', dataIndex: 'description', key: 'description', render: (v: string) => v || '-', }, ]} pagination={false} size="small" /> )} {/* Create/Edit Modal */} { setIsModalOpen(false); setEditingDraft(null); form.resetFields(); setLines([{ debit: 0, credit: 0 }, { debit: 0, credit: 0 }]); }} onOk={handleSubmit} okText="Gem" cancelText="Annuller" width={800} >
{/* Transaction Lines */}
Posteringslinjer
{lines.map((line, index) => ( ))}
Konto Debet Kredit Tekst
handleLineChange(index, 'debit', value || 0)} formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, '.') } parser={(value) => value?.replace(/\./g, '').replace(',', '.') as unknown as number } /> handleLineChange(index, 'credit', value || 0)} formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, '.') } parser={(value) => value?.replace(/\./g, '').replace(',', '.') as unknown as number } /> handleLineChange(index, 'description', e.target.value) } /> {lines.length > 2 && ( )}
{formatCurrency(balance.totalDebit)} {formatCurrency(balance.totalCredit)} {!balance.valid && ( Ubalance! )} {balance.valid && balance.totalDebit > 0 && ( Balancerer )}
); }