Targ Apps Docs

Store global

FinanceProvider, settings, toasts y persistencia Chroma-Fs (TAF-004).

Visión general

logic/store/financeStore.js expone el estado financiero mediante createContext / useContext y signals de Chroma (hookState). Es el puente entre la persistencia (persistencia) y la UI.

Chroma-Fs (finance/snapshot.json)
       ↕ logic/persist.js (cache síncrono)
repository CRUD  +  settings
FinanceProvider / SettingsProvider
       ↕ useFinance() / useSettings()
pages/ y components/

Bootstrap

En src/main.js se hidrata antes de importar App. En src/App.js, los providers usan children como función:

e(ToastProvider, null, () =>
  e(SettingsProvider, null, () =>
    e(FinanceProvider, null, () => e(AppShell))
  )
);

AppShell vive dentro de FinanceProvider. Las páginas se montan una sola vez con Show (no con un child reactivo () => e(resolvePage(route()))): Chroma hace pop del contexto al terminar los children del Provider, y un binding que re-invoca la página (p. ej. AccountForm onInput) llamaría useFinance() fuera de esa ventana.

El valor de contexto es un objeto plano con getters de signal (no el getter que devuelve useMemo: en Chroma useMemo retorna un getter, y destructurar const { addAccount } = useFinance() sobre una función deja addAccount en undefined).

El guard de onboarding corre en FinanceProvider y useRouter vía syncOnboardingRoute() (hash síncrono al montar + hashchange + useEffect sobre accounts()).

El toast global se registra en ToastProvider vía useEffect y un dispatcher de módulo (globalPushToast); showToast() en stores/páginas delega ahí sin recursión.

API de useFinance()

Colecciones (getters reactivos)

GetterTipoDescripción
transactions() => Transaction[]Todas las transacciones
categories() => Category[]Categorías de ingreso/gasto
accounts() => Account[]Cuentas (incluye archivadas)
activeAccounts / archivedAccounts() => Account[]Filtros de dominio
defaultAccount() => Account | nullCuenta activa con isDefault, o la primera activa
resolveTransactionAccountId(accountId?)() => stringCuenta para transacciones: explícita → default → lastUsedAccountId → primera activa
resolveTransferAccountIds(from?, to?)() => { fromAccountId, toAccountId }Origen = default; destino = segunda activa si falta
lastUsedAccountId() => string | nullÚltima cuenta usada al guardar un movimiento (settings)
budgets() => Budget[]Presupuestos por categoría

Cada getter es el signal de hookState: invocar transactions() dentro de children reactivos o props getter para suscribirse a cambios.

Acciones CRUD

Por entidad: add*, update*, remove* (cuentas también archiveAccount / restoreAccount / setDefaultAccount). Cada acción:

  1. Valida (logic/models.js → logic/validation.js para nombre/monto/moneda)
  2. Persiste vía repositorio → setPersisted → Chroma-Fs
  3. Actualiza el signal y dispara showToast (éxito o error)

Tras el primer addAccount válido se llama markOnboardingComplete().

Movimientos de dinero: los cuerpos están en ledger.js / transactions.js. El provider solo persiste y toasts.

AcciónNotas
addTransactionIngreso/gasto. Si type === "transfer", delega en addTransfer. Valida contra cuentas activas y persiste lastUsedAccountId.
addTransfer(from, to, amount, date, note?)Crea el par out/in. También acepta un objeto. Valida cuentas activas y persiste lastUsedAccountId del origen.
updateTransactionSi es transfer, sincroniza monto/fecha/cuentas en ambas piernas.
removeTransactionSi es transfer, borra las dos piernas.

Los errores de validación devuelven { valid: false, errors }. En movimientos, firstValidationError() (logic/validation.js) alimenta el toast con el primer mensaje accionable (p. ej. monto inválido, cuenta archivada, monedas distintas).

Selectores derivados (useMemo)

SelectorDescripción
balanceByCurrencyNet worth por moneda (ledger.computeTotalBalanceByCurrency sobre cuentas activas)
monthlyTotals{ income, expense, net } del mes actual; excluye transfer
topExpenseCategoriesTop 5 categorías de gasto del mes
recentTransactionsÚltimas 5 transacciones (incluye transfers)

También: getAccountBalance (saldo derivado), getBudgetSpent, getBudgetProgress, refreshFromStorage (tras import de backup).

Settings API

logic/settings.js (reglas, sin Chroma) + logic/store/settingsStore.js (provider). Persistido en el snapshot bajo settings.

const { settings, updateSettings, defaultCurrency, locale } = useSettings();

updateSettings({ defaultCurrency: "ARS" });
// { valid: true, settings } | { valid: false, errors }
CampoDefaultRegla
defaultCurrencyUSDDebe estar en COMMON_CURRENCIES
localeesString 2–16 caracteres (BCP-47 corto)
weekStartsOn10 domingo o 1 lunes
themesystemlight | dark | system
lastUsedAccountIdnullId de la última cuenta usada en un movimiento guardado (opcional)

TransactionForm y TransferForm preseleccionan la cuenta con resolveTransactionAccountId / resolveTransferAccountIds. Con una sola cuenta activa el selector queda deshabilitado. La preselección en el formulario usa useEffect con deps explícitas ([accounts, autoDefaultAccount, lastUsedAccountId]) para no escribir en señales mientras el efecto aún se evalúa (evita el warning de update loop de Chroma). useMemo de validación depende de los getters de los campos y de autoDefaultAccount / lastUsedAccountId; canSubmit usa optional chaining (validation()?.valid ?? false) hasta que el memo inicialice.

normalizeSettings() corrige valores inválidos a los defaults. validateSettings() devuelve { valid, errors } sin lanzar. updateSettings muestra un toast de error si la validación falla. La página de ajustes muestra el toast de éxito solo cuando result.valid es verdadero.

Exportados desde logic/store/index.js: DEFAULT_SETTINGS, normalizeSettings, validateSettings, ALLOWED_THEMES, ALLOWED_WEEK_STARTS.

Toast API

logic/store/toastStore.js:

ExportUso
ToastProviderEnvuelve la app
useToast(){ toasts, showToast, dismissToast }
showToast(message, variant)Dispatch global (success | error | info)

TTL 4s. Variante en data-variant. El CRUD de finanzas y el save de preferencias usan esta API.

Hidratación

  1. hydratePersist() carga finance/snapshot.json (o fallback)
  2. seedIfEmpty("categories", defaultCategories) si la colección está vacía
  3. hookState(repo.getAll()) por colección

Las mutaciones escriben en el cache y encolan el flush; no hace falta recargar.

Guard fuera del provider

export function useFinance() {
  const ctx = useContext(FinanceContext);
  if (!ctx) {
    throw new Error("useFinance() debe usarse dentro de <FinanceProvider>.");
  }
  return ctx;
}

On this page