Initial commit: HOA Financial Intelligence Platform MVP
Multi-tenant financial management platform for homeowner associations featuring: - NestJS backend with 16 modules (auth, accounts, transactions, budgets, units, invoices, payments, vendors, reserves, investments, capital projects, reports) - React + Mantine frontend with dashboard, CRUD pages, and financial reports - Schema-per-tenant PostgreSQL isolation with JWT-based tenant resolution - Docker Compose infrastructure (nginx, backend, frontend, postgres, redis) - Comprehensive seed data for Sunrise Valley HOA demo - 39 API endpoints with Swagger documentation - Double-entry bookkeeping with journal entries - Budget vs actual reporting and Sankey cash flow visualization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
147
frontend/src/pages/dashboard/DashboardPage.tsx
Normal file
147
frontend/src/pages/dashboard/DashboardPage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
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 the HOA Financial Platform</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user