6 Commits

Author SHA1 Message Date
Felipe Coutinho
60a52b9873 fix(inbox): alinhar horario da tooltip do card 2026-03-21 19:42:55 +00:00
Felipe Coutinho
c9205f2be9 style(drizzle): normalizar snapshots gerados 2026-03-21 19:32:49 +00:00
Felipe Coutinho
1d36b12109 style: normalizar formatacao de importacao e suporte 2026-03-21 19:32:38 +00:00
Felipe Coutinho
19a1b1e943 chore(release): preparar versao 2.0.1 2026-03-21 19:31:53 +00:00
Felipe Coutinho
d3fc81db73 fix(inbox): melhorar filtros e identidade visual 2026-03-21 19:31:38 +00:00
Felipe Coutinho
80de9501f6 fix: move proxy.ts para src/ e atualiza dependências
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:52:20 +00:00
27 changed files with 5643 additions and 5581 deletions

View File

@@ -5,6 +5,16 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [2.0.1] - 2026-03-21
### Corrigido
- Inbox: filtro por app em `/inbox` agora monta a lista completa de apps da aba a partir de todos os itens do status atual, sem depender apenas da página carregada, e o SSR deixa de quebrar quando `sourceApps` vier inconsistente
- Inbox: notificações de cartões/apps sem logo cadastrado agora exibem `default_icon.png` como fallback visual nos cards
- Inbox: select de apps em `/inbox` agora exibe os logos dos apps/cartões, com fallback para `default_icon.png` quando não houver logo mapeado
- Inbox: cabeçalhos de data entre grupos de cards agora exibem ícone e tipografia um pouco maior para melhorar a leitura
- Versionamento: `/api/health` passa a reportar a versão atual do `package.json`, evitando divergência entre healthcheck, UI e release publicada
## [2.0.0] - 2026-03-21
### Adicionado

View File

@@ -93,12 +93,8 @@
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -213,12 +209,8 @@
"name": "tokens_api_user_id_user_id_fk",
"tableFrom": "tokens_api",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -327,12 +319,8 @@
"name": "orcamentos_user_id_user_id_fk",
"tableFrom": "orcamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -340,12 +328,8 @@
"name": "orcamentos_categoria_id_categorias_id_fk",
"tableFrom": "orcamentos",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -463,12 +447,8 @@
"name": "cartoes_user_id_user_id_fk",
"tableFrom": "cartoes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -476,12 +456,8 @@
"name": "cartoes_conta_id_contas_id_fk",
"tableFrom": "cartoes",
"tableTo": "contas",
"columnsFrom": [
"conta_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["conta_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -563,12 +539,8 @@
"name": "categorias_user_id_user_id_fk",
"tableFrom": "categorias",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -683,12 +655,8 @@
"name": "contas_user_id_user_id_fk",
"tableFrom": "contas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -847,12 +815,8 @@
"name": "pre_lancamentos_user_id_user_id_fk",
"tableFrom": "pre_lancamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -860,12 +824,8 @@
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
"tableFrom": "pre_lancamentos",
"tableTo": "lancamentos",
"columnsFrom": [
"lancamento_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["lancamento_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1005,12 +965,8 @@
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "lancamentos",
"columnsFrom": [
"lancamento_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["lancamento_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1018,12 +974,8 @@
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1031,12 +983,8 @@
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1044,12 +992,8 @@
"name": "antecipacoes_parcelas_user_id_user_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1179,12 +1123,8 @@
"name": "faturas_user_id_user_id_fk",
"tableFrom": "faturas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1192,12 +1132,8 @@
"name": "faturas_cartao_id_cartoes_id_fk",
"tableFrom": "faturas",
"tableTo": "cartoes",
"columnsFrom": [
"cartao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cartao_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -1271,12 +1207,8 @@
"name": "anotacoes_user_id_user_id_fk",
"tableFrom": "anotacoes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1364,12 +1296,8 @@
"name": "passkey_userId_user_id_fk",
"tableFrom": "passkey",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1452,12 +1380,8 @@
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1465,12 +1389,8 @@
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "user",
"columnsFrom": [
"shared_with_user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["shared_with_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1478,12 +1398,8 @@
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "user",
"columnsFrom": [
"created_by_user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["created_by_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1639,12 +1555,8 @@
"name": "pagadores_user_id_user_id_fk",
"tableFrom": "pagadores",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1733,12 +1645,8 @@
"name": "insights_salvos_user_id_user_id_fk",
"tableFrom": "insights_salvos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1808,12 +1716,8 @@
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1823,9 +1727,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -2228,12 +2130,8 @@
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
"tableFrom": "lancamentos",
"tableTo": "antecipacoes_parcelas",
"columnsFrom": [
"antecipacao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["antecipacao_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2241,12 +2139,8 @@
"name": "lancamentos_user_id_user_id_fk",
"tableFrom": "lancamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2254,12 +2148,8 @@
"name": "lancamentos_cartao_id_cartoes_id_fk",
"tableFrom": "lancamentos",
"tableTo": "cartoes",
"columnsFrom": [
"cartao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cartao_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2267,12 +2157,8 @@
"name": "lancamentos_conta_id_contas_id_fk",
"tableFrom": "lancamentos",
"tableTo": "contas",
"columnsFrom": [
"conta_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["conta_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2280,12 +2166,8 @@
"name": "lancamentos_categoria_id_categorias_id_fk",
"tableFrom": "lancamentos",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2293,12 +2175,8 @@
"name": "lancamentos_pagador_id_pagadores_id_fk",
"tableFrom": "lancamentos",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -2363,9 +2241,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -2443,12 +2319,8 @@
"name": "preferencias_usuario_user_id_user_id_fk",
"tableFrom": "preferencias_usuario",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2458,9 +2330,7 @@
"preferencias_usuario_user_id_unique": {
"name": "preferencias_usuario_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},

View File

@@ -93,12 +93,8 @@
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -213,12 +209,8 @@
"name": "tokens_api_user_id_user_id_fk",
"tableFrom": "tokens_api",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -327,12 +319,8 @@
"name": "orcamentos_user_id_user_id_fk",
"tableFrom": "orcamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -340,12 +328,8 @@
"name": "orcamentos_categoria_id_categorias_id_fk",
"tableFrom": "orcamentos",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -463,12 +447,8 @@
"name": "cartoes_user_id_user_id_fk",
"tableFrom": "cartoes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -476,12 +456,8 @@
"name": "cartoes_conta_id_contas_id_fk",
"tableFrom": "cartoes",
"tableTo": "contas",
"columnsFrom": [
"conta_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["conta_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -563,12 +539,8 @@
"name": "categorias_user_id_user_id_fk",
"tableFrom": "categorias",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -683,12 +655,8 @@
"name": "contas_user_id_user_id_fk",
"tableFrom": "contas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -847,12 +815,8 @@
"name": "pre_lancamentos_user_id_user_id_fk",
"tableFrom": "pre_lancamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -860,12 +824,8 @@
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
"tableFrom": "pre_lancamentos",
"tableTo": "lancamentos",
"columnsFrom": [
"lancamento_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["lancamento_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1005,12 +965,8 @@
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "lancamentos",
"columnsFrom": [
"lancamento_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["lancamento_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1018,12 +974,8 @@
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1031,12 +983,8 @@
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1044,12 +992,8 @@
"name": "antecipacoes_parcelas_user_id_user_id_fk",
"tableFrom": "antecipacoes_parcelas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1179,12 +1123,8 @@
"name": "faturas_user_id_user_id_fk",
"tableFrom": "faturas",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1192,12 +1132,8 @@
"name": "faturas_cartao_id_cartoes_id_fk",
"tableFrom": "faturas",
"tableTo": "cartoes",
"columnsFrom": [
"cartao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cartao_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -1271,12 +1207,8 @@
"name": "anotacoes_user_id_user_id_fk",
"tableFrom": "anotacoes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1364,12 +1296,8 @@
"name": "passkey_userId_user_id_fk",
"tableFrom": "passkey",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1452,12 +1380,8 @@
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1465,12 +1389,8 @@
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "user",
"columnsFrom": [
"shared_with_user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["shared_with_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1478,12 +1398,8 @@
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
"tableFrom": "compartilhamentos_pagador",
"tableTo": "user",
"columnsFrom": [
"created_by_user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["created_by_user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1639,12 +1555,8 @@
"name": "pagadores_user_id_user_id_fk",
"tableFrom": "pagadores",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1733,12 +1645,8 @@
"name": "insights_salvos_user_id_user_id_fk",
"tableFrom": "insights_salvos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1808,12 +1716,8 @@
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1823,9 +1727,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -2228,12 +2130,8 @@
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
"tableFrom": "lancamentos",
"tableTo": "antecipacoes_parcelas",
"columnsFrom": [
"antecipacao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["antecipacao_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2241,12 +2139,8 @@
"name": "lancamentos_user_id_user_id_fk",
"tableFrom": "lancamentos",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2254,12 +2148,8 @@
"name": "lancamentos_cartao_id_cartoes_id_fk",
"tableFrom": "lancamentos",
"tableTo": "cartoes",
"columnsFrom": [
"cartao_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cartao_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2267,12 +2157,8 @@
"name": "lancamentos_conta_id_contas_id_fk",
"tableFrom": "lancamentos",
"tableTo": "contas",
"columnsFrom": [
"conta_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["conta_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2280,12 +2166,8 @@
"name": "lancamentos_categoria_id_categorias_id_fk",
"tableFrom": "lancamentos",
"tableTo": "categorias",
"columnsFrom": [
"categoria_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["categoria_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
},
@@ -2293,12 +2175,8 @@
"name": "lancamentos_pagador_id_pagadores_id_fk",
"tableFrom": "lancamentos",
"tableTo": "pagadores",
"columnsFrom": [
"pagador_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["pagador_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "cascade"
}
@@ -2363,9 +2241,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -2443,12 +2319,8 @@
"name": "preferencias_usuario_user_id_user_id_fk",
"tableFrom": "preferencias_usuario",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2458,9 +2330,7 @@
"preferencias_usuario_user_id_unique": {
"name": "preferencias_usuario_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -2552,12 +2422,8 @@
"name": "import_category_mappings_user_id_user_id_fk",
"tableFrom": "import_category_mappings",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2565,12 +2431,8 @@
"name": "import_category_mappings_category_id_categorias_id_fk",
"tableFrom": "import_category_mappings",
"tableTo": "categorias",
"columnsFrom": [
"category_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["category_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2578,10 +2440,7 @@
"compositePrimaryKeys": {
"import_category_mappings_user_id_description_key_pk": {
"name": "import_category_mappings_user_id_description_key_pk",
"columns": [
"user_id",
"description_key"
]
"columns": ["user_id", "description_key"]
}
},
"uniqueConstraints": {},

View File

@@ -1,6 +1,6 @@
{
"name": "openmonetis",
"version": "2.0.0",
"version": "2.0.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
@@ -28,9 +28,9 @@
"backup": "bash scripts/backup.sh"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.62",
"@ai-sdk/google": "^3.0.51",
"@ai-sdk/openai": "^3.0.46",
"@ai-sdk/anthropic": "^3.0.63",
"@ai-sdk/google": "^3.0.52",
"@ai-sdk/openai": "^3.0.47",
"@better-auth/passkey": "^1.5.5",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -60,7 +60,7 @@
"@tanstack/react-virtual": "^3.13.23",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.127",
"ai": "^6.0.134",
"better-auth": "1.5.5",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "0.7.1",
@@ -90,12 +90,12 @@
"@tailwindcss/postcss": "4.2.2",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "25.5.0",
"@types/pg": "^8.18.0",
"@types/pg": "^8.20.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"dotenv": "^17.3.1",
"drizzle-kit": "0.31.10",
"tailwindcss": "4.2.1",
"tailwindcss": "4.2.2",
"tsx": "4.21.0",
"typescript": "5.9.3"
}

113
pnpm-lock.yaml generated
View File

@@ -9,17 +9,17 @@ importers:
.:
dependencies:
'@ai-sdk/anthropic':
specifier: ^3.0.62
version: 3.0.62(zod@4.3.6)
specifier: ^3.0.63
version: 3.0.63(zod@4.3.6)
'@ai-sdk/google':
specifier: ^3.0.51
version: 3.0.51(zod@4.3.6)
specifier: ^3.0.52
version: 3.0.52(zod@4.3.6)
'@ai-sdk/openai':
specifier: ^3.0.46
version: 3.0.46(zod@4.3.6)
specifier: ^3.0.47
version: 3.0.47(zod@4.3.6)
'@better-auth/passkey':
specifier: ^1.5.5
version: 1.5.5(933ec2e58dee4f4ee115209e94e4bc6e)
version: 1.5.5(67982ffe784494dfa72893bff94217f8)
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -31,7 +31,7 @@ importers:
version: 3.2.2(react@19.2.4)
'@openrouter/ai-sdk-provider':
specifier: ^2.3.3
version: 2.3.3(ai@6.0.129(zod@4.3.6))(zod@4.3.6)
version: 2.3.3(ai@6.0.134(zod@4.3.6))(zod@4.3.6)
'@radix-ui/react-alert-dialog':
specifier: 1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -105,11 +105,11 @@ importers:
specifier: ^2.0.0
version: 2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
ai:
specifier: ^6.0.127
version: 6.0.129(zod@4.3.6)
specifier: ^6.0.134
version: 6.0.134(zod@4.3.6)
better-auth:
specifier: 1.5.5
version: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
canvas-confetti:
specifier: ^1.9.4
version: 1.9.4
@@ -127,7 +127,7 @@ importers:
version: 4.1.0
drizzle-orm:
specifier: 0.45.1
version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
jspdf:
specifier: ^4.2.1
version: 4.2.1
@@ -190,8 +190,8 @@ importers:
specifier: 25.5.0
version: 25.5.0
'@types/pg':
specifier: ^8.18.0
version: 8.18.0
specifier: ^8.20.0
version: 8.20.0
'@types/react':
specifier: 19.2.14
version: 19.2.14
@@ -205,8 +205,8 @@ importers:
specifier: 0.31.10
version: 0.31.10
tailwindcss:
specifier: 4.2.1
version: 4.2.1
specifier: 4.2.2
version: 4.2.2
tsx:
specifier: 4.21.0
version: 4.21.0
@@ -216,32 +216,32 @@ importers:
packages:
'@ai-sdk/anthropic@3.0.62':
resolution: {integrity: sha512-CkShXR8tmNO7QQnvpKbSMe2Vr1zUUcpqlp69iR+DYrbHm+tDJO9u6zZsjEHjcoRU9/e9z++p0W6NiuLC3aZ4Bg==}
'@ai-sdk/anthropic@3.0.63':
resolution: {integrity: sha512-SiLosFr0FfKfrNpAAj8mD/i3S5YBB/z5orb1DH3pN1yATuBNjjPMLnRE4P3Dn7Y5cQsro0uzw5g5117hkShWoQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.75':
resolution: {integrity: sha512-7Hwa0VdH+l85NFS7zqZhRRaiwZMStDxEwUoTPxPNEH6V0Vgw9wi9OGopIsYdywmfSOPfSAsPL8XXPAuaSLGchw==}
'@ai-sdk/gateway@3.0.77':
resolution: {integrity: sha512-UdwIG2H2YMuntJQ5L+EmED5XiwnlvDT3HOmKfVFxR4Nq/RSLFA/HcchhwfNXHZ5UJjyuL2VO0huLbWSZ9ijemQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/google@3.0.51':
resolution: {integrity: sha512-S5pG/iRt+E12a4TSnquBFnkHkbS+rcAJ2lRzds59vdnVqTsZGGIncaLefpGmq/MZNfbSo6JIO60duoZIpZXOqg==}
'@ai-sdk/google@3.0.52':
resolution: {integrity: sha512-HiFB4VlHnv55k9xIbgQW9tHw5OsLXzbAghnDUqrnk/S94QpQuyrDwLSDsk/tUkxJeT00B+wvhL1y6/SARdLeXw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@3.0.46':
resolution: {integrity: sha512-8grBb4sAMU0MAC6uOOD/wP/+SyX/3MMS/Lf+ToGgeUzoF9oWU9dWBLkvgjXpn7Ro81bPDeycW2GCCT63V/Vnvg==}
'@ai-sdk/openai@3.0.47':
resolution: {integrity: sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.20':
resolution: {integrity: sha512-gpUIj9uDhIGbuo9afKEgQ074BWmhvK4THJAAeBjRnroTy2yQYo6rbtGD7pQDMZM8ouXPYmT/SCdkWVJ0KcpX8A==}
'@ai-sdk/provider-utils@4.0.21':
resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -2240,8 +2240,8 @@ packages:
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/pg@8.18.0':
resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==}
'@types/pg@8.20.0':
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -2329,8 +2329,8 @@ packages:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ai@6.0.129:
resolution: {integrity: sha512-5nGckqbzwUBZD7wV9jsA8qaoYRwGpU9LVMtXD+ZrxSi2H6QNjpbrhsuuEBKS9xcMYevCviVNoFzpmSUWzn45Hw==}
ai@6.0.134:
resolution: {integrity: sha512-YalNEaavld/kE444gOcsMKXdVVRGEe0SK77fAFcWYcqLg+a7xKnEet8bdfrEAJTfnMjj01rhgrIL10903w1a5Q==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -3380,9 +3380,6 @@ packages:
tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwindcss@4.2.1:
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
tailwindcss@4.2.2:
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
@@ -3514,32 +3511,32 @@ packages:
snapshots:
'@ai-sdk/anthropic@3.0.62(zod@4.3.6)':
'@ai-sdk/anthropic@3.0.63(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6)
'@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6
'@ai-sdk/gateway@3.0.75(zod@4.3.6)':
'@ai-sdk/gateway@3.0.77(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6)
'@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
'@vercel/oidc': 3.1.0
zod: 4.3.6
'@ai-sdk/google@3.0.51(zod@4.3.6)':
'@ai-sdk/google@3.0.52(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6)
'@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6
'@ai-sdk/openai@3.0.46(zod@4.3.6)':
'@ai-sdk/openai@3.0.47(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6)
'@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6
'@ai-sdk/provider-utils@4.0.20(zod@4.3.6)':
'@ai-sdk/provider-utils@4.0.21(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@standard-schema/spec': 1.1.0
@@ -3577,12 +3574,12 @@ snapshots:
nanostores: 1.1.1
zod: 4.3.6
'@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))':
'@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))':
dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
optionalDependencies:
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
'@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)':
dependencies:
@@ -3601,14 +3598,14 @@ snapshots:
'@better-auth/utils': 0.3.1
mongodb: 7.1.0
'@better-auth/passkey@1.5.5(933ec2e58dee4f4ee115209e94e4bc6e)':
'@better-auth/passkey@1.5.5(67982ffe784494dfa72893bff94217f8)':
dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@simplewebauthn/browser': 13.2.2
'@simplewebauthn/server': 13.2.3
better-auth: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
better-auth: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
better-call: 1.3.2(zod@4.3.6)
nanostores: 1.1.1
zod: 4.3.6
@@ -4146,9 +4143,9 @@ snapshots:
'@noble/hashes@2.0.1': {}
'@openrouter/ai-sdk-provider@2.3.3(ai@6.0.129(zod@4.3.6))(zod@4.3.6)':
'@openrouter/ai-sdk-provider@2.3.3(ai@6.0.134(zod@4.3.6))(zod@4.3.6)':
dependencies:
ai: 6.0.129(zod@4.3.6)
ai: 6.0.134(zod@4.3.6)
zod: 4.3.6
'@opentelemetry/api@1.9.0': {}
@@ -5305,7 +5302,7 @@ snapshots:
'@types/pako@2.0.4': {}
'@types/pg@8.18.0':
'@types/pg@8.20.0':
dependencies:
'@types/node': 25.5.0
pg-protocol: 1.13.0
@@ -5347,11 +5344,11 @@ snapshots:
adler-32@1.3.1: {}
ai@6.0.129(zod@4.3.6):
ai@6.0.134(zod@4.3.6):
dependencies:
'@ai-sdk/gateway': 3.0.75(zod@4.3.6)
'@ai-sdk/gateway': 3.0.77(zod@4.3.6)
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6)
'@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
'@opentelemetry/api': 1.9.0
zod: 4.3.6
@@ -5378,10 +5375,10 @@ snapshots:
baseline-browser-mapping@2.10.0: {}
better-auth@1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
better-auth@1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))
'@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)
'@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)
'@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)
@@ -5400,7 +5397,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
drizzle-kit: 0.31.10
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
mongodb: 7.1.0
mysql2: 3.15.3
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -5607,12 +5604,12 @@ snapshots:
esbuild: 0.25.12
tsx: 4.21.0
drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)):
drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)):
optionalDependencies:
'@electric-sql/pglite': 0.3.15
'@opentelemetry/api': 1.9.0
'@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
'@types/pg': 8.18.0
'@types/pg': 8.20.0
kysely: 0.28.11
mysql2: 3.15.3
pg: 8.20.0
@@ -6397,8 +6394,6 @@ snapshots:
tailwind-merge@3.5.0: {}
tailwindcss@4.2.1: {}
tailwindcss@4.2.2: {}
tapable@2.3.0: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,6 +1,7 @@
import { InboxPage } from "@/features/inbox/components/inbox-page";
import {
type ResolvedInboxSearchParams,
resolveInboxApp,
resolveInboxPagination,
resolveInboxStatus,
} from "@/features/inbox/page-helpers";
@@ -8,6 +9,7 @@ import {
fetchAppLogoMap,
fetchInboxDialogData,
fetchInboxItemsPage,
fetchInboxSourceApps,
fetchInboxStatusCounts,
} from "@/features/inbox/queries";
import { getUserId } from "@/shared/lib/auth/server";
@@ -32,21 +34,31 @@ export default async function Page({ searchParams }: PageProps) {
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const activeStatus = resolveInboxStatus(resolvedSearchParams);
const activeApp = resolveInboxApp(resolvedSearchParams);
const paginationInput = resolveInboxPagination(resolvedSearchParams);
const [itemsPage, counts, dialogData, appLogoMap] = await Promise.all([
fetchInboxItemsPage(userId, activeStatus, paginationInput),
const [itemsPage, counts, sourceApps, dialogData, appLogoMap] =
await Promise.all([
fetchInboxItemsPage(userId, activeStatus, {
...paginationInput,
sourceApp: activeApp,
}),
fetchInboxStatusCounts(userId),
fetchInboxSourceApps(userId, activeStatus).catch(() => [] as string[]),
activeStatus === "pending"
? fetchInboxDialogData(userId)
: Promise.resolve(EMPTY_DIALOG_DATA),
fetchAppLogoMap(userId),
]);
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
return (
<main className="flex flex-col items-start gap-6">
<InboxPage
activeStatus={activeStatus}
activeApp={activeApp}
sourceApps={normalizedSourceApps}
items={itemsPage.items}
counts={counts}
pagination={itemsPage.pagination}

View File

@@ -1,14 +1,25 @@
import { ImportPage } from "@/features/transactions/components/import/import-page";
import {
buildOptionSets,
buildSluggedFilters,
} from "@/features/transactions/page-helpers";
import { fetchTransactionFilterSources } from "@/features/transactions/queries";
import { buildOptionSets, buildSluggedFilters } from "@/features/transactions/page-helpers";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
const userId = await getUserId();
const filterSources = await fetchTransactionFilterSources(userId);
const sluggedFilters = buildSluggedFilters(filterSources);
const { payerOptions, accountOptions, cardOptions, categoryOptions, defaultPayerId } =
buildOptionSets({ ...sluggedFilters, payerRows: filterSources.payerRows });
const {
payerOptions,
accountOptions,
cardOptions,
categoryOptions,
defaultPayerId,
} = buildOptionSets({
...sluggedFilters,
payerRows: filterSources.payerRows,
});
return (
<main className="flex flex-col gap-6">

View File

@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import { version as APP_VERSION } from "@/package.json";
import { db } from "@/shared/lib/db";
const APP_VERSION = "1.0.0";
/**
* Health check endpoint para Docker, monitoring e OpenMonetis Companion
* GET /api/health

View File

@@ -48,8 +48,7 @@ const accountBaseSchema = z.object({
.string({ message: "Selecione um logo." })
.trim()
.min(1, "Selecione um logo."),
initialBalance: z
.union([
initialBalance: z.union([
z.number(),
z
.string()

View File

@@ -20,16 +20,15 @@ import {
CardTitle,
} from "@/shared/components/ui/card";
import { Checkbox } from "@/shared/components/ui/checkbox";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { resolveLogoSrc } from "@/shared/lib/logo";
import type { InboxItem } from "./types";
// O timestamp vem do app Android em horário local mas salvo como UTC.
// Adicionamos o offset de Brasília para corrigir o cálculo de "há X tempo".
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
function adjustToBrasilia(date: Date): Date {
return new Date(date.getTime() + BRASILIA_OFFSET_MS);
}
const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
function findMatchingLogo(
sourceAppName: string | null,
@@ -78,17 +77,19 @@ export function InboxCard({
const matchedLogo = appLogoMap
? findMatchingLogo(item.sourceAppName, appLogoMap)
: null;
const displayLogo = matchedLogo ?? DEFAULT_INBOX_APP_LOGO;
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
const rawDate = new Date(item.notificationTimestamp);
const notificationDate = adjustToBrasilia(rawDate);
const createdAtDate = new Date(item.createdAt);
const timeAgo = formatDistanceToNow(notificationDate, {
const timeAgo = formatDistanceToNow(createdAtDate, {
addSuffix: true,
locale: ptBR,
});
const fullDate = format(createdAtDate, "PPpp", { locale: ptBR });
const statusDate =
item.status === "processed"
? item.processedAt
@@ -107,21 +108,32 @@ export function InboxCard({
<CardHeader className="pt-4">
<div className="flex items-center justify-between gap-2">
<CardTitle className="flex min-w-0 items-center gap-1.5 text-sm">
{matchedLogo && (
<Image
src={matchedLogo}
alt=""
width={24}
height={24}
className="shrink-0 rounded-full"
{onSelectToggle && (
<Checkbox
checked={!!selected}
onCheckedChange={() => onSelectToggle(item.id)}
aria-label="Selecionar item"
className="shrink-0"
/>
)}
<Image
src={displayLogo}
alt=""
width={32}
height={32}
className="shrink-0 rounded-full"
/>
<span className="truncate">
{item.sourceAppName || item.sourceApp}
</span>
<span className="shrink-0 text-xs font-normal text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 cursor-default text-xs font-normal text-muted-foreground underline decoration-dotted underline-offset-2">
{timeAgo}
</span>
</TooltipTrigger>
<TooltipContent>{fullDate}</TooltipContent>
</Tooltip>
</CardTitle>
{amount !== null && (
<MoneyValues amount={amount} className="shrink-0 text-sm" />
@@ -174,13 +186,6 @@ export function InboxCard({
<RiDeleteBinLine className="size-4" />
</Button>
)}
{onSelectToggle && (
<Checkbox
checked={!!selected}
onCheckedChange={() => onSelectToggle(item.id)}
aria-label="Selecionar item"
/>
)}
</div>
</CardFooter>
) : (
@@ -213,13 +218,6 @@ export function InboxCard({
>
<RiDeleteBinLine className="size-4" />
</Button>
{onSelectToggle && (
<Checkbox
checked={!!selected}
onCheckedChange={() => onSelectToggle(item.id)}
aria-label="Selecionar item"
/>
)}
</CardFooter>
)}
</Card>

View File

@@ -52,7 +52,14 @@ export function InboxDetailsDialog({
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">App</span>
<div className="flex flex-col items-end gap-0.5">
<span>{item.sourceAppName || item.sourceApp}</span>
{item.sourceAppName && (
<span className="font-mono text-xs text-muted-foreground">
{item.sourceApp}
</span>
)}
</div>
</div>
</div>
</div>
@@ -109,6 +116,11 @@ export function InboxDetailsDialog({
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Fechar
</Button>
</DialogClose>
{isPending && onProcess && (
<Button
type="button"
@@ -120,11 +132,6 @@ export function InboxDetailsDialog({
Processar
</Button>
)}
<DialogClose asChild>
<Button type="button" variant="outline">
Fechar
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -6,8 +6,12 @@ import {
RiArrowRightDoubleLine,
RiArrowRightSLine,
RiAtLine,
RiCalendarEventLine,
RiDeleteBinLine,
} from "@remixicon/react";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import Image from "next/image";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
@@ -42,6 +46,7 @@ import {
TabsList,
TabsTrigger,
} from "@/shared/components/ui/tabs";
import { resolveLogoSrc } from "@/shared/lib/logo";
import { InboxCard } from "./inbox-card";
import { InboxDetailsDialog } from "./inbox-details-dialog";
import type {
@@ -52,8 +57,71 @@ import type {
SelectOption,
} from "./types";
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
function getDateKey(date: Date): string {
const adjusted = new Date(date.getTime() + BRASILIA_OFFSET_MS);
return adjusted.toISOString().slice(0, 10);
}
function getGroupLabel(dateKey: string): string {
const now = new Date();
const todayKey = getDateKey(now);
const yesterdayKey = getDateKey(
new Date(now.getTime() - 24 * 60 * 60 * 1000),
);
if (dateKey === todayKey) return "Hoje";
if (dateKey === yesterdayKey) return "Ontem";
const [year, month, day] = dateKey.split("-").map(Number);
return format(new Date(year, month - 1, day), "d 'de' MMMM", {
locale: ptBR,
});
}
function groupItemsByDay(
items: InboxItem[],
): { label: string; items: InboxItem[] }[] {
const groups = new Map<string, InboxItem[]>();
for (const item of items) {
const key = getDateKey(new Date(item.notificationTimestamp));
const group = groups.get(key);
if (group) {
group.push(item);
} else {
groups.set(key, [item]);
}
}
const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a));
return sortedKeys.map((key) => ({
label: getGroupLabel(key),
items: groups.get(key) ?? [],
}));
}
function findMatchingLogo(
sourceAppName: string | null,
appLogoMap: Record<string, string>,
): string | null {
if (!sourceAppName) return null;
const appName = sourceAppName.toLowerCase();
if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]);
for (const [name, logo] of Object.entries(appLogoMap)) {
if (name.includes(appName) || appName.includes(name)) {
return resolveLogoSrc(logo);
}
}
return null;
}
interface InboxPageProps {
activeStatus: InboxStatus;
activeApp: string | null;
sourceApps: string[];
items: InboxItem[];
counts: InboxStatusCounts;
pagination: InboxPaginationState;
@@ -69,6 +137,8 @@ interface InboxPageProps {
export function InboxPage({
activeStatus,
activeApp,
sourceApps = [],
items,
counts,
pagination,
@@ -111,6 +181,38 @@ export function InboxPage({
const [selectionBulkStatus, setSelectionBulkStatus] =
useState<InboxStatus>("pending");
const normalizedSourceApps = useMemo(() => {
if (!Array.isArray(sourceApps)) {
return [];
}
const uniqueApps = new Set<string>();
for (const app of sourceApps) {
if (typeof app !== "string") {
continue;
}
const trimmedApp = app.trim();
if (!trimmedApp) {
continue;
}
uniqueApps.add(trimmedApp);
}
return [...uniqueApps].sort((left, right) =>
left.localeCompare(right, "pt-BR"),
);
}, [sourceApps]);
const appFilterOptions =
activeApp && !normalizedSourceApps.includes(activeApp)
? [activeApp, ...normalizedSourceApps]
: normalizedSourceApps;
const getAppLogo = (appName: string | null) =>
findMatchingLogo(appName, appLogoMap) ?? DEFAULT_INBOX_APP_LOGO;
const handleProcessOpenChange = (open: boolean) => {
setProcessOpen(open);
if (!open) {
@@ -239,7 +341,6 @@ export function InboxPage({
setSelectedIds([]);
return;
}
setSelectedIds(items.map((item) => item.id));
};
@@ -276,8 +377,42 @@ export function InboxPage({
});
};
const handleAppChange = (nextApp: string) => {
const nextParams = new URLSearchParams(searchParams.toString());
if (nextApp === "all") {
nextParams.delete("app");
} else {
nextParams.set("app", nextApp);
}
nextParams.delete("page");
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
: pathname;
router.replace(target, { scroll: false });
});
};
const handleTabChange = (nextStatus: string) => {
updateUrl(nextStatus as InboxStatus, 1, pagination.pageSize);
const nextParams = new URLSearchParams(searchParams.toString());
nextParams.delete("app");
if (nextStatus === "pending") {
nextParams.delete("status");
} else {
nextParams.set("status", nextStatus);
}
nextParams.delete("page");
if (pagination.pageSize === INBOX_DEFAULT_PAGE_SIZE) {
nextParams.delete("pageSize");
} else {
nextParams.set("pageSize", pagination.pageSize.toString());
}
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
: pathname;
router.replace(target, { scroll: false });
});
};
const handleSelectionBulkRequest = (status: InboxStatus) => {
@@ -401,16 +536,30 @@ export function InboxPage({
</Card>
);
const renderGrid = (list: InboxItem[], readonly?: boolean) =>
list.length === 0 ? (
renderEmptyState(
const renderGroupedGrid = (list: InboxItem[], readonly?: boolean) => {
if (list.length === 0) {
if (activeApp) {
return renderEmptyState("Nenhuma notificação deste app");
}
return renderEmptyState(
readonly
? "Nenhuma notificação nesta aba"
: "Nenhum pré-lançamento pendente",
)
) : (
);
}
const groups = groupItemsByDay(list);
return (
<div className="space-y-6">
{groups.map((group) => (
<div key={group.label}>
<div className="mb-3 flex items-center gap-1 text-muted-foreground">
<RiCalendarEventLine className="size-3.5 shrink-0" />
<p className="text-sm font-medium">{group.label}</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{list.map((item) => (
{group.items.map((item) => (
<InboxCard
key={item.id}
item={item}
@@ -420,13 +569,72 @@ export function InboxPage({
onDiscard={readonly ? undefined : handleDiscardRequest}
onViewDetails={readonly ? undefined : handleDetailsRequest}
onDelete={readonly ? handleDeleteRequest : undefined}
onRestoreToPending={readonly ? handleRestoreRequest : undefined}
onRestoreToPending={
readonly ? handleRestoreRequest : undefined
}
selected={selectedIds.includes(item.id)}
onSelectToggle={toggleSelection}
/>
))}
</div>
</div>
))}
</div>
);
};
const renderAppFilter = () => {
if (appFilterOptions.length === 0) {
return null;
}
return (
<Select value={activeApp ?? "all"} onValueChange={handleAppChange}>
<SelectTrigger className="w-[190px]">
<SelectValue>
<span className="flex min-w-0 items-center gap-2">
<Image
src={activeApp ? getAppLogo(activeApp) : DEFAULT_INBOX_APP_LOGO}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span className="truncate">{activeApp ?? "Todos"}</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<span className="flex items-center gap-2">
<Image
src={DEFAULT_INBOX_APP_LOGO}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span>Todos</span>
</span>
</SelectItem>
{appFilterOptions.map((app) => (
<SelectItem key={app} value={app}>
<span className="flex min-w-0 items-center gap-2">
<Image
src={getAppLogo(app)}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span className="truncate">{app}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
};
return (
<>
@@ -463,9 +671,17 @@ export function InboxPage({
</TabsList>
<TabsContent value="pending" className="mt-4">
{activeStatus === "pending" && items.length > 0 && (
<div className="mb-4 flex items-center justify-end gap-2">
<Button variant="outline" size="sm" onClick={toggleSelectAll}>
{activeStatus === "pending" &&
(appFilterOptions.length > 0 || items.length > 0) && (
<div className="mb-4 flex flex-wrap items-center gap-2">
{renderAppFilter()}
{items.length > 0 ? (
<div className="ml-auto flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={toggleSelectAll}
>
{allSelected ? "Cancelar seleção" : "Selecionar página"}
</Button>
{selectedIds.length > 0 && (
@@ -479,13 +695,23 @@ export function InboxPage({
</Button>
)}
</div>
) : null}
</div>
)}
{activeStatus === "pending" ? renderGrid(items, false) : null}
{activeStatus === "pending" ? renderGroupedGrid(items, false) : null}
</TabsContent>
<TabsContent value="processed" className="mt-4">
{activeStatus === "processed" && items.length > 0 && (
<div className="mb-4 flex items-center justify-end gap-2">
<Button variant="outline" size="sm" onClick={toggleSelectAll}>
{activeStatus === "processed" &&
(appFilterOptions.length > 0 || items.length > 0) && (
<div className="mb-4 flex flex-wrap items-center gap-2">
{renderAppFilter()}
{items.length > 0 ? (
<div className="ml-auto flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={toggleSelectAll}
>
{allSelected ? "Cancelar seleção" : "Selecionar página"}
</Button>
{selectedIds.length > 0 && (
@@ -507,13 +733,23 @@ export function InboxPage({
Limpar processados
</Button>
</div>
) : null}
</div>
)}
{activeStatus === "processed" ? renderGrid(items, true) : null}
{activeStatus === "processed" ? renderGroupedGrid(items, true) : null}
</TabsContent>
<TabsContent value="discarded" className="mt-4">
{activeStatus === "discarded" && items.length > 0 && (
<div className="mb-4 flex items-center justify-end gap-2">
<Button variant="outline" size="sm" onClick={toggleSelectAll}>
{activeStatus === "discarded" &&
(appFilterOptions.length > 0 || items.length > 0) && (
<div className="mb-4 flex flex-wrap items-center gap-2">
{renderAppFilter()}
{items.length > 0 ? (
<div className="ml-auto flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={toggleSelectAll}
>
{allSelected ? "Cancelar seleção" : "Selecionar página"}
</Button>
{selectedIds.length > 0 && (
@@ -535,8 +771,10 @@ export function InboxPage({
Limpar descartados
</Button>
</div>
) : null}
</div>
)}
{activeStatus === "discarded" ? renderGrid(items, true) : null}
{activeStatus === "discarded" ? renderGroupedGrid(items, true) : null}
</TabsContent>
</Tabs>

View File

@@ -31,6 +31,10 @@ export const resolveInboxStatus = (
: "pending";
};
export const resolveInboxApp = (
params: ResolvedInboxSearchParams,
): string | null => getSingleParam(params, "app");
export const resolveInboxPagination = (
params: ResolvedInboxSearchParams,
): Pick<InboxPaginationState, "page" | "pageSize"> => {

View File

@@ -39,18 +39,26 @@ export async function fetchInboxItemsPage(
{
page,
pageSize,
sourceApp,
}: {
page: number;
pageSize: number;
sourceApp?: string | null;
},
): Promise<{
items: InboxItem[];
pagination: InboxPaginationState;
}> {
const where = and(
eq(inboxItems.userId, userId),
eq(inboxItems.status, status),
sourceApp ? eq(inboxItems.sourceAppName, sourceApp) : undefined,
);
const [countRow] = await db
.select({ total: count() })
.from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)));
.where(where);
const totalItems = Number(countRow?.total ?? 0);
const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
@@ -60,7 +68,7 @@ export async function fetchInboxItemsPage(
const items = await db
.select()
.from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)))
.where(where)
.orderBy(desc(inboxItems.notificationTimestamp), desc(inboxItems.createdAt))
.limit(pageSize)
.offset(offset);
@@ -76,6 +84,22 @@ export async function fetchInboxItemsPage(
};
}
export async function fetchInboxSourceApps(
userId: string,
status: InboxStatus,
): Promise<string[]> {
const rows = await db
.select({ name: inboxItems.sourceAppName })
.from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)));
const seen = new Set<string>();
for (const row of rows) {
if (row.name) seen.add(row.name);
}
return [...seen].sort();
}
export async function fetchInboxStatusCounts(
userId: string,
): Promise<InboxStatusCounts> {

View File

@@ -6,7 +6,6 @@ import { normalizeDescriptionKey } from "@/features/transactions/lib/import-util
import { getUserId } from "@/shared/lib/auth/server";
import { db } from "@/shared/lib/db";
// Retorna um map de descriptionKey → categoryId para as descrições fornecidas
export async function fetchCategoryMappings(
descriptions: string[],
@@ -53,7 +52,10 @@ export async function saveCategoryMappings(
.insert(importCategoryMappings)
.values(toUpsert)
.onConflictDoUpdate({
target: [importCategoryMappings.userId, importCategoryMappings.descriptionKey],
target: [
importCategoryMappings.userId,
importCategoryMappings.descriptionKey,
],
set: {
categoryId: sql`excluded.category_id`,
updatedAt: sql`excluded.updated_at`,

View File

@@ -29,7 +29,11 @@ const importSchema = z.object({
accountId: uuidSchema("FinancialAccount").nullable().optional(),
cardId: uuidSchema("Cartão").nullable().optional(),
paymentMethod: z.string().min(1),
invoicePeriod: z.string().regex(/^\d{4}-\d{2}$/, "Período inválido.").nullable().optional(),
invoicePeriod: z
.string()
.regex(/^\d{4}-\d{2}$/, "Período inválido.")
.nullable()
.optional(),
});
export type ImportRow = z.infer<typeof importRowSchema>;
@@ -51,10 +55,7 @@ export async function checkDuplicateFitIds(
.select({ ofxFitId: transactions.ofxFitId })
.from(transactions)
.where(
and(
eq(transactions.userId, userId),
inArray(transactions.ofxFitId, ids),
),
and(eq(transactions.userId, userId), inArray(transactions.ofxFitId, ids)),
);
return rows.map((r) => r.ofxFitId).filter((id): id is string => id !== null);
@@ -67,10 +68,14 @@ export async function importTransactionsAction(
const parsed = importSchema.safeParse(input);
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message ?? "Dados inválidos." };
return {
success: false,
error: parsed.error.issues[0]?.message ?? "Dados inválidos.",
};
}
const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } = parsed.data;
const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } =
parsed.data;
// Valida ownership
const [payerOk, accountOk, cardOk] = await Promise.all([
@@ -94,14 +99,19 @@ export async function importTransactionsAction(
const records = rows.map((row) => {
const purchaseDate = parseLocalDateString(row.date);
const period = invoicePeriod ?? `${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`;
const period =
invoicePeriod ??
`${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`;
return {
name: row.description,
transactionType: row.transactionType === "income" ? "Receita" : "Despesa",
condition: "À vista" as const,
paymentMethod,
amount: (row.transactionType === "expense" ? -row.amount : row.amount).toFixed(2),
amount: (row.transactionType === "expense"
? -row.amount
: row.amount
).toFixed(2),
purchaseDate,
period,
isSettled,
@@ -143,10 +153,7 @@ export async function deleteTransactionByFitId(
await db
.delete(transactions)
.where(
and(
eq(transactions.userId, userId),
eq(transactions.ofxFitId, fitId),
),
and(eq(transactions.userId, userId), eq(transactions.ofxFitId, fitId)),
);
await revalidateForEntity("transactions", userId);

View File

@@ -33,7 +33,8 @@ export function decodeAccountCard(value: string): {
id: string;
} | null {
if (value.startsWith("card:")) return { type: "card", id: value.slice(5) };
if (value.startsWith("account:")) return { type: "account", id: value.slice(8) };
if (value.startsWith("account:"))
return { type: "account", id: value.slice(8) };
return null;
}
@@ -65,7 +66,9 @@ export function GlobalFields({
onBulkCategoryChange,
}: GlobalFieldsProps) {
const isCard = accountCardValue?.startsWith("card:") ?? false;
const expenseCategories = categoryOptions.filter((o) => o.group === "despesa");
const expenseCategories = categoryOptions.filter(
(o) => o.group === "despesa",
);
const incomeCategories = categoryOptions.filter((o) => o.group === "receita");
return (
@@ -131,7 +134,10 @@ export function GlobalFields({
<SelectContent>
{payerOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<PayerSelectContent label={opt.label} avatarUrl={opt.avatarUrl} />
<PayerSelectContent
label={opt.label}
avatarUrl={opt.avatarUrl}
/>
</SelectItem>
))}
</SelectContent>
@@ -150,7 +156,10 @@ export function GlobalFields({
<SelectLabel>Despesa</SelectLabel>
{expenseCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} />
<CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem>
))}
</SelectGroup>
@@ -163,7 +172,10 @@ export function GlobalFields({
<SelectLabel>Receita</SelectLabel>
{incomeCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} />
<CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem>
))}
</SelectGroup>

View File

@@ -1,13 +1,18 @@
"use client";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState, useTransition } from "react";
import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner";
import {
fetchCategoryMappings,
saveCategoryMappings,
} from "@/features/transactions/actions/category-memory-action";
import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils";
import {
checkDuplicateFitIds,
deleteTransactionByFitId,
@@ -27,6 +32,7 @@ import {
} from "@/features/transactions/components/import/review-table";
import { UploadZone } from "@/features/transactions/components/import/upload-zone";
import type { SelectOption } from "@/features/transactions/components/types";
import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils";
import { Button } from "@/shared/components/ui/button";
import {
Card,
@@ -82,7 +88,8 @@ export function ImportPage({
...t,
isDuplicate: t.externalId ? duplicates.has(t.externalId) : false,
selected: t.externalId ? !duplicates.has(t.externalId) : true,
categoryId: categoryMappings[normalizeDescriptionKey(t.description)] ?? null,
categoryId:
categoryMappings[normalizeDescriptionKey(t.description)] ?? null,
})),
);
} finally {
@@ -167,7 +174,9 @@ export function ImportPage({
const handleImport = () => {
if (!statement || !canImport) return;
const decoded = decodeAccountCard(accountCardValue!);
const decoded = accountCardValue
? decodeAccountCard(accountCardValue)
: null;
const cardId = decoded?.type === "card" ? decoded.id : null;
const accountId = decoded?.type === "account" ? decoded.id : null;
const paymentMethod =
@@ -197,7 +206,10 @@ export function ImportPage({
// Salva mapeamentos description → category (fire-and-forget)
saveCategoryMappings(
selectedRows.map((r) => ({ description: r.description, categoryId: r.categoryId })),
selectedRows.map((r) => ({
description: r.description,
categoryId: r.categoryId,
})),
);
const { importBatchId } = result;
@@ -236,7 +248,8 @@ export function ImportPage({
<div>
<CardTitle>Importar extrato</CardTitle>
<CardDescription>
Importe transações a partir de um arquivo .ofx ou planilha .xlsx exportado pelo seu banco.
Importe transações a partir de um arquivo .ofx ou planilha .xlsx
exportado pelo seu banco.
</CardDescription>
</div>
<ImportSteps current={currentStep} />

View File

@@ -34,7 +34,9 @@ export function ImportSteps({ current }: ImportStepsProps) {
isCompleted &&
"border-primary bg-primary text-primary-foreground",
isActive && "border-primary text-primary",
!isCompleted && !isActive && "border-muted-foreground/30 text-muted-foreground",
!isCompleted &&
!isActive &&
"border-muted-foreground/30 text-muted-foreground",
)}
>
{isCompleted ? (

View File

@@ -1,7 +1,7 @@
"use client";
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { CategorySelectContent } from "@/features/transactions/components/select-items";
import type { SelectOption } from "@/features/transactions/components/types";
import MoneyValues from "@/shared/components/money-values";
@@ -91,9 +91,7 @@ export function ReviewTable({
onCheckedChange={(v) => onToggleAll(!!v)}
aria-label="Selecionar todas"
data-state={
!allSelected && someSelected
? "indeterminate"
: undefined
!allSelected && someSelected ? "indeterminate" : undefined
}
/>
</TableHead>
@@ -114,7 +112,10 @@ export function ReviewTable({
</TableRow>
)}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]!;
const row = rows[virtualRow.index];
if (!row) {
return null;
}
const index = virtualRow.index;
return (
<TableRow
@@ -199,9 +200,7 @@ export function ReviewTable({
<TableCell>
<TransactionTypeBadge
kind={
row.transactionType === "income"
? "Receita"
: "Despesa"
row.transactionType === "income" ? "Receita" : "Despesa"
}
/>
</TableCell>

View File

@@ -37,7 +37,9 @@ export function UploadZone({ onParsed }: UploadZoneProps) {
}
onParsed(statement);
} catch {
setError("Não foi possível ler o arquivo. Verifique se é um OFX válido.");
setError(
"Não foi possível ler o arquivo. Verifique se é um OFX válido.",
);
}
};
reader.readAsText(file, "windows-1252");
@@ -119,11 +121,7 @@ export function UploadZone({ onParsed }: UploadZoneProps) {
/>
<div className="flex items-center justify-between">
{error ? (
<p className="text-destructive text-sm">{error}</p>
) : (
<span />
)}
{error ? <p className="text-destructive text-sm">{error}</p> : <span />}
<button
type="button"
onClick={handleDownloadTemplate}

View File

@@ -13,9 +13,9 @@ import {
RiCheckLine,
RiDeleteBin5Line,
RiFileCopyLine,
RiFileExcel2Line,
RiFileList2Line,
RiFlashlightFill,
RiFileExcel2Line,
RiGroupLine,
RiHistoryLine,
RiMoreFill,

View File

@@ -1,4 +1,4 @@
import type { ImportStatement, ImportedTransaction } from "./types";
import type { ImportedTransaction, ImportStatement } from "./types";
// Extrai o valor de uma tag leaf do OFX SGML: <TAG>valor
function getField(block: string, tag: string): string | null {

View File

@@ -1,5 +1,8 @@
import * as XLSX from "xlsx";
import type { ImportStatement, ImportedTransaction } from "@/shared/lib/import/types";
import type {
ImportedTransaction,
ImportStatement,
} from "@/shared/lib/import/types";
function parseDateValue(value: unknown): string | null {
if (value == null || value === "") return null;