- Rename app from "HOA Financial Platform" to "HOA LedgerIQ" across all frontend pages, backend API docs, package.json files, and seed data - Add Cash Flow Statement report (GET /reports/cash-flow) with operating and reserve fund activity breakdown, beginning/ending cash balances - Add Aging Report (GET /reports/aging) with per-unit aging buckets (current, 1-30, 31-60, 61-90, 90+ days), expandable invoice details - Add Year-End Package (GET /reports/year-end) with income statement summary, collection stats, 1099-NEC vendor report, reserve fund status - Add Settings page showing org info, user profile, and system details - Replace all PlaceholderPage references with real implementations - Bump auth store version to 3 for localStorage migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
5.7 KiB
TypeScript
148 lines
5.7 KiB
TypeScript
import {
|
|
Title, Text, SimpleGrid, Card, Group, ThemeIcon, Stack, Table,
|
|
Badge, Loader, Center,
|
|
} from '@mantine/core';
|
|
import {
|
|
IconCash,
|
|
IconFileInvoice,
|
|
IconShieldCheck,
|
|
IconAlertTriangle,
|
|
} from '@tabler/icons-react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useAuthStore } from '../../stores/authStore';
|
|
import api from '../../services/api';
|
|
|
|
interface DashboardData {
|
|
total_cash: string;
|
|
total_receivables: string;
|
|
reserve_fund_balance: string;
|
|
delinquent_units: number;
|
|
recent_transactions: {
|
|
id: string; entry_date: string; description: string; entry_type: string; amount: string;
|
|
}[];
|
|
}
|
|
|
|
export function DashboardPage() {
|
|
const currentOrg = useAuthStore((s) => s.currentOrg);
|
|
|
|
const { data, isLoading } = useQuery<DashboardData>({
|
|
queryKey: ['dashboard'],
|
|
queryFn: async () => { const { data } = await api.get('/reports/dashboard'); return data; },
|
|
enabled: !!currentOrg,
|
|
});
|
|
|
|
const fmt = (v: string | number) =>
|
|
parseFloat(String(v || '0')).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
|
|
|
const stats = [
|
|
{ title: 'Total Cash', value: fmt(data?.total_cash || '0'), icon: IconCash, color: 'green' },
|
|
{ title: 'Total Receivables', value: fmt(data?.total_receivables || '0'), icon: IconFileInvoice, color: 'blue' },
|
|
{ title: 'Reserve Fund', value: fmt(data?.reserve_fund_balance || '0'), icon: IconShieldCheck, color: 'violet' },
|
|
{ title: 'Delinquent Accounts', value: String(data?.delinquent_units || 0), icon: IconAlertTriangle, color: 'orange' },
|
|
];
|
|
|
|
const entryTypeColors: Record<string, string> = {
|
|
manual: 'gray', assessment: 'blue', payment: 'green', late_fee: 'red',
|
|
transfer: 'cyan', adjustment: 'yellow', closing: 'dark', opening_balance: 'indigo',
|
|
};
|
|
|
|
return (
|
|
<Stack>
|
|
<div>
|
|
<Title order={2}>Dashboard</Title>
|
|
<Text c="dimmed" size="sm">
|
|
{currentOrg ? `${currentOrg.name} - ${currentOrg.role}` : 'No organization selected'}
|
|
</Text>
|
|
</div>
|
|
|
|
{!currentOrg ? (
|
|
<Card withBorder p="xl" ta="center">
|
|
<Text size="lg" fw={500}>Welcome to HOA LedgerIQ</Text>
|
|
<Text c="dimmed" mt="sm">
|
|
Create or select an organization to get started.
|
|
</Text>
|
|
</Card>
|
|
) : isLoading ? (
|
|
<Center h={200}><Loader /></Center>
|
|
) : (
|
|
<>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>
|
|
{stats.map((stat) => (
|
|
<Card key={stat.title} withBorder padding="lg" radius="md">
|
|
<Group justify="space-between">
|
|
<div>
|
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
|
{stat.title}
|
|
</Text>
|
|
<Text fw={700} size="xl">
|
|
{stat.value}
|
|
</Text>
|
|
</div>
|
|
<ThemeIcon color={stat.color} variant="light" size={48} radius="md">
|
|
<stat.icon size={28} />
|
|
</ThemeIcon>
|
|
</Group>
|
|
</Card>
|
|
))}
|
|
</SimpleGrid>
|
|
|
|
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
|
<Card withBorder padding="lg" radius="md">
|
|
<Title order={4} mb="sm">Recent Transactions</Title>
|
|
{(data?.recent_transactions || []).length === 0 ? (
|
|
<Text c="dimmed" size="sm">No transactions yet. Start by entering journal entries.</Text>
|
|
) : (
|
|
<Table striped highlightOnHover>
|
|
<Table.Tbody>
|
|
{(data?.recent_transactions || []).map((tx) => (
|
|
<Table.Tr key={tx.id}>
|
|
<Table.Td>
|
|
<Text size="xs" c="dimmed">{new Date(tx.entry_date).toLocaleDateString()}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" lineClamp={1}>{tx.description}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="xs" color={entryTypeColors[tx.entry_type] || 'gray'} variant="light">
|
|
{tx.entry_type}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td ta="right" ff="monospace" fw={500}>
|
|
{fmt(tx.amount)}
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
</Card>
|
|
<Card withBorder padding="lg" radius="md">
|
|
<Title order={4}>Quick Stats</Title>
|
|
<Stack mt="sm" gap="xs">
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Cash Position</Text>
|
|
<Text size="sm" fw={500} c="green">{fmt(data?.total_cash || '0')}</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Outstanding AR</Text>
|
|
<Text size="sm" fw={500} c="blue">{fmt(data?.total_receivables || '0')}</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Reserve Funding</Text>
|
|
<Text size="sm" fw={500} c="violet">{fmt(data?.reserve_fund_balance || '0')}</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Delinquent Units</Text>
|
|
<Text size="sm" fw={500} c={data?.delinquent_units ? 'red' : 'green'}>
|
|
{data?.delinquent_units || 0}
|
|
</Text>
|
|
</Group>
|
|
</Stack>
|
|
</Card>
|
|
</SimpleGrid>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|