120 Commits

Author SHA1 Message Date
Felipe Coutinho
51652da4f8 fix(invoices): exibir ícone de anexo na fatura do cartão (2.5.1)
fetchCardTransactions não preenchia hasAttachments, então o ícone não
aparecia em /cards/[cardId]/invoice. Agora delega para
fetchTransactionsWithRelations, que já calcula o flag via EXISTS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 01:44:08 +00:00
Felipe Coutinho
7a74f9405e fix(lint): corrigir formatação do snapshot de migration e imports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:28:27 +00:00
Felipe Coutinho
94bf93194f chore: ajustes de componentes, estilos, dependências e métricas do dashboard
- dashboard: melhorias em métricas, filtros de transações e overview de período
- transactions: colunas, tabela e página com novos campos e ajustes de exibição
- ui: card, table, navigation-menu, navbar, month-picker, logo-picker, theme-toggler
- calculator: ajustes de display, keypad e estado
- calendar: melhorias de grid e day-cell
- insights: atualização de constantes
- settings: pequenos ajustes
- pnpm-lock: atualização de dependências
- pdf.worker: atualização do worker

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:53 +00:00
Felipe Coutinho
d55173e8c1 refactor(transactions): remover exports mortos dateFormatter e monthFormatter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:45 +00:00
Felipe Coutinho
4a73088c09 chore(assets): substituir dashboard-preview de webp para png
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:41 +00:00
Felipe Coutinho
eaa20448a8 chore(landing): remover seção de galeria de telas
Remove a seção "Veja o que você pode fazer" com o componente ScreenshotTabs,
as 14 imagens preview-*.webp, o link #telas do nav e o export pwaCompatList sem uso.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:37 +00:00
Felipe Coutinho
367d78d43d fix(dashboard/anotacoes): corrigir divergência de fuso no formatter de datas
Intl.DateTimeFormat sem timeZone usava o fuso do servidor (UTC) no SSR
e o fuso do browser (BRT) no cliente, causando erro de hidratação.
Ambos os formatters passam a usar timeZone: "America/Sao_Paulo" explicitamente.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:29 +00:00
Felipe Coutinho
2fc6d11d78 feat(dashboard/boletos): nome do boleto como link para lançamentos do período
- nome do boleto virou link para /transactions?q=<nome>
- quando o período selecionado não é o atual, inclui ?periodo=<mes-ano> na URL
- ícone RiExternalLinkLine ao lado do nome, mesmo padrão do widget de faturas

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:23 +00:00
Felipe Coutinho
0f5c735be0 feat(dashboard): confirmação de pagamento com conta e data para faturas e boletos
- widget de faturas abre modal com seleção de conta de origem e data antes de pagar
- widget de boletos ganha a mesma paridade: modal com conta de pagamento e data
- toggleTransactionSettlementAction aceita paymentAccountId e paymentDate opcionais
- DashboardBill expõe accountId para inicializar o modal com a conta já vinculada

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:16 +00:00
Felipe Coutinho
4bea6330bf feat(faturas/extrato): ajuste de fatura, reembolso e ajuste de saldo da conta
- botão "Ajustar fatura" na página da fatura abre dialog com input do valor real
  e preview da diferença; action faz upsert/delete idempotente do lançamento de ajuste
- opção "Reembolso" no dropdown de ações de despesas à vista cria receita espelhada
  no extrato ou fatura correta, vinculada ao lançamento original
- botão "Ajustar saldo" no extrato da conta compara saldo real informado e gera
  lançamento de ajuste por (accountId, period) via upsert/delete idempotente
- constantes INVOICE_ADJUSTMENT_NAME, ACCOUNT_BALANCE_ADJUSTMENT_NAME,
  REFUND_NOTE_PREFIX e buildRefundNote() centralizadas em shared/lib/accounts/constants.ts
- extrato agora contabiliza transferências internas em Entradas e Saídas corretamente

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:08:07 +00:00
Felipe Coutinho
8389752172 feat(cartoes): exigir limite e bloquear lançamentos acima do disponível
- campo limite passa a ser NOT NULL DEFAULT 0 no schema (migration 0029)
- validação Zod com requiredDecimalSchema garante valor positivo no formulário
- validateCardLimit() em transactions/actions/core.ts bloqueia criação e edição
  de despesas em cartão que ultrapassem o limite disponível, retornando mensagem
  com o valor exato restante
- tipos Card.limit e Card.limitAvailable deixam de ser nullable
- branch "sem limite registrado" removido de card-item.tsx

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:07:56 +00:00
Felipe Coutinho
19b5aa00ee docs(changelog): registrar vetorização dos logos em v2.4.4
Adiciona ao bloco da v2.4.4 as mudanças de logo (split em LogoIcon/
LogoText, SVGs inline, troca dos PNGs por SVGs no public/ e
rasterização em alta resolução nos PDFs) e o fix do baseUrl no
tsconfig. Também atualiza a data da release para 2026-04-27.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:13:54 +00:00
Felipe Coutinho
863ccc0fd2 refactor(exports): renderizar logos SVG em alta resolução no PDF
Atualiza loadExportLogoDataUrl para carregar SVGs e rasterizar no canvas
a 4× a resolução natural antes de retornar o data URL — preserva nitidez
quando o PDF amplia a imagem. Default do path mudou para
/images/logo_text.svg.

Os exports de categorias e lançamentos agora apontam para os arquivos
.svg em vez dos .png removidos.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:21 +00:00
Felipe Coutinho
29d99cbedb chore(assets): trocar PNGs do logo por SVGs vetorizados
- adiciona public/images/logo_small.svg e logo_text.svg com width/height
  explícitos (necessário para naturalWidth/Height funcionar via <img>)
- remove os PNGs antigos (logo_small.png e logo_text.png)
- atualiza referência no README.md (header) para logo_small.svg

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:15 +00:00
Felipe Coutinho
dbeb98bbe4 refactor(logo): vetorizar e separar LogoIcon/LogoText em arquivos próprios
Substitui as PNGs raster do componente Logo por SVGs inline e quebra
em dois subcomponentes reutilizáveis:

- LogoIcon (src/shared/components/logo-icon.tsx): SVG do ícone laranja
  (viewBox 0 0 200 200), aceita SVGProps via spread
- LogoText (src/shared/components/logo-text.tsx): SVG do wordmark
  (viewBox 0 0 574.201 89.6), fill #000 + dark:invert para alternar
  preto/branco conforme o tema
- Logo (orquestrador): mantém a API atual (variants full/compact/small,
  invertTextOnDark, colorIcon, iconClassName, textClassName) e agora
  renderiza os SVGs em vez de next/image

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:10 +00:00
Felipe Coutinho
c0436dc2ac fix(tsconfig): remover baseUrl para evitar erro de deprecação no TS 7
A remoção de "ignoreDeprecations": "6.0" no commit anterior reabriu o
erro TS5101 sobre baseUrl. Como moduleResolution: bundler resolve os
paths relativos ao próprio tsconfig.json, baseUrl é redundante e pode
ser removido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:58:09 +00:00
Felipe Coutinho
e1e76fadc0 chore(release): v2.4.4
Versão dedicada a remover a dependência de pgcrypto e a enxugar os
backups. CHANGELOG, badge do README e fluxo de restore atualizados;
script pnpm db:extensions removido do package.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:54 +00:00
Felipe Coutinho
9b2c15ef7d chore: ajustes de configuração diversos
- tsconfig: target ES2017 → ES2022, remove ignoreDeprecations 6.0
- gitignore: ignora pasta .codex
- next.config: remove linha em branco supérflua

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:50 +00:00
Felipe Coutinho
fbe3fceb9f chore(backup): escopar dumps aos schemas public e drizzle
Adiciona --schema=public --schema=drizzle aos pg_dump (modos remote e
docker), descartando os schemas internos do Supabase (auth, realtime,
storage, vault, graphql, etc.). Restaurações em PostgreSQL padrão
deixam de produzir os ~148 erros de role/extension does not exist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:42 +00:00
Felipe Coutinho
39f3cd8b20 feat(payers): gerar share_code na aplicação e remover pgcrypto
Move a geração do share_code do PostgreSQL para a camada de aplicação,
eliminando a dependência da extensão pgcrypto no setup do banco.

- schema: drop default substr(encode(gen_random_bytes(24), 'base64'), 1, 24)
  da coluna share_code em pagadores (continua NOT NULL)
- nova util generateShareCode() em shared/lib/payers/share-code.ts
  (server-only, usa crypto.randomBytes do Node)
- chamadas explícitas em createPayerAction, ensureDefaultPagadorForUser,
  resetUserAppData e mock-data ao inserir pagadores
- migration 0028_fancy_reaper renumerada (0027 já estava ocupado por
  arquivo órfão); journal e snapshot atualizados
- remove etapa de habilitação de pgcrypto do docker-entrypoint.sh
- remove scripts/postgres/ (init.sql e enable-extensions.ts)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:36 +00:00
Felipe Coutinho
791fec7751 chore(release): v2.4.3
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:47:05 +00:00
Felipe Coutinho
114e2b4011 chore(deps): bump dependencies e schema do biome
- @aws-sdk/client-s3 + s3-request-presigner: 3.1032 → 3.1037
- @better-auth/passkey: 1.6.5 → 1.6.9
- better-auth: 1.6.5 → 1.6.9
- @tanstack/react-query: 5.99.2 → 5.100.3
- @biomejs/biome: 2.4.12 → 2.4.13 (com $schema atualizado)
- @tailwindcss/postcss + tailwindcss: 4.2.2 → 4.2.4
- resend: 6.12.0 → 6.12.2
- knip: 6.4.1 → 6.7.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:46:58 +00:00
Felipe Coutinho
f15a003cef fix(docker): healthcheck usar 127.0.0.1 para hosts com IPv6 (#44)
Containers em hosts com IPv6 habilitado tentavam conectar via ::1
e falhavam por timeout antes de cair no fallback IPv4. Fixar
127.0.0.1 elimina a ambiguidade.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:46:18 +00:00
Felipe Coutinho
7f07a9cbf6 fix(i18n): corrigir rótulos pt-br em categorias e dialog de antecipação
- updateCategoryAction: mensagem de sucesso "Category atualizada com
  sucesso." → "Categoria atualizada com sucesso."
- AnticipateInstallmentsDialog: rótulos "Period" → "Fatura" e
  "Category" → "Categoria"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:46:12 +00:00
Felipe Coutinho
5fa234884e style(ui): refresh em badges de tipo, radio buttons e antecipação
- TransactionTypeBadge: substitui StatusDot por ícones direcionais
  (RiArrowRightDownLine receita, RiArrowRightUpLine despesa,
  RiArrowLeftRightLine transferência), adiciona borda e shadow sutil
  e dessaturação no dark mode; rótulo "Transferência" abreviado
  para "Transf."
- RadioGroup: indicador trocado de RiCircleLine por RiCheckLine com
  fundo sólido primary no estado selecionado
- Tabela de seleção de parcelas no dialog de antecipação reduzida
  para três colunas (estabelecimento, fatura, valor); coluna de
  vencimento removida e nome do estabelecimento absorve a parcela
- Inter agora carrega explicitamente os pesos 500, 600 e 700

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:46:05 +00:00
Felipe Coutinho
b453b432ed perf(logos): pré-resolver mapeamentos Logo.dev no servidor
Cada EstablishmentLogo dispara um GET para /api/logo/mapping por
nome único (deduplicado pelo React Query, mas ainda N requests por
página). Em /dashboard, /transactions e /payers/[payerId] agora
fazemos uma única query SQL em batch (fetchEstablishmentLogoMap) e
semeamos o cache do React Query antes do primeiro render via novo
LogoPrefetchProvider — eliminando os requests da rede.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:45:54 +00:00
Felipe Coutinho
7f05d2a681 fix(attachments): limpar arquivos órfãos no S3 em deleções e reset
Três caminhos de deleção não chamavam o cleanup de storage, deixando
arquivos órfãos no S3:

- deleteTransactionBulkAction: deleções por escopo de série (período,
  futuras, todas) agora coletam attachments vinculados antes do delete
  e disparam cleanupAttachmentsAfterTransactionDelete
- deleteMultipleTransactionsAction: mesma correção para seleção
  múltipla de lançamentos
- resetUserAppData: reset de conta em Ajustes coleta os fileKeys
  antes de truncar e remove os objetos do S3 em paralelo

Também ajusta deleteS3Object para ignorar NoSuchKey silenciosamente,
necessário para providers S3-compatíveis como Cloudflare R2 que não
são idempotentes nessa operação.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:45:45 +00:00
Felipe Coutinho
b14f487824 feat(transactions): edição cooperativa e visibilidade de divisões
Adiciona splitGroupId para vincular as duas shares de um lançamento
dividido (schema + índice + migration 0026). Habilita:

- Edição de par dividido com escolha de escopo (apenas este lado ou
  ambos) via novo SplitPairDialog e updateTransactionSplitPairAction
- Filtro "Somente divididos" (isDivided) na tabela de lançamentos
- Visibilidade de anexos para pessoas com acesso compartilhado via
  payerShares; upload e detach em massa expandem para shares irmãs
- Cópia independente de anexos no fluxo "Importar para Minha Conta"
  (novo fileKey, novo userId, S3 CopyObject) com seção read-only
  "Anexos que serão copiados" no dialog de importação
- Ícone de clipe na tabela de lançamentos da página da pessoa via
  EXISTS em fetchPagadorLancamentos

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:45:35 +00:00
Felipe Coutinho
5b03824a72 fix(security): remover header CSP de respostas de API
CSP não tem efeito em respostas JSON e expunha domínios
internos (Umami, Supabase, logo.dev) em endpoints públicos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 14:44:18 +00:00
Felipe Coutinho
74dda549f5 style(format): corrigir ordenação de imports em 3 arquivos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:57:05 +00:00
Felipe Coutinho
137b63f256 style(format): corrigir formatação Biome em 5 arquivos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:54:56 +00:00
Felipe Coutinho
f747405264 feat(calendar): agrupar parcelas da mesma série em evento único
Lançamentos parcelados com o mesmo seriesId agora são consolidados em
um único evento do tipo 'installment' no calendário, exibindo 'Nx de
R$ X' em vez de repetir o mesmo item N vezes. Legenda e modal de
detalhes atualizados para refletir o novo tipo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:50:02 +00:00
Felipe Coutinho
cbc17c8513 style(notes): polimento visual nas tarefas e modal de detalhes
Ícone de tarefa concluída em card e detalhes simplificado para
RiCheckLine verde sem caixa. Checkbox no modal de edição usa bg/border
success com texto success-foreground (claro no light, escuro no dark).
Footer do modal de detalhes reordenado: Cancelar à esquerda, Alterar
primário à direita.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:23:36 +00:00
Felipe Coutinho
c41fafc319 style(assets): atualizar previews de lançamentos e pwa
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:41:48 +00:00
Felipe Coutinho
0bc3f06b77 refactor(ui): renomear "Pagador/Pagadores" para "Pessoa/Pessoas" na interface
Todas as strings visíveis ao usuário (labels, títulos, toasts, mensagens
de erro, cabeçalhos de tabela, exportações) foram atualizadas. Acordos
de gênero em português corrigidos. Código, rotas (/payers) e schema do
banco (pagadores) permanecem inalterados — divergência intencional
documentada em CLAUDE.md e CHANGELOG.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:29:55 +00:00
Felipe Coutinho
2f68bcf039 style(changelog): destacar resumo de versão com borda e itálico discretos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:56:52 +00:00
Felipe Coutinho
41dcd5cec9 docs(claude): exigir parágrafo humano em cada versão do CHANGELOG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:56:26 +00:00
Felipe Coutinho
6391f07eb6 fix(changelog): renderizar parágrafo de resumo por versão
Parser ignorava texto livre entre o cabeçalho ## [versão] e a primeira
seção ###. Adicionado campo `summary` em ChangelogVersion e captura das
linhas de texto antes da primeira seção. ChangelogTab renderiza o resumo
logo abaixo do cabeçalho, antes das entradas técnicas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:55:09 +00:00
Felipe Coutinho
ae9dd364c4 chore(release): v2.4.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:52:26 +00:00
Felipe Coutinho
e005add233 feat(logo): migrar token Logo.dev para runtime server-side
NEXT_PUBLIC_LOGO_DEV_TOKEN renomeado para LOGO_DEV_TOKEN — lido apenas
em runtime no servidor. URL construída nos endpoints /api/logo/mapping e
/api/logo/search; cliente nunca recebe o token. Novo server.ts com
isLogoDevEnabled() e buildLogoDevUrl(). LogoDevProvider (Context) propaga
flag `enabled` para Client Components. Build arg removido do Dockerfile
e do workflow docker-publish.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:52:24 +00:00
Felipe Coutinho
6d81ff8b53 style(ui): polimento visual — tema, cards, dark mode e landing page
Raio de borda global 0.625rem → 0.7rem; ajustes finos em --card e --border.
DotPattern removido do layout, tela de auth e landing page.
Account-card redesenhado (cores de saldo, tooltip de flags de exclusão).
Budget-card, card-item, calendário (day-cell, event-modal) com layout revisado.
Auth-card-shell simplificado (sem glassmorphism/blob). Landing page com
mainFeatures + extraFeatures em grid único e dark mode nos botões de CTA.
Imagens de preview da landing atualizadas. CSS --data-7..10 removidas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:52:17 +00:00
Felipe Coutinho
5d84ae928a refactor(nav): remover sidebar, unificar navegação na navbar
Componentes da sidebar lateral (app-sidebar, nav-main, nav-secondary,
nav-user, nav-link), sidebar.tsx e use-mobile.ts removidos.
Barrel exports órfãos de shared/hooks, shared/components/providers,
shared/lib/schemas e shared/lib/types também removidos.
Navbar recebe ajustes menores de markup e acessibilidade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:52:07 +00:00
Felipe Coutinho
ba05985725 refactor(dashboard): reorganizar módulos em subdiretórios e nova arquitetura de widgets
Arquivos de queries, helpers e controllers dispersos na raiz de dashboard/
foram movidos para subdiretórios temáticos (bills/, invoices/, notes/,
notifications/, overview/, payments/, goals-progress/, categories/).
~25 widgets monolíticos obsoletos removidos em favor de nova arquitetura
baseada em widget-registry com components/widgets/. Novos componentes:
category-breakdown-chart/list, goals-progress-item, percentage-change-indicator.
Imports atualizados em fetch-dashboard-data e transaction-filters limpos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:51:56 +00:00
Felipe Coutinho
3e80d5995b chore(release): v2.4.1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:16:44 +00:00
Felipe Coutinho
68daae7926 fix(docker): fixar PGDATA para compatibilidade com postgres:18-alpine (#41)
Container do PostgreSQL falhava ao iniciar em instalações existentes
após atualização da imagem: o entrypoint passou a recusar dados no
caminho legado. Variável PGDATA fixa o caminho e preserva dados de
quem já tinha o volume populado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:16:40 +00:00
Felipe Coutinho
9413c470a8 style(ui): restaurar indentação tabs no dashboard layout
PR #42 trocou tabs por spaces no arquivo inteiro, quebrando o Biome.
Revertido pelo lint:fix para manter consistência com o resto do projeto.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:16:35 +00:00
Felipe Coutinho
ad1b0aa979 refactor(settings): remover tab órfã de Integrações
A tab foi introduzida no PR #42 mas não tinha TabsContent correspondente
e o value tinha typo ("intergrations") — UI vazia.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:16:31 +00:00
Felipe Coutinho
4d9a1c0a35 perf(db): otimizar índices — remover 7 sem uso, adicionar 17 em FKs
Baseado em análise do pg_stat_user_indexes (187 dias de estatísticas):
removidos 7 índices com 0 scans e adicionados 17 índices em foreign
keys que antes geravam sequential scans durante deletes nas tabelas pai.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 21:16:26 +00:00
Felipe Coutinho
5635705c56 feat(ui): layout animado auth, capitalização navbar (PR #42) 2026-04-16 15:19:53 +00:00
Alexsandro
4c97ed569d Merge branch 'main' into feat/fix-ui 2026-04-16 11:52:37 -03:00
Alexsandro
22a88de993 style(ui): update auth pages layout and navigation capitalization
This commit improves the visual design of the auth pages by adding a new layout wrapper with an animated blob background effect and updating the auth card shell with a glassmorphism style. It also updates the navigation items to use capitalized labels instead of lowercase for better readability.
2026-04-15 14:35:44 -03:00
Felipe Coutinho
9456aa98bc fix(ci): passar NEXT_PUBLIC_LOGO_DEV_TOKEN como build arg no Docker
NEXT_PUBLIC_* é inlined pelo Next.js em build time — a variável precisa
ser injetada via ARG no Dockerfile e build-args no workflow do CI.
Sem isso, o token fica undefined e os logos nunca são exibidos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 01:06:23 +00:00
Felipe Coutinho
21c6a8d9d0 fix(lint): corrigir schema biome.json e formatação de imports
- biome.json: bump schema 2.4.10 → 2.4.11
- establishment-logo-picker.tsx, establishment-logo.tsx, navigation-menu.tsx, logo/index.ts: organizar imports e ajustar formatação conforme Biome 2.4.11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:31:46 +00:00
Felipe Coutinho
c29ffa9a12 docs: registrar v2.4.0 — integração Logo.dev; atualizar screenshots
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:27:03 +00:00
Felipe Coutinho
8875de843b chore(deps): separar radix-ui em pacotes individuais e atualizar dependências; bump 2.4.0
- Remove pacote `radix-ui` (bundle monolítico); importa direto `@radix-ui/react-navigation-menu` e `@radix-ui/react-slider`
- Bump: @ai-sdk/* , @aws-sdk/* , @tanstack/react-query, ai, resend, dotenv, knip, @biomejs/biome, @types/node

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:26:56 +00:00
Felipe Coutinho
679ea752bb feat(logo): integração Logo.dev para logos automáticos de estabelecimentos
- Nova tabela `establishment_logos` no schema (userId + nameKey → domain)
- Utilitários: `buildLogoDevUrl`, `toNameKey`, `logoQueryKeys`, `LOGO_DEV_TOKEN`
- `EstablishmentLogo`: exibe logo via Logo.dev com fallback para iniciais; hover mostra ícone de edição
- `EstablishmentLogoPicker`: popover para buscar e fixar domínio Logo.dev por estabelecimento
- API routes: `GET /api/logo/mapping` e `GET /api/logo/search`
- Server actions/queries para persistência do mapeamento por usuário
- CSP: libera `https://img.logo.dev` em `img-src`
- `.env.example`: variáveis `NEXT_PUBLIC_LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:26:48 +00:00
Felipe Coutinho
1161e97d9e docs: reestruturar README em dois perfis; bump 2.3.8
- README: perfil Usar (só Docker) e Desenvolver (hot-reload)
- README: seção Docker simplificada; scripts atualizados
- CHANGELOG: entrada 2.3.8 com mudanças de infraestrutura Docker

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:45:19 +00:00
Felipe Coutinho
55d7dedd9a chore(scripts): reduzir scripts docker de 10 para 5
docker:up, docker:db, docker:down, docker:logs, docker:update

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:45:15 +00:00
Felipe Coutinho
ad2752b7b0 chore(docker): simplificar compose e entrypoint
- compose: removidos profiles, build e dependência de arquivo externo;
  agora standalone com curl + docker compose up -d
- compose: variáveis opcionais movidas para .env via env_file
- entrypoint: extensão pgcrypto criada via Node.js antes das migrations
- entrypoint: loop de retry reescrito; removido hack @localhost→@db

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:45:12 +00:00
Felipe Coutinho
58db357cde docs: reescrever README com guia de instalação para leigos; atualizar changelog 2.3.7
- README: seção "Como rodar" reescrita com 4 modos explicados para leigos
- README: seção Docker atualizada (sem .env obrigatório, localhost funciona)
- package.json: corrigir env:setup apontando para setup-env.sh deletado → setup.mjs
- CHANGELOG 2.3.7: documentar fix do localhost→db e default DATABASE_URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:49:40 +00:00
Felipe Coutinho
99a9ff5512 fix(docker): resolver DATABASE_URL localhost→db no container automaticamente
- docker-entrypoint.sh: substituir @localhost: por @db: via sed antes das
  migrations e do Next.js subirem — transparente para o usuário
- docker-compose.yml: adicionar valor padrão para DATABASE_URL para
  permitir subir sem .env configurado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:48:20 +00:00
Felipe Coutinho
5bcf4f69d3 chore(scripts): remover órfãos dev.ts e setup-env.sh; atualizar changelog 2.3.7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:17:20 +00:00
Felipe Coutinho
95099c1a94 chore(docker): passar PUBLIC_DOMAIN e variáveis Umami para o container
Adiciona PUBLIC_DOMAIN, UMAMI_URL, UMAMI_WEBSITE_ID e UMAMI_DOMAINS
ao bloco de environment do serviço app no docker-compose.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:16:44 +00:00
Felipe Coutinho
94912f7edc fix(scripts): corrigir install-deps.sh — spinner, corepack e PATH
- spinner_stop: adicionar || true em kill/wait para evitar exit com set -e
- suprimir prompt interativo do corepack com COREPACK_ENABLE_DOWNLOAD_PROMPT=0
- exportar PATH do Homebrew antes do resumo para pnpm --version funcionar
- remover mensagem "próximo passo" do final do script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 01:16:40 +00:00
Felipe Coutinho
bf6adfa3f1 chore(analytics): mover configuração do Umami para variáveis de ambiente
- UMAMI_URL, UMAMI_WEBSITE_ID e UMAMI_DOMAINS carregados via process.env
- script só é injetado se as vars estiverem definidas
- CSP atualizada dinamicamente com base no UMAMI_URL
- documentado no .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:50:03 +00:00
Felipe Coutinho
e4b9dd4254 chore: versão 2.3.7 — corrigir versão e consolidar CHANGELOG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:49 +00:00
Felipe Coutinho
f1907c8697 fix(settings): ajuste de indentação e texto no formulário de exclusão de conta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:46 +00:00
Felipe Coutinho
805bcb863d fix(logo-picker): corrigir renderização de miniaturas no modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:43 +00:00
Felipe Coutinho
11b4f8940f feat(landing): aba de insights de IA e screenshots atualizados em webp lossless
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:40 +00:00
Felipe Coutinho
fba9686fdb feat(dashboard): tendências top 10 e padronização de espaçamento do inbox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:38 +00:00
Felipe Coutinho
9b8ac9f71f feat(payers): upload de avatar via arquivo com redimensionamento client-side
- círculo de upload no final da grade de avatares abre seletor de arquivo
- imagem redimensionada para 200×200px via Canvas e salva como base64
- suporte a data URLs em next/image com prop unoptimized
- object-cover adicionado ao componente base Avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:43:28 +00:00
Felipe Coutinho
fa41c78a39 feat(navbar): copiar user ID ao lado do nome no menu do usuário
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 18:16:18 +00:00
Felipe Coutinho
5f7bfb98da feat(settings): aba de diagnóstico e cópia de user ID no menu do usuário
- Nova aba "Diagnóstico" em Settings com:
  - Identidade: user ID (copiável), nome, e-mail
  - Sessão: criada em / expira em
  - Aplicação: versão, NODE_ENV, build SHA (se definido)
  - Configuração do servidor: S3, e-mail e domínio público — apenas booleans, sem expor credenciais
  - Saúde: status e latência do banco de dados
  - Uso: contagem de lançamentos, anexos, anotações e itens no inbox
- Botão de cópia do user ID no dropdown do avatar (ao lado do e-mail)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 18:06:40 +00:00
Felipe Coutinho
9ecafdb15f docs: atualizar CLAUDE.md e README; adicionar script de instalação Ubuntu
- CLAUDE.md: rota attachments/ adicionada ao mapa de diretórios (app e features);
  seção Response Style substituída por Security Rules
- README.md: instruções para servidor Ubuntu 24.04, preview do Companion,
  seção de Backup; menção ao Companion atualizada
- scripts/install-deps.sh: prepara VPS Ubuntu limpa instalando Docker,
  Node.js 22 e pnpm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:39 +00:00
Felipe Coutinho
e8cc673e52 style(ui): padronizar tipografia — font-medium para font-semibold
Padronização de peso tipográfico em títulos, rótulos de seção,
nomes de entidades e valores monetários em toda a interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:34 +00:00
Felipe Coutinho
3bd8117b65 fix(i18n): corrigir textos "Payer" para "Pagador" em mensagens de erro
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:21 +00:00
Felipe Coutinho
a7268d8f05 feat(inbox): redesenho do card de pré-lançamento
Logo maior (40px), nome do app em font-semibold, data em linha
separada e valor monetário em destaque — melhor hierarquia visual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:17 +00:00
Felipe Coutinho
1f9098879e feat(parcelas): redesenho do card de grupo com dialog de detalhes
Card de grupo de parcelas ganhou um dialog ao clicar em "Ver detalhes",
separando parcelas pagas e pendentes, com seleção parcial e logo do
estabelecimento. Substituída lógica de expand inline pelo dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:14 +00:00
Felipe Coutinho
7a3bff52ac feat(dashboard): novos widgets de anexos, inbox e tendências de categoria
- Widget Anexos: resumo de arquivos do período (total, imagens, PDFs, recentes)
- Widget Inbox: snapshot de pré-lançamentos pendentes do Companion
- Widget Tendências de Categoria: redireciona para relatório de tendências
- fetch-dashboard-data: busca attachmentsSnapshot e inboxSnapshot em paralelo
- widgets-config: tipo DashboardWidgetQuickActionOptions centralizado; props
  adminPayerSlug e quickActionOptions adicionadas ao contrato do widget
- dashboard-grid-editable: usa o novo tipo unificado de quickActionOptions
- proxy.ts: frame-src adicionado à CSP para preview de PDFs via S3
- rota /attachments criada com layout próprio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:51:09 +00:00
Felipe Coutinho
dfb4126b12 feat(lancamentos): filtros de status e anexo; feedback visual de fatura paga
- Novos filtros no drawer: somente pagos, somente não pagos, com anexo
- Filtros de tipo/condição/pagamento agora usam slugs na URL (sem acentos)
- Coluna de liquidação: lançamentos de cartão com fatura paga exibem ícone
  verde com tooltip — diferenciando do estado pendente
- EstabelecimentoInput: popover respeita largura do input ao abrir
- slugify extraído para shared/utils/string.ts
- INVOICE_PAYMENT_CATEGORY_NAME adicionado em categories/constants.ts
- SETTLED_FILTER_VALUES adicionado em transactions/constants.ts
- establishment-logo.tsx removido (não utilizado)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:50:59 +00:00
Felipe Coutinho
ffead579fa feat(fontes): substituir fonte local America por Inter (Google Fonts)
Next.js self-hosta a Inter em build time — elimina os arquivos .woff2
do repositório e a dependência de localFont.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:50:50 +00:00
Felipe Coutinho
aa85cf8b29 fix(docker,s3): corrigir CRLF no entrypoint e região S3 vazia — v2.3.7
- Adicionado .gitattributes com eol=lf para scripts shell e Dockerfile
- Dockerfile: sed -i 's/\r$//' no entrypoint para eliminar CRLF em ambientes Windows/WSL2
- s3-client.ts: substituído ?? por || para tratar string vazia em S3_REGION e demais vars
- CHANGELOG, package.json e lockfile atualizados para v2.3.7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 17:50:44 +00:00
Felipe Coutinho
9a7ae0fa3d fix(docker): adicionar NODE_PATH no entrypoint para resolução do drizzle-orm
Corrige erro "Cannot find module 'drizzle-orm'" ao rodar migrations no
container — o drizzle-kit em /app/migrate/ não encontrava o módulo sem
NODE_PATH apontando para o node_modules isolado.

Closes #34

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:35:52 +00:00
Felipe Coutinho
98fe6a0f4f Update version badge from 2.3.4 to 2.3.5 2026-04-07 10:53:10 -03:00
Felipe Coutinho
d10eae13e5 Revise versioning and commit message guidelines
Updated versioning instructions to include README.md updates and clarified commit message guidelines.
2026-04-07 10:52:39 -03:00
Felipe Coutinho
43697b4fd2 fix(csp): mover CSP para proxy.ts para leitura em runtime
Content-Security-Policy estava em next.config.ts (build time),
então S3_ENDPOINT nunca era incluído no connect-src ao buildar
via Docker no CI. Movido para proxy.ts que avalia em runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:49:23 +00:00
Felipe Coutinho
27e3ba5f0d Update version badge from 2.1.2 to 2.3.4 2026-04-05 20:33:55 -03:00
Felipe Coutinho
31485eec8f fix(csp): permitir upload de anexos para o storage externo
connect-src bloqueava fetch para o Supabase Storage desde o commit
de segurança (10afef9). Adiciona a origin do S3_ENDPOINT na política.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:47:23 +00:00
Felipe Coutinho
3be64aa8d0 fix(auth): corrigir verify e unificar tokens com prefixo opm_
- Corrige /api/auth/device/verify que rejeitava tokens criados via
  Settings (revertido de JWT para hash lookup)
- Renomeia prefixo de tokens de os_ para opm_ (OpenMonetis)
- Remove rotas JWT não utilizadas (token, refresh)
- Simplifica api-token.ts mantendo apenas hashToken e extractBearerToken

BREAKING CHANGE: tokens existentes com prefixo os_ param de funcionar.
Revogar e recriar tokens após o deploy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:05:03 +00:00
Felipe Coutinho
85f6dcfc22 fix(csp): permitir unsafe-eval apenas em desenvolvimento
React precisa de eval() em dev para reconstruir stack traces.
Produção continua sem unsafe-eval.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:21:03 +00:00
Felipe Coutinho
df996df93d fix(segurança): substituir xlsx por exceljs (CVEs sem patch no npm)
xlsx@0.18.5 tem Prototype Pollution e ReDoS sem versão corrigida no
npm. Migrado para exceljs@4.4.0 nos 4 pontos de uso: parser de
importação, geração de template, exportação de lançamentos e
exportação de relatório de categorias.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:12:04 +00:00
Felipe Coutinho
10afef9fec fix(segurança): corrigir 10 vulnerabilidades do relatório de segurança
- tokens: remover aceite de expiresAt NULL e forçar TTL de 1 ano
- tokens: corrigir refresh que invalidava access token anterior
- xlsx: desabilitar parsing de fórmulas (CVE-2024-44294)
- csp: expandir Content-Security-Policy com origens explícitas
- headers: adicionar Referrer-Policy e X-Permitted-Cross-Domain-Policies
- api: retornar 401 JSON em vez de redirect 302 em rotas autenticadas
- health: remover version disclosure do /api/health
- robots.txt: simplificar para não expor rotas internas
- sitemap: corrigir URL com protocolo duplicado
- criar security.txt (RFC 9116)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:47:05 +00:00
Felipe Coutinho
fd4d90a53e ci: forçar Node.js 24 nas actions do workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:52:52 +00:00
Felipe Coutinho
a24406271c chore: corrigir formatacao do package.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:51:14 +00:00
Felipe Coutinho
a09942e3d8 chore(release): preparar changelog da versão 2.3.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:46:47 +00:00
Felipe Coutinho
96febd5904 fix(docker): separar deps drizzle do node_modules standalone
O pnpm install no Stage 3 sobrescrevia o node_modules copiado do
.next/standalone, removendo o modulo next e quebrando o startup.

Agora as deps do drizzle-kit sao instaladas em /app/migrate/ antes
de copiar o standalone, mantendo os dois node_modules isolados.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:45:48 +00:00
Felipe Coutinho
c3cfbc878c fix(tipografia): ajustar display da fonte america 2026-04-03 18:11:56 +00:00
Felipe Coutinho
55bbfabe9f chore(release): preparar changelog da versão 2.3.0 2026-04-03 18:11:34 +00:00
Felipe Coutinho
f5cdae4853 fix(ui): remover avisos visuais e destacar atualizações 2026-04-03 18:11:30 +00:00
Felipe Coutinho
5c4995961c refactor(lista): componentizar inbox e tabela de lançamentos 2026-04-03 18:10:58 +00:00
Felipe Coutinho
1b4dfaaba7 fix(lançamentos): reforçar validações e revisar formulário 2026-04-03 18:10:50 +00:00
Felipe Coutinho
549a5bdba1 fix(financeiro): alinhar saldo, métricas e relatórios 2026-04-03 18:10:43 +00:00
Felipe Coutinho
acaf9d5c27 feat(dados-client): adotar react query em leituras do app 2026-04-03 18:10:34 +00:00
Felipe Coutinho
e4c6a91350 fix(segurança): endurecer autenticação e rotas privadas 2026-04-03 18:10:23 +00:00
Felipe Coutinho
ba369e8a83 chore(infra): atualizar build, docker e tooling 2026-04-03 18:10:16 +00:00
Felipe Coutinho
d01bc8a669 fix(docker): remove chown recursivo da imagem final 2026-04-01 17:15:06 +00:00
Felipe Coutinho
e024e0d54e fix(docker): cria pasta public antes do pnpm install
O postinstall do pdfjs-dist tenta copiar pdf.worker.min.mjs para
public/, mas no stage deps do Dockerfile a pasta não existia.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:49:00 +00:00
Felipe Coutinho
c44089169f style: troca subpixel-antialiased por antialiased no body
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:35:16 +00:00
Felipe Coutinho
d04e30e3c9 fix(robots): remove estático duplicado, corrige robots.ts e llms.txt
Remove `public/robots.txt` que era ignorado pelo Next.js em favor do
`src/app/robots.ts` (Metadata API). Adiciona `/signup` à lista de
rotas bloqueadas e remove referência ao `sitemap.xml` inexistente.

Restaura o link `/robots.txt` no `llms.txt`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:31:37 +00:00
Felipe Coutinho
229b6c5bc0 chore: adiciona robots.txt bloqueando rotas privadas do app
Permite indexação apenas da landing page e do llms.txt. Bloqueia todas
as rotas autenticadas (dashboard, transações, contas, cartões, etc.)
e a API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:29:39 +00:00
Felipe Coutinho
c3b133d8d9 docs(llms.txt): adiciona stack técnico e remove links inexistentes
Inclui stack no cabeçalho do arquivo e remove referências a AGENTS.md,
robots.txt e sitemap.xml que não existem no repositório.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:24:04 +00:00
Felipe Coutinho
e9a2ab1782 chore(release): publicar versão 2.2.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:18:55 +00:00
Felipe Coutinho
c7d6e23398 chore: atualiza biome, CLAUDE.md, llms.txt e corrige optional chaining
- biome.json: schema atualizado para 2.4.9
- public/llms.txt: novo arquivo de documentação pública do projeto
- CLAUDE.md: ajustes menores de documentação interna
- invoices-queries.ts: usa optional chaining `?.startsWith` no lugar de
  verificação dupla de nullish
- CHANGELOG.md: documentadas as mudanças do ciclo atual em [Unreleased]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:15:03 +00:00
Felipe Coutinho
0514efb1c4 style(tipografia): adiciona fonte America Medium e padroniza pesos de texto
Adiciona os arquivos `america-medium.woff2` e `america-bold.woff2` e
registra o weight 500 no `font_index.ts`.

Padroniza o uso de `font-medium` em substituição a `font-semibold` e
`font-bold` em títulos, valores monetários e rótulos de destaque em
todos os componentes do app, landing page e componentes de UI base.

`Card` ganha `hover:border-primary/40` e `CardTitle` recebe `text-base`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:14:55 +00:00
Felipe Coutinho
e32fb85006 perf(cache): migração para diretiva use cache do Next.js
Todas as queries cacheadas do dashboard migram de `unstable_cache` para
a diretiva `use cache` com `cacheTag` e `cacheLife({ revalidate: 3 })`.

Todas as páginas e o layout do dashboard passam a chamar `connection()`
para garantir renderização dinâmica. O root layout envolve os filhos em
`<Suspense>`. `next.config.ts` remove `turbopackFileSystemCacheForDev`
e adota `cacheComponents: true`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:14:23 +00:00
Felipe Coutinho
96df6a1798 feat(notificações): alertas de vencimento para o período seguinte
Boletos e faturas do próximo período com vencimento dentro de 5 dias
agora geram notificações do tipo `due_soon`, evitando duplicatas com
notificações já existentes do período corrente.

A query de boletos passa a filtrar pela data de vencimento não nula e
limita a janela de busca a 12 meses anteriores ao período atual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:14:07 +00:00
Felipe Coutinho
1f8a97bd16 feat(auth): redesign visual das páginas de autenticação
O sidebar de autenticação ganha mockup animado de faturas e três itens
de funcionalidade no rodapé, substituindo o texto descritivo anterior.

As páginas de login e cadastro recebem gradiente decorativo de fundo e
exibem o logo no topo em viewports mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:14:01 +00:00
Felipe Coutinho
0ab3298cef feat(anexos): página de galeria de comprovantes e documentos
Adiciona rota `/attachments` com visualização de todos os anexos do
usuário em grade, visualização inline de imagem e PDF, navegação entre
arquivos do mesmo lançamento e download direto.

Inclui também:
- API REST em `/api/attachments` para servir os arquivos
- Actions `fetch-by-id` e `fetch-dialog-options` em transactions
- Item "Anexos" adicionado à navbar
- `formatBytes` extraído para `src/shared/utils/number.ts`
- Migrations de banco atualizadas
- Fix: uploads e remoções de anexo agora funcionam para todos os
  lançamentos, não apenas os pertencentes a séries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:13:54 +00:00
Felipe Coutinho
cad41680eb feat(pdf): adiciona suporte a visualização de PDF nos anexos
Inclui `pdfjs-dist` como dependência e configura o script `postinstall`
para copiar o web worker necessário para `public/pdf.worker.min.mjs`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:13:39 +00:00
Felipe Coutinho
3b00f328c5 Update version badge to 2.1.2 2026-03-30 15:49:54 -03:00
499 changed files with 32132 additions and 14315 deletions

View File

@@ -3,10 +3,10 @@
# ============================================
# === Database ===
# PostgreSQL local (Docker): use host "db"
# PostgreSQL local (sem Docker): use host "localhost"
# Desenvolvimento local (pnpm dev): use host "localhost" (padrão abaixo)
# Docker Compose completo: o compose.yml define DATABASE_URL automaticamente com host "db"
# PostgreSQL remoto: use URL completa do provider
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
# Credenciais do PostgreSQL (apenas para Docker local) - Alterar
POSTGRES_USER=openmonetis
@@ -44,8 +44,19 @@ GOOGLE_CLIENT_SECRET=
# Se não definido, todas as rotas ficam acessíveis.
# PUBLIC_DOMAIN=openmonetis.com
# === Analytics (Opcional) ===
# Umami: https://umami.is — self-hosted ou cloud
UMAMI_URL=
UMAMI_WEBSITE_ID=
UMAMI_DOMAINS=
# === AI Providers (Opcional) ===
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
OPENROUTER_API_KEY=
# === Logo.dev (Opcional) ===
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
LOGO_DEV_TOKEN=
LOGO_DEV_SECRET_KEY=

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
# Força LF para arquivos que precisam de line endings Unix no container
*.sh text eol=lf
docker-entrypoint.sh text eol=lf
Dockerfile text eol=lf

View File

@@ -1,58 +0,0 @@
# AI Coding Assistant Instructions for OpenMonetis
## Project Overview
OpenMonetis is a self-hosted personal finance management application built with Next.js 16, TypeScript, PostgreSQL, and Drizzle ORM. It provides manual transaction tracking, account management, budgeting, and financial insights with a Portuguese interface.
## Architecture
- **Frontend**: Next.js App Router with React 19, shadcn/ui components, Tailwind CSS
- **Backend**: Server actions in Next.js, API routes for auth/health
- **Database**: PostgreSQL with Drizzle ORM, schema in `db/schema.ts`
- **Auth**: Better Auth (OAuth + email magic links)
- **Deployment**: Docker multi-stage build, health checks
## Key Patterns
- **Server Actions**: Use `"use server"` for mutations, validate with Zod schemas, handle errors with `handleActionError`
- **Database Queries**: Use Drizzle's query API with relations, e.g., `db.query.lancamentos.findMany({ with: { categoria: true } })`
- **Authentication**: Import from `lib/auth/server`, redirect on failure
- **Revalidation**: Call `revalidateForEntity("lancamentos")` after mutations
- **Portuguese Naming**: DB fields like `nome`, `tipo_conta`, `pagador` (payer), `lancamento` (transaction)
- **Component Structure**: Feature-based folders in `components/`, shared UI in `components/ui/`
## Development Workflow
- **Start Dev**: `pnpm dev` (Turbopack), `docker compose up db -d` for DB
- **Database**: `pnpm db:push` to sync schema, `pnpm db:studio` for visual editor
- **Build**: `pnpm build`, `pnpm start` for production
- **Docker**: `pnpm docker:up` for full stack, `pnpm docker:logs` for monitoring
## Common Tasks
- **Add Transaction**: Create server action in `app/(dashboard)/lancamentos/actions.ts`, validate with Zod, insert via Drizzle
- **New Entity**: Add to `db/schema.ts`, define relations, create CRUD actions in `lib/[entity]/actions.ts`
- **UI Component**: Use shadcn/ui, place in `components/[feature]/`, export from `components/ui/`
- **API Route**: Add to `app/api/`, use `getUserSession()` for auth
## Conventions
- **Imports**: Absolute paths with `@/`, group by external/internal
- **Error Handling**: Return `{ success: false, error: string }` from actions
- **Currency**: Store as decimal strings (e.g., "123.45"), convert to cents for calculations
- **Periods**: Format as "YYYY-MM", use `parsePeriodParam()` for URL params
- **Notifications**: Send emails via `sendPagadorAutoEmails()` for payer updates
## External Integrations
- **Better Auth**: Config in `lib/auth/config.ts`, session handling
- **Drizzle**: Migrations in `drizzle/`, studio at `pnpm db:studio`
- **AI Features**: Use `@ai-sdk/*` for insights, configured in environment
- **Email**: Resend for notifications, configured via `RESEND_API_KEY`
## File Examples
- Schema: `db/schema.ts` (relations, indexes)
- Actions: `app/(dashboard)/lancamentos/actions.ts` (CRUD with validation)
- Components: `components/lancamentos/page/lancamentos-page.tsx` (client component)
- Utils: `lib/lancamentos/page-helpers.ts` (data transformation)

View File

@@ -13,10 +13,35 @@ on:
env:
DOCKER_IMAGE_NAME: openmonetis
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
build-and-push:
runs-on: ubuntu-latest
needs: quality
permissions:
contents: read
packages: write

View File

@@ -5,6 +5,9 @@ on:
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
release:
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@@ -106,6 +106,7 @@ docker-compose.override.yml
.cursor/
QWEN.md
AGENTS.md
.codex
# === Backups locais ===
/backup/

View File

@@ -5,7 +5,352 @@ 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/).
## [Unreleased]
## [2.5.1] - 2026-05-04
Versão de correção pontual focada na exibição do indicador de anexo nas tabelas de lançamentos da fatura do cartão. Em `/cards/[cardId]/invoice`, lançamentos com anexos não mostravam o ícone porque o fetcher dedicado da fatura não calculava o flag `hasAttachments`. A primeira tentativa de adicionar o EXISTS via `extras` na query relacional gerou SQL inválido (Drizzle re-aliasava `transactionAttachments.transactionId` para o alias da tabela externa). A correção definitiva troca o fetcher pela função compartilhada `fetchTransactionsWithRelations` de `features/transactions`, que já implementa o EXISTS corretamente via `select`.
### Corrigido
- Ícone de anexo voltou a aparecer na tabela de lançamentos da fatura do cartão (`/cards/[cardId]/invoice`). `fetchCardTransactions` em `features/invoices/queries.ts` agora delega para `fetchTransactionsWithRelations`, garantindo que o flag `hasAttachments` seja preenchido com a mesma EXISTS subquery usada no restante do app.
## [2.5.0] - 2026-05-01
Esta versão melhora o fechamento de faturas, a correção de lançamentos já registrados e a conferência de saldos contra o extrato do banco. O novo **ajuste de fatura** fecha a conta entre o total calculado pelo sistema e o valor real cobrado pelo banco, sem exigir que o usuário reabra lançamentos individuais. A mesma ideia foi estendida para **contas correntes**: na página do extrato, ao lado de "Saldo ao final do período", o usuário informa o saldo real e o sistema cria (ou atualiza) um lançamento de ajuste no período visualizado. Também entra o fluxo de **reembolso** para despesas à vista: pelo menu de ações do lançamento, o usuário informa a data do reembolso e o sistema cria uma receita espelhada no extrato ou na fatura correta. O widget de boletos do dashboard ganhou paridade com o widget de faturas — confirmação de pagamento agora pede conta de origem e data antes de quitar o boleto. Por fim, o **limite do cartão** passou a ser obrigatório e o sistema bloqueia despesas em cartão que ultrapassem o limite disponível, retornando uma mensagem com o valor exato disponível. As operações mantêm rastro no lançamento gerado e respeitam a proteção de faturas já pagas.
### Adicionado
- Nome do boleto no widget de Boletos agora é um link para `/transactions?q=<nome>`, incluindo `?periodo=<mes-ano>` automaticamente quando o período selecionado não é o atual. Ícone `RiExternalLinkLine` ao lado do nome, igual ao padrão do widget de Faturas.
- Botão "Ajustar fatura" ao lado do valor na página da fatura.
- Dialog `AdjustInvoiceDialog` com input de valor correto e preview da diferença.
- Action `adjustInvoiceAction` que faz upsert/delete idempotente do lançamento de ajuste.
- Botão "Ajustar saldo" ao lado do valor na página do extrato da conta.
- Dialog `AdjustBalanceDialog` com input do saldo correto e preview da diferença que será lançada (receita ou despesa).
- Action `adjustAccountBalanceAction` que faz upsert/delete idempotente do lançamento de ajuste por `(accountId, period)`.
- Opção "Reembolso" no dropdown de ações de despesas à vista, posicionada após "Copiar" e antes de "Remover".
- Dialog `RefundTransactionDialog` com seleção da data do reembolso e indicação do período de destino.
- Action `refundTransactionAction` que cria uma receita de reembolso vinculada ao lançamento original.
- Constantes compartilhadas `INVOICE_ADJUSTMENT_NAME`, `ACCOUNT_BALANCE_ADJUSTMENT_NAME`, `REFUND_NOTE_PREFIX` e `buildRefundNote()` em `shared/lib/accounts/constants.ts`.
- Validação de limite de cartão: `validateCardLimit()` em `transactions/actions/core.ts` calcula o uso atual do cartão (somando lançamentos não quitados, com a mesma regra usada em `cards/queries.ts` para recorrentes) e bloqueia criação ou edição de despesa em cartão que ultrapasse o disponível, retornando "Lançamento de R$ X excede o limite disponível do cartão (R$ Y)."
- Schema reutilizável `requiredDecimalSchema(fieldName)` em `shared/lib/schemas/common.ts` — número/string positiva (`> 0`) com mensagens parametrizáveis.
### Alterado
- **Limite do cartão é obrigatório**: campo `limite` em `cartoes` ganhou `NOT NULL DEFAULT 0` no schema, validação Zod com `requiredDecimalSchema("limite")`, atributo `required` no input do formulário e checagem client-side antes do submit. Tipos `Card.limit` e `Card.limitAvailable` deixam de ser nullable; branch "Ainda não há limite registrado" foi removido de `card-item.tsx` e a derivação defensiva em `cards/[cardId]/invoice` foi simplificada.
- Migration `0029_friendly_spitfire`: preenche com `0` registros legados antes do `SET NOT NULL` para não quebrar bancos com cartões sem limite.
- Métricas principais passam a tratar reembolsos como abatimento de despesa, não como receita comum.
- Cards de receitas/despesas, série histórica do dashboard e resumo do extrato agora preservam o efeito líquido do reembolso no balanço sem inflar entradas e saídas.
- Pagamento de fatura agora abre confirmação com conta de origem selecionável; por padrão vem a conta vinculada ao cartão, mas o usuário pode escolher outra conta antes de confirmar.
- Widget de faturas no dashboard ganhou a mesma confirmação: o modal "Confirmar pagamento" agora pede conta de origem e data antes de marcar a fatura como paga, alinhando o comportamento ao da página de fatura.
- Widget de boletos no dashboard ganhou a mesma paridade: o modal "Confirmar pagamento" passou a oferecer seleção de **conta de pagamento** e **data do pagamento**, com mesma estrutura de cards de detalhes, métricas, separator e formulário condicional do widget de faturas.
- `toggleTransactionSettlementAction` agora aceita `paymentAccountId` e `paymentDate` opcionais para boletos — quando informados, atualiza a `accountId` do lançamento e usa a data escolhida em `boletoPaymentDate` (em vez da data atual).
- `DashboardBill` passa a expor `accountId` para que o dialog inicialize a conta com o valor já vinculado ao boleto.
- Widget "Lançamentos por Categorias" agora ignora a categoria "Transferência interna" — transferências entre contas próprias deixam de poluir o ranking de categorias.
### Corrigido
- Erro de hidratação no widget de Anotações: `Intl.DateTimeFormat` sem `timeZone` usava o fuso do servidor (UTC) no SSR e o fuso do browser (BRT) no cliente, resultando em datas divergentes. Ambos os formatters passam a usar `timeZone: "America/Sao_Paulo"` explicitamente.
- Extrato da conta agora contabiliza transferências internas nos cards de **Entradas** e **Saídas**: transferência recebida soma em Entradas, transferência enviada soma em Saídas. Antes o saldo final refletia o movimento mas os cards permaneciam zerados, gerando inconsistência visível na tela (issue #47).
### Removido
- Seção "Veja o que você pode fazer" (galeria de screenshots com abas) da landing page, junto com o componente `ScreenshotTabs`, as 14 imagens `preview-*.webp`, o bloco `screenshots` em `images.ts`, o link `#telas` do nav e o export `pwaCompatList` sem uso.
- Exports mortos `dateFormatter` e `monthFormatter` de `features/transactions/formatting-helpers.ts`.
## [2.4.4] - 2026-04-27
Esta versão remove a dependência da extensão `pgcrypto` do PostgreSQL para a geração do `share_code` em pagadores. O default a nível de banco (`gen_random_bytes`) foi removido — agora a aplicação gera o código sempre via `crypto.randomBytes` do Node.js, num utilitário compartilhado. A consequência prática é que o setup inicial fica mais simples: não há mais script de habilitação de extensão, nem etapa extra no primeiro `db:push`, e bancos restaurados de dumps externos não precisam ter `pgcrypto` instalada. O script de backup também foi enxugado para gerar dumps focados nos schemas relevantes (`public` e `drizzle`), descartando os schemas internos do Supabase e eliminando os ~148 erros de restore em PostgreSQL padrão. Por fim, os logos da marca (ícone laranja e wordmark) foram vetorizados: as PNGs antigas foram substituídas por SVGs inline em componentes próprios e por arquivos `.svg` no `public/`, escalando perfeitamente em qualquer tamanho — inclusive nos PDFs exportados, que agora rasterizam o SVG em alta resolução.
### Alterado
- Schema: coluna `share_code` em `pagadores` perdeu o default `substr(encode(gen_random_bytes(24), 'base64'), 1, 24)` — campo continua `NOT NULL` e a aplicação passa a fornecer o valor explicitamente em todas as inserções
- Pagadores: nova função utilitária `generateShareCode()` em `src/shared/lib/payers/share-code.ts` (server-only) — usa `crypto.randomBytes(18).toString("base64url").slice(0, 24)`
- Pagadores: `createPayerAction`, `ensureDefaultPagadorForUser`, `resetUserAppData` (settings) e `mock-data.ts` agora chamam `generateShareCode()` ao inserir um pagador
- Backup: `scripts/backup.sh` agora dumpa apenas os schemas `public` e `drizzle` — schemas internos do Supabase (`auth`, `realtime`, `storage`, `vault`, `graphql`, `graphql_public`, `extensions`, `pgbouncer`) e suas extensions/roles deixam de poluir os dumps. Restaurações em PostgreSQL padrão passam a executar sem os ~148 erros de `role/extension does not exist`
- Logo: `Logo` foi quebrado em três arquivos — `src/shared/components/logo.tsx` (orquestrador), `logo-icon.tsx` (ícone laranja em SVG inline, viewBox `0 0 200 200`) e `logo-text.tsx` (wordmark em SVG inline, viewBox `0 0 574.201 89.6`). API pública (`variant`, `invertTextOnDark`, `colorIcon`, `iconClassName`, `textClassName`) preservada
- Assets: `public/images/logo_small.png` e `logo_text.png` substituídos por `logo_small.svg` e `logo_text.svg` (com `width`/`height` explícitos para compatibilidade com `<img>` em canvas)
- Exports: `loadExportLogoDataUrl` agora carrega SVG e rasteriza no canvas a 4× a resolução natural antes de gerar o data URL — mantém nitidez quando o PDF amplia a imagem
### Removido
- Pasta `scripts/postgres/` (continha `init.sql` e `enable-extensions.ts`)
- Script `pnpm db:extensions` no `package.json`
- Referências ao `pnpm db:extensions` no README
- `public/images/logo_small.png` e `public/images/logo_text.png` (substituídos pelos `.svg`)
### Corrigido
- Migrations: conflito de numeração resolvido — `0027_fancy_reaper` renomeado para `0028_fancy_reaper` (o número 0027 já estava ocupado pelo arquivo órfão `0027_glorious_mindworm`); journal e snapshot atualizados
- TS: removido `baseUrl` do `tsconfig.json` para evitar erro `TS5101` (deprecação no TS 7) — `moduleResolution: bundler` resolve os `paths` relativos ao próprio `tsconfig`, dispensando `baseUrl`
### Documentação
- README: seção Backup atualizada — arquivos gerados agora especificam que apenas os schemas `public` e `drizzle` são dumpados
- README: seção Restore reescrita com o fluxo correto para banco Docker (`DROP SCHEMA public CASCADE` + `pg_restore --clean --if-exists --disable-triggers`)
- README: comando rápido de Docker Compose de backup/restore substituído por `pnpm backup`
- README: header passa a apontar para `logo_small.svg`
## [2.4.3] - 2026-04-25
Esta versão amplia o trabalho com lançamentos divididos: anexos passam a ser visíveis para pessoas com acesso compartilhado, a importação para conta própria copia os arquivos de forma independente e a edição ganha a opção de aplicar a alteração nos dois lados do par. Três caminhos de deleção foram corrigidos para não deixar arquivos órfãos no storage. Também traz refresh visual nos badges de tipo e radio buttons, prefetch server-side de logos para reduzir chamadas de API no dashboard, e ajustes pontuais no healthcheck do container e em rótulos da UI.
### Adicionado
- Schema: coluna `split_group_id` (uuid, nullable) em `lancamentos` com índice `(user_id, split_group_id)` — liga as shares do mesmo evento de divisão
- Split: `buildLancamentoRecords` atribui um `splitGroupId` único por cycle (parcelado, recorrente ou único) para ambas as shares
- Split: edição cooperativa via `updateTransactionSplitPairAction` — ao editar um lançamento dividido, novo dialog `SplitPairDialog` permite escolher entre aplicar somente neste lado ou nos dois lados (nome, data, categoria e demais campos compartilhados; valor e payer permanecem por share)
- Importação: "Importar para Minha Conta" agora copia os anexos do lançamento-fonte para a conta de quem está importando (novo arquivo, novo `userId`, novo `fileKey` — cópia independente via S3 CopyObject). `createSchema` ganhou campo opcional `importFromTransactionId`; helper `copyAttachmentsForImport` valida acesso à fonte via ownership direto ou `payerShares`
- Importação: dialog "Importar para Minha Conta" exibe seção read-only "Anexos que serão copiados" listando os anexos do lançamento-fonte antes da confirmação
- Filtros: nova chave `isDivided` na tabela de lançamentos — toggle "Somente divididos" no drawer de filtros mantém o estado na URL
- Performance: prefetch server-side de mapeamentos Logo.dev no `/dashboard`, `/transactions` e `/payers/[payerId]` — uma única query SQL em batch (`fetchEstablishmentLogoMap`) semeia o cache do React Query antes do primeiro render, eliminando os N requests para `/api/logo/mapping`
### Alterado
- Anexos: `fetchTransactionAttachments` e `fetchTransactionAttachmentsAction` passam a autorizar leitura por acesso à transação (direto ou via `payerShares`), permitindo que pessoas com pagador compartilhado visualizem anexos de lançamentos divididos
- Anexos: upload (`confirmAttachmentUploadAction`) e detach em massa (`detachAttachmentBulkAction`) agora expandem `transactionIds` para incluir shares irmãs via `splitGroupId` — o vínculo em `transaction_attachments` é replicado para manter simetria
- Anexos: delete/detach continuam restritos ao criador (sem alteração de escrita); dashboard (`fetchAttachmentsForPeriod`) permanece listando apenas os anexos do próprio usuário
- Migração: lançamentos divididos criados antes desta versão ficam com `split_group_id` NULL e mantêm o comportamento antigo (anexos não visíveis para a contraparte); apenas splits novos são afetados
- Storage: `deleteS3Object` passa a ignorar `NoSuchKey` silenciosamente — providers S3-compatíveis (ex.: Cloudflare R2) lançam esse erro ao deletar objeto inexistente, ao contrário do comportamento idempotente do S3 padrão
- UI/Badges: `TransactionTypeBadge` redesenhado — substitui o `StatusDot` por ícones direcionais (`RiArrowRightDownLine` receita, `RiArrowRightUpLine` despesa, `RiArrowLeftRightLine` transferência), com borda visível, shadow sutil e variantes dark mode dessaturadas; rótulo "Transferência" abreviado para "Transf."
- UI/Forms: indicador do `RadioGroup` trocado de círculo (`RiCircleLine`) por check (`RiCheckLine`) com fundo sólido `primary` no estado selecionado
- UI/Antecipação: tabela de seleção de parcelas reduzida de quatro para três colunas (estabelecimento + fatura + valor) — informações de parcela e vencimento absorvidas pela coluna do estabelecimento
- Tipografia: fonte Inter agora carrega explicitamente os pesos 500, 600 e 700 (antes derivava de 400)
- Deps: better-auth 1.6.5 → 1.6.9, @aws-sdk/client-s3 3.1032 → 3.1037, @tanstack/react-query 5.99.2 → 5.100.3, @biomejs/biome 2.4.12 → 2.4.13, tailwindcss 4.2.2 → 4.2.4, resend 6.12.0 → 6.12.2
### Corrigido
- Anexos: deleção em massa por série (`deleteTransactionBulkAction`) não chamava cleanup de storage — arquivos ficavam órfãos no S3 após apagar "este e futuros" ou "todos" de uma série parcelada/recorrente com anexo
- Anexos: deleção múltipla por seleção (`deleteMultipleTransactionsAction`) não chamava cleanup de storage — mesmo problema ao selecionar vários lançamentos com anexo e deletar em lote
- Anexos: reset de conta em Ajustes (`resetUserAppData`) não limpava o storage — todos os arquivos do usuário ficavam órfãos no S3 após a operação de zeragem
- Página da pessoa (`/payers/[payerId]`): `fetchPagadorLancamentos` agora calcula `hasAttachments` via `EXISTS`, fazendo o ícone de clipe aparecer na tabela de lançamentos (antes só aparecia em `/transactions`)
- Categorias: mensagem de sucesso ao atualizar exibia "Category atualizada com sucesso." — corrigido para "Categoria atualizada com sucesso."
- Antecipação: rótulos "Category" e "Período" no dialog corrigidos para "Categoria" e "Fatura"
- Docker: healthcheck do container `app` agora usa `127.0.0.1:3000` em vez de `localhost:3000`, evitando connection timeout em hosts com IPv6 (resolvendo [#44](https://github.com/felipegcoutinho/openmonetis/issues/44))
## [2.4.2] - 2026-04-20
Esta versão é quase toda sobre organização e polimento. O código interno do Dashboard foi reestruturado — módulos espalhados pela raiz da feature foram agrupados em subdiretórios coesos e a arquitetura de widgets foi renovada com um novo `widget-registry`. A sidebar lateral foi aposentada em favor de uma navegação concentrada na navbar. A interface passou por um refinamento visual amplo: cards redesenhados, dark mode mais consistente e efeitos decorativos removidos para uma composição mais limpa. As imagens de preview da landing page foram atualizadas. Por fim, a integração com Logo.dev ganhou uma arquitetura mais segura — o token agora é lido apenas no servidor e nunca chega ao cliente. O conceito de "Pagador" foi renomeado para "Pessoa" em toda a interface.
### Adicionado
- Dashboard: nova arquitetura de widgets com `widget-registry` — módulos reorganizados em subdiretórios (`bills/`, `invoices/`, `notes/`, `notifications/`, `overview/`, `payments/`, `goals-progress/`, `categories/`)
- Dashboard: novos componentes `category-breakdown-chart`, `category-breakdown-list`, `goals-progress-item` e `percentage-change-indicator`
- Logo.dev: `server.ts` com `isLogoDevEnabled()` e `buildLogoDevUrl()` server-side; `LogoDevProvider` propaga flag `enabled` para Client Components
- Scripts: `mockup` adicionado ao `package.json` (`tsx scripts/mock-data.ts`)
### Alterado
- Nav: sidebar lateral removida — navegação unificada na navbar
- UI/Tema: raio de borda global 0.625rem → 0.7rem; ajustes finos em `--card` e `--border` (light e dark)
- UI: `DotPattern` removido do layout dashboard, tela de autenticação e landing page
- UI: account-card redesenhado com cores de saldo (success/destructive) e tooltip para flags de exclusão
- UI: budget-card, card-item e componentes do calendário (day-cell, event-modal) com layout revisado
- UI: auth-card-shell simplificado (removido glassmorphism e blob animado)
- Landing: imagens de preview atualizadas; `mainFeatures` + `extraFeatures` unificados em grid único; dark mode nos botões de CTA
- Navbar: dark mode corrigido no navbar-shell (`dark:bg-card`, `dark:border-b-border`)
- Logo.dev: `NEXT_PUBLIC_LOGO_DEV_TOKEN` renomeado para `LOGO_DEV_TOKEN` (agora lido em runtime server-side apenas)
- UI: conceito "Pagador/Pagadores" renomeado para **"Pessoa/Pessoas"** em toda a interface — labels, títulos, toasts, mensagens de erro, cabeçalhos de tabela e exportações. Código, rotas (`/payers`) e schema do banco (`pagadores`) permanecem inalterados; a divergência entre UI e código é intencional
- Deps: next 16.2.3 → 16.2.4, better-auth 1.6.2 → 1.6.5, ai 6.0.159 → 6.0.168 e outros patches menores
- Notas/Tarefas: ícone de tarefa concluída em visualização (card e detalhes) simplificado para `RiCheckLine` verde sem caixa; checkbox no modal de edição usa fundo e borda `success` com ícone `success-foreground` (claro no light, escuro no dark)
- Notas/Detalhes: botões do footer reordenados ("Cancelar" à esquerda, "Alterar" primário à direita)
### Removido
- Nav: componentes sidebar (`app-sidebar`, `nav-main`, `nav-secondary`, `nav-user`, `nav-link`), `sidebar.tsx` e `use-mobile.ts`
- Dashboard: ~25 widgets monolíticos obsoletos (`inbox-widget`, `bills-widget`, `notes-widget`, `payers-widget`, `my-accounts-widget` etc.)
- Dashboard: arquivos dispersos na raiz da feature movidos para subdiretórios (arquivos antigos removidos)
- CSS: variáveis `--data-7` a `--data-10` removidas do tema
- CI: build arg `NEXT_PUBLIC_LOGO_DEV_TOKEN` removido do `Dockerfile` e do workflow `docker-publish.yml` — basta configurar `LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` como variáveis de runtime no host (Coolify, Railway, etc.)
## [2.4.1] - 2026-04-16
### Adicionado
- UI/Auth: layout animado nas páginas de login e signup com efeito blob (3 círculos coloridos em movimento) e card com glassmorphism; layout compartilhado extraído para `app/(auth)/layout.tsx` eliminando duplicação (PR #42)
- DB: 17 índices em foreign keys — evita sequential scans em deletes nas tabelas pai. Impacto maior nas FKs de `lancamentos` (conta_id, categoria_id, antecipacao_id), onde deletes em `categorias` antes provocavam full scan na tabela de lançamentos
### Alterado
- UI/Navbar: labels capitalizados (Lançamentos, Categorias, Contas) em vez de caixa baixa — melhora legibilidade (PR #42)
### Removido
- DB: 7 índices sem uso — `tokens_api_user_id_idx`, `cartoes_user_id_status_idx`, `contas_user_id_status_idx`, `pagadores_user_id_status_idx`, `pagadores_user_id_role_idx`, `dashboard_notification_states_user_id_archived_idx`, `antecipacoes_parcelas_series_id_idx` (0 scans em 187 dias de estatísticas)
- UI/Settings: tab de Integrações órfã removida (não tinha `TabsContent` correspondente)
### Corrigido
- Docker: container do PostgreSQL falhava ao iniciar em instalações existentes após atualização da imagem `postgres:18-alpine` — entrypoint passou a recusar dados no caminho legado `/var/lib/postgresql/data`. Adicionada variável `PGDATA` no `docker-compose.yml` para fixar o caminho e preservar dados de quem já tinha o volume populado (resolve #41)
## [2.4.0] - 2026-04-13
### Adicionado
- Estabelecimentos: integração com Logo.dev — logos automáticos de marcas exibidos na coluna de estabelecimentos nos lançamentos
- Estabelecimentos: picker de logo por estabelecimento — clique no avatar para buscar e fixar um domínio Logo.dev específico (salvo por usuário no banco)
- API: rotas `/api/logo/search` e `/api/logo/mapping` — proxy seguro para Logo.dev Brand Search API (secret key server-side) e consulta de mapeamentos salvos
- Schema: tabela `establishment_logos` com PK composta `(user_id, name_key)` para persistir preferências de logo por usuário
### Corrigido
- Dev: `.env.example` usava host `db` no `DATABASE_URL`, causando erro `EAI_AGAIN` ao rodar `pnpm dev` localmente — corrigido para `localhost`
### Documentação
- README: tabela comparativa entre Perfil 1 (Usar) e Perfil 2 (Desenvolver) com diferenças de setup, `DATABASE_URL` e instruções de atualização
- README: seção "Variáveis de Ambiente" esclarecida — distingue contexto Docker (Perfil 1) de desenvolvimento local (Perfil 2)
- Logo.dev: crie uma conta em logo.dev para obter as chaves `NEXT_PUBLIC_LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` — plano gratuito inclui 500.000 requisições/mês
## [2.3.8] - 2026-04-12
### Alterado
- Docker: `docker-compose.yml` refatorado — removidos profiles, build e dependência de arquivo externo; compose agora é standalone (basta `curl` + `docker compose up -d`)
- Docker: `docker-entrypoint.sh` simplificado — extensão `pgcrypto` criada via Node.js antes das migrations; loop de retry reescrito; removido hack `@localhost → @db`
- Docker: scripts reduzidos de 10 para 5 — `docker:up`, `docker:db`, `docker:down`, `docker:logs`, `docker:update`
- Docs: README reestruturado em dois perfis claros — **Usar** (só Docker) e **Desenvolver** (hot-reload)
## [2.3.7] - 2026-04-11
### Adicionado
- Dashboard: novos widgets configuráveis — Anexos (resumo de arquivos do período), Inbox (snapshot de pré-lançamentos pendentes) e Tendências de Categoria
- Lançamentos: filtro por status de pagamento (somente pagos / somente não pagos) e filtro por presença de anexo
- Lançamentos: indicador visual no status de liquidação para lançamentos de cartão de crédito com fatura paga — exibe ícone verde com tooltip explicativo
- Scripts: `scripts/install-deps.sh` — script de preparação para servidores Ubuntu 24.04 limpos (instala Docker, Node.js 22, pnpm via Homebrew)
- Docker: variáveis `PUBLIC_DOMAIN`, `UMAMI_URL`, `UMAMI_WEBSITE_ID` e `UMAMI_DOMAINS` passadas ao container da aplicação no `docker-compose.yml`
### Alterado
- Fonte: substituída fonte local `America` por `Inter` (Google Fonts, self-hosted pelo Next.js) — elimina arquivos `.woff2` do repositório
- Tipografia: peso tipográfico padronizado de `font-medium` para `font-semibold` em títulos, rótulos e valores monetários em toda a interface
- Parcelas: redesenho do card de grupo de parcelas — expandindo para dialog de detalhes com parcelas pagas/pendentes separadas
- Inbox: redesenho do card de pré-lançamento — logo maior, hierarquia tipográfica melhorada
- Lançamentos: filtros de tipo, condição e forma de pagamento agora usam slugs em URL (ex: `receita` em vez do valor literal com acentos)
- Estabelecimento: popover de autocomplete agora respeita a largura do input ao abrir
- CSP: adicionado `frame-src` para permitir preview de anexos PDF via S3
### Corrigido
- Docker: corrigido crash loop no container com mensagem `exec /app/docker-entrypoint.sh: no such file or directory` causado por CRLF no `docker-entrypoint.sh` em ambientes Windows/WSL2 — adicionado `sed -i 's/\r$//'` no Dockerfile e `.gitattributes` com `eol=lf` para scripts shell
- S3: corrigido `Error: Region is missing` ao usar o app sem S3 configurado — `S3_REGION` vazio (string vazia) não era tratado pelo operador `??`; substituído por `||` em todo o `s3-client.ts`
- i18n: corrigidas mensagens de erro que exibiam "Payer" em inglês em vez de "Pagador"
- Logos: corrigido modal seletor de logos de cartões e contas para renderizar miniaturas sem avisos de proporção
- Scripts: `install-deps.sh` — spinner travava o script por `wait` retornar código não-zero com `set -e` ativo; corrigido com `|| true`
- Scripts: `install-deps.sh` — prompt interativo do corepack suprimido com `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
- Scripts: `install-deps.sh` — PATH do Homebrew não estava configurado na seção de resumo
### Removido
- Scripts: removidos arquivos órfãos `scripts/dev.ts` e `scripts/setup-env.sh` (substituídos pelo `setup.mjs`)
- Docker: `docker-compose.yml` agora funciona sem arquivo `.env``DATABASE_URL` tem valor padrão com credenciais de desenvolvimento
- Docker: `docker-entrypoint.sh` converte automaticamente `@localhost:` para `@db:` na `DATABASE_URL` ao iniciar o container, eliminando a necessidade de usar hosts diferentes no `.env` para desenvolvimento local e Docker
## [2.3.6] - 2026-04-09
### Corrigido
- Docker: adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para que o `drizzle-kit` consiga resolver `drizzle-orm` ao executar as migrations no container
## [2.3.5] - 2026-04-07
### Corrigido
- CSP: movido `Content-Security-Policy` do `next.config.ts` (build time) para `proxy.ts` (runtime), corrigindo bloqueio de upload de anexos quando `S3_ENDPOINT` não estava disponível durante o build do Docker
## [2.3.4] - 2026-04-05
### Corrigido
- Anexos: corrigido upload que falhava com `NetworkError` — CSP `connect-src` bloqueava fetch para o Storage
## [2.3.3] - 2026-04-05
### Corrigido
- Tokens: corrigido `/api/auth/device/verify` que rejeitava tokens criados via Settings (revertido de JWT para hash lookup)
### Alterado
- Tokens: prefixo renomeado de `os_` para `opm_` (OpenMonetis); tokens existentes precisam ser recriados
- Tokens: removidas rotas JWT não utilizadas (`/api/auth/device/token` e `/api/auth/device/refresh`)
- Tokens: `api-token.ts` simplificado para conter apenas `hashToken` e `extractBearerToken`
## [2.3.2] - 2026-04-04
### Segurança
- Tokens: removido aceite de tokens sem expiração (`expiresAt NULL`); tokens criados via settings agora expiram em 1 ano
- Tokens: corrigido refresh que sobrescrevia hash e invalidava access token anterior; verify agora valida JWT por assinatura
- xlsx: desabilitado parsing de fórmulas (`cellFormula: false`) para mitigar CVE-2024-44294
- CSP: expandida Content-Security-Policy com `default-src`, `script-src`, `style-src`, `img-src`, `font-src` e `connect-src`
- Headers: adicionados `Referrer-Policy` e `X-Permitted-Cross-Domain-Policies`
- API: rotas autenticadas agora retornam `401 JSON` em vez de redirect `302` para clientes não autenticados
- Health: removido campo `version` da resposta do `/api/health`
- robots.txt: simplificado para não expor mapa de rotas internas
- Sitemap: corrigida URL com protocolo duplicado (`https://https://`)
- Criado `security.txt` (RFC 9116)
## [2.3.1] - 2026-04-03
### Corrigido
- Infraestrutura: deps do drizzle-kit agora são instaladas em `/app/migrate/` separado do `node_modules` do standalone, corrigindo erro `Cannot find module 'next'` no startup do container
## [2.3.0] - 2026-04-03
### Adicionado
- Dependências: adiciona `@tanstack/react-query` e um provider global para padronizar cache, deduplicação e invalidação de leituras client-side
- Dashboard: widget "Minhas Contas" ganha preferência persistida para mostrar ou ocultar contas marcadas como não consideradas no saldo total
- Dashboard: cards de métricas ganham botão de ajuda com explicação do cálculo exibido no app
- Versionamento: menu do usuário na navbar passa a avisar quando existe release mais recente publicada no GitHub
- Qualidade: adiciona `knip` ao projeto com o script `pnpm run lint:deadcode` para auditar arquivos, exports e tipos sem uso
- Infraestrutura: imagem Docker passa a rodar migrations automaticamente via `docker-entrypoint.sh` antes de iniciar a aplicação
### Alterado
- Anexos: listagem no modal de edição/detalhes, URLs temporárias da galeria e preview deixam de depender de `useEffect` para data fetching direto no componente e passam a usar React Query sobre rotas GET dedicadas
- Insights: carregamento de análises salvas passa a usar React Query com cache por período, mantendo estado draft local apenas para análises recém-geradas ou removidas
- Parcelamentos: histórico de antecipações no diálogo passa a usar React Query com invalidação automática após cancelamento
- Dashboard, insights e relatórios passam a excluir movimentações de contas marcadas como não consideradas no saldo total; balanço e previsto também passam a considerar ajustes de transferências entre contas consideradas e não consideradas
- UX: boletos e faturas passam a exibir labels relativas como "vence hoje", "vence amanhã" e "pago ontem", com tooltip para a data completa
- Lançamentos: diálogo foi reorganizado em blocos mais claros; a criação passa a aceitar múltiplos anexos e a edição em lote preserva `purchaseDate` e `period` ao propagar alterações por série
- Inbox e tabela de lançamentos foram componentizados em partes menores, mantendo paginação e ações em lote mais simples de evoluir
- Infraestrutura: workflow de publish ganha etapa obrigatória de qualidade; `docker-compose` passa a suportar perfil local ou banco remoto; build fixa `pnpm@10.33.0`; projeto atualizado para `Next.js 16.2.2`, `Biome 2.4.10` e dependências correlatas
- Qualidade: `knip` ganha configuração inicial para reduzir falsos positivos, ignorando `src/shared/components/ui/**`, o worker público de PDF, `setup.mjs` e o falso positivo de `postcss`
### Corrigido
- Segurança: criação de antecipações agora valida se `payerId` e `categoryId` informados pertencem ao usuário autenticado antes de persistir referências cruzadas
- Segurança: histórico de antecipações endurece os joins de `transactions`, `payers` e `categories` com filtro por `userId`, evitando exposição de nomes relacionados caso exista referência inconsistente no banco
- Segurança: domínio público deixa de responder rotas `/api/*`, e o Better Auth passa a aplicar rate limits explícitos para login e cadastro por e-mail
- APIs privadas: rotas de anexos, insights salvos, histórico de antecipações e presign de download passam a responder com `Cache-Control: private, no-store`; a rota de antecipações também deixa de devolver mensagens internas de erro ao cliente
- Build: rotas web de tokens do Companion passam a ser explicitamente dinâmicas, removendo o warning de prerender no `next build`
- Lançamentos: edição em série de compras parceladas volta a persistir `purchaseDate` e `period`, permitindo mover parcelas para a fatura ou competência correta conforme o escopo escolhido
- Lançamentos: edições que tentam mover compras de cartão para faturas já pagas agora são bloqueadas com mensagem clara também no fluxo de atualização e propagação em lote
- Imagens: logos institucionais, avatares padrão e componentes com `next/image` em modo `fill` passam a usar containers fixos com `sizes`, removendo avisos de proporção e performance
- Gráficos: `ChartContainer` passa a definir `initialDimension` no `ResponsiveContainer` do Recharts, evitando avisos `width(-1)` e `height(-1)` durante a medição inicial em widgets e relatórios
## [2.2.1] - 2026-04-01
### Corrigido
- Docker: imagem de produção deixa de executar `chown -R /app` no stage final; as permissões passam a ser definidas nos `COPY --chown`, reduzindo o risco de travamento e lentidão excessiva no build/push da GitHub Action
## [2.2.0] - 2026-04-01
### Adicionado
- Anexos: nova página de galeria em `/attachments` com miniaturas, visualização inline de imagem e PDF, download direto e acesso a partir do lançamento
- Anexos: suporte a visualização de PDF diretamente no app via `pdfjs-dist`
- Autenticação: sidebar redesenhado com mockup de faturas e três itens de funcionalidade; páginas de login e cadastro ganham gradiente decorativo e logo visível no mobile
- Notificações: alertas de vencimento para boletos e faturas do período seguinte exibidos quando o vencimento está dentro de 5 dias
- Documentação: novo arquivo público `public/llms.txt` com resumo do projeto e links curados para documentação, setup e arquitetura
### Alterado
- Performance: queries de cache do dashboard migradas de `unstable_cache` para a diretiva `use cache` com `cacheTag` e `cacheLife`; todas as páginas do dashboard passam a chamar `connection()` para renderização dinâmica; `next.config.ts` adota `cacheComponents: true`
- Tipografia: adicionada fonte America Medium (weight 500); pesos tipográficos padronizados para `font-medium` em títulos, valores e rótulos em todos os componentes
- Anexos: `AttachmentPreview` foi simplificado para exibir apenas nome da transação, nome do arquivo, navegação entre anexos e ações de download, abrir em nova aba e fechar com ícone `X`
### Corrigido
- Lançamentos: uploads e remoções de anexo agora funcionam para todos os lançamentos, não apenas os pertencentes a séries
## [2.1.2] - 2026-03-30

View File

@@ -16,9 +16,10 @@
3. **Periods** usam formato `YYYY-MM` (ex: `"2025-11"`). Utils em `src/shared/utils/period/`.
4. **Moeda**: R$ com 2 decimais. DB: `numeric(12, 2)`. Utils em `src/shared/utils/currency.ts`.
5. **Revalidation**: usar `revalidateForEntity("entity")` de `src/shared/lib/actions/helpers.ts` apos mutations.
6. **Versionamento**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog, também altere o `package.json`.
6. **Versionamento**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog, também altere o `package.json` e `readme.md` (Badges do README.md). Cada versão deve ter um parágrafo introdutório em linguagem humana logo abaixo do cabeçalho `## [x.y.z]`, antes das seções `### Adicionado/Alterado/Removido` — descrevendo em prosa o que a versão representa (ex: "Esta versão foca em polimento visual e reorganização interna...").
7. **Comunicacao**: responder em portugues clara e direta com o time.
8. **Commit messages**: agrupar por natureza. em pt-br. seguindo o padrao do sistema.
9. **README.md**: sempre que fizer alteracoes significativas, atualize o README.md.
---
@@ -44,6 +45,10 @@ Use esta pergunta:
Se um contrato cruza dominios, ele deve morar em `src/shared/`.
**Excecao intencional: `attachments` depende de `transactions`**
`src/features/attachments` importa `TransactionDialog`, `TransactionDetailsDialog` e `TransactionItem` diretamente de `src/features/transactions`. Isso e uma dependencia explicita e aceita: anexos sao semanticamente uma extensao de lancamentos — existem por causa deles e nao fazem sentido sem esse contexto. Mover esses componentes para `shared/` seria errado (eles pertencem a transactions). Nao tratar isso como bug a corrigir.
Exemplos comuns:
- auth: `src/shared/lib/auth/*`
@@ -80,6 +85,7 @@ src/
│ │ ├── insights/
│ │ ├── calendar/
│ │ ├── inbox/
│ │ ├── attachments/
│ │ ├── changelog/
│ │ ├── reports/
│ │ │ ├── category-trends/
@@ -106,6 +112,7 @@ src/
│ ├── insights/
│ ├── calendar/
│ ├── inbox/
│ ├── attachments/
│ ├── reports/
│ └── settings/
├── shared/
@@ -210,7 +217,9 @@ Layouts, `loading.tsx` e metadata continuam em `src/app/`.
| `contas` | `accounts` |
| `categorias` | `categories` |
| `orcamentos` | `budgets` |
| `pagadores` | `payers` |
| `pessoas` | `payers` |
> **Nota:** o conceito de "pagador" foi renomeado para **"pessoa"** na UI (labels, toasts, textos visíveis ao usuário). O código, rotas e schema continuam usando o termo original em inglês (`payer`, `payerId`, `adminPayerId`) e em português interno (`pagador` como variável). Não renomear esses identificadores — a divergência entre UI e código é intencional e documentada.
| `anotacoes` | `notes` |
| `calendario` | `calendar` |
| `ajustes` | `settings` |
@@ -302,18 +311,29 @@ export async function fetchData(userId: string, period: string) {
---
## Response Style
## Security Rules
Quando o time pedir avaliacao de plano ou feature:
Regras aplicadas automaticamente ao gerar codigo.
1. Responder em portugues simples.
2. Listar 3-5 problemas principais.
3. Fechar com decisao pratica:
- aprova agora
- nao aprova agora
- o que ajustar antes de comecar codigo
### Secrets
Nunca colocar API keys, credenciais de banco ou tokens em codigo frontend. Evitar variaveis prefixadas com `NEXT_PUBLIC_` para dados sensiveis — estas sao bundladas no cliente. Usar variaveis server-side apenas. `.env` deve estar no `.gitignore` antes do primeiro commit. `.env.example` deve ter apenas placeholders.
Exemplo:
### Autenticacao & Autorizacao
Toda rota protegida em `src/app/api/` requer `getUser()` ou `getOptionalUserSession()` antes de qualquer logica, retornando 401 para nao autenticados. Rotas com IDs de recursos devem verificar ownership: `eq(table.userId, userId)`. Rotas admin devem checar role e retornar 403 para nao-admins. Session cookies em Better Auth ja tem `httpOnly`, `secure` e `sameSite` configurados — nao alterar.
- "Nao aprovaria para comecar codigo imediatamente."
- "Primeiro ajustaria o doc com estes 5 pontos."
### Input & Output
Usar Drizzle ORM (parametrizado por padrao) — nunca concatenar input de usuario em SQL. Validar todo input com Zod antes de usar. Upload de arquivos: usar whitelist de MIME types (`ALLOWED_MIME_TYPES`), presigned URLs para S3, token de upload assinado com verificacao pos-upload. Nunca usar `dangerouslySetInnerHTML` com conteudo de usuario.
### Headers & CSP
CSP definida em `src/proxy.ts` via middleware — alterar la, nao em `next.config.ts`. Headers de seguranca (HSTS, X-Frame-Options, etc.) definidos em `next.config.ts`. Nao remover nem enfraquecer essas configuracoes.
### Rate Limiting
Login: 5 tentativas/min. Signup: 3 tentativas/min. API tokens: 100 req/min (inbox), 20 req/min (batch). Configurado em `src/shared/lib/auth/config.ts` e nas rotas de inbox. Nao remover.
### Tratamento de Erros
Erros nao devem expor stack traces, paths ou nomes de bibliotecas ao cliente. Usar mensagens genericas: `"Algo deu errado"`. Logar detalhes apenas no servidor com `console.error()`.
### Dependencias
Verificar pacotes novos sugeridos pela IA em npmjs.com antes de instalar. Red flags: menos de 1.000 downloads/semana, publicado nos ultimos 30 dias, nome muito parecido com pacote popular. Rodar `pnpm audit` periodicamente.
---

389
DESIGN.md Normal file
View File

@@ -0,0 +1,389 @@
# Design System Inspired by OpenMonetis
## 1. Visual Theme & Atmosphere
OpenMonetis embodies a warm, approachable financial management aesthetic grounded in trust and transparency. The design system combines a rich warm-orange accent palette with a sophisticated warm-neutral foundation, creating an interface that feels both professional and inviting. The typography and spacing work together to emphasize clarity and hierarchy, supporting the open-source ethos of personal financial control. The visual atmosphere prioritizes legibility and calm navigation, with generous whitespace and deliberate color restraint—the bold orange is reserved for critical calls-to-action and highlights, while the warm grays and blacks anchor the interface with stability and focus.
**Key Characteristics**
- Warm, approachable color story with a dominant orange accent (`#FF7733`)
- Generous whitespace and breathing room between sections
- High contrast between backgrounds and text for accessibility
- Clear typographic hierarchy using Inter for all text and UI
- Minimal elevation and shadow treatment—mostly flat design
- Subtle border accents in warm grays to define surfaces
- Open-source transparency reflected in straightforward, honest design language
## 2. Color Palette & Roles
### Primary
- **Primary Accent** (`#FF7733`): Used for primary call-to-action buttons, highlights, and key interactive elements throughout the interface; draws user attention to the most important actions
- **Primary Dark** (`#443732`): Warm-brown anchor color used extensively for text, headings, and interactive elements; provides the primary text color across the system
### Interactive
- **Interactive Neutral** (`#0F0D0C`): Near-black used for primary text and strong emphasis elements; highest contrast state
- **Interactive Overlay** (`#0006`): Transparent black overlay at 24% opacity; used for hover states, modals, and depth layering
### Neutral Scale
- **Neutral 900** (`#2A2827`): Very dark warm gray; used for secondary text and disabled states
- **Neutral 800** (`#322C2A`): Dark warm gray; alternative text color for lower-emphasis content
- **Neutral 700** (`#676260`): Medium warm gray; used for tertiary text, captions, and metadata
- **Neutral 50** (`#FCF7F6`): Almost-white warm cream; primary background color for light surfaces
- **Neutral 100** (`#F8F6F4`): Very light warm gray; secondary background and subtle surface distinction
- **Neutral 200** (`#F5F2EF`): Light warm gray; tertiary background and card interiors
- **Neutral 300** (`#F0EEEC`): Pale warm gray; border and divider lines
### Surface & Borders
- **Surface** (`#FFFFFF`): Pure white; primary card and container background; high-contrast surface
- **Border Light** (`#F0EEEC`): Pale warm gray used for subtle borders between elements
- **Border Dark** (`#2A2827`): Dark warm gray; stronger borders for defined card boundaries
### Semantic / Status
- **Success** (`#0E9D6E`): Green used for positive status indicators, confirmation states, and success messages
- **Warning** (`#F7A439`): Amber used for cautionary states, warnings, and attention-drawing alerts
- **Error** (`#F53F2D`): Red-orange used for error states, destructive actions, and validation failures
- **Error Alt** (`#D40C1A`): Deep red alternative for critical errors and danger states
## 3. Typography Rules
### Font Family
**Primary:** Inter (sans-serif)
Fallback: `Inter, system-ui, -apple-system, sans-serif`
**Monospace:** ui-monospace
Fallback: `ui-monospace, 'Courier New', monospace`
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|------|------|------|--------|-------------|----------------|-------|
| Display / H1 | Inter | 60px | 600 | 60px | 0px | Page titles and hero headlines; maximum visual impact |
| Heading H2 | Inter | 36px | 600 | 40px | 0px | Section headers and major subsections |
| Heading H3 | Inter | 16px | 600 | 20px | 0px | Card titles and smaller section headers |
| Body | Inter | 20px | 400 | 28px | 0px | Descriptive text, paragraphs, and long-form content |
| Body Secondary | Inter | 16px | 400 | 24px | 0px | Standard body text, list items, and descriptions |
| Emphasis / Span | Inter | 14px | 500 | 20px | 0px | Emphasized text, labels, and badge content |
| Button | Inter | 14px | 500 | 20px | 0px | All button text; medium weight for clarity |
| Caption | Inter | 14px | 400 | 20px | 0px | Metadata, timestamps, and small supporting text |
| Code | ui-monospace | 14px | 400 | 20px | 0px | Code blocks and technical content |
### Principles
- **Contrast & Clarity:** Text color `#0F0D0C` on light backgrounds; `#FFFFFF` on dark backgrounds
- **Weight Hierarchy:** Use 600 weight for all headings; 500 for interactive/emphasized text; 400 for body
- **Scale Progression:** Sizes increase in meaningful increments (14 → 16 → 20 → 36 → 60); maintain consistent rhythm
- **Line Height:** Body text uses 1.4× line height multiplier for comfortable reading; headings use tighter ratio (1:1) for impact
- **Readability First:** Avoid line lengths over 80 characters for long-form content; increase line height on smaller screens
## 4. Component Stylings
### Buttons
#### Primary Button
- **Background:** `#FF7733`
- **Text Color:** `#FFFFFF`
- **Font Size:** `14px`
- **Font Weight:** `500`
- **Font Family:** `Inter`
- **Padding:** `8px 16px`
- **Border Radius:** `9.2px`
- **Border:** `0px solid transparent`
- **Height:** `40px`
- **Box Shadow:** `none`
- **Hover State:** Darken background to `#E55F1F`; add subtle shadow `0px 2px 8px rgba(0, 0, 0, 0.12)`
- **Active State:** Darken further to `#CC5118`; increase shadow
- **Disabled State:** Background `#E8E3E0`; text color `#999890`; cursor not-allowed
#### Secondary Button
- **Background:** `#FFFFFF`
- **Text Color:** `#2A2827`
- **Font Size:** `14px`
- **Font Weight:** `500`
- **Font Family:** `Inter`
- **Padding:** `8px 24px`
- **Border Radius:** `9.2px`
- **Border:** `1px solid #F0EEEC`
- **Height:** `40px`
- **Box Shadow:** `0px 1px 3px rgba(0, 0, 0, 0.06)`
- **Hover State:** Background `#F8F6F4`; border color `#E8E3E0`
- **Active State:** Background `#F0EEEC`; border color `#D5CCCA`
- **Disabled State:** Background `#FAFAF8`; text color `#BFBAB7`; border color `#F0EEEC`
#### Ghost Button
- **Background:** `transparent`
- **Text Color:** `#443732`
- **Font Size:** `14px`
- **Font Weight:** `500`
- **Font Family:** `Inter`
- **Padding:** `6px 8px`
- **Border Radius:** `9.2px`
- **Border:** `0px solid transparent`
- **Height:** `32px`
- **Box Shadow:** `none`
- **Hover State:** Background `rgba(68, 55, 50, 0.05)`
- **Active State:** Background `rgba(68, 55, 50, 0.1)`
- **Disabled State:** Text color `#BFBAB7`; cursor not-allowed
#### Icon Button
- **Background:** `transparent`
- **Icon Color:** `#443732`
- **Size:** `32px` × `32px`
- **Border Radius:** `9.2px`
- **Border:** `0px solid transparent`
- **Padding:** `0px`
- **Box Shadow:** `none`
- **Hover State:** Background `rgba(68, 55, 50, 0.08)`
- **Active State:** Background `rgba(68, 55, 50, 0.12)`
### Cards & Containers
#### Standard Card
- **Background:** `#FFFFFF`
- **Border:** `1px solid #F0EEEC`
- **Border Radius:** `11.2px`
- **Padding:** `24px`
- **Box Shadow:** `none`
- **Text Color:** `#2A2827`
- **Hover State:** Border color `#E8E3E0`; box-shadow `0px 4px 12px rgba(0, 0, 0, 0.08)`
#### Card with Top Border
- **Background:** `#FFFFFF`
- **Border:** `1px solid #F0EEEC`
- **Border Radius:** `15.2px 15.2px 0px 0px` (top corners only)
- **Padding:** `24px`
- **Box Shadow:** `none`
- **Top Border Color:** `#FF7733` (3px height implied)
#### Surface Container (Header/Nav)
- **Background:** `#FF7733`
- **Height:** `64px`
- **Padding:** `0px 24px`
- **Box Shadow:** `none`
- **Text Color:** `#FFFFFF`
- **Border:** `0px solid transparent`
#### Light Surface
- **Background:** `#F8F6F4`
- **Border:** `0px solid transparent`
- **Border Radius:** `11.2px`
- **Padding:** `16px`
- **Box Shadow:** `none`
### Inputs & Forms
#### Text Input
- **Background:** `#FFFFFF`
- **Border:** `1px solid #F0EEEC`
- **Border Radius:** `9.2px`
- **Padding:** `12px 16px`
- **Font Size:** `16px`
- **Text Color:** `#2A2827`
- **Line Height:** `24px`
- **Placeholder Color:** `#999890`
- **Focus State:** Border color `#FF7733`; box-shadow `0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
- **Error State:** Border color `#F53F2D`; background `#FEF5F3`
- **Disabled State:** Background `#F8F6F4`; text color `#BFBAB7`; border color `#F0EEEC`
#### Select / Dropdown
- **Background:** `#FFFFFF`
- **Border:** `1px solid #F0EEEC`
- **Border Radius:** `9.2px`
- **Padding:** `12px 16px`
- **Font Size:** `16px`
- **Text Color:** `#2A2827`
- **Focus State:** Border color `#FF7733`; outline `0px`
- **Hover State:** Background `#FAFAF8`
#### Checkbox & Radio
- **Size:** `20px` × `20px`
- **Border Radius:** `4px` (checkbox), `50%` (radio)
- **Border:** `2px solid #F0EEEC`
- **Background:** `#FFFFFF`
- **Checked Background:** `#FF7733`
- **Checked Border:** `2px solid #FF7733`
- **Checked Icon Color:** `#FFFFFF`
- **Focus:** Border `2px solid #FF7733`; box-shadow `0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
### Navigation
#### Primary Navigation
- **Background:** `#FF7733`
- **Height:** `64px`
- **Padding:** `0px 48px`
- **Display:** flex; align-items: center; gap `32px`
- **Link Color:** `#FFFFFF`
- **Link Font Size:** `16px`
- **Link Font Weight:** `400`
- **Link Hover:** Opacity `0.8`
- **Link Active:** Text decoration underline; opacity `1.0`
#### Secondary Navigation / Tabs
- **Background:** `transparent`
- **Border Bottom:** `2px solid #F0EEEC`
- **Tab Padding:** `16px 24px`
- **Tab Color:** `#676260`
- **Tab Font Size:** `16px`
- **Tab Hover:** Color `#443732`
- **Tab Active:** Color `#FF7733`; border-bottom color `#FF7733`
#### Breadcrumb Navigation
- **Font Size:** `14px`
- **Color:** `#676260`
- **Separator:** `/` with `0px 8px` margin
- **Link Color:** `#443732`
- **Link Hover:** Color `#FF7733`
- **Current (Active):** Color `#2A2827`; font-weight `500`
### Badges & Status Indicators
#### Badge Default
- **Background:** `#F8F6F4`
- **Text Color:** `#443732`
- **Padding:** `4px 12px`
- **Border Radius:** `20px`
- **Font Size:** `12px`
- **Font Weight:** `500`
- **Border:** `0px solid transparent`
#### Badge Success
- **Background:** `#E8F5F0`
- **Text Color:** `#0E9D6E`
- **Padding:** `4px 12px`
- **Border Radius:** `20px`
- **Font Size:** `12px`
- **Font Weight:** `500`
#### Badge Warning
- **Background:** `#FEF5E8`
- **Text Color:** `#F7A439`
- **Padding:** `4px 12px`
- **Border Radius:** `20px`
- **Font Size:** `12px`
- **Font Weight:** `500`
#### Badge Error
- **Background:** `#FEF5F3`
- **Text Color:** `#F53F2D`
- **Padding:** `4px 12px`
- **Border Radius:** `20px`
- **Font Size:** `12px`
- **Font Weight:** `500`
## 5. Layout Principles
### Spacing System
- **Base Unit:** `4px`
- **Scale:** `4px`, `8px`, `12px`, `16px`, `24px`, `32px`, `48px`, `56px`, `64px`, `80px`, `96px`, `128px`
**Usage Contexts:**
- **48px:** Tight spacing within compact components (icon-text pairs, inline elements)
- **1216px:** Standard padding inside cards, inputs, and buttons
- **2432px:** Section gaps, spacing between components on a page
- **4864px:** Large section separations, hero spacing
- **80128px:** Hero margins, page-level vertical rhythm
### Grid & Container
- **Max Width:** `1440px` for full-width containers
- **Content Width:** `1152px` for typical page layouts
- **Column Strategy:** 12-column grid system; gutter `24px`
- **Container Padding:** `48px` on desktop (left + right)
- **Section Pattern:** Full-width containers with internal max-width constraint
### Whitespace Philosophy
OpenMonetis prioritizes breathing room and visual clarity. Whitespace is intentional and strategic—surrounding headings, separating card groups, and framing key messages. The warm neutral backgrounds (`#F8F6F4`, `#F5F2EF`) create natural visual separation without hard borders. Minimum margin between major sections is `64px` vertically; minimum padding inside containers is `16px`.
### Border Radius Scale
- **Sharp Corners:** `0px` (utility container tops, category selectors)
- **Subtle Radius:** `9.2px` (buttons, small inputs, icon buttons)
- **Standard Radius:** `11.2px` (cards, standard containers, modals)
- **Rounded Top:** `15.2px 15.2px 0px 0px` (card headers, sheet-style containers)
- **Pill Shape:** `24px` (badges, full-rounded tags, avatar images)
- **Circle:** `50%` (avatar images, radial elements)
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (None) | No shadow, `border: 1px solid #F0EEEC` | Default cards, inputs, containers; baseline surfaces |
| Subtle (sm) | `0px 1px 3px rgba(0, 0, 0, 0.06)` | Secondary buttons, hover states on light surfaces |
| Medium (md) | `0px 4px 12px rgba(0, 0, 0, 0.08)` | Elevated cards on hover, floating actions |
| Deep (lg) | `0px 10px 24px rgba(0, 0, 0, 0.12)` | Modals, dropdowns, popover menus |
**Shadow Philosophy:**
The design system uses restrained shadow treatment aligned with a flat-modern aesthetic. Shadows emerge subtly on interaction (hover, focus) rather than as default styling. The primary depth cue is border color (`#F0EEEC`), which maintains visual hierarchy without excessive z-depth. When shadows are used, they employ warm-tinted blacks (`rgba(0, 0, 0, 0.060.12)`) to harmonize with the warm neutral palette.
## 7. Do's and Don'ts
### Do
- Use the primary orange (`#FF7733`) exclusively for the most important call-to-action buttons
- Apply generous padding (`24px48px`) around sections and inside cards for breathing room
- Stack elements vertically with `2432px` gaps for clear visual rhythm
- Use warm grays (`#443732`, `#2A2827`) for all body text to maintain the warm aesthetic
- Apply `9.2px` border radius consistently to all interactive elements (buttons, inputs)
- Keep line heights at `1.4×` or greater for comfortable reading on body text
- Use semantic colors (`#0E9D6E` success, `#F7A439` warning, `#F53F2D` error) intentionally
- Test contrast ratios; maintain WCAG AA minimum (4.5:1 for body text)
- Use the `Inter` typeface exclusively for consistency
- Implement focus states with a `3px` colored outline or border
### Don't
- Don't use orange anywhere except primary CTAs and critical highlights
- Don't reduce padding below `12px` inside cards or `8px` inside compact buttons
- Don't use dark backgrounds (`#0F0D0C`) for body text on light surfaces; stick to `#2A2827` or `#443732`
- Don't apply shadows as default styling; reserve them for elevated states (hover, focus, modal)
- Don't mix border radius values on the same component type; stick to defined scale
- Don't increase line height above `1.6×` for headings; tighten for impact
- Don't use the error color (`#F53F2D`) for general emphasis; reserve for genuine errors
- Don't create new colors outside the palette; use opacity if gradation is needed
- Don't apply multiple shadows to a single element; layer a maximum of two shadow values
- Don't forget to include focus/keyboard navigation states on all interactive elements
## 8. Responsive Behavior
### Breakpoints
| Breakpoint | Width | Key Changes |
|-----------|-------|-------------|
| Mobile | `375px599px` | Single column; container padding `16px`; font sizes reduce 12 sizes; gap scale halved |
| Tablet | `600px1023px` | Two-column grid; container padding `32px`; button height `36px`; heading sizes reduce slightly |
| Desktop | `1024px+` | Full 12-column grid; container padding `48px`; full-scale typography; max-width `1440px` |
### Touch Targets
- **Minimum Interactive Size:** `44px` × `44px` for mobile; `40px` × `40px` for desktop
- **Button Padding:** `12px` vertical, `16px` horizontal (minimum)
- **Link Padding:** `6px` vertical, `8px` horizontal minimum
- **Icon Button:** `32px` × `32px` minimum (32px on mobile preferred)
- **Spacing Between Targets:** `8px` minimum to avoid accidental activation
### Collapsing Strategy
- **Navigation:** Horizontal nav on desktop collapses to hamburger menu on tablet; menu items stack vertically with `12px` gap
- **Grid:** 12-column layout on desktop → 6-column on tablet → 2-column (stacked) on mobile
- **Cards:** Three-column card layouts collapse to single column on mobile; padding reduces from `24px` to `16px`
- **Typography:** Display (60px) → 36px on tablet → 28px on mobile; body (20px) → 18px on tablet → 16px on mobile
- **Spacing:** All spacing scale values reduce by 2533% on mobile (e.g., `24px` gap → `16px` on tablet, `12px` on mobile)
- **Buttons:** Full-width on mobile (padding `0px`); inline (auto-width) on desktop
- **Inputs:** Full-width on mobile; constrained width on desktop
## 9. Agent Prompt Guide
### Quick Color Reference
- **Primary CTA:** Warm Orange (`#FF7733`) — Buttons, highlights, key interactions
- **Primary Text:** Warm Brown (`#443732`) — Headings, strong emphasis
- **Secondary Text:** Dark Neutral (`#2A2827`) — Body text, card content
- **Background:** White (`#FFFFFF`) — Cards, primary surfaces
- **Background Alt:** Cream (`#FCF7F6`) — Alternative surfaces, light containers
- **Border:** Pale Gray (`#F0EEEC`) — Card borders, divider lines
- **Success:** Green (`#0E9D6E`) — Confirmation, positive states
- **Warning:** Amber (`#F7A439`) — Cautions, attention states
- **Error:** Red-Orange (`#F53F2D`) — Errors, destructive actions
- **Disabled:** Light Gray (`#E8E3E0`) — Inactive elements, inaccessible states
### Iteration Guide
1. **Always use `#FF7733` for primary buttons** and all main call-to-action elements; secondary buttons use `#FFFFFF` with `#F0EEEC` border
2. **Typography is always `Inter`** with weights 400 (body), 500 (emphasis), and 600 (headings); size hierarchy: 14 → 16 → 20 → 36 → 60px
3. **Spacing base is `4px`**; use multiples from the scale (8, 12, 16, 24, 32, 48, 64, 80, 96, 128px); never arbitrary values
4. **Border radius:** Apply `9.2px` to all buttons and inputs, `11.2px` to cards, `15.2px 15.2px 0px 0px` to top-bordered containers
5. **Cards default to `#FFFFFF` background with `1px solid #F0EEEC` border**; add shadow only on hover (0px 4px 12px rgba(0, 0, 0, 0.08))
6. **Form inputs:** Padding `12px 16px`, border `1px solid #F0EEEC`, focus state `border: 1px solid #FF7733` + `box-shadow: 0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
7. **Navigation background is always `#FF7733`** with white text; links in content use `#443732` with underline on active
8. **Maintain 1.4× line height minimum for body text**; tighten headings to 1:1 or 1.2:1 ratio
9. **Contrast validation:** Text `#0F0D0C` or `#2A2827` on light backgrounds (WCAG AA); text `#FFFFFF` on `#FF7733` (WCAG AAA)
10. **Responsive collapse:** Reduce padding and font by 25% on mobile; stack multi-column layouts to single column; full-width buttons on mobile only

View File

@@ -5,14 +5,16 @@
# ============================================
FROM node:22-alpine AS deps
# Instalar pnpm globalmente
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
WORKDIR /app
# Copiar apenas arquivos de dependências para aproveitar cache
COPY package.json pnpm-lock.yaml* ./
# Criar pasta public para o postinstall do pdfjs-dist
RUN mkdir -p public
# Instalar dependências (production + dev para o build)
RUN pnpm install --frozen-lockfile
@@ -21,8 +23,7 @@ RUN pnpm install --frozen-lockfile
# ============================================
FROM node:22-alpine AS builder
# Instalar pnpm globalmente
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
WORKDIR /app
@@ -32,13 +33,18 @@ COPY --from=deps /app/node_modules ./node_modules
# Copiar todo o código fonte
COPY . .
# Garantir que o pdf.worker vem da versão instalada no stage 1, não do host
COPY --from=deps /app/public/pdf.worker.min.mjs ./public/pdf.worker.min.mjs
# Variáveis de ambiente necessárias para o build
# DATABASE_URL será fornecida em runtime, mas precisa estar definida para validação
ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_ENV=production
# Nota: a integração Logo.dev não precisa mais de build args. O token
# `LOGO_DEV_TOKEN` é lido em runtime no servidor — basta configurá-lo no
# host (Coolify, Railway, etc.) junto com `LOGO_DEV_SECRET_KEY`.
# Build da aplicação Next.js
# Nota: Se houver erros de tipo, ajuste typescript.ignoreBuildErrors no next.config.ts
RUN pnpm build
# ============================================
@@ -46,8 +52,7 @@ RUN pnpm build
# ============================================
FROM node:22-alpine AS runner
# Instalar pnpm globalmente
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
WORKDIR /app
@@ -55,12 +60,27 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copiar apenas arquivos necessários para produção
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
# Instalar deps do drizzle-kit em diretório separado ANTES de copiar o standalone
# Isso evita que o pnpm install sobrescreva o node_modules do Next.js standalone
COPY --from=builder /app/package.json /tmp/pkg.json
RUN mkdir -p /app/migrate && \
node -e "\
const p=JSON.parse(require('fs').readFileSync('/tmp/pkg.json','utf8'));\
require('fs').writeFileSync('/app/migrate/package.json',JSON.stringify({\
name:'openmonetis-migrate',version:p.version,\
dependencies:{\
'drizzle-orm':p.dependencies['drizzle-orm'],\
'pg':p.dependencies['pg']\
},\
devDependencies:{'drizzle-kit':p.devDependencies['drizzle-kit']}\
}));" && \
cd /app/migrate && pnpm install --no-frozen-lockfile --ignore-scripts && \
chown -R nextjs:nodejs /app/migrate
# Copiar arquivos de build do Next.js
# Copiar apenas arquivos necessários para produção
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# Copiar arquivos de build do Next.js (inclui node_modules standalone com next)
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
@@ -69,8 +89,11 @@ COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder --chown=nextjs:nodejs /app/src/db ./src/db
# Copiar node_modules para ter drizzle-kit disponível para migrations
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
# Copiar entrypoint de migrations
COPY docker-entrypoint.sh ./
RUN sed -i 's/\r$//' /app/docker-entrypoint.sh && \
chmod +x /app/docker-entrypoint.sh && \
chown nextjs:nodejs /app/docker-entrypoint.sh
# Definir variáveis de ambiente de produção
ENV NODE_ENV=production \
@@ -81,16 +104,13 @@ ENV NODE_ENV=production \
# Expor porta
EXPOSE 3000
# Ajustar permissões para o usuário nextjs
RUN chown -R nextjs:nodejs /app
# Mudar para usuário não-root
USER nextjs
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1
CMD wget --quiet --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1
# Comando de inicialização
# Nota: Em produção com standalone build, o servidor é iniciado pelo arquivo server.js
# Entrypoint: roda migrations e depois executa o CMD
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["node", "server.js"]

322
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="./public/images/logo_small.png" alt="OpenMonetis Logo" height="80" />
<img src="./public/images/logo_small.svg" alt="OpenMonetis Logo" height="80" />
</p>
<p align="center">
@@ -8,7 +8,7 @@
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.1.1-blue?style=flat-square)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-2.5.1-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)
@@ -20,7 +20,7 @@
---
<p align="center">
<img src="./public/images/dashboard-preview-light.webp" alt="Dashboard Preview" width="800" />
<img src="./public/images/dashboard-preview-light.png" alt="Dashboard Preview" width="800" />
</p>
---
@@ -28,10 +28,12 @@
## 📖 Índice
- [Sobre o Projeto](#-sobre-o-projeto)
- [Instalação via Script](#-instalação-via-script)
- [Início Rápido (manual)](#-início-rápido)
- [Como rodar o OpenMonetis](#-como-rodar-o-openmonetis)
- [Perfil 1 — Usar](#perfil-1--usar-self-hosting)
- [Perfil 2 — Desenvolver](#perfil-2--desenvolver)
- [Scripts Disponíveis](#-scripts-disponíveis)
- [Docker](#-docker)
- [Backup](#-backup)
- [Storage S3 Compatível](#-storage-s3-compatível)
- [Variáveis de Ambiente](#-variáveis-de-ambiente)
- [Arquitetura](#-arquitetura)
@@ -53,7 +55,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
**1. Não há versão hospedada online** — Este projeto é self-hosted. Você precisa rodar no seu próprio computador ou servidor.
**2. Não há Open Finance** — Não há conexão automática com bancos. Você pode registrar transações manualmente ou importar extratos nos formatos OFX e XLS/XLSX.
**2. Não há Open Finance** — Não há conexão automática com bancos. Você pode registrar transações manualmente, usar o app companion para capturar notificações bancárias ou importar extratos nos formatos OFX e XLS/XLSX.
**3. Requer disciplina** — O OpenMonetis funciona melhor para quem tem disciplina de registrar os gastos regularmente, quer controle total sobre seus dados e gosta de entender exatamente onde o dinheiro está indo.
@@ -77,7 +79,11 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
📅 **Calendário financeiro** — Visualize todos os lançamentos em um calendário mensal.
📲 **OpenMonetis Companion** — App Android que captura notificações bancárias (Nubank, Itaú, Bradesco, Inter, C6 e outros) e envia como pré-lançamentos para revisão. [Repositório](https://github.com/felipegcoutinho/openmonetis-companion).
📲 **OpenMonetis Companion** — App Android que captura notificações bancárias (Nubank, Itaú, Bradesco, Inter, C6 e outros) e envia automaticamente como pré-lançamentos para revisão — sem digitar nada. [Repositório](https://github.com/felipegcoutinho/openmonetis-companion).
<p align="center">
<img src="./public/images/companion-preview-light.webp" alt="OpenMonetis Companion" width="300" height="600" />
</p>
⚙️ **Personalização** — Tema dark/light e modo privacidade.
@@ -93,82 +99,119 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
---
## ⚡ Instalação via Script
## 🚀 Como rodar o OpenMonetis
A forma mais rápida de instalar. O script verifica dependências, configura o `.env` interativamente e sobe o banco automaticamente.
Escolha o perfil que corresponde ao seu objetivo:
**Pré-requisito:** Node.js 22+
```bash
# Mac / Linux / WSL
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs -o setup.mjs && node setup.mjs
# Windows (PowerShell)
curl -o setup.mjs https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs ; node setup.mjs
```
O script irá:
- Verificar Node, pnpm, Git e Docker
- Perguntar se quer banco local (Docker) ou remoto (Supabase, Neon, etc.)
- Gerar o `BETTER_AUTH_SECRET` automaticamente
- Configurar opcionais: Google OAuth, e-mail, IA, domínio público
- Clonar o repositório, instalar dependências e aplicar o schema
| | Perfil 1 — Usar | Perfil 2 — Desenvolver |
|---|---|---|
| **Objetivo** | Rodar o app pronto | Modificar o código |
| **Clonar repositório** | Não | Sim |
| **Node.js / pnpm** | Não | Sim (Node 22+) |
| **Docker** | Sim | Sim |
| **Como iniciar** | `docker compose up -d` | `pnpm docker:db` + `pnpm dev` |
| **App roda em** | Container Docker | Host local (hot-reload) |
| **Banco roda em** | Container Docker | Container Docker |
| **`DATABASE_URL` (host)** | `db` (automático pelo compose) | `localhost` |
| **Banco remoto (Supabase, Neon...)** | Sim (`docker compose up -d app`) | Sim (ajustar `DATABASE_URL`) |
| **Como atualizar** | `pnpm docker:update` | `git pull` + `pnpm install` + `pnpm db:push` |
| **Indicado para** | Self-hosting, VPS, servidor | Contribuidores, customizações |
---
## 🚀 Início Rápido (manual)
### Perfil 1 — Usar (self-hosting)
### Pré-requisitos
Só quer rodar o OpenMonetis. **Não precisa clonar o repositório nem instalar Node.js** — apenas Docker.
- Node.js 22+ e pnpm
- Docker e Docker Compose
```bash
# 1. Baixe o compose
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
### Passo a Passo
# 2. Suba tudo
docker compose up -d
```
1. **Clone e instale**
Acesse em: `http://localhost:3000`
```bash
git clone https://github.com/felipegcoutinho/openmonetis.git
cd openmonetis
pnpm install
```
O banco sobe com credenciais padrão. Para personalizar (senha, Google OAuth, e-mail, IA...), crie um `.env` na mesma pasta **antes** de subir:
2. **Configure o `.env`**
```bash
# .env mínimo recomendado para produção
BETTER_AUTH_SECRET=gere-um-valor-com-openssl-rand-base64-32
BETTER_AUTH_URL=https://seu-dominio.com
```
```bash
cp .env.example .env
```
Veja todas as variáveis disponíveis em [Variáveis de Ambiente](#-variáveis-de-ambiente).
Edite o `.env` com suas credenciais. O principal é o `DATABASE_URL` e o `BETTER_AUTH_SECRET`:
**Banco remoto (Supabase, Neon, Railway...):** defina `DATABASE_URL` no `.env` e suba só o app:
```env
# Banco local (Docker): use host "localhost"
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
```bash
docker compose up -d app
```
# Banco remoto (Supabase, Neon, etc): use a URL completa do provider
# DATABASE_URL=postgresql://user:password@host:5432/database?sslmode=require
**Não tem Docker instalado?** Em servidores Ubuntu 24.04 limpos, use o script de instalação:
BETTER_AUTH_SECRET=seu-secret-aqui # gere com: openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000
```
```bash
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/scripts/install-deps.sh -o install-deps.sh
sudo sh install-deps.sh
```
3. **Suba o banco de dados** (pule se estiver usando banco remoto)
> Ao final, faça **logout e login** para as permissões do grupo `docker` terem efeito.
```bash
docker compose up db -d
pnpm db:extensions
```
#### Atualizando (Perfil 1)
4. **Execute as migrations e inicie**
```bash
pnpm docker:update
# ou equivalente:
docker compose pull && docker compose up -d
```
```bash
pnpm db:push
pnpm dev
```
O schema do banco é aplicado automaticamente no startup — nenhum passo extra necessário.
5. Acesse `http://localhost:3000`
---
> **Docker completo** (app + banco em containers): use `pnpm docker:up` ao invés dos passos 3-4.
### Perfil 2 — Desenvolver
Quer modificar o código com hot-reload. O banco roda no Docker, o app roda direto no seu servidor.
**Requisitos:** Docker + Node.js 22+ + pnpm
```bash
# 1. Clone o repositório
git clone https://github.com/felipegcoutinho/openmonetis.git
cd openmonetis
# 2. Instale as dependências
pnpm install
# 3. Configure o ambiente
cp .env.example .env
# O DATABASE_URL já vem com host "localhost" (correto para dev local).
# Edite o .env com suas configurações (BETTER_AUTH_SECRET, etc.)
# 4. Suba o banco
pnpm docker:db
# 5. Aplique o schema no banco (apenas no primeiro setup)
pnpm db:push
# 6. Inicie o app com hot-reload
pnpm dev
```
Acesse em: `http://localhost:3000`
Toda vez que salvar um arquivo, o app atualiza automaticamente sem precisar reiniciar.
#### Atualizando (Perfil 2)
```bash
git pull
pnpm install # instala dependências novas, se houver
pnpm db:push # aplica mudanças de schema, se houver
```
O `pnpm dev` já em execução detecta as mudanças de código automaticamente — não precisa reiniciar.
---
@@ -196,41 +239,54 @@ pnpm db:studio # Drizzle Studio (UI visual)
### Utilitários
```bash
pnpm backup # Backup do banco (requer scripts/backup.sh configurado)
pnpm backup # Backup completo do banco (ver seção Backup)
```
### Docker
```bash
pnpm docker:up # Subir app + banco
pnpm docker:up:d # Subir em background
pnpm docker:up:db # Subir apenas o banco
pnpm docker:down # Parar containers
pnpm docker:down:volumes # Parar e remover volumes (⚠️ apaga dados!)
pnpm docker:logs # Logs em tempo real
pnpm docker:restart # Reiniciar
pnpm docker:rebuild # Rebuild completo
pnpm docker:up # Sobe app (Docker Hub) + banco em background
pnpm docker:db # Sobe apenas o banco em background (usar com pnpm dev)
pnpm docker:down # Para e remove os containers
pnpm docker:logs # Logs em tempo real (todos os containers)
pnpm docker:update # Atualiza para a imagem mais recente do Hub e reinicia
```
---
## 🐳 Docker
O `Dockerfile` usa multi-stage build (deps → builder → runner) com imagem final ~200MB rodando como usuário não-root.
O `Dockerfile` usa multi-stage build (deps → builder → runner) com imagem final ~200MB rodando como usuário não-root. Health checks configurados para ambos os serviços (PostgreSQL via `pg_isready`, app via `GET /api/health`).
Health checks configurados para ambos os serviços (PostgreSQL via `pg_isready`, app via `GET /api/health`).
### Self-hosting (recomendado)
Baixe apenas o `docker-compose.yml` e suba tudo — sem clonar o repositório, sem instalar dependências:
```bash
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
docker compose up -d
```
As credenciais padrão do banco já estão configuradas. Para personalizar (senhas, opcionais), crie um `.env` na mesma pasta antes de subir — veja [Variáveis de Ambiente](#-variáveis-de-ambiente).
### Banco remoto (Supabase, Neon, Railway...)
Suba apenas o app e aponte para o banco externo via `DATABASE_URL` no `.env`:
```bash
docker compose up -d app
```
### Comandos úteis
```bash
docker compose exec app sh # Shell da aplicação
docker compose exec app sh # Shell da aplicação
docker compose exec db psql -U openmonetis -d openmonetis_db # Shell do banco
docker compose ps # Status
docker compose exec db pg_dump -U openmonetis openmonetis_db > backup.sql # Backup
docker compose exec -T db psql -U openmonetis -d openmonetis_db < backup.sql # Restore
pnpm backup # Backup (ver seção Backup)
```
### Customizando Portas
### Customizando portas
```env
APP_PORT=3001 # Padrão: 3000
@@ -239,6 +295,71 @@ DB_PORT=5433 # Padrão: 5432
---
## 💾 Backup
O backup é uma rotina de infraestrutura — não é uma tela no app. Ele opera diretamente sobre o banco PostgreSQL e é executado via linha de comando.
```bash
pnpm backup
```
### O que é salvo
Cada execução gera **3 arquivos** em `backup/`:
| Arquivo | Conteúdo | Uso |
|---|---|---|
| `openmonetis_YYYY-MM-DD_HH-MM.dump` | Dump custom dos schemas `public` + `drizzle` | Restore completo via `pg_restore` |
| `openmonetis_YYYY-MM-DD_HH-MM.sql.gz` | Dump SQL compactado dos schemas `public` + `drizzle` | Inspeção manual, portabilidade |
| `openmonetis_YYYY-MM-DD_HH-MM.data.sql.gz` | Apenas os dados do schema `public` (sem DDL) | Migração parcial, seed de outro ambiente |
### Modos de conexão
Configure `DB_MODE` no topo de `scripts/backup.sh`:
| Modo | Quando usar | Fonte de dados |
|---|---|---|
| `remote` (padrão) | Banco em Supabase, Neon, Railway, etc. | `DATABASE_URL` do `.env` |
| `docker` | Banco no container local | Container `openmonetis_postgres` |
### Upload para Google Drive (opcional)
Se o [rclone](https://rclone.org/) estiver instalado e configurado com um remote chamado `gdrive`, os arquivos são enviados automaticamente para `gdrive:BACKUP OPENMONETIS`. Sem o rclone, o backup funciona normalmente e fica apenas local.
**Retenção:**
- Local: 7 dias
- Google Drive: 30 dias
### Automatizar com cron
Para rodar o backup automaticamente todo dia às 3h:
```bash
crontab -e
```
```cron
0 3 * * * cd /caminho/para/openmonetis && pnpm backup >> /var/log/openmonetis-backup.log 2>&1
```
### Restore
```bash
# 1. Zerar o banco
docker exec <container-db> psql -U openmonetis -d openmonetis_db \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# 2. Restaurar schema + dados (um comando)
docker exec -i <container-db> pg_restore \
-U openmonetis -d openmonetis_db \
--clean --if-exists --disable-triggers --no-owner --no-privileges \
< backup/openmonetis_YYYY-MM-DD_HH-MM.dump
```
> `--disable-triggers` é necessário para evitar erros de FK durante o restore (os dados são inseridos fora de ordem). O usuário `openmonetis` tem permissão para isso.
---
## ☁️ Storage S3 Compatível
O suporte a anexos de lançamentos usa upload direto com URL pré-assinada. Essa configuração é opcional, mas passa a ser necessária se você quiser habilitar anexos no app.
@@ -256,20 +377,60 @@ S3_BUCKET=
### Compatibilidade
- O código atual espera um provider com API compatível com S3 e suporte a `PutObject`, `GetObject`, `HeadObject`, `DeleteObject` e URLs pré-assinadas.
- A implementação usa `endpoint` customizado e `forcePathStyle: true` em [`src/shared/lib/storage/s3-client.ts`](/home/ubuntu/github/openmonetis/src/shared/lib/storage/s3-client.ts).
- A implementação usa `endpoint` customizado e `forcePathStyle: true` em [`src/shared/lib/storage/s3-client.ts`](./src/shared/lib/storage/s3-client.ts).
- Em geral isso cobre MinIO, Cloudflare R2, Backblaze B2 S3-Compatible, DigitalOcean Spaces e AWS S3. Mas foi testado apenas no Supabase Storage.
- Se o seu provider exigir `virtual-hosted-style` em vez de `path-style`, você vai precisar ajustar essa configuração antes de usar anexos.
- Se as variáveis de S3 não forem configuradas, mantenha os anexos desabilitados no seu fluxo de uso.
---
## 🏷️ Logos de Estabelecimentos (Logo.dev)
O app exibe logos automáticos de marcas na coluna de estabelecimentos nos lançamentos. A integração usa a [Logo.dev](https://www.logo.dev) e é opcional — sem ela, o app exibe as iniciais coloridas normalmente.
### Variáveis
```env
LOGO_DEV_TOKEN=pk_... # token público (obrigatório para exibir logos)
LOGO_DEV_SECRET_KEY=sk_... # chave secreta (obrigatório para o picker de busca)
```
> **Atualizando da v2.4.1 ou anterior:** a variável foi renomeada de `NEXT_PUBLIC_LOGO_DEV_TOKEN` para `LOGO_DEV_TOKEN`. Renomeie no seu `.env` (ou nas variáveis do Coolify/host) e remova o secret homônimo do GitHub Actions — ele não é mais usado. Não há outra etapa de migração.
### Como configurar
Ambas as variáveis são lidas em **runtime** pelo servidor Next.js. Não há mais nenhuma etapa no CI nem `--build-arg` no Docker.
**Self-hosted via Docker Hub (Coolify, Railway, etc.):**
1. Adicione `LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` nas variáveis de ambiente do host
2. Reinicie o container — pronto
**Desenvolvimento local:**
Adicione as duas no `.env` e rode `pnpm dev`.
### Como usar
Após configurado, passe o mouse sobre o avatar de qualquer estabelecimento nos lançamentos — um ícone de lápis aparece. Clique para abrir o picker, busque pelo nome da marca e selecione o logo desejado. O mapeamento fica salvo por usuário no banco.
### Arquitetura
O token **nunca chega ao cliente**. O servidor constrói a URL `https://img.logo.dev/{domain}?token=...` nos endpoints `/api/logo/mapping` e `/api/logo/search`, e o cliente apenas consome a URL pronta. Um Context Provider (`LogoDevProvider`) propaga a flag `enabled` para os componentes que decidem se renderizam o picker.
---
## 🔐 Variáveis de Ambiente
Copie `.env.example` para `.env` e configure:
**Perfil 2 (dev):** copie `.env.example` para `.env` — o `DATABASE_URL` já vem com `localhost`, pronto para uso com `pnpm dev`.
**Perfil 1 (Docker):** não precisa definir `DATABASE_URL` — o compose já configura automaticamente com host `db`. Só defina se usar banco remoto (Supabase, Neon, etc.).
### Obrigatórias
```env
# Perfil 2 (dev): host "localhost" — o banco roda em container, o app no host
# Perfil 1 (Docker): não precisa definir — o compose usa "db" automaticamente
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
BETTER_AUTH_SECRET=seu-secret-aqui # openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000
@@ -306,6 +467,11 @@ ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
OPENROUTER_API_KEY=
# Logo.dev (opcional, necessário para logos automáticos de estabelecimentos)
# Ambas as variáveis são runtime — basta definir no host; nenhum build arg necessário.
LOGO_DEV_TOKEN=
LOGO_DEV_SECRET_KEY=
```
---

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -1,138 +1,52 @@
# Docker Compose para Next.js + PostgreSQL
name: openmonetis
# MODOS DE USO:
# 1. Banco LOCAL (PostgreSQL em container):
# - Configure DATABASE_URL com host "db" no .env
# - Execute: docker compose up
#
# 2. Banco REMOTO (ex: Supabase, Neon, etc):
# - Configure DATABASE_URL com a URL do banco remoto no .env
# - Execute: docker compose up app (apenas o serviço app)
#
# 3. Para parar todos os serviços:
# - Execute: docker compose down
#
# 4. Para remover volumes (CUIDADO: apaga dados do banco local):
# - Execute: docker compose down -v
services:
# ============================================
# Serviço: PostgreSQL (Banco de dados local)
# ============================================
db:
image: postgres:18-alpine
container_name: openmonetis_postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-openmonetis}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-openmonetis_dev_password}
POSTGRES_DB: ${POSTGRES_DB:-openmonetis_db}
PGDATA: /var/lib/postgresql/data
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
PGDATA: /var/lib/postgresql/data
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# Cria extensão pgcrypto inline (necessária para gen_random_bytes no schema)
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' > /docker-entrypoint-initdb.d/init.sql
exec docker-entrypoint.sh postgres
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-openmonetis} -d ${POSTGRES_DB:-openmonetis_db}",
]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-openmonetis} -d ${POSTGRES_DB:-openmonetis_db}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# Para ativar logs de queries (debug), adicione ao command acima:
# exec docker-entrypoint.sh postgres -c log_statement=all
# ============================================
# Serviço: Aplicação Next.js
# ============================================
app:
image: felipegcoutinho/openmonetis:latest
container_name: openmonetis_app
restart: unless-stopped
ports:
- "${APP_PORT:-3000}:3000"
env_file:
- path: .env
required: false
environment:
NODE_ENV: production
# Banco local: use host "db" | Banco remoto: URL completa do provider
DATABASE_URL: ${DATABASE_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
DATABASE_URL: ${DATABASE_URL:-postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
# Email (opcional)
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
# OAuth (opcional)
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
# AI providers (opcional)
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
# Só depende do 'db' se estiver usando banco local
# Para banco remoto, comente as linhas abaixo
depends_on:
db:
condition: service_healthy
# Script de inicialização: roda migrations antes de iniciar o app
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo "Aguardando banco de dados..."
sleep 5
echo "Rodando migrations..."
pnpm db:push || echo "Migrations falharam ou já estão atualizadas"
echo "Iniciando aplicação Next.js..."
node server.js
required: false
healthcheck:
test:
[
"CMD",
"wget",
"--quiet",
"--tries=1",
"--spider",
"http://localhost:3000/api/health",
]
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# ============================================
# Volumes
# ============================================
volumes:
postgres_data:
driver: local

16
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
echo "Rodando migrations..."
MIGRATED=0
for i in 1 2 3 4 5; do
if NODE_PATH=/app/migrate/node_modules /app/migrate/node_modules/.bin/drizzle-kit push; then
MIGRATED=1
break
fi
echo "Tentativa $i/5 falhou. Aguardando 5s..."
sleep 5
done
[ "$MIGRATED" -eq 0 ] && echo "Aviso: migrations não foram aplicadas."
exec "$@"

View File

@@ -0,0 +1,24 @@
DROP INDEX "tokens_api_user_id_idx";--> statement-breakpoint
DROP INDEX "cartoes_user_id_status_idx";--> statement-breakpoint
DROP INDEX "dashboard_notification_states_user_id_archived_idx";--> statement-breakpoint
DROP INDEX "contas_user_id_status_idx";--> statement-breakpoint
DROP INDEX "antecipacoes_parcelas_series_id_idx";--> statement-breakpoint
DROP INDEX "pagadores_user_id_status_idx";--> statement-breakpoint
DROP INDEX "pagadores_user_id_role_idx";--> statement-breakpoint
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "anexos_user_id_idx" ON "anexos" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "orcamentos_categoria_id_idx" ON "orcamentos" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "cartoes_conta_id_idx" ON "cartoes" USING btree ("conta_id");--> statement-breakpoint
CREATE INDEX "import_category_mappings_category_id_idx" ON "import_category_mappings" USING btree ("category_id");--> statement-breakpoint
CREATE INDEX "pre_lancamentos_lancamento_id_idx" ON "pre_lancamentos" USING btree ("lancamento_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_lancamento_id_idx" ON "antecipacoes_parcelas" USING btree ("lancamento_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_pagador_id_idx" ON "antecipacoes_parcelas" USING btree ("pagador_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_categoria_id_idx" ON "antecipacoes_parcelas" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "anotacoes_user_id_idx" ON "anotacoes" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "passkey_user_id_idx" ON "passkey" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "compartilhamentos_pagador_shared_with_user_id_idx" ON "compartilhamentos_pagador" USING btree ("shared_with_user_id");--> statement-breakpoint
CREATE INDEX "compartilhamentos_pagador_created_by_user_id_idx" ON "compartilhamentos_pagador" USING btree ("created_by_user_id");--> statement-breakpoint
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "lancamentos_conta_id_idx" ON "lancamentos" USING btree ("conta_id");--> statement-breakpoint
CREATE INDEX "lancamentos_categoria_id_idx" ON "lancamentos" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "lancamentos_antecipacao_id_idx" ON "lancamentos" USING btree ("antecipacao_id");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "lancamentos" ADD COLUMN "split_group_id" uuid;--> statement-breakpoint
CREATE INDEX "lancamentos_user_id_split_group_id_idx" ON "lancamentos" USING btree ("user_id","split_group_id");

View File

@@ -0,0 +1 @@
ALTER TABLE "pagadores" ALTER COLUMN "share_code" DROP DEFAULT;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "cartoes" ALTER COLUMN "limite" SET DEFAULT '0';--> statement-breakpoint
UPDATE "cartoes" SET "limite" = '0' WHERE "limite" IS NULL;--> statement-breakpoint
ALTER TABLE "cartoes" ALTER COLUMN "limite" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,181 +1,209 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1762993507299,
"tag": "0000_flashy_manta",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1765199006435,
"tag": "0001_young_mister_fear",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1765200545692,
"tag": "0002_slimy_flatman",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1767102605526,
"tag": "0003_green_korg",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1767104066872,
"tag": "0004_acoustic_mach_iv",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1767106121811,
"tag": "0005_adorable_bruce_banner",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1767107487318,
"tag": "0006_youthful_mister_fear",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1767118780033,
"tag": "0007_sturdy_kate_bishop",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1767125796314,
"tag": "0008_fat_stick",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1768925100873,
"tag": "0009_add_dashboard_widgets",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1769369834242,
"tag": "0010_lame_psynapse",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1769447087678,
"tag": "0011_remove_unused_inbox_columns",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1769533200000,
"tag": "0012_rename_tables_to_portuguese",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1769523352777,
"tag": "0013_fancy_rick_jones",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1769619226903,
"tag": "0014_yielding_jack_flag",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1770332054481,
"tag": "0015_concerned_kat_farrell",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1771166328908,
"tag": "0016_complete_randall",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1772400510326,
"tag": "0017_previous_warstar",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1773020417482,
"tag": "0018_rainy_epoch",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1773699152928,
"tag": "0019_ordinary_wild_pack",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1773841892114,
"tag": "0020_add-budget-invoice-unique-constraints",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1774033320053,
"tag": "0021_careful_malcolm_colcord",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1748000000000,
"tag": "0022_import-category-mappings",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1774529878374,
"tag": "0023_sturdy_wolfpack",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1774891206703,
"tag": "0024_petite_lucky_pierre",
"breakpoints": true
}
]
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1762993507299,
"tag": "0000_flashy_manta",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1765199006435,
"tag": "0001_young_mister_fear",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1765200545692,
"tag": "0002_slimy_flatman",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1767102605526,
"tag": "0003_green_korg",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1767104066872,
"tag": "0004_acoustic_mach_iv",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1767106121811,
"tag": "0005_adorable_bruce_banner",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1767107487318,
"tag": "0006_youthful_mister_fear",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1767118780033,
"tag": "0007_sturdy_kate_bishop",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1767125796314,
"tag": "0008_fat_stick",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1768925100873,
"tag": "0009_add_dashboard_widgets",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1769369834242,
"tag": "0010_lame_psynapse",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1769447087678,
"tag": "0011_remove_unused_inbox_columns",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1769533200000,
"tag": "0012_rename_tables_to_portuguese",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1769523352777,
"tag": "0013_fancy_rick_jones",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1769619226903,
"tag": "0014_yielding_jack_flag",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1770332054481,
"tag": "0015_concerned_kat_farrell",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1771166328908,
"tag": "0016_complete_randall",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1772400510326,
"tag": "0017_previous_warstar",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1773020417482,
"tag": "0018_rainy_epoch",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1773699152928,
"tag": "0019_ordinary_wild_pack",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1773841892114,
"tag": "0020_add-budget-invoice-unique-constraints",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1774033320053,
"tag": "0021_careful_malcolm_colcord",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1748000000000,
"tag": "0022_import-category-mappings",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1774529878374,
"tag": "0023_sturdy_wolfpack",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1774891206703,
"tag": "0024_petite_lucky_pierre",
"breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1776351838548,
"tag": "0025_burly_colonel_america",
"breakpoints": true
},
{
"idx": 26,
"version": "7",
"when": 1777042423451,
"tag": "0026_bored_eternity",
"breakpoints": true
},
{
"idx": 28,
"version": "7",
"when": 1777153372633,
"tag": "0028_fancy_reaper",
"breakpoints": true
},
{
"idx": 29,
"version": "7",
"when": 1777648189399,
"tag": "0029_friendly_spitfire",
"breakpoints": true
}
]
}

22
knip.jsonc Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://unpkg.com/knip@6/schema-jsonc.json",
// Exclude shared UI primitives from dead code reporting while we focus the
// cleanup on feature and domain code first.
"ignore": [
"src/shared/components/ui/**"
],
// Runtime asset referenced by string in the PDF viewer.
"ignoreFiles": [
"public/pdf.worker.min.mjs",
"setup.mjs"
],
// PostCSS is inferred from the config file, but the project only depends on
// the Tailwind PostCSS plugin directly.
"ignoreDependencies": [
"postcss"
],
"next": true,
"postcss": true,
"biome": true,
"drizzle": true
}

View File

@@ -6,16 +6,23 @@ dotenv.config();
const nextConfig: NextConfig = {
output: "standalone",
experimental: {
turbopackFileSystemCacheForDev: true,
},
cacheComponents: true,
reactCompiler: true,
images: {
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
remotePatterns: [
new URL("https://lh3.googleusercontent.com/**"),
{ protocol: "https", hostname: "**" },
{ protocol: "http", hostname: "**" },
],
},
devIndicators: {
position: "bottom-right",
},
experimental: {
prefetchInlining: true,
turbopackFileSystemCacheForDev: true,
},
// Headers for Safari compatibility
async headers() {
return [
@@ -39,8 +46,12 @@ const nextConfig: NextConfig = {
value: "DENY",
},
{
key: "Content-Security-Policy",
value: "frame-ancestors 'none';",
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-Permitted-Cross-Domain-Policies",
value: "none",
},
{
key: "Permissions-Policy",

View File

@@ -1,43 +1,46 @@
{
"name": "openmonetis",
"version": "2.1.2",
"version": "2.5.1",
"private": true,
"packageManager": "pnpm@10.33.0",
"scripts": {
"dev": "next dev --turbopack",
"db:seed": "tsx scripts/mock-data.ts",
"build": "next build",
"start": "next start",
"lint": "biome check .",
"lint:deadcode": "knip --reporter compact",
"lint:fix": "biome check --write .",
"env:setup": "bash scripts/setup-env.sh",
"env:setup": "node setup.mjs",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:extensions": "tsx scripts/postgres/enable-extensions.ts",
"db:studio": "drizzle-kit studio",
"docker:up": "docker compose up --build",
"docker:up:db": "docker compose up -d db",
"docker:up:d": "docker compose up --build -d",
"postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs",
"docker:up": "docker compose up -d",
"//docker:up": "Sobe app (Docker Hub) + banco PostgreSQL em background",
"docker:db": "docker compose up -d db",
"//docker:db": "Sobe apenas o banco em background (para usar com pnpm dev)",
"docker:down": "docker compose down",
"docker:down:volumes": "docker compose down -v",
"//docker:down": "Para e remove os containers",
"docker:logs": "docker compose logs -f",
"docker:logs:app": "docker compose logs -f app",
"docker:logs:db": "docker compose logs -f db",
"docker:restart": "docker compose restart",
"docker:rebuild": "docker compose up --build --force-recreate",
"backup": "bash scripts/backup.sh"
"//docker:logs": "Acompanha logs de todos os containers em tempo real",
"docker:update": "docker compose pull && docker compose up -d",
"//docker:update": "Atualiza para a imagem mais recente do Docker Hub e reinicia",
"backup": "bash scripts/backup.sh",
"mockup": "tsx scripts/mock-data.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.64",
"@ai-sdk/google": "^3.0.53",
"@ai-sdk/openai": "^3.0.48",
"@aws-sdk/client-s3": "^3.1019.0",
"@aws-sdk/s3-request-presigner": "^3.1019.0",
"@better-auth/passkey": "^1.5.6",
"@ai-sdk/anthropic": "^3.0.74",
"@ai-sdk/google": "^3.0.67",
"@ai-sdk/openai": "^3.0.57",
"@aws-sdk/client-s3": "^3.1040.0",
"@aws-sdk/s3-request-presigner": "^3.1040.0",
"@better-auth/passkey": "^1.6.9",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@openrouter/ai-sdk-provider": "^2.3.3",
"@openrouter/ai-sdk-provider": "^2.9.0",
"@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
@@ -46,11 +49,13 @@
"@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
@@ -58,45 +63,52 @@
"@radix-ui/react-toggle-group": "1.1.11",
"@radix-ui/react-tooltip": "1.2.8",
"@remixicon/react": "4.9.0",
"@tanstack/react-query": "^5.100.7",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.13.23",
"ai": "^6.0.141",
"better-auth": "1.5.6",
"@tanstack/react-virtual": "^3.13.24",
"ai": "^6.0.173",
"better-auth": "1.6.9",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "0.45.2",
"exceljs": "^4.4.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"next": "16.1.7",
"next": "16.2.4",
"next-themes": "0.4.6",
"pdfjs-dist": "^5.7.284",
"pg": "8.20.0",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react": "19.2.5",
"react-day-picker": "^9.14.0",
"react-dom": "19.2.4",
"react-dom": "19.2.5",
"recharts": "3.8.1",
"resend": "^6.9.4",
"resend": "^6.12.2",
"sonner": "2.0.7",
"tailwind-merge": "3.5.0",
"vaul": "1.1.2",
"xlsx": "^0.18.5",
"zod": "4.3.6"
"zod": "4.4.1"
},
"pnpm": {
"overrides": {
"defu": "6.1.7"
}
},
"devDependencies": {
"@biomejs/biome": "2.4.9",
"@tailwindcss/postcss": "4.2.2",
"@biomejs/biome": "2.4.13",
"@tailwindcss/postcss": "4.2.4",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "25.5.0",
"@types/node": "25.6.0",
"@types/pg": "^8.20.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"dotenv": "^17.3.1",
"dotenv": "^17.4.2",
"drizzle-kit": "0.31.10",
"tailwindcss": "4.2.2",
"knip": "^6.10.0",
"tailwindcss": "4.2.4",
"tsx": "4.21.0",
"typescript": "6.0.2"
"typescript": "6.0.3"
}
}

4775
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
Contact: https://github.com/felipegcoutinho/openmonetis/security/advisories
Expires: 2027-04-04T00:00:00.000Z
Preferred-Languages: pt-BR, en
Canonical: https://openmonetis.com/.well-known/security.txt

Binary file not shown.

View File

@@ -1,15 +1,10 @@
import localFont from "next/font/local";
import { Inter } from "next/font/google";
export const america = localFont({
src: [
{
path: "./america-regular.woff2",
weight: "400",
style: "normal",
},
],
export const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-america",
variable: "--font-inter",
fallback: ["ui-sans-serif", "system-ui"],
weight: ["500", "600", "700"],
preload: true,
});
export const americaFontVariable = america.variable;

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" role="img" aria-label="OpenMonetis">
<path fill="#ff7733" d="M 77.66,165.64 L 37.77,141.54 L 63.30,108.72 L 27.13,97.44 L 46.81,50.77 L 77.66,63.08 L 81.91,30.26 L 126.40,29.23 L 126.06,33.85 L 122.87,67.69 L 158.51,50.77 L 178.19,90.26 L 140.96,104.62 L 162.23,127.18 L 132.98,162.56 L 103.19,131.79 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 92 KiB

37
public/llms.txt Normal file
View File

@@ -0,0 +1,37 @@
# OpenMonetis
> OpenMonetis is a self-hosted personal finance web app for manual financial control. It helps users manage accounts, cards, invoices, budgets, notes, reports, attachments, and AI-generated insights. The product UI is in Brazilian Portuguese, the codebase uses English folder and import names, and there is no hosted SaaS version.
>
> **Stack:** Next.js 16, React 19, PostgreSQL, Drizzle ORM, Better Auth, Tailwind CSS 4, shadcn/ui. Package manager: pnpm. Linter: Biome.
OpenMonetis is meant to be deployed by the user on their own machine or server.
There is no Open Finance or automatic bank synchronization.
Transactions can be entered manually or imported from OFX and XLS/XLSX files.
Attachments are optional and require S3-compatible storage.
The public website is mainly a landing page; the main technical documentation lives in the GitHub repository.
## Docs
- [Landing page](/): Public homepage and high-level product overview
- [README](https://github.com/felipegcoutinho/openmonetis/blob/main/README.md): Main project documentation covering features, installation, Docker, environment variables, architecture, contributing, and license
- [CHANGELOG](https://github.com/felipegcoutinho/openmonetis/blob/main/CHANGELOG.md): Release history and notable changes
- [LICENSE](https://github.com/felipegcoutinho/openmonetis/blob/main/LICENSE): CC BY-NC-SA 4.0 license terms
## Setup
- [Setup script](https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs): Interactive installer for local or self-hosted setup
- [Environment example](https://github.com/felipegcoutinho/openmonetis/blob/main/.env.example): Required and optional environment variables
- [Docker Compose](https://github.com/felipegcoutinho/openmonetis/blob/main/docker-compose.yml): Local app and PostgreSQL stack definition
## Architecture
- [CLAUDE.md](https://github.com/felipegcoutinho/openmonetis/blob/main/CLAUDE.md): Project architecture, naming rules, query rules, and feature checklist
## Optional
- [robots.txt](/robots.txt): Crawl policy for the public site
## Related Projects
- [OpenMonetis Companion](https://github.com/felipegcoutinho/openmonetis-companion): Android app that captures bank notifications and sends them to the OpenMonetis inbox for review

31
public/pdf.worker.min.mjs Normal file

File diff suppressed because one or more lines are too long

View File

@@ -58,21 +58,26 @@ DATA_FILE="$BACKUP_DIR/openmonetis_${TIMESTAMP}.data.sql.gz"
log "Iniciando backup (modo: $DB_MODE)..."
# Schemas relevantes do OpenMonetis (descarta lixo Supabase: auth, realtime, storage, vault, graphql, etc.)
SCHEMA_FLAGS=(--schema=public --schema=drizzle)
# --- Dump ---
if [[ "$DB_MODE" == "remote" ]]; then
# --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente)
pg_dump --format=custom --no-owner --no-privileges \
"${SCHEMA_FLAGS[@]}" \
"$REMOTE_DB_URL" > "$DUMP_FILE"
pg_dump --no-owner --no-privileges \
"${SCHEMA_FLAGS[@]}" \
"$REMOTE_DB_URL" | gzip > "$SQL_FILE"
elif [[ "$DB_MODE" == "docker" ]]; then
docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" -Fc "$DOCKER_DB_NAME" > "$DUMP_FILE"
-U "$DOCKER_DB_USER" -Fc "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" > "$DUMP_FILE"
docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
-U "$DOCKER_DB_USER" "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
else
log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'."

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env tsx
import { execSync } from "node:child_process";
import { config } from "dotenv";
// Carregar variáveis de ambiente
config();
const port = process.env.PORT || "3000";
console.log(`Starting Next.js development server on port ${port}...`);
// Executar next dev com a porta especificada
execSync(`npx next dev --turbopack --port ${port}`, {
stdio: "inherit",
env: { ...process.env, PORT: port },
});

245
scripts/install-deps.sh Executable file
View File

@@ -0,0 +1,245 @@
#!/bin/sh
# install-deps.sh — Instala pré-requisitos do OpenMonetis
# Testado apenas em Ubuntu Server 24.04 LTS
# Uso: curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/scripts/install-deps.sh -o install-deps.sh
# sudo sh install-deps.sh
set -e
LOG_FILE="/tmp/openmonetis-install.log"
> "$LOG_FILE"
# Suprimir prompt interativo do corepack ao chamar pnpm/node versioning
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# ── Cores ──────────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
ok() { printf "${GREEN}${RESET} %s\n" "$1"; }
warn() { printf "${YELLOW}!${RESET} %s\n" "$1"; }
info() { printf "${CYAN}${RESET} %s\n" "$1"; }
fail() { printf "${RED}${RESET} %s\n" "$1"; exit 1; }
# ── Contador de etapas ─────────────────────────────────────────────────────────
_STEP=0
_TOTAL=5
section() {
_STEP=$((_STEP + 1))
printf "\n${BOLD}[%d/%d] %s${RESET}\n" "$_STEP" "$_TOTAL" "$1"
}
# ── Spinner ────────────────────────────────────────────────────────────────────
_spin_pid=""
spinner_start() {
_spin_label="$1"
( i=0
while true; do
case $((i % 4)) in
0) d=" " ;; 1) d=". " ;; 2) d=".. " ;; *) d="..." ;;
esac
printf "\r${CYAN}${RESET} %s%s" "$_spin_label" "$d"
i=$((i + 1))
sleep 0.4
done
) &
_spin_pid=$!
}
spinner_stop() {
if [ -n "$_spin_pid" ]; then
kill "$_spin_pid" 2>/dev/null || true
wait "$_spin_pid" 2>/dev/null || true
_spin_pid=""
printf "\r\033[2K"
fi
}
# ── Executores silenciosos com spinner ─────────────────────────────────────────
# run_quiet "label" cmd [args...] — roda comando com spinner, falha mostra log
run_quiet() {
_rq_label="$1"; shift
spinner_start "$_rq_label"
if ! "$@" >> "$LOG_FILE" 2>&1; then
spinner_stop
printf "${RED}✗ Falha em: %s${RESET}\n" "$_rq_label"
printf " Log completo: %s\n\n" "$LOG_FILE"
tail -20 "$LOG_FILE"
exit 1
fi
spinner_stop
}
# run_as_user "label" "comando_shell" — roda comando como $CURRENT_USER com spinner
run_as_user() {
_ru_label="$1"; shift
spinner_start "$_ru_label"
if ! su - "$CURRENT_USER" -c "$*" >> "$LOG_FILE" 2>&1; then
spinner_stop
printf "${RED}✗ Falha em: %s${RESET}\n" "$_ru_label"
printf " Log completo: %s\n\n" "$LOG_FILE"
tail -20 "$LOG_FILE"
exit 1
fi
spinner_stop
}
# ── Cleanup no Ctrl+C ──────────────────────────────────────────────────────────
cleanup() {
spinner_stop
printf "\n${YELLOW}Instalação interrompida.${RESET} Log em: %s\n" "$LOG_FILE"
exit 1
}
trap cleanup INT TERM
# ── Tempo total ────────────────────────────────────────────────────────────────
_START=$(date +%s)
elapsed() {
_secs=$(( $(date +%s) - _START ))
printf "%dm%ds" $((_secs / 60)) $((_secs % 60))
}
# ── Root check ─────────────────────────────────────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
fail "Execute como root ou com sudo: sudo sh install-deps.sh"
fi
CURRENT_USER="${SUDO_USER:-$(whoami)}"
printf "\n${BOLD}OpenMonetis — Instalação de Dependências${RESET}\n"
printf "Usuário: ${CYAN}%s${RESET} | Log: %s\n" "$CURRENT_USER" "$LOG_FILE"
# ── [1/5] Dependências base ────────────────────────────────────────────────────
section "Dependências base"
run_quiet "Atualizando lista de pacotes" apt-get update -qq
run_quiet "Instalando git, curl, ca-certificates" apt-get install -y -qq ca-certificates curl git
ok "git $(git --version | cut -d' ' -f3) · curl · ca-certificates"
# ── [2/5] Docker ───────────────────────────────────────────────────────────────
section "Docker"
if command -v docker > /dev/null 2>&1; then
ok "Docker já instalado: $(docker --version | cut -d',' -f1)"
else
info "Adicionando repositório oficial do Docker..."
install -m 0755 -d /etc/apt/keyrings
run_quiet "Baixando chave GPG do Docker" \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
mkdir -p /etc/apt/sources.list.d
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' \
"$(dpkg --print-architecture)" "$VERSION_CODENAME" \
> /etc/apt/sources.list.d/docker.list
run_quiet "Atualizando lista de pacotes" apt-get update -qq
run_quiet "Instalando Docker Engine (pode levar alguns minutos)" \
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker > /dev/null 2>&1 || true
systemctl start docker > /dev/null 2>&1 || true
ok "Docker $(docker --version | cut -d',' -f1 | cut -d' ' -f3) instalado"
fi
if docker compose version > /dev/null 2>&1; then
ok "Docker Compose $(docker compose version | cut -d' ' -f4)"
else
run_quiet "Instalando Docker Compose plugin" \
sh -c 'mkdir -p /usr/local/lib/docker/cli-plugins && curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" -o /usr/local/lib/docker/cli-plugins/docker-compose && chmod +x /usr/local/lib/docker/cli-plugins/docker-compose'
ok "Docker Compose $(docker compose version | cut -d' ' -f4) instalado"
fi
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
if ! groups "$CURRENT_USER" | grep -q docker; then
usermod -aG docker "$CURRENT_USER"
warn "Usuário '$CURRENT_USER' adicionado ao grupo docker — faça logout/login para aplicar"
else
ok "Usuário '$CURRENT_USER' já está no grupo docker"
fi
fi
# ── [3/5] Homebrew ─────────────────────────────────────────────────────────────
section "Homebrew"
if command -v brew > /dev/null 2>&1; then
ok "Homebrew já instalado: $(brew --version | head -1)"
else
warn "Esta etapa pode levar de 5 a 10 minutos."
run_quiet "Instalando dependências de compilação" \
apt-get install -y -qq build-essential procps file
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
run_as_user "Instalando Homebrew" \
'NONINTERACTIVE=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
BREW_PROFILE="/home/$CURRENT_USER/.bashrc"
BREW_EVAL='eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"'
grep -qxF "$BREW_EVAL" "$BREW_PROFILE" 2>/dev/null || echo "$BREW_EVAL" >> "$BREW_PROFILE"
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
else
fail "Homebrew não pode ser instalado como root. Use sudo com um usuário normal."
fi
ok "Homebrew instalado"
fi
# ── [4/5] Node.js 22 ───────────────────────────────────────────────────────────
section "Node.js 22"
NODE_MAJOR=0
if command -v node > /dev/null 2>&1; then
NODE_MAJOR=$(node -e "process.stdout.write(String(parseInt(process.versions.node)))")
fi
if [ "$NODE_MAJOR" -ge 22 ] 2>/dev/null; then
ok "Node.js já instalado: $(node --version)"
else
warn "Node.js via Homebrew pode levar alguns minutos."
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
run_as_user "Instalando Node.js 22" \
'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew install node@22 && brew link node@22 --force --overwrite'
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
else
fail "Node.js via Homebrew não pode ser instalado como root."
fi
ok "Node.js $(node --version) instalado"
fi
# ── [5/5] pnpm ─────────────────────────────────────────────────────────────────
section "pnpm"
if command -v pnpm > /dev/null 2>&1; then
ok "pnpm já instalado: $(pnpm --version)"
else
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
run_as_user "Instalando pnpm via corepack" \
'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@latest --activate'
else
run_quiet "Instalando pnpm via corepack" \
sh -c 'corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@latest --activate'
fi
ok "pnpm instalado"
fi
# ── Resumo ─────────────────────────────────────────────────────────────────────
# Garantir que node/pnpm do brew estejam no PATH para o resumo
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
printf "\n${BOLD}Concluído em $(elapsed)${RESET}\n"
ok "git: $(git --version | cut -d' ' -f3)"
ok "docker: $(docker --version | cut -d',' -f1 | cut -d' ' -f3)"
ok "docker compose: $(docker compose version | cut -d' ' -f4)"
ok "node: $(node --version)"
ok "pnpm: $(pnpm --version)"

View File

@@ -44,6 +44,7 @@ import {
PAYER_ROLE_THIRD_PARTY,
PAYER_STATUS_OPTIONS,
} from "@/shared/lib/payers/constants";
import { generateShareCode } from "@/shared/lib/payers/share-code";
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
import {
addMonthsToDate,
@@ -537,6 +538,7 @@ async function ensureAdminPayer(targetUser: typeof user.$inferSelect) {
note: null,
role: PAYER_ROLE_ADMIN,
isAutoSend: false,
shareCode: generateShareCode(),
userId: targetUser.id,
})
.returning({ id: payers.id, name: payers.name });
@@ -870,6 +872,7 @@ async function main() {
note: definition.note,
role: PAYER_ROLE_THIRD_PARTY,
isAutoSend: definition.isAutoSend,
shareCode: generateShareCode(),
userId: targetUser.id,
})
.returning({ id: payers.id });

View File

@@ -1,45 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { config } from "dotenv";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
// Load environment variables from .env
config();
async function initDatabase() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const pool = new Pool({ connectionString: databaseUrl });
const db = drizzle(pool);
try {
console.log("🔧 Initializing database extensions...");
// Read and execute init.sql as a single query
const initSqlPath = path.join(
process.cwd(),
"scripts",
"postgres",
"init.sql",
);
const initSql = fs.readFileSync(initSqlPath, "utf-8");
console.log("Executing init.sql...");
await db.execute(initSql);
console.log("✅ Database initialization completed");
} catch (error) {
console.error("❌ Database initialization failed:", error);
process.exit(1);
} finally {
await pool.end();
}
}
initDatabase();

View File

@@ -1,10 +0,0 @@
-- Script de inicialização do PostgreSQL para Docker
-- Este script é executado automaticamente quando o banco é criado pela primeira vez
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Log de sucesso
DO $$
BEGIN
RAISE NOTICE '✅ Extensão pgcrypto habilitada com sucesso';
END $$;

View File

@@ -1,49 +0,0 @@
#!/bin/bash
# Script para configurar ambiente de forma segura
# Cria backup do .env atual antes de sobrescrever
set -e
echo "🔧 Configurando ambiente..."
# Se .env já existe, criar backup
if [ -f .env ]; then
BACKUP_FILE=".env.backup.$(date +%Y%m%d_%H%M%S)"
echo "⚠️ Arquivo .env existente detectado!"
echo "📦 Criando backup em: $BACKUP_FILE"
cp .env "$BACKUP_FILE"
echo "✅ Backup criado com sucesso!"
echo ""
read -p "Deseja sobrescrever o .env atual com .env.example? (s/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Ss]$ ]]; then
echo "❌ Operação cancelada. Seu .env não foi modificado."
exit 0
fi
fi
# Copiar .env.example para .env
if [ -f .env.example ]; then
cp .env.example .env
echo "✅ Arquivo .env criado a partir de .env.example"
else
echo "❌ Erro: .env.example não encontrado!"
exit 1
fi
# Gerar BETTER_AUTH_SECRET automaticamente
if command -v openssl &> /dev/null; then
SECRET=$(openssl rand -base64 32)
sed -i.bak "s|BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$SECRET|" .env && rm -f .env.bak
echo "✅ BETTER_AUTH_SECRET gerado automaticamente"
else
echo "⚠️ openssl não encontrado — configure BETTER_AUTH_SECRET manualmente:"
echo " openssl rand -base64 32"
fi
echo ""
echo "⚠️ IMPORTANTE: Edite o arquivo .env e configure:"
echo " - DATABASE_URL"
echo " - BETTER_AUTH_URL"
echo " - Demais variáveis opcionais (OAuth, e-mail, IA)"

23
src/app/(auth)/layout.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { Logo } from "@/shared/components/logo";
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="relative flex min-h-svh flex-col items-center justify-center overflow-hidden bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
<div className="pointer-events-none absolute inset-0 overflow-hidden flex items-center justify-center">
<div className="absolute -right-32 top-0 h-96 w-96 rounded-full bg-primary/10 blur-3xl animate-blob mix-blend-multiply" />
<div className="absolute -left-32 bottom-0 h-96 w-96 rounded-full bg-primary/7 blur-3xl animate-blob animation-delay-2000 mix-blend-multiply" />
<div className="absolute -bottom-32 left-1/2 h-80 w-80 rounded-full bg-secondary/30 blur-3xl animate-blob animation-delay-4000 mix-blend-multiply" />
</div>
<div className="relative mb-6 flex md:hidden z-20">
<Logo variant="compact" colorIcon />
</div>
<div className="relative w-full max-w-sm md:max-w-5xl">{children}</div>
</div>
);
}

View File

@@ -1,11 +1,5 @@
import { LoginForm } from "@/features/auth/components/login-form";
export default function LoginPage() {
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
<div className="w-full max-w-sm md:max-w-5xl">
<LoginForm />
</div>
</div>
);
return <LoginForm />;
}

View File

@@ -1,11 +1,5 @@
import { SignupForm } from "@/features/auth/components/signup-form";
export default function Page() {
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
<div className="w-full max-w-sm md:max-w-5xl">
<SignupForm />
</div>
</div>
);
export default function SignupPage() {
return <SignupForm />;
}

View File

@@ -1,7 +1,9 @@
import { RiPencilLine } from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { AccountDialog } from "@/features/accounts/components/account-dialog";
import { AccountStatementCard } from "@/features/accounts/components/account-statement-card";
import { AdjustBalanceDialog } from "@/features/accounts/components/adjust-balance-dialog";
import type { Account } from "@/features/accounts/components/types";
import {
fetchAccountData,
@@ -42,6 +44,7 @@ const capitalize = (value: string) =>
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { accountId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -139,6 +142,13 @@ export default async function Page({ params, searchParams }: PageProps) {
totalIncomes={totalIncomes}
totalExpenses={totalExpenses}
logo={account.logo}
balanceAdjustment={
<AdjustBalanceDialog
accountId={account.id}
period={selectedPeriod}
currentBalance={currentBalance}
/>
}
actions={
<AccountDialog
mode="update"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankLine />}
title="Contas"

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { AccountsPage } from "@/features/accounts/components/accounts-page";
import { fetchAllAccountsForUser } from "@/features/accounts/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeAccounts, archivedAccounts, logoOptions } =
await fetchAllAccountsForUser(userId);

View File

@@ -0,0 +1,26 @@
import { RiAttachmentLine } from "@remixicon/react";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import PageDescription from "@/shared/components/page-description";
export const metadata = {
title: "Anexos",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="space-y-6">
<PageDescription
icon={<RiAttachmentLine />}
title="Anexos"
subtitle="Gerencie os anexos das suas transações"
/>
<MonthNavigation />
{children}
</section>
);
}

View File

@@ -0,0 +1,38 @@
import { Skeleton } from "@/shared/components/ui/skeleton";
export default function AnexosLoading() {
return (
<main className="flex flex-col gap-6">
<div className="w-full space-y-6">
{/* Header */}
<Skeleton className="h-10 w-40 rounded-md bg-foreground/10" />
{/* Month navigation */}
<Skeleton className="h-10 w-64 rounded-md bg-foreground/10" />
{/* Count */}
<Skeleton className="h-4 w-20 rounded-md bg-foreground/10" />
{/* Grid */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className="flex flex-col overflow-hidden rounded-lg border"
>
<Skeleton className="aspect-square w-full bg-foreground/10" />
<div className="space-y-1.5 p-2.5">
<Skeleton className="h-3 w-3/4 rounded bg-foreground/10" />
<Skeleton className="h-3 w-full rounded bg-foreground/10" />
<div className="flex justify-between">
<Skeleton className="h-3 w-16 rounded bg-foreground/10" />
<Skeleton className="h-3 w-12 rounded bg-foreground/10" />
</div>
</div>
</div>
))}
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,36 @@
import { connection } from "next/server";
import { AttachmentsPage } from "@/features/attachments/components/attachments-page";
import { fetchAttachmentsForPeriod } from "@/features/attachments/queries";
import { getUserId } from "@/shared/lib/auth/server";
import { parsePeriodParam } from "@/shared/utils/period";
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
type PageProps = {
searchParams?: PageSearchParams;
};
const getSingleParam = (
params: Record<string, string | string[] | undefined> | undefined,
key: string,
) => {
const value = params?.[key];
if (!value) return null;
return Array.isArray(value) ? (value[0] ?? null) : value;
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period } = parsePeriodParam(periodoParam);
const attachments = await fetchAttachmentsForPeriod(userId, period);
return (
<main className="flex flex-col gap-6">
<AttachmentsPage attachments={attachments} />
</main>
);
}

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBarChart2Line />}
title="Orçamentos"

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { BudgetsPage } from "@/features/budgets/components/budgets-page";
import { fetchBudgetsForUser } from "@/features/budgets/queries";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
@@ -19,21 +20,13 @@ const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value;
};
const capitalize = (value: string) =>
value.length === 0 ? value : value[0]?.toUpperCase() + value.slice(1);
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const {
period: selectedPeriod,
monthName: rawMonthName,
year,
} = parsePeriodParam(periodoParam);
const periodLabel = `${capitalize(rawMonthName)} ${year}`;
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
userId,
@@ -47,7 +40,6 @@ export default async function Page({ searchParams }: PageProps) {
budgets={budgets}
categories={categoriesOptions}
selectedPeriod={selectedPeriod}
periodLabel={periodLabel}
/>
</main>
);

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiCalendarEventLine />}
title="Calendário"

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { MonthlyCalendar } from "@/features/calendar/components/monthly-calendar";
import { fetchCalendarData } from "@/features/calendar/queries";
import {
@@ -16,6 +17,7 @@ type PageProps = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedParams = searchParams ? await searchParams : undefined;

View File

@@ -1,5 +1,6 @@
import { RiPencilLine } from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import type { FinancialAccount } from "@/db/schema";
import { CardDialog } from "@/features/cards/components/card-dialog";
import type { Card } from "@/features/cards/components/types";
@@ -39,6 +40,7 @@ type PageProps = {
};
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { cardId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -116,6 +118,8 @@ export default async function Page({ params, searchParams }: PageProps) {
financialAccount.id === card.accountId,
)?.name ?? "Conta";
const limitAmount = Number(card.limit);
const cardDialogData: Card = {
id: card.id,
name: card.name,
@@ -125,19 +129,14 @@ export default async function Page({ params, searchParams }: PageProps) {
dueDay: card.dueDay,
note: card.note ?? null,
logo: card.logo,
limit:
card.limit !== null && card.limit !== undefined
? Number(card.limit)
: null,
limit: limitAmount,
accountId: card.accountId,
accountName,
limitInUse: 0,
limitAvailable: null,
limitAvailable: limitAmount,
};
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
const limitAmount =
card.limit !== null && card.limit !== undefined ? Number(card.limit) : null;
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
1,
@@ -161,6 +160,12 @@ export default async function Page({ params, searchParams }: PageProps) {
limitAmount={limitAmount}
invoiceStatus={invoiceStatus}
paymentDate={paymentDate}
defaultPaymentAccountId={card.accountId}
paymentAccountOptions={accountOptions.map((option) => ({
value: option.value,
label: option.label,
logo: option.logo ?? null,
}))}
logo={card.logo}
actions={
<CardDialog

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankCard2Line />}
title="Cartões"

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { CardsPage } from "@/features/cards/components/cards-page";
import { fetchAllCardsForUser } from "@/features/cards/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeCards, archivedCards, accounts, logoOptions } =
await fetchAllCardsForUser(userId);

View File

@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header";
import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries";
import { fetchUserPreferences } from "@/features/settings/queries";
@@ -32,6 +33,7 @@ const getSingleParam = (
};
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { categoryId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -1,9 +1,11 @@
import { connection } from "next/server";
import { fetchCategoryHistory } from "@/features/dashboard/categories/category-history-queries";
import { CategoryHistoryWidget } from "@/features/dashboard/components/category-history-widget";
import { CategoryHistoryWidget } from "@/features/dashboard/components/widgets/category-history-widget";
import { getUser } from "@/shared/lib/auth/server";
import { getCurrentPeriod } from "@/shared/utils/period";
export default async function HistoricoCategoriasPage() {
await connection();
const user = await getUser();
const currentPeriod = getCurrentPeriod();

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiPriceTag3Line />}
title="Categorias"

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { CategoriesPage } from "@/features/categories/components/categories-page";
import { fetchCategoriesForUser } from "@/features/categories/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const categories = await fetchCategoriesForUser(userId);

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiHistoryLine />}
title="Changelog"

View File

@@ -1,10 +1,14 @@
import { connection } from "next/server";
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
import { extractDashboardLogoNames } from "@/features/dashboard/extract-logo-names";
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
import { getSingleParam } from "@/features/transactions/page-helpers";
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { getUser } from "@/shared/lib/auth/server";
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
import { parsePeriodParam } from "@/shared/utils/period";
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
@@ -14,6 +18,7 @@ type PageProps = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
@@ -23,17 +28,24 @@ export default async function Page({ searchParams }: PageProps) {
await fetchDashboardPageData(user.id, selectedPeriod);
const { dashboardWidgets } = preferences;
const logoMappings = await prefetchLogoMappings(
user.id,
extractDashboardLogoNames(dashboardData),
);
return (
<main className="flex flex-col gap-4">
<DashboardWelcome name={user.name} />
<MonthNavigation />
<DashboardMetricsCards metrics={dashboardData.metrics} />
<DashboardGridEditable
data={dashboardData}
period={selectedPeriod}
initialPreferences={dashboardWidgets}
quickActionOptions={quickActionOptions}
/>
<LogoPrefetchProvider mappings={logoMappings}>
<DashboardGridEditable
data={dashboardData}
period={selectedPeriod}
initialPreferences={dashboardWidgets}
quickActionOptions={quickActionOptions}
/>
</LogoPrefetchProvider>
</main>
);
}

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiAtLine />}
title="Pré-Lançamentos"

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { InboxPage } from "@/features/inbox/components/inbox-page";
import {
type ResolvedInboxSearchParams,
@@ -31,6 +32,7 @@ const EMPTY_DIALOG_DATA = {
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const activeStatus = resolveInboxStatus(resolvedSearchParams);

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiSparklingLine />}
title="Insights"

View File

@@ -1,3 +1,4 @@
import { connection } from "next/server";
import { InsightsPage } from "@/features/insights/components/insights-page";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { parsePeriodParam } from "@/shared/utils/period";
@@ -18,6 +19,7 @@ const getSingleParam = (
};
export default async function Page({ searchParams }: PageProps) {
await connection();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParam);

View File

@@ -1,43 +1,38 @@
import { connection } from "next/server";
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
import { LogoDevProvider } from "@/shared/components/providers/logo-dev-provider";
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
import { DotPattern } from "@/shared/components/ui/dot-pattern";
import { getUserSession } from "@/shared/lib/auth/server";
import { isLogoDevEnabled } from "@/shared/lib/logo/server";
export default async function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
await connection();
const session = await getUserSession();
const navbarData = await fetchDashboardNavbarData(session.user.id);
const logoDevEnabled = isLogoDevEnabled();
return (
<PrivacyProvider>
<AppNavbar
user={{ ...session.user, image: session.user.image ?? null }}
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
preLancamentosCount={navbarData.preLancamentosCount}
notificationsSnapshot={navbarData.notificationsSnapshot}
/>
<div className="relative flex flex-1 flex-col pt-16">
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden md:h-36">
<DotPattern
width={20}
height={20}
cx={1.25}
cy={1.25}
cr={1.25}
className="text-primary/10 mask-[linear-gradient(to_bottom,black_0%,transparent_100%)]"
/>
<div className="absolute inset-0 bg-linear-to-b from-primary/6 to-transparent" />
</div>
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
{children}
<LogoDevProvider enabled={logoDevEnabled}>
<PrivacyProvider>
<AppNavbar
user={{ ...session.user, image: session.user.image ?? null }}
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
preLancamentosCount={navbarData.preLancamentosCount}
notificationsSnapshot={navbarData.notificationsSnapshot}
/>
<div className="relative flex flex-1 flex-col pt-16">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
{children}
</div>
</div>
</div>
</div>
</PrivacyProvider>
</PrivacyProvider>
</LogoDevProvider>
);
}

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiTodoLine />}
title="Anotações"

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { NotesPage } from "@/features/notes/components/notes-page";
import { fetchAllNotesForUser } from "@/features/notes/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);

View File

@@ -4,6 +4,7 @@ import {
RiWallet3Line,
} from "@remixicon/react";
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
import { PayerHistoryCard } from "@/features/payers/components/details/payer-history-card";
@@ -40,6 +41,7 @@ import {
fetchRecentEstablishments,
fetchTransactionFilterSources,
} from "@/features/transactions/queries";
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import {
@@ -49,6 +51,7 @@ import {
TabsTrigger,
} from "@/shared/components/ui/tabs";
import { getUserId } from "@/shared/lib/auth/server";
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
import { getPayerAccess } from "@/shared/lib/payers/access";
import {
fetchPagadorBoletoItems,
@@ -79,6 +82,9 @@ const EMPTY_FILTERS: TransactionSearchFilters = {
categoryFilter: null,
accountCardFilter: null,
searchFilter: null,
settledFilter: null,
attachmentFilter: null,
dividedFilter: null,
};
const createEmptySlugMaps = (): SlugMaps => ({
@@ -91,6 +97,7 @@ const createEmptySlugMaps = (): SlugMaps => ({
type OptionSet = ReturnType<typeof buildOptionSets>;
export default async function Page({ params, searchParams }: PageProps) {
await connection();
const { payerId } = await params;
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -303,104 +310,113 @@ export default async function Page({ params, searchParams }: PageProps) {
lancamentoCount: transactionData.length,
};
const logoMappings = await prefetchLogoMappings(dataOwnerId, [
...transactionData.map((t) => t.name),
...boletoItems.map((b) => b.name),
]);
return (
<main className="flex flex-col gap-6">
<MonthNavigation />
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-2">
<TabsTrigger value="profile">Perfil</TabsTrigger>
<TabsTrigger value="painel">Painel</TabsTrigger>
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
</TabsList>
<PayerHeaderCard
payer={payerData}
selectedPeriod={selectedPeriod}
summary={summaryPreview}
/>
<LogoPrefetchProvider mappings={logoMappings}>
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-2">
<TabsTrigger value="profile">Perfil</TabsTrigger>
<TabsTrigger value="painel">Painel</TabsTrigger>
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
</TabsList>
<PayerHeaderCard
payer={payerData}
selectedPeriod={selectedPeriod}
summary={summaryPreview}
/>
<TabsContent value="profile" className="space-y-4">
<PagadorInfoCard payer={payerData} />
{canEdit && payerData.shareCode ? (
<PayerSharingCard
payerId={pagador.id}
shareCode={payerData.shareCode}
shares={payerSharesData}
/>
) : null}
{!canEdit && currentUserShare ? (
<PayerLeaveShareCard
shareId={currentUserShare.id}
pagadorName={payerData.name}
createdAt={currentUserShare.createdAt}
/>
) : null}
</TabsContent>
<TabsContent value="profile" className="space-y-4">
<PagadorInfoCard payer={payerData} />
{canEdit && payerData.shareCode ? (
<PayerSharingCard
payerId={pagador.id}
shareCode={payerData.shareCode}
shares={payerSharesData}
/>
) : null}
{!canEdit && currentUserShare ? (
<PayerLeaveShareCard
shareId={currentUserShare.id}
pagadorName={payerData.name}
createdAt={currentUserShare.createdAt}
/>
) : null}
</TabsContent>
<TabsContent value="painel" className="space-y-4">
<section className="grid gap-3 lg:grid-cols-2">
<PayerMonthlySummaryCard
periodLabel={periodLabel}
breakdown={monthlyBreakdown}
/>
<PayerHistoryCard data={historyData} />
</section>
<TabsContent value="painel" className="space-y-4">
<section className="grid gap-3 lg:grid-cols-2">
<PayerMonthlySummaryCard
periodLabel={periodLabel}
breakdown={monthlyBreakdown}
/>
<PayerHistoryCard data={historyData} />
</section>
<section className="grid gap-3 lg:grid-cols-3">
<ExpandableWidgetCard
title="Minhas Faturas"
subtitle="Valores por cartão neste período"
icon={<RiBankCard2Line className="size-4" />}
>
<PayerCardUsageCard items={cardUsage} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Boletos"
subtitle="Boletos registrados neste período"
icon={<RiBarcodeLine className="size-4" />}
>
<PayerBoletoCard items={boletoItems} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Status de Pagamento"
subtitle="Situação das despesas no período"
icon={<RiWallet3Line className="size-4" />}
>
<PayerPaymentStatusCard data={paymentStatus} />
</ExpandableWidgetCard>
</section>
</TabsContent>
<section className="grid gap-3 lg:grid-cols-3">
<ExpandableWidgetCard
title="Minhas Faturas"
subtitle="Valores por cartão neste período"
icon={<RiBankCard2Line className="size-4" />}
>
<PayerCardUsageCard items={cardUsage} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Boletos"
subtitle="Boletos registrados neste período"
icon={<RiBarcodeLine className="size-4" />}
>
<PayerBoletoCard items={boletoItems} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Status de Pagamento"
subtitle="Situação das despesas no período"
icon={<RiWallet3Line className="size-4" />}
>
<PayerPaymentStatusCard data={paymentStatus} />
</ExpandableWidgetCard>
</section>
</TabsContent>
<TabsContent value="lancamentos">
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
transactions={transactionData}
payerOptions={optionSets.payerOptions}
splitPayerOptions={optionSets.splitPayerOptions}
defaultPayerId={pagador.id}
accountOptions={optionSets.accountOptions}
cardOptions={optionSets.cardOptions}
categoryOptions={optionSets.categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={optionSets.categoryFilterOptions}
accountCardFilterOptions={optionSets.accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
allowCreate={canEdit}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
importPayerOptions={loggedUserOptionSets?.payerOptions}
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
importAccountOptions={loggedUserOptionSets?.accountOptions}
importCardOptions={loggedUserOptionSets?.cardOptions}
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
/>
</section>
</TabsContent>
</Tabs>
<TabsContent value="lancamentos">
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
transactions={transactionData}
payerOptions={optionSets.payerOptions}
splitPayerOptions={optionSets.splitPayerOptions}
defaultPayerId={pagador.id}
accountOptions={optionSets.accountOptions}
cardOptions={optionSets.cardOptions}
categoryOptions={optionSets.categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={optionSets.categoryFilterOptions}
accountCardFilterOptions={optionSets.accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
allowCreate={canEdit}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
importPayerOptions={loggedUserOptionSets?.payerOptions}
importSplitPayerOptions={
loggedUserOptionSets?.splitPayerOptions
}
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
importAccountOptions={loggedUserOptionSets?.accountOptions}
importCardOptions={loggedUserOptionSets?.cardOptions}
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
/>
</section>
</TabsContent>
</Tabs>
</LogoPrefetchProvider>
</main>
);
}

View File

@@ -2,7 +2,7 @@ import { RiGroupLine } from "@remixicon/react";
import PageDescription from "@/shared/components/page-description";
export const metadata = {
title: "Pagadores",
title: "Pessoas",
};
export default function RootLayout({
@@ -11,10 +11,10 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiGroupLine />}
title="Pagadores"
title="Pessoas"
subtitle="Gerencie as pessoas ou entidades responsáveis pelos pagamentos."
/>
{children}

View File

@@ -1,7 +1,7 @@
import { Skeleton } from "@/shared/components/ui/skeleton";
/**
* Loading state para a página de pagadores
* Loading state para a página de pessoas
* Layout: Header + Input de compartilhamento + Grid de cards
*/
export default function PagadoresLoading() {
@@ -17,7 +17,7 @@ export default function PagadoresLoading() {
</div>
</div>
{/* Grid de cards de pagadores */}
{/* Grid de cards de pessoas */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="rounded-md border p-6 space-y-4">

View File

@@ -1,8 +1,10 @@
import { connection } from "next/server";
import { PayersPage } from "@/features/payers/components/payers-page";
import { fetchPayersForUser } from "@/features/payers/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
await connection();
const userId = await getUserId();
const { payers, avatarOptions } = await fetchPayersForUser(userId);

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankCard2Line />}
title="Uso de Cartões"

View File

@@ -1,4 +1,5 @@
import { RiBankCard2Line } from "@remixicon/react";
import { connection } from "next/server";
import { fetchCartoesReportData } from "@/features/reports/cards-report-queries";
import { CardCategoryBreakdown } from "@/features/reports/components/cards/card-category-breakdown";
import { CardInvoiceStatus } from "@/features/reports/components/cards/card-invoice-status";
@@ -28,6 +29,7 @@ const getSingleParam = (
export default async function RelatorioCartoesPage({
searchParams,
}: PageProps) {
await connection();
const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
@@ -69,7 +71,7 @@ export default async function RelatorioCartoesPage({
<div className="flex size-14 items-center justify-center rounded-full bg-muted mb-4">
<RiBankCard2Line className="size-7 text-muted-foreground" />
</div>
<p className="text-base font-medium">Nenhum cartão selecionado</p>
<p className="text-base font-semibold">Nenhum cartão selecionado</p>
<p className="text-sm text-muted-foreground mt-1">
Selecione um cartão para ver os detalhes de uso.
</p>

Some files were not shown because too many files have changed in this diff Show More