wallets-api / client /src /features /analytics /useAnalyticsStats.ts
z1amez's picture
v.1
2dddd1f
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
};
}