Skip to main content

Overview

Categories are the foundation of BudgetView’s organizational system. Create custom categories for both income and expenses to track spending patterns, set budgets, and generate meaningful financial reports.

Income Categories

Track revenue sources like salary, freelance, investments, etc.

Expense Categories

Organize spending by services, food, transport, entertainment, etc.

Real-Time Analytics

See spending distribution and percentage breakdowns

BCV Conversion

View category totals in both USD and Venezuelan Bolivars

Category Types

Categories for outgoing money:
  • Services (utilities, subscriptions)
  • Food & Dining
  • Transportation
  • Entertainment
  • Healthcare
  • Shopping

Creating Categories

1

Click 'Nueva Categoría'

Open the category creation dialog from the categories page.
2

Enter Name

Provide a descriptive name (maximum 13 characters).
3

Select Type

Choose either “Gasto” (expense) or “Ingreso” (income).
4

Save Category

The category is immediately available for transaction assignment.

Name Validation

const NAME_MAX_LENGTH = 13

const handleSubmitCategory = async (event: React.FormEvent) => {
  event.preventDefault()
  const trimmed = categoryName.trim()
  
  if (!trimmed) {
    setFormError("El nombre es obligatorio.")
    return
  }
  
  if (trimmed.length > NAME_MAX_LENGTH) {
    setFormError(`El nombre no puede tener más de ${NAME_MAX_LENGTH} caracteres.`)
    return
  }

  // Insert into database
}
Category names are limited to 13 characters to ensure they display properly in all UI components.

Category Structure

type CategoryRow = {
  id: string
  nombre: string | null
  tipo: "gasto" | "ingreso" | null
}

type CategoryAggregate = {
  id: string
  name: string
  total: number        // Total amount in this category
  count: number        // Number of transactions
  percentage: number   // Percentage of total (income/expense)
}

Analytics & Statistics

The categories page displays comprehensive statistics:

Total Gastos

Sum of all expense transactions across all expense categories

Total Ingresos

Sum of all income transactions across all income categories

Categorías Activas

Total number of categories with at least one transaction

Data Aggregation

Category statistics are calculated by aggregating all transactions:
const loadData = async () => {
  // Fetch all transactions with category info
  const { data: transactions } = await supabase
    .from("transacciones")
    .select("monto,tipo,categorias(id,nombre,tipo)")

  // Aggregate by category
  const stats = new Map<string, CategoryStat>()
  
  transactions.forEach((tx) => {
    const categoryId = tx.categorias?.id ?? `sin-${tx.tipo}`
    const categoryName = tx.categorias?.nombre?.trim() || "Sin categoría"
    const amount = Number(tx.monto ?? 0)
    
    const stat = stats.get(categoryId)
    
    if (tx.tipo === "gasto") {
      stat.expenseTotal += amount
      stat.expenseCount += 1
    } else {
      stat.incomeTotal += amount
      stat.incomeCount += 1
    }
  })
}

Category Cards

Each category is displayed in a detailed card showing:
  • Category Name: Displayed prominently at top
  • Transaction Count: Number of transactions in this category
  • Total Amount: Sum in USD with BCV conversion
  • Percentage Badge: Share of total expenses/income
  • Progress Bar: Visual representation of percentage
  • Options Menu: Edit and delete actions

Color Coding

const categoryCardStyles: Record<CategorySection, {...}> = {
  gasto: {
    card: "from-red-100/80 to-white text-red-700 border-red-200 ...",
    value: "text-red-700 dark:text-red-200",
    badge: "bg-red-500/10 text-red-700 ...",
    bar: "bg-red-500 dark:bg-red-400",
  },
  ingreso: {
    card: "from-emerald-100/80 to-white text-emerald-700 border-emerald-200 ...",
    value: "text-emerald-700 dark:text-emerald-200",
    badge: "bg-emerald-500/10 text-emerald-700 ...",
    bar: "bg-emerald-500 dark:bg-emerald-400",
  },
}
Expense categories use red color schemes, while income categories use green for instant visual recognition.

Percentage Calculation

Category percentages are calculated relative to their type total:
// For expense categories
const percentage = totalExpenses === 0 
  ? 0 
  : (categoryTotal / totalExpenses) * 100

// For income categories
const percentage = totalIncome === 0
  ? 0
  : (categoryTotal / totalIncome) * 100

Editing Categories

1

Open Options Menu

Click the three-dot menu icon on any category card.
2

Select 'Editar'

Choose the edit option from the dropdown menu.
3

Update Fields

Modify the category name or type as needed.
4

Save Changes

Submit to update the category and refresh all displays.

Type Switching

Important: When changing category type from “Gasto” to “Ingreso” (or vice versa):
  • All existing transactions remain assigned to this category
  • Transaction types (income/expense) are NOT changed
  • This can create mismatches (e.g., income transaction in expense category)
  • Use with caution and review associated transactions

Deleting Categories

Categories with associated transactions cannot be deleted:
const handleDeleteCategory = async (categoryId: string) => {
  // Check for linked transactions
  const { count: linkedTransactions } = await supabase
    .from("transacciones")
    .select("id", { count: "exact", head: true })
    .eq("categoria_id", categoryId)

  if ((linkedTransactions ?? 0) > 0) {
    setError("Primero elimina o reasigna las transacciones asociadas a esta categoría.")
    return
  }

  // Delete category
  await supabase
    .from("categorias")
    .delete()
    .eq("id", categoryId)
}
Deletion Requirements:
  • Must have zero associated transactions
  • Cannot be undone once deleted
  • Budgets using this category may become invalid

”Sin categoría” (Uncategorized)

Transactions without assigned categories appear under “Sin categoría”:
const categoryId = categoryRecord?.id ?? `sin-${tx.tipo}`
const categoryName = categoryRecord?.nombre?.trim() || "Sin categoría"
Uncategorized transactions are tracked separately for income and expenses, appearing as distinct “Sin categoría” entries.

Real-Time Synchronization

Categories broadcast updates when created, edited, or deleted:
// Broadcast category updates
window.dispatchEvent(new CustomEvent("categories:updated"))

// Listen for updates in transaction forms
window.addEventListener("categories:updated", () => {
  loadData() // Refresh category list
})
This ensures:
  • Transaction forms show updated category lists
  • Budget forms reflect new categories
  • All analytics recalculate automatically

Category Sections

The page displays categories in two distinct sections:

Categorías de Gastos

  • Shows all expense categories
  • Sorted by total spending (highest first)
  • Displays percentage of total expenses
  • Red color scheme

Categorías de Ingresos

  • Shows all income categories
  • Sorted by total income (highest first)
  • Displays percentage of total income
  • Green color scheme
const sections = [
  {
    title: "Categorías de Gastos",
    description: "Distribución de tus gastos por categoría.",
    data: categoryGroups.gastos,
    type: "gasto",
  },
  {
    title: "Categorías de Ingresos",
    description: "Resumen de dónde provienen tus ingresos.",
    data: categoryGroups.ingresos,
    type: "ingreso",
  },
]

BCV Currency Display

All category totals show BCV conversions:
const categoryBcv = formatBcvAmount(category.total)

// Display in card
{categoryBcv && (
  <p className="text-xs text-muted-foreground">≈ {categoryBcv} BCV</p>
)}

Transaction Counting

const transactionLabel = `${category.count} ${
  category.count === 1 ? "transacción" : "transacciones"
}`
Proper Spanish pluralization ensures professional presentation throughout the interface.

Best Practices

Create descriptive categories like “Supermercado” instead of generic “Comida”.
Too many categories make reporting complex. Aim for 8-12 main categories.
Always assign categories to transactions for accurate analytics.
Check category analytics monthly to identify spending patterns.
Consolidate overlapping categories by reassigning transactions then deleting.

Database Schema

CREATE TABLE categorias (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  nombre VARCHAR(13) NOT NULL,
  tipo VARCHAR(10) NOT NULL CHECK (tipo IN ('gasto', 'ingreso')),
  usuario_id UUID REFERENCES auth.users(id),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

CREATE INDEX idx_categorias_tipo ON categorias(tipo);
CREATE INDEX idx_categorias_usuario ON categorias(usuario_id);
Indexes on tipo and usuario_id ensure fast filtering when loading category lists.

Build docs developers (and LLMs) love