Bug & tweak sprint: fix financial calculations, add quarterly report, enhance dashboard
- Fix Accounts page: include investment accounts in Est. Monthly Interest calc, add Fund column to investment table, split summary cards into Operating/Reserve - Fix Cash Flow: ending balance now respects includeInvestments toggle - Fix Budget Manager: separate operating/reserve income in summary cards - Fix Projects: default sort by planned_date instead of name - Add Vendors: last_negotiated date field with migration, CSV import/export - New Quarterly Financial Report: budget vs actuals, over-budget flagging, YTD - Enhance Dashboard: separate Operating/Reserve fund cards, expanded Quick Stats with monthly interest, YTD interest earned, planned capital spend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
Title, Text, SimpleGrid, Card, Group, ThemeIcon, Stack, Table,
|
||||
Badge, Loader, Center,
|
||||
Badge, Loader, Center, Divider,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconCash,
|
||||
IconFileInvoice,
|
||||
IconShieldCheck,
|
||||
IconAlertTriangle,
|
||||
IconBuildingBank,
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
@@ -20,6 +21,14 @@ interface DashboardData {
|
||||
recent_transactions: {
|
||||
id: string; entry_date: string; description: string; entry_type: string; amount: string;
|
||||
}[];
|
||||
// Enhanced split data
|
||||
operating_cash: string;
|
||||
reserve_cash: string;
|
||||
operating_investments: string;
|
||||
reserve_investments: string;
|
||||
est_monthly_interest: string;
|
||||
interest_earned_ytd: string;
|
||||
planned_capital_spend: string;
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
@@ -34,12 +43,8 @@ export function DashboardPage() {
|
||||
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 opInv = parseFloat(data?.operating_investments || '0');
|
||||
const resInv = parseFloat(data?.reserve_investments || '0');
|
||||
|
||||
const entryTypeColors: Record<string, string> = {
|
||||
manual: 'gray', assessment: 'blue', payment: 'green', late_fee: 'red',
|
||||
@@ -67,23 +72,52 @@ export function DashboardPage() {
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
))}
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>Operating Fund</Text>
|
||||
<Text fw={700} size="xl">{fmt(data?.operating_cash || '0')}</Text>
|
||||
{opInv > 0 && <Text size="xs" c="teal">Investments: {fmt(opInv)}</Text>}
|
||||
</div>
|
||||
<ThemeIcon color="green" variant="light" size={48} radius="md">
|
||||
<IconCash size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>Reserve Fund</Text>
|
||||
<Text fw={700} size="xl">{fmt(data?.reserve_cash || '0')}</Text>
|
||||
{resInv > 0 && <Text size="xs" c="teal">Investments: {fmt(resInv)}</Text>}
|
||||
</div>
|
||||
<ThemeIcon color="violet" variant="light" size={48} radius="md">
|
||||
<IconShieldCheck size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>Total Receivables</Text>
|
||||
<Text fw={700} size="xl">{fmt(data?.total_receivables || '0')}</Text>
|
||||
</div>
|
||||
<ThemeIcon color="blue" variant="light" size={48} radius="md">
|
||||
<IconFileInvoice size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>Delinquent Accounts</Text>
|
||||
<Text fw={700} size="xl">{String(data?.delinquent_units || 0)}</Text>
|
||||
</div>
|
||||
<ThemeIcon color="orange" variant="light" size={48} radius="md">
|
||||
<IconAlertTriangle size={28} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
@@ -120,17 +154,31 @@ export function DashboardPage() {
|
||||
<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>
|
||||
<Text size="sm" c="dimmed">Operating Cash</Text>
|
||||
<Text size="sm" fw={500} c="green">{fmt(data?.operating_cash || '0')}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Reserve Cash</Text>
|
||||
<Text size="sm" fw={500} c="violet">{fmt(data?.reserve_cash || '0')}</Text>
|
||||
</Group>
|
||||
<Divider my={4} />
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Est. Monthly Interest</Text>
|
||||
<Text size="sm" fw={500} c="blue">{fmt(data?.est_monthly_interest || '0')}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Interest Earned YTD</Text>
|
||||
<Text size="sm" fw={500} c="teal">{fmt(data?.interest_earned_ytd || '0')}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">Planned Capital Spend</Text>
|
||||
<Text size="sm" fw={500} c="orange">{fmt(data?.planned_capital_spend || '0')}</Text>
|
||||
</Group>
|
||||
<Divider my={4} />
|
||||
<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'}>
|
||||
|
||||
Reference in New Issue
Block a user