2026-01-30 22:42:00 +01:00
|
|
|
import { Row, Col, Card, Statistic, Typography, Space, Tag, Progress, Skeleton, Empty } from 'antd';
|
2026-01-18 02:52:30 +01:00
|
|
|
import {
|
|
|
|
|
BankOutlined,
|
|
|
|
|
RiseOutlined,
|
|
|
|
|
FallOutlined,
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
WarningOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import { Line, Pie, Column } from '@ant-design/charts';
|
2026-01-30 22:42:00 +01:00
|
|
|
import dayjs from 'dayjs';
|
2026-01-18 02:52:30 +01:00
|
|
|
import { useCompany } from '@/hooks/useCompany';
|
2026-01-30 22:42:00 +01:00
|
|
|
import { useCompanyStore } from '@/stores/companyStore';
|
|
|
|
|
import { usePeriodStore } from '@/stores/periodStore';
|
|
|
|
|
import { useAccountBalances } from '@/api/queries/accountQueries';
|
|
|
|
|
import { useInvoices } from '@/api/queries/invoiceQueries';
|
|
|
|
|
import { useVatReport } from '@/api/queries/vatQueries';
|
2026-01-18 02:52:30 +01:00
|
|
|
import { formatCurrency, formatDate } from '@/lib/formatters';
|
|
|
|
|
import { accountingColors } from '@/styles/theme';
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
2026-01-30 22:42:00 +01:00
|
|
|
// Types for chart data
|
|
|
|
|
interface CashFlowDataPoint {
|
|
|
|
|
month: string;
|
|
|
|
|
inflow: number;
|
|
|
|
|
outflow: number;
|
|
|
|
|
balance: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ExpenseBreakdownItem {
|
|
|
|
|
category: string;
|
|
|
|
|
value: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RecentTransaction {
|
|
|
|
|
id: string;
|
|
|
|
|
date: string;
|
|
|
|
|
description: string;
|
|
|
|
|
amount: number;
|
|
|
|
|
type: 'income' | 'expense';
|
|
|
|
|
}
|
2026-01-18 02:52:30 +01:00
|
|
|
|
|
|
|
|
export default function Dashboard() {
|
|
|
|
|
const { company } = useCompany();
|
2026-01-30 22:42:00 +01:00
|
|
|
const { activeCompany } = useCompanyStore();
|
|
|
|
|
const { currentFiscalYear } = usePeriodStore();
|
|
|
|
|
|
|
|
|
|
// Define date interval
|
|
|
|
|
const periodStart = currentFiscalYear?.startDate || dayjs().startOf('year').format('YYYY-MM-DD');
|
|
|
|
|
const periodEnd = currentFiscalYear?.endDate || dayjs().endOf('year').format('YYYY-MM-DD');
|
|
|
|
|
|
|
|
|
|
const { data: balances = [], isLoading: balancesLoading } = useAccountBalances(
|
|
|
|
|
activeCompany?.id,
|
|
|
|
|
currentFiscalYear ? {
|
|
|
|
|
startDate: dayjs(currentFiscalYear.startDate),
|
|
|
|
|
endDate: dayjs(currentFiscalYear.endDate),
|
|
|
|
|
} : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { data: invoices = [], isLoading: invoicesLoading } = useInvoices(activeCompany?.id);
|
|
|
|
|
const { data: vatReport, isLoading: vatLoading } = useVatReport(
|
|
|
|
|
activeCompany?.id,
|
|
|
|
|
periodStart,
|
|
|
|
|
periodEnd
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const isLoading = balancesLoading || invoicesLoading || vatLoading;
|
|
|
|
|
|
|
|
|
|
// Calculate metrics from data
|
|
|
|
|
const cashAccounts = balances.filter(b => b.accountType === 'asset' && b.accountNumber.startsWith('10'));
|
|
|
|
|
const cashPosition = cashAccounts.reduce((sum, acc) => sum + acc.netChange, 0);
|
|
|
|
|
|
|
|
|
|
const receivableAccounts = balances.filter(b => b.accountNumber.startsWith('11'));
|
|
|
|
|
const accountsReceivable = receivableAccounts.reduce((sum, acc) => sum + acc.netChange, 0);
|
|
|
|
|
|
|
|
|
|
const payableAccounts = balances.filter(b => b.accountNumber.startsWith('20'));
|
|
|
|
|
const accountsPayable = Math.abs(payableAccounts.reduce((sum, acc) => sum + acc.netChange, 0));
|
|
|
|
|
|
|
|
|
|
const revenueAccounts = balances.filter(b => b.accountType === 'revenue');
|
|
|
|
|
const monthlyRevenue = Math.abs(revenueAccounts.reduce((sum, acc) => sum + acc.netChange, 0));
|
|
|
|
|
|
|
|
|
|
const expenseAccounts = balances.filter(b => ['expense', 'cogs', 'personnel'].includes(b.accountType));
|
|
|
|
|
const monthlyExpenses = expenseAccounts.reduce((sum, acc) => sum + acc.netChange, 0);
|
|
|
|
|
|
|
|
|
|
const vatLiability = vatReport?.netVat ?? 0;
|
|
|
|
|
|
|
|
|
|
const pendingInvoices = invoices.filter(i => i.status === 'sent' || i.status === 'issued').length;
|
|
|
|
|
const overdueInvoices = invoices.filter(i =>
|
|
|
|
|
(i.status === 'sent' || i.status === 'issued') &&
|
|
|
|
|
i.dueDate &&
|
|
|
|
|
dayjs(i.dueDate).isBefore(dayjs())
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
const metrics = {
|
|
|
|
|
cashPosition,
|
|
|
|
|
cashChange: 0, // Requires historical data
|
|
|
|
|
accountsReceivable,
|
|
|
|
|
arChange: 0,
|
|
|
|
|
accountsPayable,
|
|
|
|
|
apChange: 0,
|
|
|
|
|
monthlyRevenue,
|
|
|
|
|
revenueChange: 0,
|
|
|
|
|
monthlyExpenses,
|
|
|
|
|
expenseChange: 0,
|
|
|
|
|
vatLiability,
|
|
|
|
|
unreconciledCount: 0, // Requires bank reconciliation data
|
|
|
|
|
pendingInvoices,
|
|
|
|
|
overdueInvoices,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Calculate expense breakdown from balances
|
|
|
|
|
const totalExpenses = expenseAccounts.reduce((sum, acc) => sum + Math.abs(acc.netChange), 0);
|
|
|
|
|
|
|
|
|
|
const calculatePercentage = (accountType: string, accountPrefix?: string): number => {
|
|
|
|
|
if (totalExpenses === 0) return 0;
|
|
|
|
|
let relevantAccounts = balances.filter(b => b.accountType === accountType);
|
|
|
|
|
if (accountPrefix && accountPrefix !== 'other') {
|
|
|
|
|
relevantAccounts = relevantAccounts.filter(b => b.accountNumber.startsWith(accountPrefix));
|
|
|
|
|
} else if (accountPrefix === 'other') {
|
|
|
|
|
// "Other" is everything not in specific categories
|
|
|
|
|
const specificPrefixes = ['61', '62', '64'];
|
|
|
|
|
relevantAccounts = relevantAccounts.filter(b =>
|
|
|
|
|
!specificPrefixes.some(prefix => b.accountNumber.startsWith(prefix))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const categoryTotal = relevantAccounts.reduce((sum, acc) => sum + Math.abs(acc.netChange), 0);
|
|
|
|
|
return Math.round((categoryTotal / totalExpenses) * 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const personnelExpenses = balances.filter(b => b.accountType === 'personnel');
|
|
|
|
|
const personnelTotal = personnelExpenses.reduce((sum, acc) => sum + Math.abs(acc.netChange), 0);
|
|
|
|
|
const personnelPercentage = totalExpenses > 0 ? Math.round((personnelTotal / totalExpenses) * 100) : 0;
|
|
|
|
|
|
|
|
|
|
const expenseBreakdown: ExpenseBreakdownItem[] = [
|
|
|
|
|
{ category: 'Personale', value: personnelPercentage },
|
|
|
|
|
{ category: 'Lokaler', value: calculatePercentage('expense', '61') },
|
|
|
|
|
{ category: 'IT & Software', value: calculatePercentage('expense', '62') },
|
|
|
|
|
{ category: 'Marketing', value: calculatePercentage('expense', '64') },
|
|
|
|
|
{ category: 'Andet', value: calculatePercentage('expense', 'other') },
|
|
|
|
|
].filter(e => e.value > 0);
|
|
|
|
|
|
|
|
|
|
// For cashFlowData and recentTransactions - show empty charts or loading
|
|
|
|
|
const cashFlowData: CashFlowDataPoint[] = [];
|
|
|
|
|
const recentTransactions: RecentTransaction[] = [];
|
|
|
|
|
|
|
|
|
|
// Loading state
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<Skeleton active paragraph={{ rows: 1 }} />
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
|
|
|
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 6 }} /></Col>
|
|
|
|
|
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 6 }} /></Col>
|
|
|
|
|
</Row>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-18 02:52:30 +01:00
|
|
|
|
|
|
|
|
const cashFlowConfig = {
|
2026-01-30 22:42:00 +01:00
|
|
|
data: cashFlowData,
|
2026-01-18 02:52:30 +01:00
|
|
|
xField: 'month',
|
|
|
|
|
yField: 'balance',
|
|
|
|
|
smooth: true,
|
|
|
|
|
color: accountingColors.balance,
|
|
|
|
|
point: { size: 4, shape: 'circle' },
|
|
|
|
|
area: { style: { fillOpacity: 0.1 } },
|
|
|
|
|
yAxis: {
|
|
|
|
|
label: {
|
|
|
|
|
formatter: (v: string) => `${parseInt(v) / 1000}k`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
height: 200,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const expenseConfig = {
|
2026-01-30 22:42:00 +01:00
|
|
|
data: expenseBreakdown,
|
2026-01-18 02:52:30 +01:00
|
|
|
angleField: 'value',
|
|
|
|
|
colorField: 'category',
|
|
|
|
|
radius: 0.8,
|
|
|
|
|
innerRadius: 0.6,
|
|
|
|
|
label: {
|
|
|
|
|
type: 'outer',
|
|
|
|
|
formatter: (datum: { category: string; value: number }) =>
|
|
|
|
|
`${datum.category}: ${datum.value}%`,
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
position: 'right' as const,
|
|
|
|
|
},
|
|
|
|
|
height: 200,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const revenueExpenseConfig = {
|
2026-01-30 22:42:00 +01:00
|
|
|
data: cashFlowData.flatMap((d) => [
|
2026-01-18 02:52:30 +01:00
|
|
|
{ month: d.month, type: 'Indtaegter', value: d.inflow },
|
|
|
|
|
{ month: d.month, type: 'Udgifter', value: d.outflow },
|
|
|
|
|
]),
|
|
|
|
|
isGroup: true,
|
|
|
|
|
xField: 'month',
|
|
|
|
|
yField: 'value',
|
|
|
|
|
seriesField: 'type',
|
|
|
|
|
color: [accountingColors.credit, accountingColors.debit],
|
|
|
|
|
yAxis: {
|
|
|
|
|
label: {
|
|
|
|
|
formatter: (v: string) => `${parseInt(v) / 1000}k`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
height: 200,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div style={{ marginBottom: 24 }}>
|
|
|
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
|
|
|
Dashboard
|
|
|
|
|
</Title>
|
|
|
|
|
<Text type="secondary">
|
|
|
|
|
{company?.name} - {formatDate(new Date().toISOString(), 'MMMM YYYY')}
|
|
|
|
|
</Text>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* KPI Cards */}
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
{/* Cash Position */}
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Likviditet"
|
2026-01-30 22:42:00 +01:00
|
|
|
value={metrics.cashPosition}
|
2026-01-18 02:52:30 +01:00
|
|
|
precision={2}
|
|
|
|
|
prefix={<BankOutlined />}
|
|
|
|
|
suffix="kr."
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
<Tag
|
2026-01-30 22:42:00 +01:00
|
|
|
color={metrics.cashChange >= 0 ? 'green' : 'red'}
|
|
|
|
|
icon={metrics.cashChange >= 0 ? <RiseOutlined /> : <FallOutlined />}
|
2026-01-18 02:52:30 +01:00
|
|
|
>
|
2026-01-30 22:42:00 +01:00
|
|
|
{metrics.cashChange >= 0 ? '+' : ''}
|
|
|
|
|
{(metrics.cashChange * 100).toFixed(1)}% denne maaned
|
2026-01-18 02:52:30 +01:00
|
|
|
</Tag>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* Accounts Receivable */}
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Tilgodehavender"
|
2026-01-30 22:42:00 +01:00
|
|
|
value={metrics.accountsReceivable}
|
2026-01-18 02:52:30 +01:00
|
|
|
precision={2}
|
|
|
|
|
suffix="kr."
|
|
|
|
|
valueStyle={{ color: accountingColors.credit }}
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
<Space size={4}>
|
2026-01-30 22:42:00 +01:00
|
|
|
<Tag color="blue">{metrics.pendingInvoices} afventer</Tag>
|
|
|
|
|
{metrics.overdueInvoices > 0 && (
|
2026-01-18 02:52:30 +01:00
|
|
|
<Tag color="red" icon={<WarningOutlined />}>
|
2026-01-30 22:42:00 +01:00
|
|
|
{metrics.overdueInvoices} forfaldne
|
2026-01-18 02:52:30 +01:00
|
|
|
</Tag>
|
|
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* Accounts Payable */}
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Kreditorer"
|
2026-01-30 22:42:00 +01:00
|
|
|
value={metrics.accountsPayable}
|
2026-01-18 02:52:30 +01:00
|
|
|
precision={2}
|
|
|
|
|
suffix="kr."
|
|
|
|
|
valueStyle={{ color: accountingColors.debit }}
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
2026-01-30 22:42:00 +01:00
|
|
|
<Tag color={metrics.apChange >= 0 ? 'orange' : 'green'}>
|
|
|
|
|
{metrics.apChange >= 0 ? '+' : ''}
|
|
|
|
|
{(metrics.apChange * 100).toFixed(1)}% denne maaned
|
2026-01-18 02:52:30 +01:00
|
|
|
</Tag>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* VAT Liability */}
|
|
|
|
|
<Col xs={24} sm={12} lg={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Moms til betaling"
|
2026-01-30 22:42:00 +01:00
|
|
|
value={metrics.vatLiability}
|
2026-01-18 02:52:30 +01:00
|
|
|
precision={2}
|
|
|
|
|
suffix="kr."
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
<Tag color="blue">Naeste frist: 1. marts</Tag>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* Charts Row */}
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
|
|
|
{/* Cash Flow Chart */}
|
|
|
|
|
<Col xs={24} lg={12}>
|
|
|
|
|
<Card title="Pengestroemme" size="small">
|
2026-01-30 22:42:00 +01:00
|
|
|
{cashFlowData.length > 0 ? (
|
|
|
|
|
<Line {...cashFlowConfig} />
|
|
|
|
|
) : (
|
|
|
|
|
<Empty description="Ingen pengestroemsdata tilgaengelig endnu" style={{ height: 200 }} />
|
|
|
|
|
)}
|
2026-01-18 02:52:30 +01:00
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* Revenue vs Expenses */}
|
|
|
|
|
<Col xs={24} lg={12}>
|
|
|
|
|
<Card title="Indtaegter vs. Udgifter" size="small">
|
2026-01-30 22:42:00 +01:00
|
|
|
{cashFlowData.length > 0 ? (
|
|
|
|
|
<Column {...revenueExpenseConfig} />
|
|
|
|
|
) : (
|
|
|
|
|
<Empty description="Ingen historiske data tilgaengelig endnu" style={{ height: 200 }} />
|
|
|
|
|
)}
|
2026-01-18 02:52:30 +01:00
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* Bottom Row */}
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
|
|
|
{/* Expense Breakdown */}
|
|
|
|
|
<Col xs={24} lg={8}>
|
|
|
|
|
<Card title="Udgiftsfordeling" size="small">
|
2026-01-30 22:42:00 +01:00
|
|
|
{expenseBreakdown.length > 0 ? (
|
|
|
|
|
<Pie {...expenseConfig} />
|
|
|
|
|
) : (
|
|
|
|
|
<Empty description="Ingen udgiftsdata tilgaengelig" style={{ height: 200 }} />
|
|
|
|
|
)}
|
2026-01-18 02:52:30 +01:00
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* Reconciliation Status */}
|
|
|
|
|
<Col xs={24} lg={8}>
|
|
|
|
|
<Card title="Afstemningsstatus" size="small">
|
|
|
|
|
<div style={{ padding: '16px 0' }}>
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Uafstemte transaktioner"
|
2026-01-30 22:42:00 +01:00
|
|
|
value={metrics.unreconciledCount}
|
2026-01-18 02:52:30 +01:00
|
|
|
prefix={<FileTextOutlined />}
|
|
|
|
|
/>
|
|
|
|
|
<Progress
|
2026-01-30 22:42:00 +01:00
|
|
|
percent={metrics.unreconciledCount === 0 ? 100 : 75}
|
2026-01-18 02:52:30 +01:00
|
|
|
status="active"
|
|
|
|
|
strokeColor={accountingColors.balance}
|
|
|
|
|
style={{ marginTop: 16 }}
|
|
|
|
|
/>
|
2026-01-30 22:42:00 +01:00
|
|
|
<Text type="secondary">
|
|
|
|
|
{metrics.unreconciledCount === 0 ? '100% afstemt' : 'Bankafstemning ikke implementeret endnu'}
|
|
|
|
|
</Text>
|
2026-01-18 02:52:30 +01:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
{/* Recent Transactions */}
|
|
|
|
|
<Col xs={24} lg={8}>
|
|
|
|
|
<Card
|
|
|
|
|
title="Seneste transaktioner"
|
|
|
|
|
size="small"
|
|
|
|
|
bodyStyle={{ padding: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ maxHeight: 240, overflow: 'auto' }}>
|
2026-01-30 22:42:00 +01:00
|
|
|
{recentTransactions.length > 0 ? (
|
|
|
|
|
recentTransactions.map((tx) => (
|
|
|
|
|
<div
|
|
|
|
|
key={tx.id}
|
2026-01-18 02:52:30 +01:00
|
|
|
style={{
|
2026-01-30 22:42:00 +01:00
|
|
|
padding: '8px 16px',
|
|
|
|
|
borderBottom: '1px solid #f0f0f0',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
2026-01-18 02:52:30 +01:00
|
|
|
}}
|
|
|
|
|
>
|
2026-01-30 22:42:00 +01:00
|
|
|
<div>
|
|
|
|
|
<Text style={{ display: 'block' }}>{tx.description}</Text>
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
{formatDate(tx.date)}
|
|
|
|
|
</Text>
|
|
|
|
|
</div>
|
|
|
|
|
<Text
|
|
|
|
|
strong
|
|
|
|
|
className="tabular-nums"
|
|
|
|
|
style={{
|
|
|
|
|
color: tx.amount >= 0 ? accountingColors.credit : accountingColors.debit,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{formatCurrency(tx.amount, { showSign: true })}
|
|
|
|
|
</Text>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<Empty description="Ingen seneste transaktioner" style={{ padding: 24 }} />
|
|
|
|
|
)}
|
2026-01-18 02:52:30 +01:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* Quick Actions */}
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
|
|
|
<Col span={24}>
|
|
|
|
|
<Card title="Hurtige handlinger" size="small">
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
<Col>
|
|
|
|
|
<Space>
|
|
|
|
|
<CheckCircleOutlined style={{ color: accountingColors.credit }} />
|
2026-01-30 22:42:00 +01:00
|
|
|
<Text>{metrics.unreconciledCount} transaktioner klar til afstemning</Text>
|
2026-01-18 02:52:30 +01:00
|
|
|
</Space>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col>
|
|
|
|
|
<Space>
|
|
|
|
|
<ClockCircleOutlined style={{ color: accountingColors.warning }} />
|
|
|
|
|
<Text>Momsindberetning forfalder om 14 dage</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
</Col>
|
2026-01-30 22:42:00 +01:00
|
|
|
{metrics.overdueInvoices > 0 && (
|
|
|
|
|
<Col>
|
|
|
|
|
<Space>
|
|
|
|
|
<WarningOutlined style={{ color: accountingColors.debit }} />
|
|
|
|
|
<Text>{metrics.overdueInvoices} fakturaer er forfaldne</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
</Col>
|
|
|
|
|
)}
|
2026-01-18 02:52:30 +01:00
|
|
|
</Row>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|