import { useState, useEffect, useMemo } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Plus, Wallet, Receipt, TrendingUp, TrendingDown, Calendar } from "lucide-react" import { ChartContainer, ChartTooltip } from "@/components/ui/chart" import { Bar, XAxis, YAxis, ResponsiveContainer, PieChart as RechartsPieChart, Pie, Cell, Line, ComposedChart } from "recharts" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { DatePicker } from "@/components/ui/date-picker" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Legend } from "recharts" import axios from "axios" import { formatCurrency } from "@/constants/currencies" import { formatLargeNumber } from "@/utils/numberFormat" import { fetchExchangeRates, convertToIDR } from "@/utils/exchangeRate" import { WalletDialog } from "@/components/dialogs/WalletDialog" import { TransactionDialog } from "@/components/dialogs/TransactionDialog" interface Wallet { id: string name: string kind: "money" | "asset" currency?: string | null unit?: string | null initialAmount?: number | null pricePerUnit?: number | null deletedAt?: string | null } interface Transaction { id: string walletId: string date: string amount: number direction: "in" | "out" category?: string | null memo?: string | null } const API = "/api" type DateRange = 'this_month' | 'last_month' | 'this_year' | 'last_year' | 'all_time' | 'custom' type TrendPeriod = 'daily' | 'weekly' | 'monthly' | 'yearly' // Helper function to filter transactions by date range function getFilteredTransactions(transactions: Transaction[], dateRange: DateRange, customStartDate?: Date, customEndDate?: Date): Transaction[] { if (dateRange === 'all_time') return transactions if (dateRange === 'custom' && customStartDate && customEndDate) { return transactions.filter(tx => { const txDate = new Date(tx.date) return txDate >= customStartDate && txDate <= customEndDate }) } const now = new Date() let startDate: Date let endDate: Date = new Date(now.getFullYear(), now.getMonth() + 1, 0) // End of current month switch (dateRange) { case 'this_month': startDate = new Date(now.getFullYear(), now.getMonth(), 1) endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0) break case 'last_month': startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1) endDate = new Date(now.getFullYear(), now.getMonth(), 0) break case 'this_year': startDate = new Date(now.getFullYear(), 0, 1) endDate = new Date(now.getFullYear(), 11, 31) break case 'last_year': startDate = new Date(now.getFullYear() - 1, 0, 1) endDate = new Date(now.getFullYear() - 1, 11, 31) break default: return transactions } return transactions.filter(tx => { const txDate = new Date(tx.date) return txDate >= startDate && txDate <= endDate }) } // Helper function to get date range label function getDateRangeLabel(dateRange: DateRange, customStartDate?: Date, customEndDate?: Date): string { switch (dateRange) { case 'this_month': return 'This Month' case 'last_month': return 'Last Month' case 'this_year': return 'This Year' case 'last_year': return 'Last Year' case 'all_time': return 'All Time' case 'custom': if (customStartDate && customEndDate) { return `${customStartDate.toLocaleDateString()} - ${customEndDate.toLocaleDateString()}` } return 'Custom Range' default: return 'All Time' } } // Helper function to format Y-axis values with k/m suffix function formatYAxisValue(value: number): string { const absValue = Math.abs(value) const sign = value < 0 ? '-' : '' if (absValue >= 1000000) { return `${sign}${(absValue / 1000000).toFixed(1)}m` } else if (absValue >= 1000) { return `${sign}${(absValue / 1000).toFixed(1)}k` } return value.toLocaleString() } export function Overview() { const [wallets, setWallets] = useState([]) const [transactions, setTransactions] = useState([]) const [exchangeRates, setExchangeRates] = useState>({}) const [loading, setLoading] = useState(true) const [walletDialogOpen, setWalletDialogOpen] = useState(false) const [transactionDialogOpen, setTransactionDialogOpen] = useState(false) const [dateRange, setDateRange] = useState('this_month') const [customStartDate, setCustomStartDate] = useState() const [customEndDate, setCustomEndDate] = useState() const [incomeChartWallet, setIncomeChartWallet] = useState('all') const [expenseChartWallet, setExpenseChartWallet] = useState('all') const [trendPeriod, setTrendPeriod] = useState('monthly') useEffect(() => { loadData() loadExchangeRates() }, []) const loadData = async () => { try { setLoading(true) const [walletsRes, transactionsRes] = await Promise.all([ axios.get(`${API}/wallets`), axios.get(`${API}/wallets/transactions`) ]) const activeWallets = walletsRes.data.filter((w: Wallet) => !w.deletedAt) setWallets(activeWallets) setTransactions(transactionsRes.data) } catch (error) { console.error('Failed to load data:', error) } finally { setLoading(false) } } const loadExchangeRates = async () => { try { const rates = await fetchExchangeRates() setExchangeRates(rates) } catch (error) { console.error('Failed to load exchange rates:', error) } } // Calculate totals across all wallets in IDR const totals = useMemo(() => { let totalBalance = 0 let totalIncome = 0 let totalExpense = 0 // Filter transactions by date range const filteredTransactions = getFilteredTransactions(transactions, dateRange, customStartDate, customEndDate) // Calculate income and expense from filtered transactions filteredTransactions.forEach(tx => { const wallet = wallets.find(w => w.id === tx.walletId) if (!wallet) return let idrAmount = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrAmount = convertToIDR(Number(tx.amount), currency, exchangeRates) } else { // For assets, multiply by price per unit and convert to IDR if needed const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = Number(tx.amount) * pricePerUnit idrAmount = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } if (tx.direction === 'in') { totalIncome += idrAmount } else { totalExpense += idrAmount } }) // Calculate balance for each wallet (filtered transactions + initial amount) wallets.forEach(wallet => { const walletTransactions = filteredTransactions.filter(tx => tx.walletId === wallet.id) const transactionBalance = walletTransactions.reduce((sum, tx) => { return sum + (tx.direction === 'in' ? Number(tx.amount) : -Number(tx.amount)) }, 0) // Add initial amount if exists const initialAmount = Number(wallet.initialAmount) || 0 const walletBalance = transactionBalance + initialAmount // Convert to IDR based on wallet type let idrBalance = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrBalance = convertToIDR(walletBalance, currency, exchangeRates) } else { // For assets, multiply by price per unit and convert to IDR if needed const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = walletBalance * pricePerUnit idrBalance = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } totalBalance += idrBalance }) return { totalBalance, totalIncome, totalExpense } }, [wallets, transactions, exchangeRates, dateRange, customStartDate, customEndDate]) // Wallet breakdown data const walletBreakdown = useMemo(() => { const filteredTransactions = getFilteredTransactions(transactions, dateRange, customStartDate, customEndDate) return wallets.map(wallet => { const walletTransactions = filteredTransactions.filter(tx => tx.walletId === wallet.id) const transactionBalance = walletTransactions.reduce((sum, tx) => { const amount = Number(tx.amount); const direction = tx.direction; return sum + (direction === 'in' ? amount : -amount); }, 0) // Add initial amount const initialAmount = Number(wallet.initialAmount) || 0 const balance = transactionBalance + initialAmount // Convert balance to IDR for display let idrBalance = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrBalance = convertToIDR(balance, currency, exchangeRates) } else { // For assets, multiply by price per unit and convert to IDR if needed const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = balance * pricePerUnit idrBalance = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } return { id: wallet.id, name: wallet.name, balance: balance, // Show original currency balance idrBalance: idrBalance, // IDR equivalent for reference currency: wallet.currency || wallet.unit || 'IDR', // Show original currency transactionCount: walletTransactions.length, kind: wallet.kind } }) }, [wallets, transactions, exchangeRates, dateRange, customStartDate, customEndDate]) // Flexible trend data based on selected period and date range const trendData = useMemo(() => { const filteredTransactions = getFilteredTransactions(transactions, dateRange, customStartDate, customEndDate) // Generate periods based on selected trend period and date range const generatePeriods = () => { const periods = [] const now = new Date() // Determine smart defaults based on date range let periodsCount = 6 let currentPeriod = trendPeriod if (dateRange === 'this_month' || dateRange === 'last_month') { currentPeriod = trendPeriod === 'monthly' ? 'daily' : trendPeriod periodsCount = trendPeriod === 'daily' ? 30 : 4 } else if (dateRange === 'this_year' || dateRange === 'last_year') { periodsCount = currentPeriod === 'yearly' ? 1 : currentPeriod === 'monthly' ? 12 : 52 } switch (currentPeriod) { case 'daily': for (let i = periodsCount - 1; i >= 0; i--) { const date = new Date(now) date.setDate(date.getDate() - i) periods.push({ start: new Date(date.getFullYear(), date.getMonth(), date.getDate()), end: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59), label: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) }) } break case 'weekly': for (let i = periodsCount - 1; i >= 0; i--) { const date = new Date(now) date.setDate(date.getDate() - (i * 7)) const weekStart = new Date(date) weekStart.setDate(date.getDate() - date.getDay()) const weekEnd = new Date(weekStart) weekEnd.setDate(weekStart.getDate() + 6) weekEnd.setHours(23, 59, 59) periods.push({ start: weekStart, end: weekEnd, label: `${weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` }) } break case 'monthly': for (let i = periodsCount - 1; i >= 0; i--) { const date = new Date(now.getFullYear(), now.getMonth() - i, 1) const monthStart = new Date(date.getFullYear(), date.getMonth(), 1) const monthEnd = new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59) periods.push({ start: monthStart, end: monthEnd, label: date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }) }) } break case 'yearly': for (let i = periodsCount - 1; i >= 0; i--) { const year = now.getFullYear() - i const yearStart = new Date(year, 0, 1) const yearEnd = new Date(year, 11, 31, 23, 59, 59) periods.push({ start: yearStart, end: yearEnd, label: year.toString() }) } break } return periods } const periods = generatePeriods() return periods.map(period => { const periodTransactions = filteredTransactions.filter(tx => { const txDate = new Date(tx.date) return txDate >= period.start && txDate <= period.end }) let income = 0 let expense = 0 periodTransactions.forEach(tx => { const wallet = wallets.find(w => w.id === tx.walletId) if (!wallet) return let idrAmount = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrAmount = convertToIDR(Number(tx.amount), currency, exchangeRates) } else { const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = Number(tx.amount) * pricePerUnit idrAmount = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } if (tx.direction === 'in') { income += idrAmount } else { expense += idrAmount } }) return { period: period.label, income, expense, net: income - expense } }) }, [transactions, wallets, exchangeRates, trendPeriod, dateRange, customStartDate, customEndDate]) // Pie chart data for income by category const incomeChartData = useMemo(() => { const filteredTransactions = getFilteredTransactions(transactions, dateRange, customStartDate, customEndDate) const incomeTransactions = filteredTransactions.filter(tx => { if (tx.direction !== 'in') return false if (incomeChartWallet === 'all') return true return tx.walletId === incomeChartWallet }) const categoryTotals: Record = {} incomeTransactions.forEach(tx => { const wallet = wallets.find(w => w.id === tx.walletId) if (!wallet) return const category = tx.category || 'Uncategorized' let idrAmount = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrAmount = convertToIDR(Number(tx.amount), currency, exchangeRates) } else { const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = Number(tx.amount) * pricePerUnit idrAmount = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } categoryTotals[category] = (categoryTotals[category] || 0) + idrAmount }) return Object.entries(categoryTotals).map(([category, amount]) => ({ name: category, value: amount, percentage: 0 // Will be calculated in the component })) }, [transactions, dateRange, incomeChartWallet, wallets, exchangeRates, customStartDate, customEndDate]) // Pie chart data for expense by category const expenseChartData = useMemo(() => { const filteredTransactions = getFilteredTransactions(transactions, dateRange, customStartDate, customEndDate) const expenseTransactions = filteredTransactions.filter(tx => { if (tx.direction !== 'out') return false if (expenseChartWallet === 'all') return true return tx.walletId === expenseChartWallet }) const categoryTotals: Record = {} expenseTransactions.forEach(tx => { const wallet = wallets.find(w => w.id === tx.walletId) if (!wallet) return const category = tx.category || 'Uncategorized' let idrAmount = 0 if (wallet.kind === 'money') { const currency = wallet.currency || 'IDR' idrAmount = convertToIDR(Number(tx.amount), currency, exchangeRates) } else { const pricePerUnit = Number(wallet.pricePerUnit) || 1 const assetCurrency = wallet.currency || 'IDR' const assetValueInCurrency = Number(tx.amount) * pricePerUnit idrAmount = convertToIDR(assetValueInCurrency, assetCurrency, exchangeRates) } categoryTotals[category] = (categoryTotals[category] || 0) + idrAmount }) return Object.entries(categoryTotals).map(([category, amount]) => ({ name: category, value: amount, percentage: 0 // Will be calculated in the component })) }, [transactions, dateRange, expenseChartWallet, wallets, exchangeRates, customStartDate, customEndDate]) // Colors for pie charts // const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FFC658', '#FF7C7C'] const COLORS = [ "var(--chart-1)", // Teal "var(--chart-2)", // Gold "var(--chart-3)", // Dark Gray "var(--chart-4)", // Info Blue "var(--chart-5)", // Danger Red ]; if (loading) { return (
{[...Array(4)].map((_, i) => (
))}
) } return (
{/* Header */}

Overview

Your financial dashboard and quick actions

{/* Date Range Filter */}
{/* Custom Date Fields */} {dateRange === 'custom' && (
)}

{/* Stats Cards */}
Total Balance
{formatLargeNumber(totals.totalBalance, 'IDR')}

Across {wallets.length} wallets

Total Income
{formatLargeNumber(totals.totalIncome, 'IDR')}

{getDateRangeLabel(dateRange, customStartDate, customEndDate)} income

Total Expense
{formatLargeNumber(totals.totalExpense, 'IDR')}

{getDateRangeLabel(dateRange, customStartDate, customEndDate)} expense

{/* Second Row: Wallet Breakdown (Full Width) */}
{/* Wallet Breakdown */} Wallet Breakdown Balance distribution across wallets
Name Currency/Unit Transactions Total Balance Domination {walletBreakdown.map((wallet) => { const dominationPercentage = totals.totalBalance > 0 ? (wallet.idrBalance / totals.totalBalance * 100).toFixed(1) : '0.0' return (
{wallet.currency}
{formatCurrency(wallet.balance, wallet.currency)}
{wallet.currency !== 'IDR' && (
≈ {formatCurrency(wallet.idrBalance, 'IDR')}
)}
{dominationPercentage}%
) })}
{/* Third Row: Pie Charts (1/2 each) */}
{/* Income by Category */} Income by Category
Category breakdown for {getDateRangeLabel(dateRange, customStartDate, customEndDate).toLowerCase()}
{incomeChartData.length > 0 ? (
{/* Mobile Layout */}
{incomeChartData.map((_, index) => ( ))} { if (active && payload && payload.length) { const data = payload[0].payload return (

{data.name}

{formatCurrency(data.value, 'IDR')}

) } return null }} /> {value}} />
{/* Desktop Layout */}
{incomeChartData.map((_, index) => ( ))} { if (active && payload && payload.length) { const data = payload[0].payload return (

{data.name}

{formatCurrency(data.value, 'IDR')}

) } return null }} /> {value}} />
) : (
No income data for selected period
)}
{/* Expense by Category */} Expense by Category
Category breakdown for {getDateRangeLabel(dateRange, customStartDate, customEndDate).toLowerCase()}
{expenseChartData.length > 0 ? (
{/* Mobile Layout */}
{expenseChartData.map((_, index) => ( ))} { if (active && payload && payload.length) { const data = payload[0].payload return (

{data.name}

{formatCurrency(data.value, 'IDR')}

) } return null }} /> {value}} />
{/* Desktop Layout */}
{expenseChartData.map((_, index) => ( ))} { if (active && payload && payload.length) { const data = payload[0].payload return (

{data.name}

{formatCurrency(data.value, 'IDR')}

) } return null }} /> {value}} />
) : (
No expense data for selected period
)}
{/* Fourth Row: Trend Chart (Full Width) */}
Financial Trend
Income vs Expense over time
{ if (active && payload && payload.length) { return (

{label}

{payload.map((entry, index) => (
{entry.name}: {formatCurrency(entry.value as number, 'IDR')}
))}
) } return null }} />
{/* Dialogs */}
) }