Files
HOA_Financial_Platform/frontend/src/pages/dashboard/DashboardPage.tsx
olsch01 e0272f9d8a Rename to HOA LedgerIQ and implement remaining report pages
- 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>
2026-02-18 09:09:50 -05:00

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>
);
}