Skip to main content

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:
const start = startOfDay(subDays(now, 6))
const end = endOfDay(now)
Quick view of recent activity for weekly analysis.

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

  • Percentage < 80%
  • Green progress bar and badge
  • Normal spending level
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.

Performance Optimization

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])

Build docs developers (and LLMs) love