import { useState, useMemo } from 'react'; import { Title, Text, Stack, Card, Group, SimpleGrid, ThemeIcon, SegmentedControl, Loader, Center, ActionIcon, Tooltip, Badge, } from '@mantine/core'; import { IconCash, IconBuildingBank, IconChartAreaLine, IconArrowLeft, IconArrowRight, IconCalendar, } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { usePreferencesStore } from '../../stores/preferencesStore'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, ReferenceLine, } from 'recharts'; import api from '../../services/api'; interface Datapoint { month: string; year: number; monthNum: number; is_forecast: boolean; operating_cash: number; operating_investments: number; reserve_cash: number; reserve_investments: number; } interface ForecastData { start_year: number; months: number; current_month: string; datapoints: Datapoint[]; } const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); const fmtAxis = (v: number) => { if (Math.abs(v) >= 1000000) return `$${(v / 1000000).toFixed(1)}M`; if (Math.abs(v) >= 1000) return `$${(v / 1000).toFixed(0)}K`; return `$${v}`; }; // Custom tooltip component function CustomTooltip({ active, payload, label }: any) { if (!active || !payload?.length) return null; const dp = payload[0]?.payload as Datapoint; const isForecast = dp?.is_forecast; return ( {label} {isForecast && ( Forecast )} {payload.map((entry: any) => (
{entry.name} {fmt(entry.value)} ))} Total {fmt(payload.reduce((s: number, p: any) => s + p.value, 0))} ); } export function CashFlowForecastPage() { const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth() + 1; const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark'; // Filter: All, Operating, Reserve const [fundFilter, setFundFilter] = useState('all'); // View window: which 12-month period to show const [viewStartIndex, setViewStartIndex] = useState(0); // Fetch 36 months of data starting from current fiscal year (Jan) const { data, isLoading } = useQuery({ queryKey: ['cash-flow-forecast', currentYear], queryFn: async () => { const { data } = await api.get( `/reports/cash-flow-forecast?startYear=${currentYear}&months=36`, ); return data; }, }); const datapoints = data?.datapoints || []; // Determine available view windows (12-month slices) const maxStartIndex = Math.max(0, datapoints.length - 12); // Compute the chart data based on the current view window const viewData = useMemo(() => { return datapoints.slice(viewStartIndex, viewStartIndex + 12); }, [datapoints, viewStartIndex]); // Compute summary stats for the current view const summaryStats = useMemo(() => { if (!viewData.length) return null; const last = viewData[viewData.length - 1]; const first = viewData[0]; const totalOperating = last.operating_cash + last.operating_investments; const totalReserve = last.reserve_cash + last.reserve_investments; const totalAll = totalOperating + totalReserve; const firstTotal = first.operating_cash + first.operating_investments + first.reserve_cash + first.reserve_investments; const netChange = totalAll - firstTotal; return { totalOperating, totalReserve, totalAll, netChange, periodStart: first.month, periodEnd: last.month, }; }, [viewData]); // Determine the first forecast month index within the view const forecastStartLabel = useMemo(() => { const idx = viewData.findIndex((d) => d.is_forecast); return idx >= 0 ? viewData[idx].month : null; }, [viewData]); // Transform data for chart based on filter const chartData = useMemo(() => { return viewData.map((d) => { const base: any = { month: d.month, is_forecast: d.is_forecast }; if (fundFilter === 'all' || fundFilter === 'operating') { base['Operating Cash'] = d.operating_cash; base['Operating Investments'] = d.operating_investments; } if (fundFilter === 'all' || fundFilter === 'reserve') { base['Reserve Cash'] = d.reserve_cash; base['Reserve Investments'] = d.reserve_investments; } return base; }); }, [viewData, fundFilter]); // Determine view period label const viewLabel = viewData.length > 0 ? `${viewData[0].month} - ${viewData[viewData.length - 1].month}` : ''; if (isLoading) return
; return (
Cash Flow Cash and investment balances over time with forward projections
{/* Summary Cards */} {summaryStats && ( Operating Total {fmt(summaryStats.totalOperating)} Reserve Total {fmt(summaryStats.totalReserve)} Combined Total {fmt(summaryStats.totalAll)} = 0 ? 'green' : 'red'} size="sm" > Period Change = 0 ? 'green' : 'red'} > {fmt(summaryStats.netChange)} )} {/* Chart Navigation */} setViewStartIndex(Math.max(0, viewStartIndex - 12))} > {viewLabel} = maxStartIndex} onClick={() => setViewStartIndex(Math.min(maxStartIndex, viewStartIndex + 12))} > {forecastStartLabel && ( Forecast begins {forecastStartLabel} )} {viewData.filter((d) => !d.is_forecast).length} actual {viewData.filter((d) => d.is_forecast).length} projected {/* Stacked Area Chart */} } /> ( {value} )} /> {/* Draw a reference line where forecast begins */} {forecastStartLabel && ( )} {(fundFilter === 'all' || fundFilter === 'operating') && ( <> )} {(fundFilter === 'all' || fundFilter === 'reserve') && ( <> )} {/* Data Table */} Monthly Detail
{(fundFilter === 'all' || fundFilter === 'operating') && ( <> )} {(fundFilter === 'all' || fundFilter === 'reserve') && ( <> )} {viewData.map((d, i) => { const opTotal = d.operating_cash + d.operating_investments; const resTotal = d.reserve_cash + d.reserve_investments; const grandTotal = (fundFilter === 'all' ? opTotal + resTotal : fundFilter === 'operating' ? opTotal : resTotal); return ( {(fundFilter === 'all' || fundFilter === 'operating') && ( <> )} {(fundFilter === 'all' || fundFilter === 'reserve') && ( <> )} ); })}
Month TypeOp. Cash Op. Investments Op. TotalRes. Cash Res. Investments Res. TotalGrand Total
{d.month} {d.is_forecast ? 'Forecast' : 'Actual'} {fmt(d.operating_cash)} {fmt(d.operating_investments)} {fmt(opTotal)} {fmt(d.reserve_cash)} {fmt(d.reserve_investments)} {fmt(resTotal)} {fmt(grandTotal)}
); }