Skip to main content

Overview

BudgetView’s budget management system helps you control spending by setting monthly limits for expense categories. Track progress in real-time with visual indicators and alerts when approaching or exceeding limits.

Category Budgets

Set spending limits for individual expense categories

Monthly Periods

Budgets are configured per month for precise tracking

Real-Time Tracking

See live spending progress against your limits

Alert System

Get warnings at 90% and alerts when exceeding budgets

Creating Budgets

1

Click 'Nuevo Presupuesto'

Open the budget creation dialog from the budgets page.
2

Select Category

Choose an expense category (only “gasto” categories are available).
3

Set Limit Amount

Enter the maximum spending limit in USD for this category.
4

Choose Period

Select the month using the month/year picker.
5

Save Budget

The budget is created and immediately starts tracking expenses.
Period Restrictions:
  • Can only create budgets for current or future months
  • Past months are disabled to prevent backdating
  • One budget per category per month

Budget Structure

type BudgetWithStats = {
  id: string
  categoriaId: string
  categoriaNombre: string
  limit: number           // Budget limit in USD
  spent: number          // Amount spent in period
  available: number      // Remaining budget (limit - spent)
  percentage: number     // Spent / limit * 100
  exceeded: boolean      // True if spent > limit
  nearLimit: boolean     // True if percentage >= 90%
  periodISO: string      // YYYY-MM-DD format
}

Period Selection

Budgets use a custom month/year picker component:
const MonthYearPicker = ({
  selected,
  onSelect,
  fromYear = 2018,
  toYear = 2035,
  minMonth,
}) => {
  // Renders grid of months with year selector
  // Disables months before minMonth if provided
}
Most common use case - set budgets for the current month.

Budget Calculation

Spending is calculated by aggregating expense transactions for the budget period:
const loadBudgets = async (selectedMonth: string) => {
  const periodDateOnly = `${selectedMonth}-01`
  
  // Load budgets for period
  const { data: budgetsData } = await supabase
    .from("presupuestos")
    .select("id,monto,periodo,categoria_id,categorias(id,nombre)")
    .eq("periodo", periodDateOnly)

  // Calculate spending for each category
  const start = new Date(`${selectedMonth}-01T00:00:00.000Z`)
  const end = new Date(start)
  end.setMonth(end.getMonth() + 1)

  const { data: expensesData } = await supabase
    .from("transacciones")
    .select("categoria_id,monto")
    .in("categoria_id", categoryIds)
    .eq("tipo", "gasto")
    .gte("fecha_transaccion", start.toISOString())
    .lt("fecha_transaccion", end.toISOString())

  // Aggregate spending by category
  const spendMap = new Map<string, number>()
  expensesData.forEach(row => {
    const amount = Number(row.monto ?? 0)
    spendMap.set(
      row.categoria_id,
      (spendMap.get(row.categoria_id) ?? 0) + amount
    )
  })
}

Budget Status Indicators

Budgets display different visual states based on spending:
  • Percentage < 90%
  • Green color scheme
  • Status: “Disponible”
  • Safe spending level
const statusLabel = budget.exceeded
  ? "Excedido"
  : budget.nearLimit
  ? "90% del límite"
  : "Disponible"

const statusColor = budget.exceeded
  ? "bg-red-100 text-red-700"
  : budget.nearLimit
  ? "bg-amber-100 text-amber-700"
  : "bg-emerald-100 text-emerald-700"

Summary Cards

The budget page displays four key metrics:

Presupuesto Total

Sum of all budget limits for the selected period

Gastado

Total amount spent across all budgeted categories

Disponible

Remaining budget (can be negative if exceeded)

Estado

Count of budgets in control vs. exceeded
const summary = useMemo(() => {
  const totalBudget = budgets.reduce((acc, budget) => acc + budget.limit, 0)
  const totalSpent = budgets.reduce((acc, budget) => acc + budget.spent, 0)
  const available = totalBudget - totalSpent
  const controlCount = budgets.filter((budget) => !budget.exceeded).length
  const exceededCount = budgets.length - controlCount
  
  return {
    totalBudget,
    totalSpent,
    available,
    controlCount,
    exceededCount,
  }
}, [budgets])

Budget Cards

Each budget is displayed in a detailed card showing:
  • Category Name & Icon: First letter in circular badge
  • Status Badge: Color-coded status indicator
  • Progress Bar: Visual representation of spending percentage
  • Spent vs Limit: Amounts in USD with BCV conversion
  • Available/Exceeded: Remaining budget or overspending amount
  • Edit & Delete Actions: Manage budget settings

Progress Bar Colors

const progressBarColor = budget.exceeded
  ? "bg-red-500"
  : budget.nearLimit
  ? "bg-amber-500"
  : "bg-emerald-500"

Editing Budgets

1

Click 'Editar'

Open the edit dialog from any budget card.
2

Modify Fields

Update category, amount, or period as needed.
3

Validate Period

Cannot move budgets to past months.
4

Save Changes

Updates apply immediately and recalculate progress.

Edit Validation

const originalPeriod = editingBudget ? extractMonthValue(editingBudget.periodISO) : null

if (isPastMonthValue(formPeriod) && (!editingBudget || formPeriod !== originalPeriod)) {
  setFormError(
    editingBudget
      ? "Solo puedes mover el presupuesto a meses actuales o futuros."
      : "Solo puedes crear presupuestos desde el mes actual en adelante."
  )
  return
}

Duplicate Prevention

The system prevents creating multiple budgets for the same category in the same month:
// Check for duplicates before insert/update
const { data: duplicateRows } = await supabase
  .from("presupuestos")
  .select("id")
  .eq("categoria_id", formCategoryId)
  .eq("periodo", normalizedPeriod)
  .neq("id", editingBudget?.id) // Exclude current budget when editing
  .limit(1)

if (duplicateRows && duplicateRows.length > 0) {
  setFormError("Ya tienes un presupuesto para esa categoría en el periodo seleccionado.")
  return
}

Deleting Budgets

Deletion Warning:
  • Deleting budgets is permanent and cannot be undone
  • Confirmation dialog appears before deletion
  • Historical spending data remains intact
const handleDeleteBudget = async (budgetId: string) => {
  const confirmed = window.confirm(
    "¿Eliminar este presupuesto? Esta acción no se puede deshacer."
  )
  if (!confirmed) return

  const { error } = await supabase
    .from("presupuestos")
    .delete()
    .eq("id", budgetId)

  if (!error) {
    await loadBudgets() // Refresh budget list
  }
}

Real-Time Updates

Budgets automatically refresh when transactions are created or edited:
React.useEffect(() => {
  const handleTransactionsChange = () => {
    loadBudgets() // Recalculate spending
  }
  
  window.addEventListener("transactions:updated", handleTransactionsChange)
  
  return () => {
    window.removeEventListener("transactions:updated", handleTransactionsChange)
  }
}, [loadBudgets])

BCV Conversion

All budget amounts show BCV equivalents:
const spentBcv = formatBcvAmount(budget.spent)
const limitBcv = formatBcvAmount(budget.limit)
const availableBcv = formatBcvAmount(Math.abs(budget.available))
BCV conversions update automatically when the exchange rate refreshes.

Best Practices

Begin with conservative budget limits and adjust based on actual spending patterns.
Check budget performance at month-end and adjust for the next period.
Set budgets for your highest-spending categories first.
When a budget reaches 90%, review spending and adjust behavior.
Create budgets for future months to prepare for upcoming expenses.

Database Schema

CREATE TABLE presupuestos (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  categoria_id UUID NOT NULL REFERENCES categorias(id),
  monto DECIMAL(12,2) NOT NULL,
  periodo DATE NOT NULL,
  usuario_id UUID NOT NULL REFERENCES auth.users(id),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
  UNIQUE(categoria_id, periodo, usuario_id)
);
The unique constraint prevents duplicate budgets per category/period combination.

Build docs developers (and LLMs) love