Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Davidmallega/Gastos-App/llms.txt
Use this file to discover all available pages before exploring further.
El estado global de GastosApp no usa Redux, Zustand ni React Context: vive en una variable de módulo (globalState) declarada a nivel de archivo en src/store/useStore.js. Este patrón singleton garantiza que todos los componentes que llamen a useStore() compartan exactamente el mismo estado en memoria, y que cada mutación persista de forma inmediata en localStorage y notifique a todos los consumidores registrados. La simplicidad intencional de este diseño refleja la naturaleza de GastosApp: una aplicación de escritorio de usuario único sin necesidad de concurrencia ni middleware.
Patrón singleton
El corazón del store son tres elementos a nivel de módulo:
let globalState = null // estado en memoria (único para toda la app)
let listeners = [] // callbacks de los componentes suscritos
La función setState es el único punto de escritura del estado. Acepta tanto un objeto parcial como una función actualizadora (el mismo patrón de useState):
function setState(updater) {
const prev = getState()
const next = typeof updater === 'function' ? updater(prev) : { ...prev, ...updater }
globalState = next
saveToStorage(next)
listeners.forEach(fn => fn(next))
}
- Se obtiene el estado previo (o se carga desde
localStorage si es la primera llamada).
- Se calcula el estado siguiente aplicando el updater.
- Se actualiza
globalState en memoria.
- Se persiste en
localStorage de forma síncrona e inmediata.
- Se notifica a todos los componentes suscritos para que re-rendericen.
getState() implementa lazy initialization: si globalState es null, llama a loadFromStorage(), que lee localStorage, aplica migraciones y purga la papelera antes de retornar el estado inicial.
Hook useStore()
El hook useStore() conecta el singleton al ciclo de vida de React:
export function useStore() {
const [state, setLocalState] = useState(getState)
useEffect(() => {
const listener = (s) => setLocalState({ ...s })
listeners.push(listener)
return () => { listeners = listeners.filter(l => l !== listener) }
}, [])
// ... funciones de mutación
return { ...state, addGastoCaja, updateFactura, ... }
}
Al montar el componente, useState(getState) inicializa el estado local con el valor actual del singleton (sin causar una lectura extra de localStorage). El useEffect registra un listener en el array global y lo desregistra al desmontar. Cada vez que cualquier componente llama a una función de mutación (como addFactura o deleteGastoCaja), setState persiste el cambio y ejecuta todos los listeners, lo que desencadena un re-render en todos los componentes que tengan useStore() montado.
Estado inicial
Si no hay datos previos en localStorage, el store se inicializa con defaultState:
const defaultState = {
gastosCaja: [],
facturas: [],
compromisos: [],
categorias: [
'Arriendo',
'Luz / Electricidad',
'Agua',
'Gas',
'Teléfono / Internet',
'Personal / Remuneraciones',
'Insumos / Materiales',
'Transporte',
'Mantención',
'Servicios Externos',
'Impuestos / Patentes',
'Seguros',
'Otros',
],
proveedores: [],
papelera: [],
}
Las 13 categorías por defecto cubren los rubros de gasto más comunes para una empresa chilena pequeña. El usuario puede agregar, renombrar o eliminar categorías desde el módulo de Categorías en la app.
Migraciones de esquema
La constante SCHEMA_VERSION = 4 define la versión actual del schema. Al cargar datos desde localStorage o al importar un backup, la función migrateData aplica secuencialmente todos los bloques de migración necesarios:
function migrateData(data) {
let d = { ...data }
const v = d.schemaVersion || 0
// v0 → v1: proveedores strings → objetos
if (v < 1 && d.proveedores?.length > 0 && typeof d.proveedores[0] === 'string') {
d.proveedores = d.proveedores.map((name, i) => ({
id: 'legacy_' + i, rut: '', razonSocial: name,
nombreFantasia: '', contacto: '', categoria: '', observaciones: '',
}))
}
// v1 → v2: agregar facturaRefId a facturas existentes
if (v < 2 && d.facturas) {
d.facturas = d.facturas.map(f => ({ facturaRefId: null, ...f }))
}
// v2 → v3: agregar papelera
if (v < 3) {
d.papelera = d.papelera || []
}
// v3 → v4: agregar plazoPago a proveedores existentes
if (v < 4 && d.proveedores) {
d.proveedores = d.proveedores.map(p => ({ plazoPago: null, ...p }))
}
d.schemaVersion = SCHEMA_VERSION
return d
}
Cada bloque de migración es idempotente y aditivo: solo agrega campos o transforma datos existentes, nunca elimina información. Esto garantiza que un backup creado con la v1 de la app pueda importarse correctamente en la v4 sin pérdida de datos.
| Versión | Cambio |
|---|
| v0 → v1 | proveedores era un array de strings; se convierte a array de objetos con id, rut, razonSocial, etc. Los proveedores migrados reciben IDs del tipo 'legacy_0', 'legacy_1'. |
| v1 → v2 | Se agrega el campo facturaRefId: null a todas las facturas existentes para soportar la vinculación de Notas de Crédito. |
| v2 → v3 | Se agrega el array papelera: [] para soportar el soft-delete. Los datos anteriores no tienen papelera, así que se inicializa vacía. |
| v3 → v4 | Se agrega plazoPago: null a todos los proveedores existentes para soportar el cálculo de fecha de vencimiento por proveedor. |
API completa del hook
useStore() retorna el estado completo del store junto con todas las funciones de mutación. Las funciones están memoizadas con useCallback (dependencias vacías) para no causar re-renders innecesarios en componentes hijos.
Gastos Caja
| Función | Descripción |
|---|
addGastoCaja(gasto) | Agrega un nuevo gasto de caja. Retorna el id generado. |
addGastosCaja(gastos[]) | Importación masiva (usada por el importador CSV). Agrega múltiples gastos en una sola mutación. |
updateGastoCaja(id, data) | Actualiza parcialmente un gasto por su id. |
deleteGastoCaja(id) | Mueve el gasto a papelera[] con deletedAt y deletedFrom: 'gastosCaja'. |
Facturas
| Función | Descripción |
|---|
addFactura(factura) | Agrega una nueva factura o NC. Retorna el id generado. |
addFacturas(facturas[]) | Importación masiva desde CSV SII. |
updateFactura(id, data) | Actualiza parcialmente una factura por su id. |
deleteFactura(id) | Mueve la factura a papelera[] con deletedFrom: 'facturas'. |
marcarPagado(tipo, id, metodoPago, fechaPago) | Marca un documento (caja o factura) como pagado. Si no se provee fechaPago, usa la fecha actual. |
revertirPago(tipo, id) | Revierte un pago: establece pagado: false y fechaPago: null. |
Categorías
| Función | Descripción |
|---|
addCategoria(nombre) | Agrega una categoría al array. Usa Set para evitar duplicados. |
updateCategoria(oldName, newName) | Renombra la categoría en el catálogo y en todos los proveedores. No actualiza documentos (eso lo hace cascadeCategoriaRename). |
deleteCategoria(nombre) | Elimina la categoría del catálogo. No afecta documentos existentes. |
cascadeCategoriaRename(oldName, newName, scope) | Actualiza la categoría en gastos y facturas. scope puede ser 'pending' (solo pendientes), 'all' (todos) o cualquier otro valor (ninguno). |
Proveedores
| Función | Descripción |
|---|
addProveedor(nameOrObj) | Acepta un string (crea proveedor mínimo) o un objeto completo. Previene duplicados comparando razón social y nombre fantasía en minúsculas. |
updateProveedor(id, data) | Actualiza parcialmente un proveedor. |
deleteProveedor(id) | Elimina el proveedor del directorio. No va a papelera. |
cascadeProveedorCategoria(prov, newCategoria, scope) | Actualiza la categoría en documentos asociados al proveedor. La coincidencia se hace por razón social, nombre fantasía o RUT. |
Compromisos
| Función | Descripción |
|---|
addCompromiso(comp) | Crea un nuevo compromiso con activo: true, ultimoRegistro: null y creadoEn con la fecha actual. Retorna el id. |
updateCompromiso(id, data) | Actualiza parcialmente un compromiso. |
deleteCompromiso(id) | Mueve el compromiso a papelera[] con deletedFrom: 'compromisos'. |
registrarCompromiso(id, overrides) | Genera un documento (GastoCaja o Factura) a partir del compromiso y actualiza ultimoRegistro y ultimoRegistroId. Si tipo es 'factura', calcula neto e IVA automáticamente. |
Papelera
| Función | Descripción |
|---|
restaurarDePapelera(id) | Mueve el ítem de vuelta a su array de origen (gastosCaja, facturas o compromisos) eliminando los campos deletedAt y deletedFrom. |
eliminarDePapelera(id) | Elimina permanentemente un ítem de la papelera. No se puede deshacer. |
vaciarPapelera() | Elimina permanentemente todos los ítems de papelera[]. |
Datos
| Función | Descripción |
|---|
exportData() | Serializa el estado completo como JSON y lo descarga como gastosapp_backup_YYYY-MM-DD.json. Incluye schemaVersion, exportedAt y appName en el payload. |
importData(raw) | Aplica migrateData al objeto importado y reemplaza el estado completo. Se usa desde el módulo de Respaldo. |
reloadData() | Recarga el estado desde localStorage y notifica a todos los listeners. Útil para sincronizar si los datos se modificaron externamente. |
Clave de almacenamiento
Todo el estado se persiste bajo una única clave de localStorage:
localStorage.getItem('gastos_app_data')
// Retorna el JSON completo del estado, incluyendo schemaVersion: 4
Para inspeccionar o depurar los datos directamente, abre las DevTools de Electron (disponibles en modo desarrollo) y ejecuta:
JSON.parse(localStorage.getItem('gastos_app_data'))
El objeto retornado contiene los arrays gastosCaja, facturas, compromisos, categorias, proveedores y papelera, además de schemaVersion: 4.
El límite de localStorage en Electron es aproximadamente 5 MB. Para empresas con muchos registros, se recomienda exportar backups periódicos desde el módulo de Respaldo. Si el volumen de datos crece significativamente, está documentada en la deuda técnica la migración a SQLite (better-sqlite3) como alternativa con mayor capacidad y rendimiento.