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
Gasto (Expense)
Ingreso (Income)
Categories for outgoing money:
Services (utilities, subscriptions)
Food & Dining
Transportation
Entertainment
Healthcare
Shopping
Categories for incoming money:
Salary
Freelance/Contract work
Investments
Gifts
Business revenue
Other income sources
Creating Categories
Click 'Nueva Categoría'
Open the category creation dialog from the categories page.
Enter Name
Provide a descriptive name (maximum 13 characters).
Select Type
Choose either “Gasto” (expense) or “Ingreso” (income).
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
Open Options Menu
Click the three-dot menu icon on any category card.
Select 'Editar'
Choose the edit option from the dropdown menu.
Update Fields
Modify the category name or type as needed.
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.