Overview
The BudgetView dashboard provides a complete financial snapshot with interactive charts, real-time metrics, and budget monitoring. View all your data at a glance or filter by wallet and time period for detailed analysis.
Summary Metrics Track income, expenses, balance, and transaction count
Spending Breakdown Visualize expense distribution by category
Trend Analysis Monitor daily income and expense patterns
Budget Alerts Real-time notifications for budget limits
Key Metrics
Four primary metric cards provide instant financial insights:
Ingresos (Income)
< Card >
< CardHeader >
< CardTitle > Ingresos </ CardTitle >
< ArrowUpCircle className = "text-emerald-500" />
</ CardHeader >
< CardContent >
< div className = "text-2xl font-bold" >
{ currencyFormatter . format ( summary . income )}
</ div >
{ incomeBcv && (
< p className = "text-emerald-600" > ≈ { incomeBcv } BCV </ p >
)}
</ CardContent >
</ Card >
Total income in selected period
USD amount with BCV conversion
Green indicator for positive flow
Gastos (Expenses)
Total expenses in selected period
USD amount with BCV conversion
Red indicator for outgoing money
Balance (Net)
const net = summary . income - summary . expense
Net result (income - expenses)
Color-coded: green if positive, red if negative
Shows financial health for the period
Transacciones (Transactions)
Total transaction count
All types (income + expenses)
Activity level indicator
Period Filtering
Analyze different timeframes with flexible period selection:
Últimos 7 días
Este mes
Mes pasado
Personalizado
const start = startOfDay ( subDays ( now , 6 ))
const end = endOfDay ( now )
Quick view of recent activity for weekly analysis. const start = startOfDay ( new Date ( now . getFullYear (), now . getMonth (), 1 ))
const end = endOfDay ( now )
Current month-to-date financial performance. const start = startOfDay ( new Date ( now . getFullYear (), now . getMonth () - 1 , 1 ))
const end = endOfDay ( new Date ( now . getFullYear (), now . getMonth (), 0 ))
Complete previous month for historical comparison. Select specific start and end dates using calendar pickers for custom analysis periods.
Filter Implementation
const filteredTransactions = useMemo (() => {
const { start , end } = getRangeForFilter ( periodFilter , customStartDate , customEndDate )
return transactions . filter (( tx ) => {
const txDate = new Date ( tx . date )
if ( Number . isNaN ( txDate . getTime ())) return false
if ( start && txDate < start ) return false
if ( end && txDate > end ) return false
return true
})
}, [ transactions , periodFilter , customStartDate , customEndDate ])
Expense Breakdown Chart
Interactive pie chart showing spending distribution by category:
Chart Data Preparation
const expenseBreakdown = useMemo (() => {
const totals = new Map < string , { label : string ; value : number }>()
filteredTransactions . forEach (( tx ) => {
if ( tx . type !== "gasto" ) return
const key = tx . categoryName . toLowerCase ()
const current = totals . get ( key )
const nextValue = ( current ?. value ?? 0 ) + tx . amount
totals . set ( key , { label: tx . categoryName , value: nextValue })
})
const totalExpense = Array . from ( totals . values ())
. reduce (( acc , entry ) => acc + entry . value , 0 )
return Array . from ( totals . values ()). map (( entry ) => ({
name: entry . label ,
value: entry . value ,
percentage: totalExpense === 0 ? 0 : ( entry . value / totalExpense ) * 100 ,
}))
}, [ filteredTransactions ])
Chart Configuration
const PIE_COLORS = [
"#F97316" , // Orange
"#6366F1" , // Indigo
"#EC4899" , // Pink
"#0EA5E9" , // Sky
"#22C55E" , // Green
"#FACC15" , // Yellow
"#8B5CF6" , // Violet
]
const expenseChartConfig : ChartConfig = {}
expenseBreakdown . forEach (( entry , index ) => {
const key = slugify ( entry . name )
expenseChartConfig [ key ] = {
label: ` ${ entry . name } ( ${ entry . percentage . toFixed ( 1 ) } %)` ,
color: PIE_COLORS [ index % PIE_COLORS . length ],
}
})
Recharts Implementation
< ChartContainer config = { expenseChartConfig } >
< ResponsiveContainer width = "100%" height = "100%" >
< PieChart >
< ChartTooltip
content = {
< ChartTooltipContent
hideLabel
valueFormatter = {(value) => {
const usdValue = currencyFormatter . format ( value )
const vesValue = formatBcvAmount ( value )
return vesValue ? ` ${ usdValue } • ${ vesValue } BCV` : usdValue
}}
/>
}
/>
< Pie
data = { expenseChartData }
dataKey = "value"
nameKey = "slice"
innerRadius = { 0 }
outerRadius = { 110 }
paddingAngle = { 2 }
label = { renderPieLabel }
labelLine = { false }
/>
</ PieChart >
</ ResponsiveContainer >
</ ChartContainer >
Custom tooltips show both USD and BCV values for complete financial context.
Custom Label Rendering
function renderPieLabel ( props : PieLabelRenderProps ) {
const { cx = 0 , cy = 0 , midAngle = 0 , outerRadius = 0 , percent = 0 } = props
const RADIAN = Math . PI / 180
const radius = outerRadius + 18
const x = cx + radius * Math . cos ( - midAngle * RADIAN )
const y = cy + radius * Math . sin ( - midAngle * RADIAN )
const displayPercent = ` ${ ( percent * 100 ). toFixed ( 1 ) } %`
return (
< text
x = { x }
y = { y }
fill = "var(--foreground)"
textAnchor = {x > cx ? "start" : "end" }
dominantBaseline = "central"
>
{ displayPercent }
</ text >
)
}
Trend Line Chart
Daily income vs. expenses comparison over time:
Data Preparation
const lineChartData = useMemo (() => {
const { start : rangeStart , end : rangeEnd } = getRangeForFilter (
periodFilter ,
customStartDate ,
customEndDate
)
const end = rangeEnd ?? endOfDay ( new Date ())
const start = rangeStart ?? startOfDay ( subDays ( end , 29 ))
// Create bucket for each day
const days = eachDayOfInterval ({ start , end })
const buckets = days . map (( day ) => ({
dateLabel: format ( day , "dd/MM" , { locale: es }),
gastos: 0 ,
ingresos: 0 ,
}))
// Aggregate transactions into daily buckets
filteredTransactions . forEach (( tx ) => {
const txDate = new Date ( tx . date )
if ( txDate < start || txDate > end ) return
const label = format ( txDate , "dd/MM" , { locale: es })
const bucket = bucketMap . get ( label )
if ( ! bucket ) return
if ( tx . type === "gasto" ) {
bucket . gastos += tx . amount
} else {
bucket . ingresos += tx . amount
}
})
return buckets
}, [ filteredTransactions , periodFilter , customStartDate , customEndDate ])
Chart Implementation
< ResponsiveContainer width = "100%" height = "100%" >
< LineChart data = { lineChartData } margin = {{ top : 10 , right : 20 , bottom : 0 , left : 0 }} >
< CartesianGrid strokeDasharray = "3 3" className = "stroke-muted" />
< XAxis dataKey = "dateLabel" tick = {{ fontSize : 12 }} />
< YAxis
tickFormatter = {(value) => currencyFormatter.format(value)}
tick = {{ fontSize : 12 }}
width = { 90 }
/>
< Tooltip
formatter = {( value: number ) => {
const usdValue = currencyFormatter . format ( value )
const vesValue = formatBcvAmount ( value )
return vesValue ? ` ${ usdValue } • ${ vesValue } BCV` : usdValue
}}
/>
< Legend />
< Line
type = "monotone"
dataKey = "gastos"
stroke = "#ef4444"
strokeWidth = { 2 }
dot = { false }
/>
< Line
type = "monotone"
dataKey = "ingresos"
stroke = "#10b981"
strokeWidth = { 2 }
dot = { false }
/>
</ LineChart >
</ ResponsiveContainer >
The trend chart uses the eachDayOfInterval function from date-fns to ensure all days are represented, even with no transactions.
Budget Alerts
Real-time budget monitoring with visual progress indicators:
Budget Matching
const selectedMonthKey = useMemo (() => {
const { start } = getRangeForFilter ( periodFilter , customStartDate , customEndDate )
const reference = start ?? customStartDate ?? customEndDate ?? new Date ()
return ` ${ reference . getFullYear () } - ${ reference . getMonth () } `
}, [ periodFilter , customStartDate , customEndDate ])
const monthlyBudgets = useMemo (() => {
if ( budgets . length === 0 ) return []
// Try to match budgets for selected month
const scoped = budgets . filter (( budget ) => budget . periodKey === selectedMonthKey )
if ( scoped . length > 0 ) return scoped
// Fallback to budgets without specific period
return budgets . filter (( budget ) => ! budget . periodKey )
}, [ budgets , selectedMonthKey ])
Spending Calculation
const budgetAlerts = useMemo < BudgetAlert []>(() => {
if ( consolidatedBudgets . length === 0 ) return []
const expenseTotalsById = new Map < string , number >()
const expenseTotalsByLabel = new Map < string , number >()
filteredTransactions . forEach (( tx ) => {
if ( tx . type !== "gasto" ) return
if ( tx . categoryId ) {
expenseTotalsById . set (
tx . categoryId ,
( expenseTotalsById . get ( tx . categoryId ) ?? 0 ) + tx . amount
)
}
const labelKey = tx . categoryName . toLowerCase ()
expenseTotalsByLabel . set (
labelKey ,
( expenseTotalsByLabel . get ( labelKey ) ?? 0 ) + tx . amount
)
})
return consolidatedBudgets . map (( config ) => {
const spent = config . categoryId
? expenseTotalsById . get ( config . categoryId ) ?? 0
: expenseTotalsByLabel . get ( config . label . toLowerCase ()) ?? 0
const percentage = config . limit === 0 ? 0 : ( spent / config . limit ) * 100
let status : "ok" | "warn" | "danger" = "ok"
if ( percentage >= 100 ) {
status = "danger"
} else if ( percentage >= 80 ) {
status = "warn"
}
return { ... config , spent , percentage , status }
})
}, [ filteredTransactions , consolidatedBudgets ])
Alert Display
OK Status
Warning Status
Danger Status
Percentage < 80%
Green progress bar and badge
Normal spending level
Percentage >= 80% and < 100%
Amber/yellow progress bar and badge
Approaching limit
Percentage >= 100%
Red progress bar and badge
Alert icon: “Presupuesto superado”
const statusColors = {
ok: "bg-emerald-500" ,
warn: "bg-amber-500" ,
danger: "bg-red-500" ,
}
const badgeColors = {
ok: "text-emerald-600 bg-emerald-500/15" ,
warn: "text-amber-600 bg-amber-500/15" ,
danger: "text-red-600 bg-red-500/15" ,
}
Wallet Integration
Dashboard supports both specific wallet and global views:
Wallet Selection
const loadTransactions = useCallback ( async ( activeWallet : string ) => {
const query = supabase
. from ( "transacciones" )
. select ( "id,monto,tipo,fecha_transaccion,categorias(id,nombre)" )
. order ( "fecha_transaccion" , { ascending: false })
// Filter by wallet if not global
if ( activeWallet !== GLOBAL_WALLET_ID ) {
query . eq ( "billetera_id" , activeWallet )
}
const { data } = await query
return transformTransactions ( data )
}, [])
Wallet Change Handler
React . useEffect (() => {
const handleWalletChanged = ( event : Event ) => {
const detail = ( event as CustomEvent <{ walletId ?: string }>). detail
if ( ! detail ?. walletId ) return
setWalletId ( detail . walletId )
}
window . addEventListener ( "wallet:changed" , handleWalletChanged )
return () => {
window . removeEventListener ( "wallet:changed" , handleWalletChanged )
}
}, [])
Real-Time Updates
Dashboard automatically refreshes when transactions change:
React . useEffect (() => {
if ( ! walletId ) return
const handleTransactionsUpdated = ( event : Event ) => {
const detail = ( event as CustomEvent <{ walletId ?: string }>). detail
// If viewing specific wallet, only update for matching wallet
if (
walletId !== GLOBAL_WALLET_ID &&
detail ?. walletId &&
detail . walletId !== walletId
) {
return
}
loadTransactions ( walletId )
. then (( data ) => setTransactions ( data ))
. catch ( console . error )
}
window . addEventListener ( "transactions:updated" , handleTransactionsUpdated )
return () => {
window . removeEventListener ( "transactions:updated" , handleTransactionsUpdated )
}
}, [ walletId , loadTransactions ])
Top Insights
Dashboard highlights key spending patterns:
const topExpenseCategory = expenseBreakdown [ 0 ] // Highest spending category
< CardFooter >
< div className = "flex items-center gap-2" >
< TrendingUp className = "text-emerald-500" />
{ topExpenseCategory . name } concentra { topExpenseCategory . percentage . toFixed ( 1 )} % del gasto .
</ div >
< p > Revisa tus hábitos para equilibrar mejor tu presupuesto . </ p >
</ CardFooter >
Best Practices
Check the dashboard daily to stay on top of your financial situation.
Use period filters to compare current vs. previous months for trends.
Address budget warnings immediately to avoid overspending.
Review pie chart to identify highest spending categories for optimization.
Use line chart to spot spending patterns and seasonal variations.
All chart data is computed using useMemo hooks to prevent unnecessary recalculations on re-renders.
const expenseBreakdown = useMemo (() => {
// Expensive calculation only runs when dependencies change
}, [ filteredTransactions ])
const lineChartData = useMemo (() => {
// Another expensive calculation
}, [ filteredTransactions , periodFilter , customStartDate , customEndDate ])