| import { useState, useEffect, memo } from 'react'; |
| import { useAuth } from '../context/AuthContext'; |
| import { useNavigate } from 'react-router-dom'; |
| import api from '../api/axios'; |
| import { |
| LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, |
| PieChart, Pie, Cell, Legend, BarChart, Bar, AreaChart, Area, ReferenceLine |
| } from 'recharts'; |
| import { TrendingUp, TrendingDown, RefreshCw, DollarSign, Activity, Settings, X, Check, FileText, Download, Globe, Target, Mail, ChevronDown, ArrowDown, ArrowUp, Shield, Zap, ShoppingCart, ArrowLeft } from 'lucide-react'; |
| import { useSettings } from '../context/SettingsContext'; |
| import ForecastChart from '../components/ForecastChart'; |
| import AnomalyFeed from '../components/AnomalyFeed'; |
| import AuroraLayout from '../components/AuroraLayout'; |
|
|
| const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#3b82f6']; |
|
|
| const FinancialHealthMetrics = memo(({ summary, currencySymbol }) => ( |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '1.5rem' }}> |
| <div className="stat-card" style={{ |
| background: 'rgba(99, 102, 241, 0.1)', |
| border: '1px solid rgba(99, 102, 241, 0.2)', |
| borderRadius: '50px', |
| padding: '0.75rem 1rem', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| boxShadow: '0 4px 15px rgba(99, 102, 241, 0.1)' |
| }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> |
| <div style={{ background: 'rgba(99, 102, 241, 0.2)', padding: '0.5rem', borderRadius: '50%' }}><Activity size={18} color="#6366f1" /></div> |
| <div> |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Financial Health</div> |
| <div style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#fff' }}>{summary?.health_score || 0}%</div> |
| </div> |
| </div> |
| </div> |
| |
| <div className="stat-card" style={{ |
| background: 'rgba(236, 72, 153, 0.1)', |
| border: '1px solid rgba(236, 72, 153, 0.2)', |
| borderRadius: '50px', |
| padding: '0.75rem 1rem', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| boxShadow: '0 4px 15px rgba(236, 72, 153, 0.1)' |
| }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> |
| <div style={{ background: 'rgba(236, 72, 153, 0.2)', padding: '0.5rem', borderRadius: '50%' }}><TrendingUp size={18} color="#ec4899" /></div> |
| <div> |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Savings Rate</div> |
| <div style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#fff' }}>{summary?.savings_rate || 0}%</div> |
| </div> |
| </div> |
| </div> |
| |
| <div className="stat-card" style={{ |
| background: 'rgba(16, 185, 129, 0.1)', |
| border: '1px solid rgba(16, 185, 129, 0.2)', |
| borderRadius: '50px', |
| padding: '0.75rem 1rem', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| boxShadow: '0 4px 15px rgba(16, 185, 129, 0.1)' |
| }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> |
| <div style={{ background: 'rgba(16, 185, 129, 0.2)', padding: '0.5rem', borderRadius: '50%' }}><Shield size={18} color="#10b981" /></div> |
| <div> |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Budget Control</div> |
| <div style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#fff' }}>{summary?.expense_control || 0}%</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| )); |
|
|
| const MonthlyTrendsChart = memo(({ summary, currencySymbol }) => ( |
| <div className="glass-panel" style={{ padding: '1rem', minHeight: '350px' }}> |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}> |
| <h3 style={{ margin: 0, fontSize: '1.1rem' }}>Monthly Trends</h3> |
| <div style={{ background: 'rgba(255,255,255,0.05)', padding: '0.25rem 0.75rem', borderRadius: '20px', fontSize: '0.8rem', color: 'var(--text-muted)' }}> |
| All Time |
| </div> |
| </div> |
| <div style={{ height: '350px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={(summary?.monthly_trends || []).filter(t => (t.income || 0) !== 0 || (t.expense || 0) !== 0)}> |
| <defs> |
| <linearGradient id="colorIncome" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#10b981" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#10b981" stopOpacity={0} /> |
| </linearGradient> |
| <linearGradient id="colorExpense" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#ef4444" stopOpacity={0.3} /> |
| <stop offset="95%" stopColor="#ef4444" stopOpacity={0} /> |
| </linearGradient> |
| </defs> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="month" stroke="var(--text-muted)" fontSize={12} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| formatter={(value, name) => [`${currencySymbol}${value.toLocaleString()}`, name]} |
| /> |
| <Legend /> |
| <Area type="monotone" dataKey="income" name="Income" stroke="#10b981" fillOpacity={1} fill="url(#colorIncome)" strokeWidth={2} /> |
| <Area type="monotone" dataKey="expense" name="Expense" stroke="#ef4444" fillOpacity={1} fill="url(#colorExpense)" strokeWidth={2} /> |
| <Line type="monotone" dataKey="net" name="Net Balance" stroke="#6366f1" strokeWidth={2} dot={{ fill: '#6366f1' }} /> |
| </AreaChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| )); |
|
|
| const SavingsRateTrendChart = memo(({ summary, currencySymbol }) => ( |
| <div className="glass-panel" style={{ padding: '1rem', minHeight: '350px' }}> |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}> |
| <h3 style={{ margin: 0, fontSize: '1.1rem' }}>Savings Trend</h3> |
| <div style={{ background: 'rgba(255,255,255,0.05)', padding: '0.25rem 0.75rem', borderRadius: '20px', fontSize: '0.8rem', color: 'var(--text-muted)' }}> |
| All Time |
| </div> |
| </div> |
| <div style={{ height: '350px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <LineChart data={(summary?.monthly_trends || []).map(item => ({ |
| month: item.month, |
| savings: item.income - item.expense, |
| income: item.income, |
| expense: item.expense |
| })).filter(t => (t.income || 0) !== 0 || (t.expense || 0) !== 0)}> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="month" stroke="var(--text-muted)" fontSize={12} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| formatter={(value, name) => [`${currencySymbol}${value.toLocaleString()}`, name]} |
| /> |
| <Legend /> |
| <ReferenceLine y={0} stroke="#64748b" strokeDasharray="3 3" /> |
| <Line type="monotone" dataKey="savings" name="Savings" stroke="#10b981" strokeWidth={3} dot={{ fill: '#10b981', r: 4 }} /> |
| </LineChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| )); |
|
|
| const ExpenseBreakdownChart = memo(({ summary, currencySymbol, colors }) => ( |
| <div className="glass-panel" style={{ padding: '1.5rem' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.75rem', marginBottom: '1.5rem', fontSize: '1.2rem' }}>Expense Breakdown</h3> |
| <div style={{ height: '400px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <PieChart> |
| <Pie |
| data={summary?.expense_breakdown || []} |
| dataKey="value" |
| nameKey="name" |
| cx="50%" |
| cy="50%" |
| outerRadius={110} |
| innerRadius={60} |
| label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(1)}%`} |
| > |
| {(summary?.expense_breakdown || []).map((entry, index) => ( |
| <Cell key={`cell-${index}`} fill={colors[index % colors.length]} /> |
| ))} |
| </Pie> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: '#fff' }} |
| formatter={(value, name, props) => [`${currencySymbol}${value.toLocaleString()}`, props.payload.name]} |
| /> |
| <Legend /> |
| </PieChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| )); |
|
|
| const IncomeBreakdownChart = memo(({ summary, currencySymbol, colors }) => ( |
| <div className="glass-panel" style={{ padding: '1.5rem' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.75rem', marginBottom: '1.5rem', fontSize: '1.2rem' }}>Income Breakdown</h3> |
| <div style={{ height: '400px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <PieChart> |
| <Pie |
| data={summary?.income_sources || []} |
| dataKey="value" |
| nameKey="name" |
| cx="50%" |
| cy="50%" |
| outerRadius={110} |
| innerRadius={60} |
| label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(1)}%`} |
| > |
| {(summary?.income_sources || []).map((entry, index) => ( |
| <Cell key={`income-cell-${index}`} fill={colors[index % colors.length]} /> |
| ))} |
| </Pie> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: '#fff' }} |
| formatter={(value, name, props) => [`${currencySymbol}${value.toLocaleString()}`, props.payload.name]} |
| /> |
| <Legend /> |
| </PieChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| )); |
|
|
| const DailyIncomeTrendChart = memo(({ summary, currencySymbol, colors }) => { |
| const allDailyCats = new Set(); |
| (summary?.daily_income_trend || []).forEach(day => { |
| Object.keys(day).forEach(k => { |
| if (k !== 'date' && k !== 'total_amount' && k !== 'moving_avg_30d') allDailyCats.add(k); |
| }); |
| }); |
|
|
| return ( |
| <div className="glass-panel" style={{ padding: '1rem', gridColumn: 'span 12' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.5rem', marginBottom: '1rem' }}> |
| Daily Income Trend (All-Time History) |
| </h3> |
| <div style={{ height: '350px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart data={(summary?.daily_income_trend || []).filter(d => (d.total_amount || 0) > 0)}> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="date" stroke="var(--text-muted)" fontSize={10} interval={Math.max(6, Math.floor((summary?.daily_income_trend?.length || 0) / 10))} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| cursor={{ fill: 'rgba(255,255,255,0.05)' }} |
| formatter={(value, name) => { |
| if (name === 'moving_avg_30d') return [`${currencySymbol}${value}`, "30-Day Moving Avg"]; |
| if (name === 'total_amount') return [null, null]; |
| return [`${currencySymbol}${value}`, name]; |
| }} |
| labelStyle={{ color: 'var(--primary-color)', fontWeight: 'bold' }} |
| filterNull={true} |
| /> |
| <Legend /> |
| {Array.from(allDailyCats).map((cat, idx) => ( |
| <Bar |
| key={cat} |
| dataKey={cat} |
| name={cat} |
| stackId="day" |
| fill={colors[idx % colors.length]} |
| radius={[0, 0, 0, 0]} |
| /> |
| ))} |
| <Line |
| type="monotone" |
| dataKey="moving_avg_30d" |
| name="30-Day Avg" |
| stroke="#6366f1" |
| strokeWidth={2} |
| strokeDasharray="5 5" |
| dot={false} |
| /> |
| <ReferenceLine |
| y={summary?.lifetime_daily_income_average || 0} |
| stroke="#fbbf24" |
| strokeWidth={2} |
| strokeDasharray="3 3" |
| label={{ value: `Avg: ${currencySymbol}${Math.round(summary?.lifetime_daily_income_average || 0)}`, position: 'insideTopRight', fill: '#fbbf24', fontSize: 12, fontWeight: 'bold' }} |
| /> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| ); |
| }); |
|
|
| const IncomeCategoryTrendsChart = memo(({ summary, currencySymbol, colors }) => { |
| const allCats = new Set(); |
| (summary?.income_category_trends || []).forEach(item => { |
| Object.keys(item).forEach(k => { |
| if (k !== 'month' && k !== 'total') allCats.add(k); |
| }); |
| }); |
|
|
| return ( |
| <div className="glass-panel" style={{ padding: '1rem', marginBottom: '1.5rem' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.75rem', marginBottom: '1.5rem', fontSize: '1.2rem' }}> |
| Income Category Trends (All Time) |
| </h3> |
| <div style={{ height: '450px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart data={(summary?.income_category_trends || []).filter(item => { |
| const entries = Object.entries(item); |
| return entries.some(([key, val]) => key !== 'month' && key !== 'total' && val !== 0); |
| })}> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="month" stroke="var(--text-muted)" fontSize={12} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| formatter={(value, name) => [`${currencySymbol}${value}`, name]} |
| /> |
| <Legend /> |
| {Array.from(allCats).map((cat, idx) => ( |
| <Bar |
| key={cat} |
| dataKey={cat} |
| stackId="a" |
| fill={colors[idx % colors.length]} |
| /> |
| ))} |
| <Line type="monotone" dataKey="total" name="Total Income" stroke="#10b981" strokeWidth={3} dot={{ fill: '#10b981', r: 4 }} /> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| ); |
| }); |
|
|
| const CategoryTrendsChart = memo(({ summary, currencySymbol, colors }) => { |
| const allCats = new Set(); |
| (summary?.category_trends || []).forEach(item => { |
| Object.keys(item).forEach(k => { |
| if (k !== 'month') allCats.add(k); |
| }); |
| }); |
|
|
| return ( |
| <div className="glass-panel" style={{ padding: '1rem', marginBottom: '1.5rem' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.5rem', marginBottom: '1rem' }}> |
| Category Spending Trends (All Time) |
| </h3> |
| <div style={{ height: '450px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart data={(summary?.category_trends || []).filter(item => { |
| const entries = Object.entries(item); |
| return entries.some(([key, val]) => key !== 'month' && val !== 0); |
| })}> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="month" stroke="var(--text-muted)" fontSize={12} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| formatter={(value, name) => [`${currencySymbol}${value}`, name]} |
| /> |
| <Legend /> |
| {Array.from(allCats).map((cat, idx) => ( |
| <Bar |
| key={cat} |
| dataKey={cat} |
| stackId="a" |
| fill={colors[idx % colors.length]} |
| /> |
| ))} |
| <Line type="monotone" dataKey="total" name="Total Expense" stroke="#10b981" strokeWidth={3} dot={{ fill: '#10b981', r: 4 }} /> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| ); |
| }); |
|
|
| const DailySpendingTrendChart = memo(({ summary, currencySymbol, colors }) => { |
| const allDailyCats = new Set(); |
| (summary?.daily_spending_trend || []).forEach(day => { |
| Object.keys(day).forEach(k => { |
| if (k !== 'date' && k !== 'total_amount' && k !== 'moving_avg_30d') allDailyCats.add(k); |
| }); |
| }); |
|
|
| return ( |
| <div className="glass-panel" style={{ padding: '1rem', gridColumn: 'span 12' }}> |
| <h3 style={{ borderBottom: '1px solid var(--glass-border)', paddingBottom: '0.5rem', marginBottom: '1rem' }}> |
| Daily Spending Trend (All-Time History) |
| </h3> |
| <div style={{ height: '350px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart data={(summary?.daily_spending_trend || []).filter(d => (d.total_amount || 0) > 0)}> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" /> |
| <XAxis dataKey="date" stroke="var(--text-muted)" fontSize={10} interval={Math.max(6, Math.floor((summary?.daily_spending_trend?.length || 0) / 10))} /> |
| <YAxis stroke="var(--text-muted)" fontSize={12} /> |
| <Tooltip |
| contentStyle={{ backgroundColor: 'var(--bg-card)', border: '1px solid var(--glass-border)', borderRadius: '0.5rem' }} |
| itemStyle={{ color: 'white' }} |
| cursor={{ fill: 'rgba(255,255,255,0.05)' }} |
| formatter={(value, name) => { |
| if (name === 'moving_avg_30d') return [`${currencySymbol}${value}`, "30-Day Moving Avg"]; |
| if (name === 'total_amount') return [null, null]; |
| return [`${currencySymbol}${value}`, name]; |
| }} |
| labelStyle={{ color: 'var(--primary-color)', fontWeight: 'bold' }} |
| filterNull={true} |
| /> |
| <Legend /> |
| {Array.from(allDailyCats).map((cat, idx) => ( |
| <Bar |
| key={cat} |
| dataKey={cat} |
| name={cat} |
| stackId="day" |
| fill={colors[idx % colors.length]} |
| radius={[0, 0, 0, 0]} |
| /> |
| ))} |
| <Line |
| type="monotone" |
| dataKey="moving_avg_30d" |
| name="30-Day Avg" |
| stroke="#ec4899" |
| strokeWidth={3} |
| dot={false} |
| animationDuration={1500} |
| /> |
| {summary?.lifetime_daily_average && ( |
| <ReferenceLine |
| y={summary.lifetime_daily_average} |
| label={{ |
| value: `Avg: ${currencySymbol}${summary.lifetime_daily_average}`, |
| position: 'insideTopRight', |
| fill: '#F59E0B', |
| fontSize: 13, |
| fontWeight: 'bold' |
| }} |
| stroke="#F59E0B" |
| strokeWidth={3} |
| strokeDasharray="3 3" |
| /> |
| )} |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| ); |
| }); |
|
|
| const FinancialInsightsWidget = memo(({ summary, currencySymbol }) => { |
| const totalExpense = summary?.total_expense || 0; |
| const totalIncome = summary?.total_income || 0; |
| const recentTransactions = summary?.recent_transactions || []; |
|
|
| const avgTransaction = recentTransactions.length > 0 |
| ? (recentTransactions.reduce((sum, t) => sum + parseFloat(t.amount), 0) / recentTransactions.length).toFixed(2) |
| : 0; |
|
|
| const largestExpense = summary?.expense_breakdown?.[0] || { name: 'N/A', value: 0 }; |
| const largestIncome = summary?.income_sources?.[0] || { name: 'N/A', value: 0 }; |
|
|
| const spendingRatio = totalIncome > 0 ? ((totalExpense / totalIncome) * 100).toFixed(1) : 0; |
| const savingsRate = totalIncome > 0 ? (((totalIncome - totalExpense) / totalIncome) * 100).toFixed(1) : 0; |
|
|
| const insights = [ |
| { |
| icon: <Activity size={20} />, |
| label: 'Burn Rate (Monthly)', |
| value: `${currencySymbol}${summary?.burn_rate || 0}`, |
| subValue: 'Last 90d avg', |
| color: '#ef4444', |
| bgColor: 'rgba(239, 68, 68, 0.1)' |
| }, |
| { |
| icon: <Target size={20} />, |
| label: 'Financial Runway', |
| value: `${summary?.runway_months || 0} Months`, |
| subValue: `~${summary?.runway_days || 0} Days remaining`, |
| color: (summary?.runway_months || 0) > 3 ? '#10b981' : (summary?.runway_months || 0) > 1 ? '#f59e0b' : '#ef4444', |
| bgColor: (summary?.runway_months || 0) > 3 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)' |
| }, |
| { |
| icon: <ShoppingCart size={20} />, |
| label: 'Projected Spend', |
| value: `${currencySymbol}${summary?.momentum?.projected_spending || 0}`, |
| subValue: 'Estimated end of month', |
| color: (summary?.momentum?.projected_spending || 0) > (summary?.total_income || 0) ? '#ef4444' : '#6366f1', |
| bgColor: 'rgba(99, 102, 241, 0.1)' |
| }, |
| { |
| icon: <TrendingUp size={20} />, |
| label: 'Top Category Surge', |
| value: summary?.momentum?.spiking_category || 'N/A', |
| subValue: 'Highest MoM increase', |
| color: '#f59e0b', |
| bgColor: 'rgba(245, 158, 11, 0.1)' |
| }, |
| { |
| icon: <TrendingDown size={20} />, |
| label: 'Spending Momentum', |
| value: `${summary?.momentum?.spending > 0 ? '+' : ''}${summary?.momentum?.spending || 0}%`, |
| subValue: 'Change vs last month', |
| color: (summary?.momentum?.spending || 0) > 0 ? '#ef4444' : '#10b981', |
| bgColor: (summary?.momentum?.spending || 0) > 0 ? 'rgba(239, 68, 68, 0.1)' : 'rgba(16, 185, 129, 0.1)' |
| }, |
| { |
| icon: <TrendingUp size={20} />, |
| label: 'Income Momentum', |
| value: `${summary?.momentum?.income > 0 ? '+' : ''}${summary?.momentum?.income || 0}%`, |
| subValue: 'Change vs last month', |
| color: (summary?.momentum?.income || 0) >= 0 ? '#10b981' : '#ef4444', |
| bgColor: (summary?.momentum?.income || 0) >= 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)' |
| }, |
| { |
| icon: <Shield size={20} />, |
| label: 'Savings Velocity', |
| value: `${summary?.momentum?.savings > 0 ? '+' : ''}${summary?.momentum?.savings || 0}%`, |
| subValue: 'Savings rate shift', |
| color: (summary?.momentum?.savings || 0) >= 0 ? '#10b981' : '#ef4444', |
| bgColor: (summary?.momentum?.savings || 0) >= 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)' |
| }, |
| { |
| icon: <Zap size={20} />, |
| label: 'Volatility Score', |
| value: `${summary?.volatility_score || 0}%`, |
| subValue: 'Spending stability', |
| color: (summary?.volatility_score || 0) < 50 ? '#10b981' : '#f59e0b', |
| bgColor: 'rgba(16, 185, 129, 0.1)' |
| }, |
| { |
| icon: <RefreshCw size={20} />, |
| label: 'Recurring Subs', |
| value: summary?.recurring_count || 0, |
| subValue: 'Detected patterns', |
| color: '#6366f1', |
| bgColor: 'rgba(99, 102, 241, 0.1)' |
| }, |
| { |
| icon: <Activity size={20} />, |
| label: 'Current Txns', |
| value: summary?.monthly_txn_count || 0, |
| subValue: 'Count this month', |
| color: '#8b5cf6', |
| bgColor: 'rgba(139, 92, 246, 0.1)' |
| } |
| ]; |
|
|
| return ( |
| <div className="glass-panel" style={{ padding: '1.5rem', marginBottom: '2rem' }}> |
| <h3 style={{ margin: '0 0 1.5rem 0', fontSize: '1.2rem', display: 'flex', alignItems: 'center', gap: '0.75rem' }}> |
| <Activity size={24} className="text-gradient" /> |
| Real-Time Financial Insights |
| </h3> |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' }}> |
| {insights.map((insight, idx) => ( |
| <div key={idx} style={{ |
| padding: '1.25rem', |
| background: 'rgba(30, 41, 59, 0.4)', |
| borderRadius: '1rem', |
| border: '1px solid rgba(255,255,255,0.05)', |
| transition: 'all 0.3s' |
| }} className="hover-lift"> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.75rem' }}> |
| <div style={{ |
| width: '40px', |
| height: '40px', |
| borderRadius: '10px', |
| background: insight.bgColor, |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| color: insight.color |
| }}> |
| {insight.icon} |
| </div> |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', fontWeight: '500' }}> |
| {insight.label} |
| </div> |
| </div> |
| <div style={{ fontSize: '1.5rem', fontWeight: '700', color: insight.color, marginBottom: '0.25rem' }}> |
| {insight.value} |
| </div> |
| {insight.subValue && ( |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}> |
| {insight.subValue} |
| </div> |
| )} |
| </div> |
| ))} |
| </div> |
| </div> |
| ); |
| }); |
|
|
| const AnalyticsPage = () => { |
| const { user } = useAuth(); |
| const navigate = useNavigate(); |
| const { currencySymbol } = useSettings(); |
| const [summary, setSummary] = useState(null); |
| const [loading, setLoading] = useState(true); |
|
|
| const fetchData = async () => { |
| try { |
| const summaryRes = await api.get('finance/dashboard-summary/'); |
| setSummary(summaryRes.data); |
| setLoading(false); |
| } catch (err) { |
| setLoading(false); |
| } |
| }; |
|
|
| useEffect(() => { |
| fetchData(); |
| }, []); |
|
|
| return ( |
| <AuroraLayout> |
| <div className="container" style={{ paddingBottom: '4rem' }}> |
| <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}> |
| <div> |
| <h1 style={{ marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem' }}> |
| <TrendingUp className="text-gradient" size={32} /> |
| <span className="text-gradient">Financial Analytics</span> |
| </h1> |
| <p style={{ color: 'var(--text-muted)', margin: 0 }}>Comprehensive visualization of your economic trends</p> |
| </div> |
| <button onClick={() => navigate('/dashboard')} className="btn-primary" style={{ |
| background: 'linear-gradient(135deg, #0891b2 0%, #0ea5e9 40%, #38bdf8 100%)', |
| border: 'none', color: 'white', display: 'flex', alignItems: 'center', gap: '0.5rem', |
| padding: '0.5rem 1.1rem', borderRadius: '8px', fontWeight: 600, |
| boxShadow: '0 4px 14px rgba(8,145,178,0.4)' |
| }}> |
| <ArrowLeft size={18} /> Dashboard |
| </button> |
| </header> |
| |
| {loading ? ( |
| <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}> |
| <div style={{ textAlign: 'center' }}> |
| <div className="spinner" style={{ margin: '0 auto 1rem' }}></div> |
| <p style={{ color: 'var(--text-muted)' }}>Analyzing financial data...</p> |
| </div> |
| </div> |
| ) : ( |
| <> |
| <FinancialInsightsWidget summary={summary} currencySymbol={currencySymbol} /> |
| <FinancialHealthMetrics summary={summary} currencySymbol={currencySymbol} /> |
| |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: '1rem', marginBottom: '1.5rem' }}> |
| <div style={{ gridColumn: 'span 8', display: 'flex', flexDirection: 'column', gap: '1rem' }}> |
| <ForecastChart /> |
| <MonthlyTrendsChart summary={summary} currencySymbol={currencySymbol} /> |
| </div> |
| <div style={{ gridColumn: 'span 4', display: 'flex', flexDirection: 'column', gap: '1rem' }}> |
| <AnomalyFeed /> |
| <SavingsRateTrendChart summary={summary} currencySymbol={currencySymbol} /> |
| </div> |
| </div> |
| |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1rem', marginBottom: '1.5rem' }}> |
| <ExpenseBreakdownChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| <IncomeBreakdownChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| </div> |
| |
| <CategoryTrendsChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| <IncomeCategoryTrendsChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '1rem', marginBottom: '1.5rem' }}> |
| <DailySpendingTrendChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| <DailyIncomeTrendChart summary={summary} currencySymbol={currencySymbol} colors={COLORS} /> |
| </div> |
| </> |
| )} |
| |
| </div> |
| </AuroraLayout> |
| ); |
| }; |
|
|
| export default AnalyticsPage; |
|
|