- {mockMetrics.pendingInvoices} afventer
- {mockMetrics.overdueInvoices > 0 && (
+ {metrics.pendingInvoices} afventer
+ {metrics.overdueInvoices > 0 && (
}>
- {mockMetrics.overdueInvoices} forfaldne
+ {metrics.overdueInvoices} forfaldne
)}
@@ -181,16 +289,16 @@ export default function Dashboard() {
formatCurrency(value as number)}
/>
- = 0 ? 'orange' : 'green'}>
- {mockMetrics.apChange >= 0 ? '+' : ''}
- {(mockMetrics.apChange * 100).toFixed(1)}% denne maaned
+ = 0 ? 'orange' : 'green'}>
+ {metrics.apChange >= 0 ? '+' : ''}
+ {(metrics.apChange * 100).toFixed(1)}% denne maaned
@@ -201,7 +309,7 @@ export default function Dashboard() {
formatCurrency(value as number)}
@@ -218,14 +326,22 @@ export default function Dashboard() {
{/* Cash Flow Chart */}
-
+ {cashFlowData.length > 0 ? (
+
+ ) : (
+
+ )}
{/* Revenue vs Expenses */}
-
+ {cashFlowData.length > 0 ? (
+
+ ) : (
+
+ )}
@@ -235,7 +351,11 @@ export default function Dashboard() {
{/* Expense Breakdown */}
-
+ {expenseBreakdown.length > 0 ? (
+
+ ) : (
+
+ )}
@@ -245,16 +365,18 @@ export default function Dashboard() {
}
/>
-
75% afstemt denne maaned
+
+ {metrics.unreconciledCount === 0 ? '100% afstemt' : 'Bankafstemning ikke implementeret endnu'}
+
@@ -267,34 +389,38 @@ export default function Dashboard() {
bodyStyle={{ padding: 0 }}
>
- {mockRecentTransactions.map((tx) => (
-
-
- {tx.description}
-
- {formatDate(tx.date)}
-
-
-
0 ? (
+ recentTransactions.map((tx) => (
+ = 0 ? accountingColors.credit : accountingColors.debit,
+ padding: '8px 16px',
+ borderBottom: '1px solid #f0f0f0',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
}}
>
- {formatCurrency(tx.amount, { showSign: true })}
-
-
- ))}
+
+ {tx.description}
+
+ {formatDate(tx.date)}
+
+
+ = 0 ? accountingColors.credit : accountingColors.debit,
+ }}
+ >
+ {formatCurrency(tx.amount, { showSign: true })}
+
+
+ ))
+ ) : (
+
+ )}
@@ -308,7 +434,7 @@ export default function Dashboard() {
- 23 transaktioner klar til afstemning
+ {metrics.unreconciledCount} transaktioner klar til afstemning
@@ -317,12 +443,14 @@ export default function Dashboard() {
Momsindberetning forfalder om 14 dage
-
-
-
- 3 fakturaer er forfaldne
-
-
+ {metrics.overdueInvoices > 0 && (
+
+
+
+ {metrics.overdueInvoices} fakturaer er forfaldne
+
+
+ )}
diff --git a/frontend/src/pages/Kassekladde.tsx b/frontend/src/pages/Kassekladde.tsx
index ab64240..f1ba651 100644
--- a/frontend/src/pages/Kassekladde.tsx
+++ b/frontend/src/pages/Kassekladde.tsx
@@ -13,6 +13,8 @@ import {
Tag,
Tooltip,
Dropdown,
+ Skeleton,
+ Empty,
} from 'antd';
import {
PlusOutlined,
@@ -26,90 +28,33 @@ import {
import type { MenuProps } from 'antd';
import dayjs from 'dayjs';
import DataTable, { DataTableColumn } from '@/components/tables/DataTable';
-import { useCompany } from '@/hooks/useCompany';
+import { useCompanyStore } from '@/stores/companyStore';
+import { useActiveAccounts } from '@/api/queries/accountQueries';
+import { useJournalEntryDrafts } from '@/api/queries/draftQueries';
import { formatCurrency } from '@/lib/formatters';
import { validateDoubleEntry } from '@/lib/accounting';
-import type { Transaction, TransactionLine, Account } from '@/types/accounting';
+import type { TransactionLine, JournalEntryDraft } from '@/types/accounting';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
-// Mock data - will be replaced with GraphQL queries
-const mockAccounts: Account[] = [
- { id: '1', companyId: '1', accountNumber: '1000', name: 'Bank', type: 'asset', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '2', companyId: '1', accountNumber: '1100', name: 'Debitorer', type: 'asset', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '3', companyId: '1', accountNumber: '2000', name: 'Kreditorer', type: 'liability', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '4', companyId: '1', accountNumber: '4000', name: 'Salg af varer', type: 'revenue', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '5', companyId: '1', accountNumber: '5000', name: 'Varekøb', type: 'cogs', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '6', companyId: '1', accountNumber: '6100', name: 'Husleje', type: 'expense', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
- { id: '7', companyId: '1', accountNumber: '6800', name: 'Kontorartikler', type: 'expense', isActive: true, balance: 0, createdAt: '', updatedAt: '' },
-];
-
-const mockTransactions: Transaction[] = [
- {
- id: '1',
- companyId: '1',
- transactionNumber: '2025-0001',
- date: '2025-01-15',
- description: 'Salg faktura #1234',
- lines: [
- { id: '1-1', transactionId: '1', accountId: '2', debit: 15625, credit: 0, description: 'Debitor' },
- { id: '1-2', transactionId: '1', accountId: '4', debit: 0, credit: 12500, description: 'Salg' },
- { id: '1-3', transactionId: '1', accountId: '8', debit: 0, credit: 3125, description: 'Moms' },
- ],
- isReconciled: true,
- isVoided: false,
- attachments: [],
- createdAt: '2025-01-15T10:00:00Z',
- updatedAt: '2025-01-15T10:00:00Z',
- createdBy: 'user1',
- },
- {
- id: '2',
- companyId: '1',
- transactionNumber: '2025-0002',
- date: '2025-01-14',
- description: 'Husleje januar 2025',
- lines: [
- { id: '2-1', transactionId: '2', accountId: '6', debit: 15000, credit: 0, description: 'Husleje' },
- { id: '2-2', transactionId: '2', accountId: '1', debit: 0, credit: 15000, description: 'Bank' },
- ],
- isReconciled: false,
- isVoided: false,
- attachments: [],
- createdAt: '2025-01-14T09:00:00Z',
- updatedAt: '2025-01-14T09:00:00Z',
- createdBy: 'user1',
- },
- {
- id: '3',
- companyId: '1',
- transactionNumber: '2025-0003',
- date: '2025-01-13',
- description: 'Køb af kontorartikler',
- lines: [
- { id: '3-1', transactionId: '3', accountId: '7', debit: 500, credit: 0, description: 'Kontorartikler' },
- { id: '3-2', transactionId: '3', accountId: '1', debit: 0, credit: 500, description: 'Bank' },
- ],
- isReconciled: true,
- isVoided: false,
- attachments: [],
- createdAt: '2025-01-13T14:00:00Z',
- updatedAt: '2025-01-13T14:00:00Z',
- createdBy: 'user1',
- },
-];
-
-// Extend transaction with calculated fields for display
-interface TransactionDisplay extends Transaction {
+// Display type for journal entry drafts
+interface DraftDisplay {
+ id: string;
+ transactionNumber: string;
+ date: string;
+ description: string;
totalDebit: number;
totalCredit: number;
+ isReconciled: boolean;
+ isVoided: boolean;
+ lines: JournalEntryDraft['lines'];
}
export default function Kassekladde() {
- const { company } = useCompany();
+ const { activeCompany } = useCompanyStore();
const [isModalOpen, setIsModalOpen] = useState(false);
- const [editingTransaction, setEditingTransaction] = useState
(null);
+ const [editingDraft, setEditingDraft] = useState(null);
const [dateFilter, setDateFilter] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
const [form] = Form.useForm();
const [lines, setLines] = useState[]>([
@@ -117,14 +62,26 @@ export default function Kassekladde() {
{ debit: 0, credit: 0 },
]);
- // Process transactions for display
- const displayData: TransactionDisplay[] = mockTransactions.map((tx) => ({
- ...tx,
- totalDebit: tx.lines.reduce((sum, line) => sum + (line.debit || 0), 0),
- totalCredit: tx.lines.reduce((sum, line) => sum + (line.credit || 0), 0),
+ // 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[] = 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',
}));
- const columns: DataTableColumn[] = [
+ const columns: DataTableColumn[] = [
{
dataIndex: 'transactionNumber',
title: 'Bilagsnr.',
@@ -167,9 +124,9 @@ export default function Kassekladde() {
return Annulleret;
}
return value ? (
- Afstemt
+ Bogfort
) : (
- Uafstemt
+ Kladde
);
},
},
@@ -222,24 +179,22 @@ export default function Kassekladde() {
},
];
- const handleAction = (action: string, record: TransactionDisplay) => {
+ const handleAction = (action: string, record: DraftDisplay) => {
switch (action) {
case 'view':
- // TODO: Show transaction details modal
message.info(`Vis detaljer for bilag ${record.transactionNumber}`);
break;
case 'edit':
- setEditingTransaction(record);
+ setEditingDraft(record);
setIsModalOpen(true);
break;
case 'copy':
- // TODO: Copy transaction
message.success(`Bilag ${record.transactionNumber} kopieret`);
break;
case 'void':
Modal.confirm({
title: 'Annuller bilag',
- content: `Er du sikker på at du vil annullere bilag ${record.transactionNumber}?`,
+ content: `Er du sikker pa at du vil annullere bilag ${record.transactionNumber}?`,
okText: 'Annuller bilag',
okType: 'danger',
cancelText: 'Fortryd',
@@ -283,12 +238,11 @@ export default function Kassekladde() {
const validation = validateDoubleEntry(lines as TransactionLine[]);
if (!validation.valid) {
message.error(
- `Debet (${formatCurrency(validation.totalDebit)}) skal være lig kredit (${formatCurrency(validation.totalCredit)})`
+ `Debet (${formatCurrency(validation.totalDebit)}) skal vaere lig kredit (${formatCurrency(validation.totalCredit)})`
);
return;
}
- // TODO: Submit via GraphQL mutation
console.log('Submitting:', { ...values, lines });
message.success('Bilag oprettet');
setIsModalOpen(false);
@@ -301,6 +255,29 @@ export default function Kassekladde() {
const balance = validateDoubleEntry(lines as TransactionLine[]);
+ if (isLoading) {
+ return (
+
+
+
+
+ Kassekladde
+
+ {activeCompany?.name}
+
+
+
+
+ );
+ }
+
return (
{/* Header */}
@@ -316,13 +293,13 @@ export default function Kassekladde() {
Kassekladde
- {company?.name}
+ {activeCompany?.name}
}
onClick={() => {
- setEditingTransaction(null);
+ setEditingDraft(null);
setIsModalOpen(true);
}}
>
@@ -342,7 +319,7 @@ export default function Kassekladde() {
placeholder="Konto"
style={{ width: 200 }}
allowClear
- options={mockAccounts.map((acc) => ({
+ options={accounts.map((acc) => ({
value: acc.id,
label: `${acc.accountNumber} - ${acc.name}`,
}))}
@@ -352,29 +329,33 @@ export default function Kassekladde() {
style={{ width: 120 }}
allowClear
options={[
- { value: 'reconciled', label: 'Afstemt' },
- { value: 'unreconciled', label: 'Uafstemt' },
- { value: 'voided', label: 'Annulleret' },
+ { value: 'posted', label: 'Bogfort' },
+ { value: 'draft', label: 'Kladde' },
+ { value: 'discarded', label: 'Annulleret' },
]}
/>
}>Flere filtre
{/* Data Table */}
-
- data={displayData}
- columns={columns}
- exportFilename="kassekladde"
- rowSelection="multiple"
- onRowClick={(record) => handleAction('view', record)}
- rowClassName={(record) =>
- record.isVoided ? 'voided-row' : ''
- }
- />
+ {displayData.length === 0 ? (
+
+ ) : (
+
+ data={displayData}
+ columns={columns}
+ exportFilename="kassekladde"
+ rowSelection="multiple"
+ onRowClick={(record) => handleAction('view', record)}
+ rowClassName={(record) =>
+ record.isVoided ? 'voided-row' : ''
+ }
+ />
+ )}
{/* Create/Edit Modal */}
{
setIsModalOpen(false);
@@ -392,7 +373,7 @@ export default function Kassekladde() {
@@ -428,12 +409,12 @@ export default function Kassekladde() {
|
@@ -497,7 +478,7 @@ export default function Kassekladde() {
|
|
(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -85,11 +66,29 @@ export default function Kontooversigt() {
const isMobile = !screens.md;
+ // Fetch accounts and balances from API
+ const { data: accounts = [], isLoading: accountsLoading } = useAccounts(activeCompany?.id);
+ const { data: balances = [], isLoading: balancesLoading } = useAccountBalances(
+ activeCompany?.id,
+ currentFiscalYear ? {
+ startDate: dayjs(currentFiscalYear.startDate),
+ endDate: dayjs(currentFiscalYear.endDate),
+ } : undefined
+ );
+
+ const isLoading = accountsLoading || balancesLoading;
+
+ // Combine accounts with balances
+ const accountsWithBalances = accounts.map(acc => {
+ const balance = balances.find(b => b.id === acc.id);
+ return { ...acc, balance: balance?.netChange ?? 0 };
+ });
+
// Build tree data from accounts
const buildTreeData = (): DataNode[] => {
return accountTypes.map((type) => {
const range = getAccountNumberRange(type);
- const typeAccounts = mockAccounts.filter((acc) => acc.type === type);
+ const typeAccounts = accountsWithBalances.filter((acc) => acc.type === type);
const typeBalance = typeAccounts.reduce((sum, acc) => sum + acc.balance, 0);
return {
@@ -142,7 +141,7 @@ export default function Kontooversigt() {
const handleSelectAccount = (selectedKeys: React.Key[]) => {
const key = selectedKeys[0];
if (key && !accountTypes.includes(key as AccountType)) {
- const account = mockAccounts.find((acc) => acc.id === key);
+ const account = accountsWithBalances.find((acc) => acc.id === key);
setSelectedAccount(account || null);
}
};
@@ -170,82 +169,68 @@ export default function Kontooversigt() {
}
};
- // Account transactions for selected account
- const accountTransactions = selectedAccount
- ? mockTransactions.filter((tx) => tx.accountId === selectedAccount.id)
- : [];
-
- const transactionColumns = [
- {
- dataIndex: 'date',
- title: 'Dato',
- width: 100,
- render: (value: string) => formatDate(value),
- },
- {
- dataIndex: 'description',
- title: 'Beskrivelse',
- ellipsis: true,
- },
- {
- dataIndex: 'debit',
- title: 'Debet',
- width: 120,
- align: 'right' as const,
- render: (value: number) =>
- value > 0 ? (
- {formatCurrency(value)}
- ) : null,
- },
- {
- dataIndex: 'credit',
- title: 'Kredit',
- width: 120,
- align: 'right' as const,
- render: (value: number) =>
- value > 0 ? (
- {formatCurrency(value)}
- ) : null,
- },
- {
- dataIndex: 'balance',
- title: 'Saldo',
- width: 120,
- align: 'right' as const,
- render: (value: number) => (
- = 0 ? accountingColors.credit : accountingColors.debit,
- }}
- >
- {formatCurrency(value)}
-
- ),
- },
- ];
-
- // Calculate totals
- const totalAssets = mockAccounts
+ // Calculate totals from actual data
+ const totalAssets = accountsWithBalances
.filter((a) => a.type === 'asset')
.reduce((sum, a) => sum + a.balance, 0);
- const totalLiabilities = mockAccounts
+ const totalLiabilities = accountsWithBalances
.filter((a) => ['liability', 'equity'].includes(a.type))
.reduce((sum, a) => sum + Math.abs(a.balance), 0);
- const totalRevenue = mockAccounts
+ const totalRevenue = accountsWithBalances
.filter((a) => a.type === 'revenue')
.reduce((sum, a) => sum + Math.abs(a.balance), 0);
- const totalExpenses = mockAccounts
+ const totalExpenses = accountsWithBalances
.filter((a) => ['cogs', 'expense', 'personnel', 'financial'].includes(a.type))
.reduce((sum, a) => sum + a.balance, 0);
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (accounts.length === 0) {
+ return (
+
+ }
+ onClick={handleCreateAccount}
+ aria-label="Opret ny konto"
+ >
+ Ny konto
+
+ }
+ />
+
+
+ );
+ }
+
return (
- String(index)}
- size="small"
- pagination={{ pageSize: 10 }}
- aria-label={`Bevaegelser for konto ${selectedAccount?.accountNumber}`}
- />
+
),
},
@@ -480,7 +458,7 @@ export default function Kontooversigt() {
{ required: true, message: 'Indtast kontonummer' },
{
pattern: /^\d{4}$/,
- message: 'Kontonummer skal være 4 cifre',
+ message: 'Kontonummer skal vaere 4 cifre',
},
]}
>
@@ -498,10 +476,10 @@ export default function Kontooversigt() {
|