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)
| Getter | Tipo | Descripció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 | null | Cuenta activa con isDefault, o la primera activa |
resolveTransactionAccountId(accountId?) | () => string | Cuenta 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:
- Valida (
logic/models.js→logic/validation.jspara nombre/monto/moneda) - Persiste vía repositorio →
setPersisted→ Chroma-Fs - 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ón | Notas |
|---|---|
addTransaction | Ingreso/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. |
updateTransaction | Si es transfer, sincroniza monto/fecha/cuentas en ambas piernas. |
removeTransaction | Si 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)
| Selector | Descripción |
|---|---|
balanceByCurrency | Net worth por moneda (ledger.computeTotalBalanceByCurrency sobre cuentas activas) |
monthlyTotals | { income, expense, net } del mes actual; excluye transfer |
topExpenseCategories | Top 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 }| Campo | Default | Regla |
|---|---|---|
defaultCurrency | USD | Debe estar en COMMON_CURRENCIES |
locale | es | String 2–16 caracteres (BCP-47 corto) |
weekStartsOn | 1 | 0 domingo o 1 lunes |
theme | system | light | dark | system |
lastUsedAccountId | null | Id 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:
| Export | Uso |
|---|---|
ToastProvider | Envuelve 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
hydratePersist()cargafinance/snapshot.json(o fallback)seedIfEmpty("categories", defaultCategories)si la colección está vacíahookState(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;
}