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:
102
frontend/src/pages/reports/BalanceSheetPage.tsx
Normal file
102
frontend/src/pages/reports/BalanceSheetPage.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Title, Table, Group, Stack, Text, Card, Loader, Center, Divider, Badge,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '../../services/api';
|
||||
|
||||
interface AccountLine { account_number: number; name: string; balance: string; fund_type: string; }
|
||||
interface BalanceSheetData {
|
||||
as_of: string;
|
||||
assets: AccountLine[]; liabilities: AccountLine[]; equity: AccountLine[];
|
||||
total_assets: string; total_liabilities: string; total_equity: string;
|
||||
}
|
||||
|
||||
export function BalanceSheetPage() {
|
||||
const [asOf, setAsOf] = useState(new Date());
|
||||
const dateStr = asOf.toISOString().split('T')[0];
|
||||
|
||||
const { data, isLoading } = useQuery<BalanceSheetData>({
|
||||
queryKey: ['balance-sheet', dateStr],
|
||||
queryFn: async () => { const { data } = await api.get(`/reports/balance-sheet?as_of=${dateStr}`); return data; },
|
||||
});
|
||||
|
||||
const fmt = (v: string | number) => parseFloat(String(v || '0')).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
||||
|
||||
if (isLoading) return <Center h={300}><Loader /></Center>;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Balance Sheet</Title>
|
||||
<DateInput label="As of" value={asOf} onChange={(v) => v && setAsOf(v)} w={200} />
|
||||
</Group>
|
||||
|
||||
<Card withBorder>
|
||||
<Title order={4} mb="md">Assets</Title>
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
{(data?.assets || []).map((a) => (
|
||||
<Table.Tr key={a.account_number}>
|
||||
<Table.Td w={80}>{a.account_number}</Table.Td>
|
||||
<Table.Td>{a.name} <Badge size="xs" variant="light">{a.fund_type}</Badge></Table.Td>
|
||||
<Table.Td ta="right" ff="monospace" w={140}>{fmt(a.balance)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
<Table.Tfoot>
|
||||
<Table.Tr><Table.Td colSpan={2} fw={700}>Total Assets</Table.Td>
|
||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(data?.total_assets || '0')}</Table.Td></Table.Tr>
|
||||
</Table.Tfoot>
|
||||
</Table>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Title order={4} mb="md">Liabilities</Title>
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
{(data?.liabilities || []).map((a) => (
|
||||
<Table.Tr key={a.account_number}>
|
||||
<Table.Td w={80}>{a.account_number}</Table.Td>
|
||||
<Table.Td>{a.name}</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace" w={140}>{fmt(a.balance)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
<Table.Tfoot>
|
||||
<Table.Tr><Table.Td colSpan={2} fw={700}>Total Liabilities</Table.Td>
|
||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(data?.total_liabilities || '0')}</Table.Td></Table.Tr>
|
||||
</Table.Tfoot>
|
||||
</Table>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Title order={4} mb="md">Equity</Title>
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
{(data?.equity || []).map((a) => (
|
||||
<Table.Tr key={a.account_number}>
|
||||
<Table.Td w={80}>{a.account_number}</Table.Td>
|
||||
<Table.Td>{a.name}</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace" w={140}>{fmt(a.balance)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
<Table.Tfoot>
|
||||
<Table.Tr><Table.Td colSpan={2} fw={700}>Total Equity</Table.Td>
|
||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(data?.total_equity || '0')}</Table.Td></Table.Tr>
|
||||
</Table.Tfoot>
|
||||
</Table>
|
||||
|
||||
<Divider my="md" />
|
||||
<Group justify="space-between" px="sm">
|
||||
<Text fw={700} size="lg">Total Liabilities + Equity</Text>
|
||||
<Text fw={700} size="lg" ff="monospace">
|
||||
{fmt(String(parseFloat(data?.total_liabilities || '0') + parseFloat(data?.total_equity || '0')))}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user