Camada de persistência
O SaldoMais não tem backend. Todos os dados ficam no localStorage do navegador sob o domínio/origem que serve os arquivos. Dados são perdidos se o usuário limpar o armazenamento do navegador ou abrir o app em outro dispositivo sem importar backup.
Limite prático: O localStorage suporta ~5–10 MB dependendo do navegador. Para o volume esperado (dezenas de categorias, centenas de lançamentos), esse limite não é uma preocupação real.
Acesso via helpers
const get = key => JSON.parse(localStorage.getItem(key)) || [];
const set = (key, val) => localStorage.setItem(key, JSON.stringify(val));
get sempre retorna um array — nunca null. Se o item não existir ou o JSON for inválido, retorna [].Chaves do localStorage
Definidas em core.js como STORAGE:
| Constante | Chave | Entidade |
|---|---|---|
STORAGE.categorias | saldomain_categorias | Categorias de gasto |
STORAGE.orcamentos | saldomain_orcamentos | Orçamentos mensais |
STORAGE.lancamentos | saldomain_lancamentos | Transações registradas |
STORAGE.receitas | saldomain_receitas | Receitas mensais |
STORAGE.gastosFixos | saldomain_gastos_fixos | Despesas recorrentes mensais |
CARTEIRA_STORAGE.carteiras | saldomain_carteiras | Carteiras de investimento |
CARTEIRA_STORAGE.ativa | saldomain_carteira_ativa | ID da carteira ativa (string) |
CARTEIRA_STORAGE.historico | saldomain_historico_aportes | Histórico de cálculos de aporte (máx 50) |
| — | saldomain_sidebar_collapsed | Estado de colapso da sidebar no desktop |
Entidade: Categoria
{
id: number, // timestamp Unix (Date.now() + índice na criação)
nome: string, // nome único (case-insensitive), máx. 30 caracteres
percentual: number, // inteiro 0-100, % do orçamento alocado
cor_hex: string // cor hexadecimal, ex: "#f59e0b"
}
Invariantes
- A soma dos percentuais de todas as categorias deve ser
100para que os limites sejam calculados corretamente. O app impede salvar percentuais que não somem 100. nomeé único (comparação case-insensitive). O app rejeita duplicatas na criação e edição.- Novas categorias criadas pelo usuário recebem
percentual: 0— o usuário deve ajustar manualmente os sliders.
Entidade: Orçamento
{
id: number, // timestamp Unix gerado na criação
mes_referencia: string, // formato "YYYY-MM", ex: "2026-04"
valor_total: number // valor em reais (float), ex: 5000.00
}
Invariantes
- Existe no máximo um orçamento por
mes_referencia. Se o usuário salvar o orçamento num mês que já tem registro, ovalor_totalé atualizado (upsert). resetarMes()zeravalor_totalpara0mas não remove o registro — o orçamento do mês ainda existe, com valor zero.
Entidade: Lançamento
{
id: number, // timestamp Unix gerado na criação
id_orcamento: number, // FK → Orçamento.id
id_categoria: number, // FK → Categoria.id
valor: number, // valor em reais (float), sempre > 0
descricao: string, // texto livre, obrigatório
data: string // data no formato "YYYY-MM-DD" (opcional em registros antigos)
}
Invariantes
id_orcamentoreferencia o orçamento do mês em que o lançamento foi criado.- Ao deletar uma categoria, todos os lançamentos com aquele
id_categoriasão removidos em cascata. - O campo
dataé preenchido automaticamente com a data atual na criação. Pode ser editado posteriormente viaeditarLancamentoHandler(). - Lançamentos são exibidos ordenados por
datadecrescente (mais recentes primeiro), com desempate porid. - Registros criados antes da introdução do campo
datanão terão essa propriedade — o código trata a ausência com fallback para string vazia.
Entidade: Receita
{
id: number, // timestamp Unix gerado na criação
descricao: string, // texto livre, obrigatório
valor: number, // valor em reais (float), sempre > 0
tipo: 'salario' | 'freelance' | 'rendimento' | 'outro',
data: string, // formato "YYYY-MM-DD"
mes_referencia: string // formato "YYYY-MM" — define o mês da receita
}
Invariantes
- Cada receita pertence a um mês via
mes_referencia. - A soma das receitas do mês atualiza automaticamente o
valor_totaldo orçamento ao adicionar ou remover uma receita (sincronizarOrcamentoComReceitas()). - Enquanto houver receitas, o campo de orçamento manual fica desabilitado — o orçamento é controlado pelas receitas.
Entidade: GastoFixo
{
id: number, // timestamp Unix gerado na criação
nome: string, // descrição da despesa recorrente
valor: number, // valor em reais (float), sempre > 0
id_categoria: number // FK → Categoria.id
}
Invariantes
- Gastos fixos são aplicados automaticamente no primeiro dia de cada mês como lançamentos normais.
- O lançamento gerado recebe o campo extra
id_gasto_fixocom o id do gasto fixo de origem, evitando aplicação duplicada no mesmo mês. - Não são afetados por
resetarMes()— persistem entre meses.
Entidade: Carteira
{
id: string, // timestamp Unix como string, gerado na criação
nome: string, // nome da carteira
ativos: { // mapa ativoId → percentual alocado (0–100)
[ativoId: string]: number
}
}
Invariantes
- A soma dos percentuais dos ativos marcados deve ser exatamente 100 para habilitar o botão de salvar.
- O catálogo de ativos (
CATALOGO) é fixo — 30 ativos em 5 classes. Não é editável pelo usuário. - A carteira ativa é armazenada separadamente como string em
saldomain_carteira_ativa. - Alerta de concentração exibido quando qualquer ativo supera 40% do total.
Entidade: HistoricoAporte
{
id: number, // timestamp Unix gerado na criação
data: string, // data formatada "DD/MM/YYYY"
carteira: string, // nome da carteira no momento do cálculo
valor: number, // valor total do aporte calculado
resumo: string // primeiros 3 ativos: "Nome: R$ X,XX · ..."
}
Invariantes
- Gerado automaticamente a cada clique em "Calcular Aporte".
- Limitado a 50 entradas — as mais antigas são descartadas quando o limite é atingido.
- Pode ser apagado completamente pelo botão "Limpar Histórico" na aba Histórico.
Relacionamentos
Categoria (1) ──── (N) Lançamento
Orçamento (1) ──── (N) Lançamento
Não há foreign key enforced — as relações são resolvidas em memória nas funções de render e cálculo, sempre via find() ou filter() nos arrays lidos do storage.
Exemplo: join para calcular gasto por categoria
const o = orcamentoAtual();
const cats = get(STORAGE.categorias);
const lanc = get(STORAGE.lancamentos).filter(l => l.id_orcamento === o.id);
cats.forEach(c => {
const limite = o.valor_total * c.percentual / 100;
const gasto = lanc
.filter(l => l.id_categoria === c.id)
.reduce((s, l) => s + l.valor, 0);
});
IDs com Date.now()
Todos os IDs são gerados via Date.now() no momento da criação. Isso garante unicidade na prática (sem concorrência num app single-user), mas não é um UUID.
Date.now() + índice.Formato do arquivo de backup
{
"_versao": 2,
"_exportado_em": "2026-04-18T14:30:00.000Z",
"categorias": [
{ "id": 1713369600000, "nome": "Custos fixos", "percentual": 30, "cor_hex": "#f59e0b" }
],
"orcamentos": [
{ "id": 1713369601000, "mes_referencia": "2026-04", "valor_total": 5000 }
],
"lancamentos": [
{ "id": 1713369602000, "id_orcamento": 1713369601000, "id_categoria": 1713369600000, "valor": 150, "descricao": "Mercado", "data": "2026-04-18" }
],
"receitas": [
{ "id": 1713369603000, "descricao": "Salário", "valor": 5000, "tipo": "salario", "data": "2026-04-05", "mes_referencia": "2026-04" }
]
}
Isolamento de dados por mês
O app não filtra dados históricos por padrão — todos os lançamentos de todos os meses ficam no mesmo array. O isolamento acontece sempre que se filtra por id_orcamento:
// Apenas lançamentos do mês atual:
get(STORAGE.lancamentos).filter(l => l.id_orcamento === orcamentoAtual().id)
Isso significa que get(STORAGE.lancamentos) cresce indefinidamente com o tempo. Não há mecanismo de arquivamento automático — apenas o backup/restore e o resetarMes() (que afeta somente o mês atual).