import { Row, Col, Card, Statistic, Typography, Space, Tag, Progress, Skeleton, Empty } from 'antd'; import { BankOutlined, RiseOutlined, FallOutlined, FileTextOutlined, CheckCircleOutlined, ClockCircleOutlined, WarningOutlined, } from '@ant-design/icons'; import { Line, Pie, Column } from '@ant-design/charts'; import dayjs from 'dayjs'; import { useCompany } from '@/hooks/useCompany'; 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'; import { formatCurrency, formatDate } from '@/lib/formatters'; import { accountingColors } from '@/styles/theme'; const { Title, Text } = Typography; // 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'; } export default function Dashboard() { const { company } = useCompany(); 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 (
); } const cashFlowConfig = { data: cashFlowData, 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 = { data: expenseBreakdown, 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 = { data: cashFlowData.flatMap((d) => [ { 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 (
{/* Header */}
Dashboard {company?.name} - {formatDate(new Date().toISOString(), 'MMMM YYYY')}
{/* KPI Cards */} {/* Cash Position */} } suffix="kr." formatter={(value) => formatCurrency(value as number)} />
= 0 ? 'green' : 'red'} icon={metrics.cashChange >= 0 ? : } > {metrics.cashChange >= 0 ? '+' : ''} {(metrics.cashChange * 100).toFixed(1)}% denne maaned
{/* Accounts Receivable */} formatCurrency(value as number)} />
{metrics.pendingInvoices} afventer {metrics.overdueInvoices > 0 && ( }> {metrics.overdueInvoices} forfaldne )}
{/* Accounts Payable */} formatCurrency(value as number)} />
= 0 ? 'orange' : 'green'}> {metrics.apChange >= 0 ? '+' : ''} {(metrics.apChange * 100).toFixed(1)}% denne maaned
{/* VAT Liability */} formatCurrency(value as number)} />
Naeste frist: 1. marts
{/* Charts Row */} {/* Cash Flow Chart */} {cashFlowData.length > 0 ? ( ) : ( )} {/* Revenue vs Expenses */} {cashFlowData.length > 0 ? ( ) : ( )} {/* Bottom Row */} {/* Expense Breakdown */} {expenseBreakdown.length > 0 ? ( ) : ( )} {/* Reconciliation Status */}
} /> {metrics.unreconciledCount === 0 ? '100% afstemt' : 'Bankafstemning ikke implementeret endnu'}
{/* Recent Transactions */}
{recentTransactions.length > 0 ? ( recentTransactions.map((tx) => (
{tx.description} {formatDate(tx.date)}
= 0 ? accountingColors.credit : accountingColors.debit, }} > {formatCurrency(tx.amount, { showSign: true })}
)) ) : ( )}
{/* Quick Actions */} {metrics.unreconciledCount} transaktioner klar til afstemning Momsindberetning forfalder om 14 dage {metrics.overdueInvoices > 0 && ( {metrics.overdueInvoices} fakturaer er forfaldne )}
); }