import { useState, useEffect, useCallback } from 'react'; import { Title, Text, Stack, Card, SimpleGrid, Group, Button, Table, Badge, Loader, Center, Alert, ThemeIcon, Divider, Accordion, Paper, Tabs, Collapse, ActionIcon, } from '@mantine/core'; import { IconBulb, IconCash, IconBuildingBank, IconChartAreaLine, IconAlertTriangle, IconSparkles, IconRefresh, IconCoin, IconPigMoney, IconChevronDown, IconChevronUp, } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { notifications } from '@mantine/notifications'; import api from '../../services/api'; // ── Types ── interface FinancialSummary { operating_cash: number; reserve_cash: number; operating_investments: number; reserve_investments: number; total_operating: number; total_reserve: number; total_all: number; } interface FinancialSnapshot { summary: FinancialSummary; investment_accounts: Array<{ id: string; name: string; institution: string; investment_type: string; fund_type: string; principal: string; interest_rate: string; maturity_date: string | null; current_value: string; }>; } interface MarketRate { bank_name: string; apy: string; min_deposit: string | null; term: string; term_months: number | null; rate_type: string; fetched_at: string; } interface MarketRatesResponse { cd: MarketRate[]; money_market: MarketRate[]; high_yield_savings: MarketRate[]; } interface Recommendation { type: string; priority: 'high' | 'medium' | 'low'; title: string; summary: string; details: string; fund_type: string; suggested_amount?: number; suggested_term?: string; suggested_rate?: number; bank_name?: string; rationale: string; } interface AIResponse { recommendations: Recommendation[]; overall_assessment: string; risk_notes: string[]; } interface SavedRecommendation { id: string; recommendations: Recommendation[]; overall_assessment: string; risk_notes: string[]; response_time_ms: number; created_at: string; status: 'processing' | 'complete' | 'error'; last_failed: boolean; error_message?: string; } // ── Helpers ── const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); const priorityColors: Record = { high: 'red', medium: 'yellow', low: 'blue', }; const typeIcons: Record = { cd_ladder: IconChartAreaLine, new_investment: IconBuildingBank, reallocation: IconRefresh, maturity_action: IconCash, liquidity_warning: IconAlertTriangle, general: IconBulb, }; const typeLabels: Record = { cd_ladder: 'CD Ladder', new_investment: 'New Investment', reallocation: 'Reallocation', maturity_action: 'Maturity Action', liquidity_warning: 'Liquidity', general: 'General', }; // ── Rate Table Component ── function RateTable({ rates, showTerm }: { rates: MarketRate[]; showTerm: boolean }) { if (rates.length === 0) { return ( No rates available. Run the market rate fetcher to populate data. ); } return ( Bank APY {showTerm && Term} Min Deposit {rates.map((r, i) => ( {r.bank_name} {parseFloat(r.apy).toFixed(2)}% {showTerm && {r.term}} {r.min_deposit ? `$${parseFloat(r.min_deposit).toLocaleString()}` : '-'} ))}
); } // ── Recommendations Display Component ── function RecommendationsDisplay({ aiResult, lastUpdated, lastFailed, }: { aiResult: AIResponse; lastUpdated?: string; lastFailed?: boolean; }) { return ( {/* Last Updated timestamp + failure message */} {lastUpdated && ( Last updated: {new Date(lastUpdated).toLocaleString()} {lastFailed && ( last analysis failed — showing cached data )} )} {/* Overall Assessment */} {aiResult.overall_assessment} {/* Risk Notes */} {aiResult.risk_notes && aiResult.risk_notes.length > 0 && ( } > {aiResult.risk_notes.map((note, i) => ( {note} ))} )} {/* Recommendation Cards */} {aiResult.recommendations.length > 0 ? ( {aiResult.recommendations.map((rec, i) => { const Icon = typeIcons[rec.type] || IconBulb; return (
{rec.title} {rec.priority} {typeLabels[rec.type] || rec.type} {rec.fund_type} {rec.summary}
{rec.suggested_amount != null && ( {fmt(rec.suggested_amount)} )}
{rec.details} {(rec.suggested_term || rec.suggested_rate != null || rec.bank_name) && ( {rec.suggested_term && (
Suggested Term {rec.suggested_term}
)} {rec.suggested_rate != null && (
Target Rate {rec.suggested_rate}% APY
)} {rec.bank_name && (
Bank {rec.bank_name}
)}
)} {rec.rationale}
); })}
) : ( No specific recommendations at this time. )}
); } // ── Main Component ── export function InvestmentPlanningPage() { const [ratesExpanded, setRatesExpanded] = useState(true); const [isTriggering, setIsTriggering] = useState(false); // Load financial snapshot on mount const { data: snapshot, isLoading: snapshotLoading } = useQuery({ queryKey: ['investment-planning-snapshot'], queryFn: async () => { const { data } = await api.get('/investment-planning/snapshot'); return data; }, }); // Load market rates (all types) on mount const { data: marketRates, isLoading: ratesLoading } = useQuery({ queryKey: ['investment-planning-market-rates'], queryFn: async () => { const { data } = await api.get('/investment-planning/market-rates'); return data; }, }); // Load saved recommendation — polls every 3s when processing const { data: savedRec } = useQuery({ queryKey: ['investment-planning-saved-recommendation'], queryFn: async () => { const { data } = await api.get('/investment-planning/saved-recommendation'); return data; }, refetchInterval: (query) => { const rec = query.state.data; // Poll every 3 seconds while processing if (rec?.status === 'processing') return 3000; // Also poll if we just triggered (status may not be 'processing' yet) if (isTriggering) return 3000; return false; }, }); // Derive display state from saved recommendation const isProcessing = savedRec?.status === 'processing' || isTriggering; const lastFailed = savedRec?.last_failed || false; const hasResults = savedRec && savedRec.status === 'complete' && savedRec.recommendations.length > 0; const hasError = savedRec?.status === 'error' && !savedRec?.recommendations?.length; // Clear triggering flag once backend confirms processing or completes useEffect(() => { if (isTriggering && savedRec?.status === 'processing') { setIsTriggering(false); } if (isTriggering && savedRec?.status === 'complete') { setIsTriggering(false); } }, [savedRec?.status, isTriggering]); // Show notification when processing completes (transition from processing) const prevStatusRef = useState(null); useEffect(() => { const [prevStatus, setPrevStatus] = prevStatusRef; if (prevStatus === 'processing' && savedRec?.status === 'complete') { notifications.show({ message: `Generated ${savedRec.recommendations.length} investment recommendations`, color: 'green', }); } if (prevStatus === 'processing' && savedRec?.status === 'error') { notifications.show({ message: savedRec.error_message || 'AI recommendation analysis failed', color: 'red', }); } setPrevStatus(savedRec?.status || null); }, [savedRec?.status]); // eslint-disable-line react-hooks/exhaustive-deps // Trigger AI recommendations (async — returns immediately) const handleTriggerAI = useCallback(async () => { setIsTriggering(true); try { await api.post('/investment-planning/recommendations'); } catch (err: any) { setIsTriggering(false); notifications.show({ message: err.response?.data?.message || 'Failed to start AI analysis', color: 'red', }); } }, []); // Build AI result from saved recommendation for display const aiResult: AIResponse | null = hasResults ? { recommendations: savedRec!.recommendations, overall_assessment: savedRec!.overall_assessment, risk_notes: savedRec!.risk_notes, } : (lastFailed && savedRec?.recommendations?.length) ? { recommendations: savedRec!.recommendations, overall_assessment: savedRec!.overall_assessment, risk_notes: savedRec!.risk_notes, } : null; if (snapshotLoading) { return (
); } const s = snapshot?.summary; // Determine the latest fetched_at timestamp across all rate types const allRatesList = [ ...(marketRates?.cd || []), ...(marketRates?.money_market || []), ...(marketRates?.high_yield_savings || []), ]; const latestFetchedAt = allRatesList.length > 0 ? allRatesList.reduce((latest, r) => new Date(r.fetched_at) > new Date(latest.fetched_at) ? r : latest, ).fetched_at : null; const totalRateCount = (marketRates?.cd?.length || 0) + (marketRates?.money_market?.length || 0) + (marketRates?.high_yield_savings?.length || 0); return ( {/* Page Header */}
Investment Planning Account overview, market rates, and AI-powered investment recommendations
{/* ── Section 1: Financial Snapshot Cards ── */} {s && ( Operating Cash {fmt(s.operating_cash)} Investments: {fmt(s.operating_investments)} Reserve Cash {fmt(s.reserve_cash)} Investments: {fmt(s.reserve_investments)} Total All Funds {fmt(s.total_all)} Operating: {fmt(s.total_operating)} | Reserve: {fmt(s.total_reserve)} Total Invested {fmt(s.operating_investments + s.reserve_investments)} Earning interest across all accounts )} {/* ── Section 2: Current Investments Table ── */} {snapshot?.investment_accounts && snapshot.investment_accounts.length > 0 && ( Current Investments Name Institution Type Fund Principal Rate Maturity {snapshot.investment_accounts.map((inv) => ( {inv.name} {inv.institution || '-'} {inv.investment_type} {inv.fund_type} {fmt(parseFloat(inv.principal))} {parseFloat(inv.interest_rate || '0').toFixed(2)}% {inv.maturity_date ? new Date(inv.maturity_date).toLocaleDateString() : '-'} ))}
)} {/* ── Section 3: Today's Market Rates (Collapsible with Tabs) ── */} Today's Market Rates {totalRateCount > 0 && ( {totalRateCount} rates )} {latestFetchedAt && ( Last fetched: {new Date(latestFetchedAt).toLocaleString()} )} setRatesExpanded((v) => !v)} title={ratesExpanded ? 'Collapse rates' : 'Expand rates'} > {ratesExpanded ? : } {ratesLoading ? (
) : ( CDs {(marketRates?.cd?.length || 0) > 0 && ( {marketRates?.cd?.length} )} Money Market {(marketRates?.money_market?.length || 0) > 0 && ( {marketRates?.money_market?.length} )} High Yield Savings {(marketRates?.high_yield_savings?.length || 0) > 0 && ( {marketRates?.high_yield_savings?.length} )} )}
{/* ── Section 4: AI Investment Recommendations ── */}
AI Investment Recommendations Powered by AI analysis of your complete financial picture
{/* Processing State */} {isProcessing && (
Analyzing your financial data and market rates... You can navigate away — results will appear when ready
)} {/* Error State (no cached data) */} {hasError && !isProcessing && ( {savedRec?.error_message || 'The last AI analysis failed. Please try again.'} )} {/* Results (with optional failure watermark) */} {aiResult && !isProcessing && ( )} {/* Empty State */} {!aiResult && !isProcessing && !hasError && ( AI-Powered Investment Analysis Click "Get AI Recommendations" to analyze your accounts, cash flow, budget, and capital projects against current market rates. The AI will suggest specific investment moves to maximize interest income while maintaining adequate liquidity. )}
); }