Spaces:
Sleeping
Sleeping
| import { useState, useMemo } from 'react'; | |
| import { useTransactions, useRates, useWallets, useExchanges, useLoans } from '../../hooks/queries'; | |
| import { useAppStore } from '../../store/useAppStore'; | |
| import { | |
| startOfMonth, | |
| endOfMonth, | |
| eachDayOfInterval, | |
| format, | |
| subDays, | |
| isSameDay, | |
| startOfDay | |
| } from 'date-fns'; | |
| export function useAnalyticsStats() { | |
| const { data: transactions = [], isLoading: txLoading } = useTransactions(); | |
| const { data: wallets = [], isLoading: wlLoading } = useWallets(); | |
| const { data: exchanges = [], isLoading: exLoading } = useExchanges(); | |
| const { data: loans = [], isLoading: lnLoading } = useLoans(); | |
| const { data: rates, isLoading: ratesLoading } = useRates(); | |
| const mainCurrency = useAppStore(s => s.mainCurrency); | |
| // New State for Year/Month Selector | |
| const [selectedYear, setSelectedYear] = useState(() => new Date().getFullYear()); | |
| const [selectedMonth, setSelectedMonth] = useState<number | null>(() => new Date().getMonth() + 1); // 1-12 | |
| const exchangeRates = rates || { 'USD': 1, 'IQD': 1500, 'RMB': 7.2 }; | |
| const toMain = (amount: number, currency: string) => { | |
| const rate = exchangeRates[currency] || 1; | |
| return (amount / rate) * (exchangeRates[mainCurrency] || 1); | |
| }; | |
| const stats = useMemo(() => { | |
| // Filter interval based on selectedYear/Month | |
| let start: Date; | |
| let end: Date; | |
| if (selectedMonth !== null) { | |
| start = startOfMonth(new Date(selectedYear, selectedMonth - 1)); | |
| end = endOfMonth(new Date(selectedYear, selectedMonth - 1)); | |
| } else { | |
| start = new Date(selectedYear, 0, 1); | |
| end = new Date(selectedYear, 11, 31, 23, 59, 59); | |
| } | |
| let periodIncome = 0; | |
| let periodExpense = 0; | |
| let totalDeposits = 0; | |
| let totalWithdrawals = 0; | |
| const categories: Record<string, { total: number, count: number }> = {}; | |
| const incomeCategories: Record<string, { total: number, count: number }> = {}; | |
| const dailyData: Record<string, { date: string, income: number, expense: number, balance: number }> = {}; | |
| const monthlyData: Record<string, { month: string, income: number, expense: number, balance: number }> = {}; | |
| // Initialize daily data | |
| const days = eachDayOfInterval({ start, end }); | |
| days.forEach(day => { | |
| const dateStr = format(day, 'yyyy-MM-dd'); | |
| dailyData[dateStr] = { date: dateStr, income: 0, expense: 0, balance: 0 }; | |
| }); | |
| const filteredTxs = transactions.filter((t: any) => { | |
| const txDate = new Date(t.date); | |
| return txDate >= start && txDate <= end; | |
| }); | |
| filteredTxs.forEach((tx: any) => { | |
| const amountInMain = toMain(tx.amount, tx.currency); | |
| const dateStr = tx.date.split('T')[0]; | |
| const monthKey = dateStr.substring(0, 7); // YYYY-MM | |
| if (!monthlyData[monthKey]) { | |
| monthlyData[monthKey] = { month: monthKey, income: 0, expense: 0, balance: 0 }; | |
| } | |
| if (tx.type === 'income') { | |
| periodIncome += amountInMain; | |
| totalDeposits += amountInMain; | |
| if (dailyData[dateStr]) dailyData[dateStr].income += amountInMain; | |
| monthlyData[monthKey].income += amountInMain; | |
| const cat = tx.category || 'Other'; | |
| if (!incomeCategories[cat]) incomeCategories[cat] = { total: 0, count: 0 }; | |
| incomeCategories[cat].total += amountInMain; | |
| incomeCategories[cat].count += 1; | |
| } else if (tx.type === 'expense') { | |
| periodExpense += amountInMain; | |
| totalWithdrawals += amountInMain; | |
| if (dailyData[dateStr]) dailyData[dateStr].expense += amountInMain; | |
| monthlyData[monthKey].expense += amountInMain; | |
| const cat = tx.category || 'Other'; | |
| if (!categories[cat]) categories[cat] = { total: 0, count: 0 }; | |
| categories[cat].total += amountInMain; | |
| categories[cat].count += 1; | |
| } | |
| }); | |
| // 1. Advanced Loan Metrics (Global) | |
| let totalLent = 0; | |
| let totalBorrowed = 0; | |
| loans.forEach((l: any) => { | |
| const amountInMain = toMain(l.amount, l.currency); | |
| const paidInMain = toMain(l.paid || 0, l.currency); | |
| const remaining = amountInMain - paidInMain; | |
| if (l.type === 'borrowed_from_me') { | |
| totalLent += remaining; | |
| } else { | |
| totalBorrowed += remaining; | |
| } | |
| }); | |
| // 2. Spending Deltas (Today vs Yesterday) | |
| const today = startOfDay(new Date()); | |
| const yesterday = subDays(today, 1); | |
| const todaySpend = transactions | |
| .filter((t: any) => t.type === 'expense' && isSameDay(new Date(t.date), today)) | |
| .reduce((acc, t) => acc + toMain(t.amount, t.currency), 0); | |
| const yesterdaySpend = transactions | |
| .filter((t: any) => t.type === 'expense' && isSameDay(new Date(t.date), yesterday)) | |
| .reduce((acc, t) => acc + toMain(t.amount, t.currency), 0); | |
| const dailySpendChange = yesterdaySpend > 0 | |
| ? ((todaySpend - yesterdaySpend) / yesterdaySpend) * 100 | |
| : todaySpend > 0 ? 100 : 0; | |
| // 3. Asset & Net Worth Calculation | |
| const cBalances: Record<string, number> = { 'USD': 0, 'IQD': 0, 'RMB': 0 }; | |
| wallets.forEach((w: any) => { | |
| const txBal = transactions.reduce((acc: number, tx: any) => { | |
| let effectiveAmount = tx.amount; | |
| if (tx.currency !== w.currency) { | |
| const txRate = exchangeRates[tx.currency] || 1; | |
| const walletRate = exchangeRates[w.currency] || 1; | |
| effectiveAmount = (tx.amount / txRate) * walletRate; | |
| } | |
| if (tx.type === 'income' && tx.wallet_id === w.id) return acc + effectiveAmount; | |
| if (tx.type === 'expense' && tx.wallet_id === w.id) return acc - effectiveAmount; | |
| if (tx.type === 'transfer') { | |
| if (tx.wallet_id === w.id) return acc - effectiveAmount; | |
| if (tx.to_wallet_id === w.id) return acc + effectiveAmount; | |
| } | |
| return acc; | |
| }, 0); | |
| const exBal = exchanges.reduce((acc: number, ex: any) => { | |
| let bal = 0; | |
| if (ex.from_wallet_id === w.id) bal -= ex.from_amount; | |
| if (ex.to_wallet_id === w.id) bal += ex.to_amount; | |
| return acc + bal; | |
| }, 0); | |
| cBalances[w.currency] = (cBalances[w.currency] || 0) + (txBal + exBal); | |
| }); | |
| const liquidAssets = Object.entries(cBalances).reduce((acc, [curr, amt]) => { | |
| const rate = exchangeRates[curr] || 1; | |
| return acc + (amt / rate * (exchangeRates[mainCurrency] || 1)); | |
| }, 0); | |
| const netWorth = liquidAssets + totalLent - totalBorrowed; | |
| // Cumulative Net Worth Trend (Historical) | |
| const netCashFlowSinceStart = transactions.reduce((acc, tx) => { | |
| if (new Date(tx.date) < start) return acc; // Simplified: only trend within selected period | |
| const amt = toMain(tx.amount, tx.currency); | |
| if (tx.type === 'income') return acc + amt; | |
| if (tx.type === 'expense') return acc - amt; | |
| return acc; | |
| }, 0); | |
| const startingNetWorth = liquidAssets - netCashFlowSinceStart; | |
| let cumulative = startingNetWorth; | |
| const cumulativeHistory = Object.values(dailyData).map(d => { | |
| cumulative += d.income - d.expense; | |
| return { date: d.date, balance: cumulative }; | |
| }); | |
| // 4. Advanced Risk & Efficiency Metrics | |
| const numDays = days.length || 1; | |
| const avgDailySpend = periodExpense / numDays; | |
| // Sharpe Ratio Calculation (Daily Volatility of Balances) | |
| const dailyReturns = []; | |
| for (let i = 1; i < cumulativeHistory.length; i++) { | |
| const prev = cumulativeHistory[i-1].balance; | |
| const curr = cumulativeHistory[i].balance; | |
| if (prev !== 0) dailyReturns.push((curr - prev) / Math.abs(prev)); | |
| } | |
| const avgReturn = dailyReturns.length > 0 ? dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length : 0; | |
| const stdDev = dailyReturns.length > 0 ? Math.sqrt(dailyReturns.reduce((a, b) => a + Math.pow(b - avgReturn, 2), 0) / dailyReturns.length) : 0; | |
| const sharpeRatio = stdDev !== 0 ? (avgReturn / stdDev) * Math.sqrt(365) : 0; // Annualized | |
| // Max Drawdown | |
| let peak = -Infinity; | |
| let maxDrawdown = 0; | |
| cumulativeHistory.forEach(d => { | |
| if (d.balance > peak) peak = d.balance; | |
| const drawdown = peak !== 0 ? (peak - d.balance) / peak : 0; | |
| if (drawdown > maxDrawdown) maxDrawdown = drawdown; | |
| }); | |
| // Velocity of Money (Total Flow / Avg Balance) | |
| const avgBalance = cumulativeHistory.reduce((a, b) => a + b.balance, 0) / cumulativeHistory.length || 1; | |
| const velocity = (periodIncome + periodExpense) / avgBalance; | |
| // 5. 50/30/20 Analysis | |
| const RULES = { needs: 0, wants: 0, savings: 0 }; | |
| const NEEDS_CATS = ['market', 'bills', 'health', 'transport', 'tax', 'rent', 'utilities', 'bills']; | |
| Object.entries(categories).forEach(([name, data]) => { | |
| if (NEEDS_CATS.includes(name.toLowerCase())) RULES.needs += data.total; | |
| else RULES.wants += data.total; | |
| }); | |
| RULES.savings = Math.max(0, periodIncome - periodExpense); | |
| const totalRuleBase = (RULES.needs + RULES.wants + RULES.savings) || 1; | |
| const ruleAnalysis = { | |
| needs: (RULES.needs / totalRuleBase) * 100, | |
| wants: (RULES.wants / totalRuleBase) * 100, | |
| savings: (RULES.savings / totalRuleBase) * 100 | |
| }; | |
| // 6. Trend Projection (Linear Regression: y = mx + b) | |
| const n = cumulativeHistory.length; | |
| let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; | |
| cumulativeHistory.forEach((d, i) => { | |
| sumX += i; | |
| sumY += d.balance; | |
| sumXY += i * d.balance; | |
| sumX2 += i * i; | |
| }); | |
| const m = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX) || 0; | |
| const b = (sumY - m * sumX) / n; | |
| const projectionDays = selectedMonth ? (endOfMonth(new Date(selectedYear, selectedMonth - 1)).getDate()) : 365; | |
| const projectedBalance = m * projectionDays + b; | |
| // 7. Spending Heatmap Data (Day of Week vs Hour) | |
| const heatmap: Record<string, number> = {}; | |
| transactions.forEach((tx: any) => { | |
| if (tx.type !== 'expense') return; | |
| const d = new Date(tx.date); | |
| if (d < start || d > end) return; | |
| const key = `${d.getDay()}-${d.getHours()}`; | |
| heatmap[key] = (heatmap[key] || 0) + toMain(tx.amount, tx.currency); | |
| }); | |
| // 8. Formatting Return Data | |
| const savingsRate = periodIncome > 0 ? ((periodIncome - periodExpense) / periodIncome) * 100 : 0; | |
| const debtRatio = (liquidAssets + totalLent) > 0 ? (totalBorrowed / (liquidAssets + totalLent)) * 100 : 0; | |
| const expenseRatio = periodIncome > 0 ? (periodExpense / periodIncome) * 100 : 0; | |
| // Top spending categories | |
| const totalExpenseForPct = periodExpense || 1; | |
| const spendingByCategory = Object.entries(categories) | |
| .map(([name, data]) => ({ | |
| name, | |
| value: data.total, | |
| count: data.count, | |
| pct: (data.total / totalExpenseForPct) * 100 | |
| })) | |
| .sort((a, b) => b.value - a.value); | |
| const incomeBreakdown = Object.entries(incomeCategories) | |
| .map(([name, data]) => ({ | |
| name, | |
| value: data.total, | |
| count: data.count, | |
| pct: (data.total / (periodIncome || 1)) * 100 | |
| })) | |
| .sort((a, b) => b.value - a.value); | |
| const comparisonTableData = selectedMonth | |
| ? Object.values(dailyData).sort((a, b) => b.date.localeCompare(a.date)).map(d => ({ | |
| date: d.date, | |
| income: d.income, | |
| expense: d.expense, | |
| balance: d.income - d.expense | |
| })) | |
| : Object.values(monthlyData).sort((a, b) => b.month.localeCompare(a.month)).map(m => ({ | |
| date: m.month, | |
| income: m.income, | |
| expense: m.expense, | |
| balance: m.income - m.expense | |
| })); | |
| const largestTransaction = filteredTxs.length > 0 | |
| ? filteredTxs.reduce((prev: any, curr: any) => (toMain(curr.amount, curr.currency) > toMain(prev.amount, prev.currency) ? curr : prev)) | |
| : null; | |
| return { | |
| periodIncome, | |
| periodExpense, | |
| netCashFlow: periodIncome - periodExpense, | |
| totalDeposits, | |
| totalWithdrawals, | |
| liquidAssets, | |
| totalLent, | |
| totalBorrowed, | |
| netWorth, | |
| spendingByCategory, | |
| incomeBreakdown, | |
| avgDailySpend, | |
| savingsRate, | |
| debtRatio, | |
| expenseRatio, | |
| todaySpend, | |
| dailySpendChange, | |
| cumulativeHistory, | |
| comparisonTableData, | |
| sharpeRatio, | |
| maxDrawdown, | |
| velocity, | |
| ruleAnalysis, | |
| projectedBalance, | |
| heatmap, | |
| trendLine: { m, b }, | |
| largestTransaction: largestTransaction ? { | |
| ...largestTransaction, | |
| mainAmount: toMain(largestTransaction.amount, largestTransaction.currency) | |
| } : null, | |
| currencyDistribution: Object.entries(cBalances).map(([name, value]) => { | |
| const rate = exchangeRates[name] || 1; | |
| return { | |
| name, | |
| value: value / rate * (exchangeRates[mainCurrency] || 1), | |
| originalValue: value | |
| }; | |
| }).filter(d => d.value !== 0), | |
| }; | |
| }, [transactions, wallets, exchanges, loans, rates, mainCurrency, selectedYear, selectedMonth, exchangeRates]); | |
| // Available Years | |
| const availableYears = useMemo(() => { | |
| const currentYear = new Date().getFullYear(); | |
| return [currentYear, currentYear - 1, currentYear - 2]; | |
| }, []); | |
| return { | |
| isLoaded: !txLoading && !wlLoading && !exLoading && !lnLoading && !ratesLoading, | |
| mainCurrency, | |
| selectedYear, setSelectedYear, | |
| selectedMonth, setSelectedMonth, | |
| availableYears, | |
| ...stats | |
| }; | |
| } | |