36 Commits

Author SHA1 Message Date
Felipe Coutinho
18893bfe02 docs: atualiza árvore de diretórios em README e CLAUDE com estrutura pós-refatoração
- Adiciona subpastas novas em shared/components/ (brand, widgets, feedback)
- Documenta o padrão interno de feature: actions.ts, queries.ts, actions/,
  components/, hooks/, lib/
- Atualiza shared/lib/ com pastas que já existiam mas faltavam listar
  (import, notifications, storage, version)
- Atualiza shared/utils/ com fetch-json.ts (movido do shared/lib) e id.ts
- Inclui lib/ no checklist de criação de nova feature

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 18:49:44 +00:00
Felipe Coutinho
7fdf9e2876 chore: prepara versão 2.5.4
Bump de patch refletindo a refatoração estrutural do commit anterior.
Sem mudanças funcionais — usuário não percebe nenhuma diferença.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 18:43:01 +00:00
Felipe Coutinho
7d0781b035 refactor: faxina arquitetural — código morto, identificadores em inglês e estrutura padronizada
Refatoração estrutural sem mudanças funcionais. Saldo líquido: −428 linhas.

Removido:
- 14 funções/constantes mortas verificadas via grep no repo todo: validateCategoriaOwnership,
  getInstallmentAnticipationsAction, getAnticipationDetailsAction, formatDecimalForDb,
  currencyFormatterNoCents, optionalDecimalSchema, formatMonthLabel,
  getGoalProgressStatusColorClass, MONTH_PERIOD_PARAM, calculateRemainingInstallments,
  e 5 funções fetch* não usadas em inbox/queries.ts.
- 1 tipo morto (ImportRow) + 2 órfãos consequentes (InstallmentAnticipationWithRelations,
  GoalProgressStatus convertido em interno).
- ~30 export keywords desnecessários (símbolos usados apenas no próprio arquivo).
- Re-exports mortos em barrels: EstablishmentLogoPicker, CategoryReportSkeleton,
  WidgetSkeleton, toNameKey.
- Arquivo features/reports/types.ts (barrel inteiro era órfão).

Padronizado (PT-BR→EN em identificadores expostos):
- 4 constantes globais (LANCAMENTOS_* → TRANSACTIONS_*).
- 12 tipos/interfaces (Lancamento*/Pagador*/Estabelecimento* → equivalentes EN).
- 13 funções/components exportados (fetchPagador*, EstabelecimentoInput, PagadorInfoCard, etc.).
- 5 props cross-file (preLancamentosCount → inboxPendingCount, pagadorAvatarUrl → payerAvatarUrl, etc.).
- Mantidas em PT-BR conforme exceção do CLAUDE.md: variáveis locais (pagador, categoria,
  lancamento), accessor key pagadorName (persistida em preferências), strings de UI.

Reorganizado:
- transactions/: 14 helpers soltos na raiz movidos para lib/; barrel actions.ts reduzido
  de 76 linhas de wrappers para 14 linhas de re-exports puros; anticipation-actions.ts
  movido para actions/anticipation.ts.
- dashboard/: 8 helpers soltos consolidados em dashboard/lib/.
- reports/: 5 query files na raiz consolidados em reports/lib/.
- payers/: detail-actions.ts (21KB) e detail-queries.ts movidos para payers/lib/.
- shared/components/: 9 dos 16 componentes soltos agrupados em brand/, widgets/, feedback/.
- shared/lib/fetch-json.ts movido para shared/utils/fetch-json.ts.

Validação: pnpm exec tsc --noEmit (0 erros), biome check (0 issues), knip (sem unused).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 18:42:54 +00:00
Felipe Coutinho
b9b843b9db Revise README for .env setup and remove acknowledgments
Updated instructions for creating a .env file and removed acknowledgments section.
2026-05-05 14:51:36 -03:00
Felipe Coutinho
01215b3124 chore: prepara versao 2.5.3 2026-05-05 17:17:33 +00:00
Felipe Coutinho
d70223e7b3 chore: renova massa de dados mock 2026-05-05 17:17:26 +00:00
Felipe Coutinho
6ea064e1bd style: ajusta espacamento de telas do dashboard 2026-05-05 17:17:19 +00:00
Felipe Coutinho
9c0669a152 feat: padroniza contas e cartoes inativos 2026-05-05 17:17:13 +00:00
Felipe Coutinho
b2d4b29cb5 feat: aprimora detalhes de lancamentos 2026-05-05 17:17:06 +00:00
Felipe Coutinho
1df2ba787d chore: bump versão para 2.5.2 e atualizar changelog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:51 +00:00
Felipe Coutinho
e5d9b66cca feat(dashboard): complementar texto de recorrências com "mensais"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:48 +00:00
Felipe Coutinho
37edb1b76d feat(notes): substituir ícone de tarefa pendente por RiSubtractLine em contextos read-only
No card e no modal de detalhes de anotações, onde não há interação
de marcação, tarefas não concluídas exibem RiSubtractLine em vez
do quadrado com borda. Locais interativos mantêm o comportamento atual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:44 +00:00
Felipe Coutinho
6288f5f8d4 feat(budgets, cards): progress bar em cor destructive quando limite excedido
Adiciona prop indicatorClassName ao componente Progress. Orçamentos
estourados e cartões com 100% do limite utilizado exibem a barra
com indicador e fundo na cor destructive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:39 +00:00
Felipe Coutinho
57ac326c2a feat(transactions): filtro de contas por tipo dinheiro e sinal + em transferências recebidas
Ao selecionar "Dinheiro" como forma de pagamento, exibe apenas contas
do tipo "Dinheiro". Transferências recebidas (amount > 0) passam a
exibir sinal + mantendo a cor azul.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:35 +00:00
Felipe Coutinho
dccc18b1c1 fix(transfers): corrigir forma de pagamento de pix para transferência bancária
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:31 +00:00
Felipe Coutinho
0cb01a1d4c feat(accounts): adicionar tipos de conta dinheiro e outros com ícones no seletor
Adiciona "Dinheiro" (issue #50) e "Outros" à lista de tipos de conta.
Implementa AccountTypeSelectContent com ícones distintos por tipo via
getAccountTypeIcon em shared/utils/icons.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:27 +00:00
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
338 changed files with 10713 additions and 2555 deletions

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,146 @@ 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.4] - 2026-05-06
Esta versão é uma faxina arquitetural de larga escala sem nenhuma mudança visível ao usuário. Removido código morto, padronizamos identificadores em inglês conforme a convenção do projeto, simplificamos o barrel de Server Actions e consolidamos os arquivos de helpers/queries soltos nas raízes das features dentro de pastas `lib/`. O resultado é uma estrutura previsível e consistente entre features (`actions.ts`, `queries.ts`, `actions/`, `components/`, `hooks/`, `lib/`) e um saldo líquido de 428 linhas de código com zero impacto em comportamento, performance ou banco de dados.
### Alterado
- Padronização da estrutura de `transactions/`: 14 helpers soltos na raiz movidos para `lib/`; barrel `actions.ts` reduzido de 76 linhas de wrappers redundantes para 14 linhas de re-exports puros; `anticipation-actions.ts` movido para `actions/anticipation.ts`.
- Reorganização de `dashboard/`: 8 helpers soltos consolidados em `dashboard/lib/`; orquestradores (`fetch-dashboard-data.ts`, `page-data-queries.ts`) permanecem na raiz como entry points.
- Reorganização de `reports/`: 5 query files na raiz consolidados em `reports/lib/`.
- Reorganização de `payers/`: god file `detail-actions.ts` (21KB) e `detail-queries.ts` movidos para `payers/lib/`.
- `shared/components/`: 9 dos 16 componentes soltos agrupados em 3 novas subpastas temáticas (`brand/`, `widgets/`, `feedback/`).
- `shared/lib/fetch-json.ts` movido para `shared/utils/fetch-json.ts` (categorização correta — utilitário genérico de transporte HTTP).
- Padronização EN dos identificadores remanescentes: 4 constantes globais (`LANCAMENTOS_*``TRANSACTIONS_*`), 12 tipos/interfaces (`Lancamento*`/`Pagador*`/`Estabelecimento*` → equivalentes em EN), 13 funções/components exportados (`fetchPagador*`, `EstabelecimentoInput`, `PagadorInfoCard`, etc.), 5 props cross-file (`preLancamentosCount``inboxPendingCount`, etc.).
- Server Actions de `insights/` simplificadas: barrel reduzido para re-exports puros.
- Mantidas intencionalmente em PT-BR conforme exceção do `CLAUDE.md`: variáveis locais (`pagador`, `categoria`, `lancamento`), accessor key `pagadorName` (persistida em preferências do usuário), strings de UI.
### Removido
- 14 funções/constantes mortas verificadas via `grep` em todo o repo: `validateCategoriaOwnership`, `getInstallmentAnticipationsAction`, `getAnticipationDetailsAction`, `formatDecimalForDb`, `currencyFormatterNoCents`, `optionalDecimalSchema`, `formatMonthLabel`, `getGoalProgressStatusColorClass`, `MONTH_PERIOD_PARAM`, `calculateRemainingInstallments`, e 5 funções `fetch*` não usadas em `inbox/queries.ts`.
- 1 tipo morto: `ImportRow` em `transactions/actions/import-action.ts`.
- 2 tipos órfãos consequentes: `InstallmentAnticipationWithRelations`, `GoalProgressStatus` (este último convertido em interno).
- ~30 `export` keywords desnecessários (símbolos usados apenas no próprio arquivo) — visibilidade reduzida sem mudar comportamento.
- Re-exports mortos em barrels: `EstablishmentLogoPicker` em `entity-avatar/index.ts`, `CategoryReportSkeleton` e `WidgetSkeleton` em `skeletons/index.ts`, `toNameKey` em `establishment-logo-queries.ts`.
- Arquivo `features/reports/types.ts` (barrel inteiro era órfão — todos os 5 tipos eram importados direto de `@/shared/lib/types/reports`).
## [2.5.3] - 2026-05-05
Esta versão foca em polimento do diálogo de detalhes do lançamento, refresh visual da linha do tempo de parcelas e limpeza terminológica em torno de contas/cartões inativos. O diálogo de detalhes ganhou logo da conta/cartão, ícone colorido por categoria e avatar do responsável; a barra de progresso de parcelas foi redesenhada num layout horizontal compacto; e o widget "Minhas Contas" do dashboard passou a ocultar automaticamente contas marcadas como inativas. Internamente, o termo "arquivadas" foi padronizado como "inativas" nas tabs de contas e cartões, surgiram constantes compartilhadas para formas de pagamento liquidáveis e um helper `isAccountInactive`, e o seed de mock data ganhou cobertura mais realista (novas pessoas, contas, cartões e assinaturas recorrentes).
### Adicionado
- Logo da conta/cartão, ícone colorido por categoria e avatar do responsável no diálogo de detalhes do lançamento.
- Constantes `SETTLEABLE_PAYMENT_METHODS` e `CREDIT_CARD_PAYMENT_METHOD` em `features/transactions/constants.ts`.
- Helper `isAccountInactive(status)` em `shared/lib/accounts/constants.ts`, reaproveitado em `account-card.tsx` e `my-accounts-widget.tsx`.
### Alterado
- Widget "Minhas Contas" do dashboard agora oculta contas inativas (filtra antes de aplicar a regra de "não consideradas") e ajusta o empty state quando o usuário só tem contas inativas.
- Linha do tempo de parcelas (`InstallmentTimeline`) redesenhada: layout horizontal com barra de progresso, datas de compra e quitação alinhadas nas pontas e contador "N restante(s)" / "Última parcela" abaixo.
- Diálogo de detalhes do lançamento: badge de status "Pendente" virou "Em aberto" com variante `info`, "Resumo" virou "Total" e ID do lançamento passou a exibir o UUID completo em fonte monoespaçada (sem truncar).
- Tabs em contas e cartões: "Arquivadas/Arquivados" renomeadas para "Inativas/Inativos".
- Legenda do calendário envolvida em `Card` para destacar visualmente do conteúdo da página.
- Páginas `cards`, `categories`, `inbox`, `notes`, `payers` perderam `items-start` no `<main>` (alinhamento natural à largura total); `calendar` ajustou gap de 3 para 4.
- Tabela de lançamentos: extraído IIFE de payment-method dos botões de liquidação com as novas constantes compartilhadas; bloco logo+label da coluna Conta/Cartão deduplicado via reuso de variável JSX; removido `capitalize` redundante do label "Venc.".
- Mock data renovado em `scripts/mock-data.ts`: novas pessoas (Mario), novas contas (Itaú Personnalité, Banco Inter), novo cartão Inter Black, e cobertura mais ampla de assinaturas recorrentes (Vivo, Sabesp, Disney+, HBO Max, Amazon Prime, OpenAI, Apple iCloud, Notion, YouTube Premium).
### Removido
- Comentário narrativo `{/* Opções de Antecipação */}` em `transactions-columns.tsx`.
- Helper local `shortTransactionId` em `transaction-details-dialog.tsx` (substituído pela exibição do UUID completo).
## [2.5.2] - 2026-05-04
Esta versão traz melhorias visuais e de usabilidade em contas, lançamentos, orçamentos, cartões e anotações: novos tipos de conta, ícones no seletor, feedback visual de limite excedido nas progress bars e refinamentos nos ícones de tarefas em anotações.
### Adicionado
- Novos tipos de conta `"Dinheiro"` e `"Outros"` na lista padrão do diálogo de contas (issue #50).
- Ícones por tipo de conta no seletor (Conta Corrente, Poupança, Carteira Digital, Investimento, Pré-Pago, Dinheiro, Outros).
- Filtro automático: ao selecionar `"Dinheiro"` como forma de pagamento em lançamentos, o select de conta exibe apenas contas do tipo `"Dinheiro"`.
- Sinal `+` no valor de transferências recebidas na tabela de lançamentos (mantém cor azul).
### Alterado
- Forma de pagamento de novas transferências entre contas alterada de `"Pix"` para `"Transferência bancária"`.
- Progress bar de orçamentos excedidos agora exibe indicador e fundo na cor `destructive`.
- Progress bar de cartões com 100% do limite utilizado agora exibe indicador e fundo na cor `destructive`.
- Ícone de tarefa não concluída no card e no modal de detalhes de anotações substituído por `RiSubtractLine` (locais sem interação de marcação).
## [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
@@ -64,7 +203,7 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
- 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/60`)
- 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

View File

@@ -97,7 +97,7 @@ src/
│ ├── api/
│ ├── globals.css
│ └── layout.tsx
├── features/
├── features/ # cada feature segue: actions.ts, queries.ts, actions/, components/, hooks/, lib/
│ ├── auth/
│ ├── landing/
│ ├── dashboard/
@@ -117,9 +117,12 @@ src/
│ └── settings/
├── shared/
│ ├── components/
│ │ ├── ui/
│ │ ├── navigation/
│ │ ├── providers/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── navigation/ # navbar, sidebar, breadcrumbs
│ │ ├── providers/ # React context providers
│ │ ├── brand/ # logos do app (logo, logo-icon, logo-text)
│ │ ├── widgets/ # widget-card, widget-empty-state, expandable-widget-card
│ │ ├── feedback/ # empty-state, status-dot, payment-success
│ │ ├── month-picker/
│ │ ├── logo-picker/
│ │ ├── calculator/
@@ -134,34 +137,56 @@ src/
│ │ ├── calculator/
│ │ ├── categories/
│ │ ├── email/
│ │ ├── import/
│ │ ├── installments/
│ │ ├── invoices/
│ │ ├── logo/
│ │ ├── notifications/
│ │ ├── payers/
│ │ ├── schemas/
│ │ ├── storage/
│ │ ├── transfers/
│ │ ├── types/
│ │ ├── version/
│ │ └── db.ts
│ └── utils/
│ ├── period/
│ ├── calculator.ts
│ ├── calendar.ts
│ ├── category-colors.ts
│ ├── currency.ts
│ ├── date.ts
│ ├── export-branding.ts
│ ├── fetch-json.ts
│ ├── financial-dates.ts
│ ├── percentage.ts
│ ├── category-colors.ts
│ ├── calendar.ts
│ ├── icons.tsx
│ ├── id.ts
│ ├── initials.ts
│ ├── math.ts
│ ├── number.ts
│ ├── percentage.ts
│ ├── string.ts
── initials.ts
│ ├── icons.tsx
│ ├── export-branding.ts
│ ├── ui.ts
│ └── calculator.ts
── ui.ts
└── db/
└── schema.ts
```
### Estrutura interna padrão de uma feature
Toda feature em `src/features/<nome>/` segue:
```text
<feature>/
├── actions.ts # entry point de Server Actions (barrel quando há actions/)
├── queries.ts # entry point de leitura do banco
├── actions/ # (opcional) Server Actions divididas por domínio quando o volume cresce
├── components/ # componentes de UI da feature
├── hooks/ # React hooks específicos da feature
└── lib/ # helpers, types, sub-queries e constantes internas
```
`actions.ts` e `queries.ts` são as portas de entrada da feature. Tudo que é helper interno fica em `lib/`. Componentes e hooks ficam nas pastas com nome óbvio.
---
## Import Patterns
@@ -299,9 +324,11 @@ export async function fetchData(userId: string, period: string) {
2. Criar a feature em `src/features/<feature>/`
3. Separar:
- `components/`
- `queries.ts`
- `actions.ts`
- `types.ts` ou `schemas.ts` quando fizer sentido
- `queries.ts` (entry point de leitura)
- `actions.ts` (entry point de Server Actions; vira barrel quando crescer e migrar para `actions/`)
- `lib/` para helpers internos, sub-queries por tópico, types e constantes da feature
- `types.ts` ou `schemas.ts` quando fizer sentido (alternativa a `lib/`)
- `hooks/` quando houver hooks específicos da feature
4. Extrair para `src/shared/` tudo que for reutilizavel
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
6. Rodar:

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

@@ -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.4.3-blue?style=flat-square)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-2.5.4-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,11 +20,7 @@
---
<p align="center">
<img src="./public/images/dashboard-preview-light.webp" alt="Dashboard Preview" width="800" />
</p>
<p align="center">
<img src="./public/images/preview-lancamentos-light.webp" alt="Lançamentos" width="800" />
<img src="./public/images/dashboard-preview-light.png" alt="Dashboard Preview" width="800" />
</p>
---
@@ -131,21 +127,20 @@ Só quer rodar o OpenMonetis. **Não precisa clonar o repositório nem instalar
# 1. Baixe o compose
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
# 2. Suba tudo
# 2. Crie um .en na mesma pasta.
# .env mínimo recomendado para produção
BETTER_AUTH_SECRET=gere-um-valor-com-openssl-rand-base64-32
BETTER_AUTH_URL=http://seu-dominio.com
# 3. Suba tudo
docker compose up -d
```
Acesse em: `http://localhost:3000`
O banco sobe com credenciais padrão. Para personalizar (senha, Google OAuth, e-mail, IA...), crie um `.env` na mesma pasta **antes** de subir:
O banco sobe com credenciais padrão. Para personalizar (senha, Google OAuth, e-mail, IA...), crie um `.env` na mesma pasta **antes** de subir.
```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
```
Veja todas as variáveis disponíveis em [Variáveis de Ambiente](#-variáveis-de-ambiente).
Mais sobre .env em [Variáveis de Ambiente](#-variáveis-de-ambiente).
**Banco remoto (Supabase, Neon, Railway...):** defina `DATABASE_URL` no `.env` e suba só o app:
@@ -196,13 +191,10 @@ cp .env.example .env
# 4. Suba o banco
pnpm docker:db
# 5. Habilite extensões do PostgreSQL (apenas no primeiro setup)
pnpm db:extensions
# 6. Aplique o schema no banco (apenas no primeiro setup)
# 5. Aplique o schema no banco (apenas no primeiro setup)
pnpm db:push
# 7. Inicie o app com hot-reload
# 6. Inicie o app com hot-reload
pnpm dev
```
@@ -240,7 +232,6 @@ pnpm lint:fix # Biome auto-fix
pnpm db:generate # Gerar migrations
pnpm db:migrate # Executar migrations
pnpm db:push # Push schema direto (dev)
pnpm db:extensions # Habilitar extensões PostgreSQL (rodar uma vez)
pnpm db:studio # Drizzle Studio (UI visual)
```
@@ -291,8 +282,7 @@ docker compose up -d app
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
@@ -318,9 +308,9 @@ Cada execução gera **3 arquivos** em `backup/`:
| Arquivo | Conteúdo | Uso |
|---|---|---|
| `openmonetis_YYYY-MM-DD_HH-MM.dump` | Dump custom do PostgreSQL (binário) | Restore completo via `pg_restore` |
| `openmonetis_YYYY-MM-DD_HH-MM.sql.gz` | Dump SQL completo compactado | Inspeção manual, portabilidade |
| `openmonetis_YYYY-MM-DD_HH-MM.data.sql.gz` | Apenas os dados da schema `public` | Migração parcial, seed de outro ambiente |
| `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
@@ -354,16 +344,19 @@ crontab -e
### Restore
```bash
# A partir do .dump (recomendado — mais rápido)
pg_restore --clean --no-owner --no-privileges \
-d "postgresql://user:senha@host:5432/openmonetis_db" \
backup/openmonetis_YYYY-MM-DD_HH-MM.dump
# 1. Zerar o banco
docker exec <container-db> psql -U openmonetis -d openmonetis_db \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# A partir do .sql.gz (banco local via Docker)
gunzip -c backup/openmonetis_YYYY-MM-DD_HH-MM.sql.gz | \
docker compose exec -T db psql -U openmonetis -d openmonetis_db
# 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
@@ -514,7 +507,18 @@ openmonetis/
│ │ └── auth/ # Formulários de autenticação
│ │
│ ├── shared/ # Código reutilizado entre features
│ │ ├── components/ # UI compartilhada (shadcn/ui, navigation, skeletons...)
│ │ ├── components/ # UI compartilhada
│ │ │ ├── ui/ # shadcn/ui primitives
│ │ │ ├── navigation/ # navbar, sidebar, breadcrumbs
│ │ │ ├── brand/ # logos do app
│ │ │ ├── widgets/ # widget-card e variantes
│ │ │ ├── feedback/ # empty-state, status-dot, payment-success
│ │ │ ├── entity-avatar/ # avatares de categoria/estabelecimento
│ │ │ ├── month-picker/ # seletor de período
│ │ │ ├── logo-picker/ # seletor de logos
│ │ │ ├── calculator/ # calculadora de cálculos rápidos
│ │ │ ├── skeletons/ # loading skeletons
│ │ │ └── providers/ # React context providers
│ │ ├── hooks/ # React hooks globais
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
@@ -530,6 +534,22 @@ openmonetis/
└── proxy.ts # Middleware (auth + multi-domínio)
```
### Estrutura interna de uma feature
Toda feature em `src/features/<nome>/` segue o mesmo padrão:
```
<feature>/
├── actions.ts # Server Actions (entry point — barrel re-export quando há actions/)
├── queries.ts # Funções de leitura do banco (entry point)
├── actions/ # (opcional) Server Actions divididas por domínio quando o volume cresce
├── components/ # Componentes de UI da feature
├── hooks/ # React hooks específicos da feature
└── lib/ # Helpers, types, sub-queries e constantes
```
A regra é: `actions.ts` e `queries.ts` são as portas de entrada da feature. Tudo que é helper interno fica em `lib/`. Componentes e hooks ficam nas pastas com nome óbvio.
---
## 🤝 Contribuindo
@@ -569,12 +589,6 @@ Para o texto legal completo, consulte o arquivo [LICENSE](LICENSE) ou visite [cr
---
## 🙏 Agradecimentos
[Next.js](https://nextjs.org/) · [Better Auth](https://better-auth.com/) · [Drizzle ORM](https://orm.drizzle.team/) · [shadcn/ui](https://ui.shadcn.com/) · [Biome](https://biomejs.dev/) · [Vercel](https://vercel.com/)
---
**Desenvolvido por:** Felipe Coutinho — [@felipegcoutinho](https://github.com/felipegcoutinho)
<div align="center">

View File

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

View File

@@ -1,15 +1,5 @@
#!/bin/sh
echo "Habilitando extensão pgcrypto..."
node -e "
const { Client } = require('/app/migrate/node_modules/pg');
const c = new Client({ connectionString: process.env.DATABASE_URL });
c.connect()
.then(() => c.query('CREATE EXTENSION IF NOT EXISTS pgcrypto'))
.then(() => c.end())
.catch((e) => { console.error('Aviso pgcrypto:', e.message); process.exit(0); });
"
echo "Rodando migrations..."
MIGRATED=0
for i in 1 2 3 4 5; do

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

View File

@@ -190,6 +190,20 @@
"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
}
]
}

View File

@@ -12,8 +12,10 @@
],
// PostCSS is inferred from the config file, but the project only depends on
// the Tailwind PostCSS plugin directly.
// `server-only` is provided implicitly by Next.js — no install needed.
"ignoreDependencies": [
"postcss"
"postcss",
"server-only"
],
"next": true,
"postcss": true,

View File

@@ -8,7 +8,6 @@ const nextConfig: NextConfig = {
output: "standalone",
cacheComponents: true,
reactCompiler: true,
images: {
remotePatterns: [
new URL("https://lh3.googleusercontent.com/**"),
@@ -22,6 +21,7 @@ const nextConfig: NextConfig = {
experimental: {
prefetchInlining: true,
turbopackFileSystemCacheForDev: true,
optimizePackageImports: ["@remixicon/react"],
},
// Headers for Safari compatibility

View File

@@ -1,6 +1,6 @@
{
"name": "openmonetis",
"version": "2.4.3",
"version": "2.5.4",
"private": true,
"packageManager": "pnpm@10.33.0",
"scripts": {
@@ -15,7 +15,6 @@
"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",
"postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs",
"docker:up": "docker compose up -d",
@@ -32,16 +31,16 @@
"mockup": "tsx scripts/mock-data.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.71",
"@ai-sdk/google": "^3.0.64",
"@ai-sdk/openai": "^3.0.53",
"@aws-sdk/client-s3": "^3.1037.0",
"@aws-sdk/s3-request-presigner": "^3.1037.0",
"@ai-sdk/anthropic": "^3.0.74",
"@ai-sdk/google": "^3.0.67",
"@ai-sdk/openai": "^3.0.60",
"@aws-sdk/client-s3": "^3.1042.0",
"@aws-sdk/s3-request-presigner": "^3.1042.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.8.0",
"@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",
@@ -64,10 +63,10 @@
"@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.3",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"ai": "^6.0.168",
"ai": "^6.0.175",
"better-auth": "1.6.9",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "0.7.1",
@@ -80,7 +79,7 @@
"jspdf-autotable": "^5.0.7",
"next": "16.2.4",
"next-themes": "0.4.6",
"pdfjs-dist": "^5.6.205",
"pdfjs-dist": "^5.7.284",
"pg": "8.20.0",
"react": "19.2.5",
"react-day-picker": "^9.14.0",
@@ -90,7 +89,7 @@
"sonner": "2.0.7",
"tailwind-merge": "3.5.0",
"vaul": "1.1.2",
"zod": "4.3.6"
"zod": "4.4.3"
},
"pnpm": {
"overrides": {
@@ -98,7 +97,7 @@
}
},
"devDependencies": {
"@biomejs/biome": "2.4.13",
"@biomejs/biome": "2.4.14",
"@tailwindcss/postcss": "4.2.4",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "25.6.0",
@@ -107,7 +106,7 @@
"@types/react-dom": "19.2.3",
"dotenv": "^17.4.2",
"drizzle-kit": "0.31.10",
"knip": "^6.7.0",
"knip": "^6.11.0",
"tailwindcss": "4.2.4",
"tsx": "4.21.0",
"typescript": "6.0.3"

898
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 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: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

BIN
public/logos/dinheiro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

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'."

File diff suppressed because it is too large Load Diff

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,4 +1,4 @@
import { Logo } from "@/shared/components/logo";
import { Logo } from "@/shared/components/brand/logo";
export default function AuthLayout({
children,

View File

@@ -3,11 +3,12 @@ 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,
fetchAccountLancamentosPage,
fetchAccountSummary,
fetchAccountTransactionsPage,
} from "@/features/accounts/statement-queries";
import { fetchUserPreferences } from "@/features/settings/queries";
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
@@ -21,7 +22,7 @@ import {
mapTransactionsData,
type ResolvedSearchParams,
resolveTransactionPagination,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import {
fetchRecentEstablishments,
fetchTransactionFilterSources,
@@ -88,7 +89,7 @@ export default async function Page({ params, searchParams }: PageProps) {
accountId: account.id,
});
const transactionsPage = await fetchAccountLancamentosPage(
const transactionsPage = await fetchAccountTransactionsPage(
filters,
pagination,
);
@@ -141,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

@@ -19,7 +19,6 @@ export default function RootLayout({
subtitle="Gerencie os anexos das suas transações"
/>
<MonthNavigation />
{children}
</section>
);

View File

@@ -25,9 +25,7 @@ export default async function Page({ searchParams }: PageProps) {
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
userId,
selectedPeriod,

View File

@@ -4,7 +4,7 @@ import { fetchCalendarData } from "@/features/calendar/queries";
import {
getSingleParam,
type ResolvedSearchParams,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { getUserId } from "@/shared/lib/auth/server";
import type { CalendarPeriod } from "@/shared/lib/types/calendar";
@@ -36,7 +36,7 @@ export default async function Page({ searchParams }: PageProps) {
};
return (
<main className="flex flex-col gap-3">
<main className="flex flex-col gap-4">
<MonthNavigation />
<MonthlyCalendar
period={calendarPeriod}

View File

@@ -21,7 +21,7 @@ import {
getSingleParam,
mapTransactionsData,
type ResolvedSearchParams,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import {
fetchRecentEstablishments,
fetchTransactionFilterSources,
@@ -118,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,
@@ -127,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,
@@ -163,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

@@ -10,7 +10,7 @@ export default async function Page() {
await fetchAllCardsForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<main className="flex flex-col gap-6">
<CardsPage
cards={activeCards}
archivedCards={archivedCards}

View File

@@ -7,7 +7,7 @@ import { TransactionsPage } from "@/features/transactions/components/page/transa
import {
buildOptionSets,
buildSluggedFilters,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import {
fetchRecentEstablishments,
fetchTransactionFilterSources,

View File

@@ -9,7 +9,7 @@ export default async function Page() {
const categories = await fetchCategoriesForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<main className="flex flex-col gap-6">
<CategoriesPage categories={categories} />
</main>
);

View File

@@ -2,9 +2,9 @@ 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 { extractDashboardLogoNames } from "@/features/dashboard/lib/extract-logo-names";
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
import { getSingleParam } from "@/features/transactions/page-helpers";
import { getSingleParam } from "@/features/transactions/lib/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";

View File

@@ -56,7 +56,7 @@ export default async function Page({ searchParams }: PageProps) {
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
return (
<main className="flex flex-col items-start gap-6">
<main className="flex flex-col gap-6">
<InboxPage
activeStatus={activeStatus}
activeApp={activeApp}

View File

@@ -1,5 +1,5 @@
import { connection } from "next/server";
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
import { fetchDashboardNavbarData } from "@/features/dashboard/lib/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";
@@ -21,8 +21,8 @@ export default async function DashboardLayout({
<PrivacyProvider>
<AppNavbar
user={{ ...session.user, image: session.user.image ?? null }}
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
preLancamentosCount={navbarData.preLancamentosCount}
payerAvatarUrl={navbarData.payerAvatarUrl}
inboxPendingCount={navbarData.inboxPendingCount}
notificationsSnapshot={navbarData.notificationsSnapshot}
/>
<div className="relative flex flex-1 flex-col pt-16">

View File

@@ -9,7 +9,7 @@ export default async function Page() {
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<main className="flex flex-col gap-6">
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
</main>
);

View File

@@ -4,7 +4,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
* Loading state para a página de detalhes do pagador.
* Layout: navegação mensal + tabs com card compartilhado do pagador.
*/
export default function PagadorDetailsLoading() {
export default function PayerDetailsLoading() {
return (
<main className="flex flex-col gap-6">
<div className="h-[60px] animate-pulse rounded-md bg-foreground/10" />

View File

@@ -8,7 +8,7 @@ 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";
import { PagadorInfoCard } from "@/features/payers/components/details/payer-info-card";
import { PayerInfoCard } from "@/features/payers/components/details/payer-info-card";
import { PayerLeaveShareCard } from "@/features/payers/components/details/payer-leave-share-card";
import { PayerMonthlySummaryCard } from "@/features/payers/components/details/payer-monthly-summary-card";
import {
@@ -16,12 +16,12 @@ import {
PayerPaymentStatusCard,
} from "@/features/payers/components/details/payer-payment-method-cards";
import { PayerSharingCard } from "@/features/payers/components/details/payer-sharing-card";
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
import {
fetchCurrentUserShare,
fetchPagadorLancamentos,
fetchPayerShares,
} from "@/features/payers/detail-queries";
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
fetchPayerTransactions,
} from "@/features/payers/lib/detail-queries";
import { fetchUserPreferences } from "@/features/settings/queries";
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
import {
@@ -36,13 +36,12 @@ import {
type SluggedFilters,
type SlugMaps,
type TransactionSearchFilters,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
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 {
Tabs,
@@ -50,16 +49,17 @@ import {
TabsList,
TabsTrigger,
} from "@/shared/components/ui/tabs";
import { ExpandableWidgetCard } from "@/shared/components/widgets/expandable-widget-card";
import { getUserId } from "@/shared/lib/auth/server";
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
import { getPayerAccess } from "@/shared/lib/payers/access";
import {
fetchPagadorBoletoItems,
fetchPagadorBoletoStats,
fetchPagadorCardUsage,
fetchPagadorPaymentStatus,
fetchPayerBoletoItems,
fetchPayerBoletoStats,
fetchPayerCardUsage,
fetchPayerHistory,
fetchPayerMonthlyBreakdown,
fetchPayerPaymentStatus,
type PayerCardUsageItem,
} from "@/shared/lib/payers/details";
import { parsePeriodParam } from "@/shared/utils/period";
@@ -182,7 +182,7 @@ export default async function Page({ params, searchParams }: PageProps) {
estabelecimentos,
userPreferences,
] = await Promise.all([
fetchPagadorLancamentos(filters),
fetchPayerTransactions(filters),
fetchPayerMonthlyBreakdown({
userId: dataOwnerId,
payerId: pagador.id,
@@ -193,22 +193,22 @@ export default async function Page({ params, searchParams }: PageProps) {
payerId: pagador.id,
period: selectedPeriod,
}),
fetchPagadorCardUsage({
fetchPayerCardUsage({
userId: dataOwnerId,
payerId: pagador.id,
period: selectedPeriod,
}),
fetchPagadorBoletoStats({
fetchPayerBoletoStats({
userId: dataOwnerId,
payerId: pagador.id,
period: selectedPeriod,
}),
fetchPagadorBoletoItems({
fetchPayerBoletoItems({
userId: dataOwnerId,
payerId: pagador.id,
period: selectedPeriod,
}),
fetchPagadorPaymentStatus({
fetchPayerPaymentStatus({
userId: dataOwnerId,
payerId: pagador.id,
period: selectedPeriod,
@@ -333,7 +333,7 @@ export default async function Page({ params, searchParams }: PageProps) {
/>
<TabsContent value="profile" className="space-y-4">
<PagadorInfoCard payer={payerData} />
<PayerInfoCard payer={payerData} />
{canEdit && payerData.shareCode ? (
<PayerSharingCard
payerId={pagador.id}

View File

@@ -4,7 +4,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
* Loading state para a página de pessoas
* Layout: Header + Input de compartilhamento + Grid de cards
*/
export default function PagadoresLoading() {
export default function PayersLoading() {
return (
<main className="flex flex-col items-start gap-6">
<div className="w-full space-y-6">

View File

@@ -9,7 +9,7 @@ export default async function Page() {
const { payers, avatarOptions } = await fetchPayersForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<main className="flex flex-col gap-6">
<PayersPage payers={payers} avatarOptions={avatarOptions} />
</main>
);

View File

@@ -1,11 +1,11 @@
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";
import { CardTopExpenses } from "@/features/reports/components/cards/card-top-expenses";
import { CardUsageChart } from "@/features/reports/components/cards/card-usage-chart";
import { CardsOverview } from "@/features/reports/components/cards/cards-overview";
import { fetchCartoesReportData } from "@/features/reports/lib/cards-report-queries";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { Card } from "@/shared/components/ui/card";
import { getUser } from "@/shared/lib/auth/server";

View File

@@ -1,15 +1,15 @@
import { redirect } from "next/navigation";
import { connection } from "next/server";
import type { Category } from "@/db/schema";
import { fetchCategoryChartData } from "@/features/reports/category-chart-queries";
import { fetchCategoryReport } from "@/features/reports/category-report-queries";
import { fetchUserCategories } from "@/features/reports/category-trends-queries";
import { CategoryReportPage } from "@/features/reports/components/category-report-page";
import type {
CategoryOption,
FilterState,
} from "@/features/reports/components/types";
import { validateDateRange } from "@/features/reports/utils";
import { fetchCategoryChartData } from "@/features/reports/lib/category-chart-queries";
import { fetchCategoryReport } from "@/features/reports/lib/category-report-queries";
import { fetchUserCategories } from "@/features/reports/lib/category-trends-queries";
import { validateDateRange } from "@/features/reports/lib/utils";
import { getUserId } from "@/shared/lib/auth/server";
import type { CategoryReportFilters } from "@/shared/lib/types/reports";
import { addMonthsToPeriod, getCurrentPeriod } from "@/shared/utils/period";

View File

@@ -34,7 +34,7 @@ const validatePeriodFilter = (value: string | null): PeriodFilter => {
return "6";
};
export default async function TopEstabelecimentosPage({
export default async function TopEstablishmentsPage({
searchParams,
}: PageProps) {
await connection();

View File

@@ -51,7 +51,7 @@ export default async function Page() {
<TabsTrigger value="passkeys">Passkeys</TabsTrigger>
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
<TabsTrigger value="deletar" className="text-destructive">
Deletar conta
Ações perigosas
</TabsTrigger>
</TabsList>
</div>
@@ -190,7 +190,6 @@ export default async function Page() {
ou excluir sua conta inteira de forma irreversível.
</p>
</div>
<Separator />
<DeleteAccountForm />
</div>
</Card>

View File

@@ -3,7 +3,7 @@ import { ImportPage } from "@/features/transactions/components/import/import-pag
import {
buildOptionSets,
buildSluggedFilters,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import { fetchTransactionFilterSources } from "@/features/transactions/queries";
import { getUserId } from "@/shared/lib/auth/server";

View File

@@ -8,7 +8,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
* Loading state para a página de lançamentos
* Mantém o mesmo layout da página final
*/
export default function LancamentosLoading() {
export default function TransactionsLoading() {
return (
<main className="flex flex-col gap-6">
{/* Month Picker placeholder */}

View File

@@ -11,7 +11,7 @@ import {
mapTransactionsData,
type ResolvedSearchParams,
resolveTransactionPagination,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import {
fetchRecentEstablishments,
fetchTransactionFilterSources,

View File

@@ -9,7 +9,6 @@ import Image from "next/image";
import Link from "next/link";
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
import { MobileNav } from "@/features/landing/components/mobile-nav";
import { ScreenshotTabs } from "@/features/landing/components/screenshot-tabs";
import { SetupTabs } from "@/features/landing/components/setup-tabs";
import {
companionBanks,
@@ -25,7 +24,7 @@ import {
import { landingImages } from "@/features/landing/images";
import { fetchGitHubStats } from "@/features/landing/queries";
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
import { Logo } from "@/shared/components/logo";
import { Logo } from "@/shared/components/brand/logo";
import { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
@@ -51,19 +50,19 @@ export default async function Page() {
{/* Navigation */}
<NavbarShell>
{/* Center Navigation Links */}
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
{navLinks.map(({ href, label }) => (
<a
<Link
key={href}
href={href}
className="rounded-md px-2 py-1.5 text-sm font-medium text-black/75 hover:text-black hover:bg-black/10 transition-colors dark:text-white/75 dark:hover:text-white dark:hover:bg-white/10"
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-sm font-medium leading-none text-primary-foreground/75 transition-colors hover:bg-primary-foreground/10 hover:text-primary-foreground dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
{label}
</a>
</Link>
))}
</nav>
<nav className="ml-auto flex items-center gap-2 md:gap-3">
<nav className="ml-auto flex items-center gap-1">
<AnimatedThemeToggler variant="navbar" />
{!isPublicDomain &&
(session?.user ? (
@@ -71,26 +70,27 @@ export default async function Page() {
<Button
variant="navbar"
size="sm"
className="border border-black/20 dark:border-white/20"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Dashboard
</Button>
</Link>
) : (
<div className="hidden md:flex items-center gap-2">
<div className="hidden md:flex items-center gap-1">
<Link href="/login">
<Button
variant="ghost"
size="sm"
className="text-black/75 hover:bg-black/10 hover:text-black shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Entrar
</Button>
</Link>
<Link href="/signup">
<Button
variant="ghost"
size="sm"
className="bg-black/10 border border-black/20 text-black shadow-none hover:bg-black/20 gap-2 dark:bg-white/10 dark:border-white/20 dark:text-white dark:hover:bg-white/20"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Começar
</Button>
@@ -207,31 +207,6 @@ export default async function Page() {
</div>
</section>
{/* Screenshots Gallery Section */}
<section id="telas" className="py-12 md:py-24">
<div className="max-w-8xl mx-auto px-4">
<div className="mx-auto max-w-6xl">
<AnimateOnScroll>
<div className="text-center mb-8 md:mb-12">
<Badge variant="outline" className="mb-4">
Conheça as telas
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Veja o que você pode fazer
</h2>
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
Explore as principais telas do OpenMonetis
</p>
</div>
</AnimateOnScroll>
<AnimateOnScroll>
<ScreenshotTabs />
</AnimateOnScroll>
</div>
</div>
</section>
{/* Features Section */}
<section id="funcionalidades" className="py-12 md:py-24 bg-muted/40">
<div className="max-w-8xl mx-auto px-4">

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { fetchTransactionAttachments } from "@/features/transactions/attachment-queries";
import { fetchTransactionAttachments } from "@/features/transactions/lib/attachment-queries";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
const PRIVATE_RESPONSE_HEADERS = {

View File

@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { fetchInstallmentAnticipations } from "@/features/transactions/anticipation-queries";
import { fetchInstallmentAnticipations } from "@/features/transactions/lib/anticipation-queries";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
const PRIVATE_RESPONSE_HEADERS = {

View File

@@ -8,7 +8,7 @@
}
:root {
--background: oklch(97.412% 0.00332 67.032);
--background: oklch(95.99% 0.00411 55.512);
--foreground: oklch(27% 0.008 45);
--card: oklch(100% 0 0);
--card-foreground: var(--foreground);
@@ -36,7 +36,7 @@
--destructive: oklch(55% 0.22 27);
--destructive-foreground: oklch(98% 0.005 30);
--border: oklch(92.323% 0.01276 63.703);
--border: oklch(87.356% 0.01221 67.486);
--input: var(--border);
--ring: var(--primary);
@@ -116,7 +116,7 @@
--destructive: oklch(62% 0.2 28);
--destructive-foreground: oklch(98% 0.005 30);
--border: oklch(28% 0.0035 55);
--border: oklch(24.957% 0.00355 48.274);
--input: var(--border);
--ring: var(--primary);

View File

@@ -9,7 +9,7 @@ import { inter } from "@/public/fonts/font_index";
export const metadata: Metadata = {
title: {
default: "OpenMonetis | Suas finanças, do seu jeito",
template: "%s | OpenMonetis",
template: "OpenMonetis | %s",
},
description:
"Controle suas finanças pessoais de forma simples e transparente.",
@@ -40,7 +40,7 @@ export default function RootLayout({
/>
)}
</head>
<body className="subpixel-antialiased" suppressHydrationWarning>
<body className="antialiased" suppressHydrationWarning>
<ThemeProvider attribute="class" defaultTheme="light">
<QueryProvider>
<Suspense>{children}</Suspense>

View File

@@ -236,9 +236,7 @@ export const payers = pgTable(
note: text("anotacao"),
role: text("role"),
isAutoSend: boolean("is_auto_send").notNull().default(false),
shareCode: text("share_code")
.notNull()
.default(sql`substr(encode(gen_random_bytes(24), 'base64'), 1, 24)`),
shareCode: text("share_code").notNull(),
lastMailAt: timestamp("last_mail", {
mode: "date",
withTimezone: true,
@@ -303,7 +301,9 @@ export const cards = pgTable(
closingDay: text("dt_fechamento").notNull(),
dueDay: text("dt_vencimento").notNull(),
note: text("anotacao"),
limit: numeric("limite", { precision: 10, scale: 2 }),
limit: numeric("limite", { precision: 10, scale: 2 })
.notNull()
.default("0"),
brand: text("bandeira"),
logo: text("logo"),
status: text("status").notNull(),

View File

@@ -4,6 +4,7 @@ import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { categories, financialAccounts, transactions } from "@/db/schema";
import {
ACCOUNT_BALANCE_ADJUSTMENT_NAME,
INITIAL_BALANCE_CATEGORY_NAME,
INITIAL_BALANCE_CONDITION,
INITIAL_BALANCE_NOTE,
@@ -17,6 +18,7 @@ import {
} from "@/shared/lib/actions/helpers";
import { getUser } from "@/shared/lib/auth/server";
import { db } from "@/shared/lib/db";
import { PERIOD_FORMAT_REGEX } from "@/shared/lib/invoices";
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
import {
@@ -26,8 +28,11 @@ import {
TRANSFER_ESTABLISHMENT_SAIDA,
TRANSFER_PAYMENT_METHOD,
} from "@/shared/lib/transfers/constants";
import { formatDecimalForDbRequired } from "@/shared/utils/currency";
import { getTodayInfo } from "@/shared/utils/date";
import {
formatCurrency,
formatDecimalForDbRequired,
} from "@/shared/utils/currency";
import { getBusinessTodayDate, getTodayInfo } from "@/shared/utils/date";
import { normalizeFilePath } from "@/shared/utils/string";
const accountBaseSchema = z.object({
@@ -99,7 +104,7 @@ export async function createAccountAction(
if (hasInitialBalance && !adminPayerId) {
throw new Error(
"Pessoa com papel administrador não encontrado. Crie um pessoa admin antes de definir um saldo inicial.",
"Pessoa com papel administrador não encontrada. Crie uma pessoa admin antes de definir um saldo inicial.",
);
}
@@ -299,7 +304,7 @@ export async function transferBetweenAccountsAction(
if (!adminPayerId) {
throw new Error(
"Pessoa administrador não encontrado. Por favor, crie um pessoa admin.",
"Pessoa administrador não encontrada. Por favor, crie uma pessoa admin.",
);
}
@@ -391,3 +396,120 @@ export async function transferBetweenAccountsAction(
return handleActionError(error);
}
}
const adjustAccountBalanceSchema = z.object({
accountId: uuidSchema("FinancialAccount"),
period: z
.string({ message: "Período inválido." })
.regex(PERIOD_FORMAT_REGEX, "Período inválido."),
currentBalance: z.number({ message: "Saldo atual inválido." }),
targetBalance: z.number({ message: "Saldo correto inválido." }),
});
type AdjustAccountBalanceInput = z.infer<typeof adjustAccountBalanceSchema>;
export async function adjustAccountBalanceAction(
input: AdjustAccountBalanceInput,
): Promise<ActionResult> {
try {
const user = await getUser();
const data = adjustAccountBalanceSchema.parse(input);
const adminPayerId = await getAdminPayerId(user.id);
if (!adminPayerId) {
throw new Error(
"Pessoa com papel administrador não encontrada. Crie uma pessoa admin antes de ajustar o saldo.",
);
}
let message = "Ajuste de saldo registrado.";
await db.transaction(async (tx: typeof db) => {
const account = await tx.query.financialAccounts.findFirst({
columns: { id: true },
where: and(
eq(financialAccounts.id, data.accountId),
eq(financialAccounts.userId, user.id),
),
});
if (!account) {
throw new Error("Conta não encontrada.");
}
const existing = await tx.query.transactions.findFirst({
columns: { id: true, amount: true },
where: and(
eq(transactions.userId, user.id),
eq(transactions.accountId, data.accountId),
eq(transactions.period, data.period),
eq(transactions.name, ACCOUNT_BALANCE_ADJUSTMENT_NAME),
),
});
const existingAmount = Number(existing?.amount ?? 0);
const baseBalance = data.currentBalance - existingAmount;
const adjustmentAmount =
Math.round((data.targetBalance - baseBalance) * 100) / 100;
if (adjustmentAmount === 0) {
if (existing) {
await tx.delete(transactions).where(eq(transactions.id, existing.id));
message = "Ajuste de saldo removido.";
} else {
message = "Nada a ajustar — o saldo já está correto.";
}
return;
}
const isExpense = adjustmentAmount < 0;
const categoryName = isExpense ? "Outras despesas" : "Outras receitas";
const category = await tx.query.categories.findFirst({
columns: { id: true },
where: and(
eq(categories.userId, user.id),
eq(categories.name, categoryName),
),
});
const amount = formatDecimalForDbRequired(adjustmentAmount);
const note = `O saldo era ${formatCurrency(baseBalance)} mas o correto é ${formatCurrency(data.targetBalance)}.`;
const payload = {
condition: INITIAL_BALANCE_CONDITION,
name: ACCOUNT_BALANCE_ADJUSTMENT_NAME,
paymentMethod: INITIAL_BALANCE_PAYMENT_METHOD,
note,
amount,
purchaseDate: getBusinessTodayDate(),
transactionType: isExpense
? ("Despesa" as const)
: ("Receita" as const),
period: data.period,
isSettled: true,
userId: user.id,
accountId: data.accountId,
cardId: null,
categoryId: category?.id ?? null,
payerId: adminPayerId,
};
if (existing) {
await tx
.update(transactions)
.set(payload)
.where(eq(transactions.id, existing.id));
} else {
await tx.insert(transactions).values(payload);
}
});
revalidateForEntity("accounts", user.id);
revalidateForEntity("transactions", user.id);
return { success: true, message };
} catch (error) {
return handleActionError(error);
}
}

View File

@@ -15,6 +15,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { isAccountInactive } from "@/shared/lib/accounts/constants";
import { cn } from "@/shared/utils/ui";
interface AccountCardProps {
@@ -46,7 +47,7 @@ export function AccountCard({
onTransfer,
className,
}: AccountCardProps) {
const isInactive = status?.toLowerCase() === "inativa";
const isInactive = isAccountInactive(status);
const balanceColor =
balance > 0
@@ -145,6 +146,7 @@ export function AccountCard({
<span className="text-xs text-muted-foreground">Saldo</span>
<MoneyValues
amount={balance}
showPositiveSign
className={cn("text-2xl font-semibold", balanceColor)}
/>
</div>

View File

@@ -37,7 +37,9 @@ const DEFAULT_ACCOUNT_TYPES = [
"Conta Poupança",
"Carteira Digital",
"Conta Investimento",
"Dinheiro",
"Pré-Pago | VR/VA",
"Outros",
] as const;
const DEFAULT_ACCOUNT_STATUS = ["Ativa", "Inativa"] as const;

View File

@@ -12,7 +12,10 @@ import {
SelectValue,
} from "@/shared/components/ui/select";
import { Textarea } from "@/shared/components/ui/textarea";
import { StatusSelectContent } from "./account-select-items";
import {
AccountTypeSelectContent,
StatusSelectContent,
} from "./account-select-items";
import type { AccountFormValues } from "./types";
@@ -54,12 +57,16 @@ export function AccountFormFields({
onValueChange={(value) => onChange("accountType", value)}
>
<SelectTrigger id="account-type" className="w-full">
<SelectValue placeholder="Selecione o tipo" />
<SelectValue placeholder="Selecione o tipo">
{values.accountType && (
<AccountTypeSelectContent label={values.accountType} />
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{accountTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
<AccountTypeSelectContent label={type} />
</SelectItem>
))}
</SelectContent>

View File

@@ -1,6 +1,18 @@
"use client";
import StatusDot from "@/shared/components/status-dot";
import StatusDot from "@/shared/components/feedback/status-dot";
import { getAccountTypeIcon } from "@/shared/utils/icons";
export function AccountTypeSelectContent({ label }: { label: string }) {
const icon = getAccountTypeIcon(label);
return (
<span className="flex items-center gap-2">
{icon}
<span>{label}</span>
</span>
);
}
export function StatusSelectContent({ label }: { label: string }) {
const isActive = label === "Ativa";

View File

@@ -26,6 +26,7 @@ type AccountStatementCardProps = {
totalExpenses: number;
logo?: string | null;
actions?: React.ReactNode;
balanceAdjustment?: React.ReactNode;
};
const getAccountStatusBadgeVariant = (
@@ -45,6 +46,7 @@ export function AccountStatementCard({
totalExpenses,
logo,
actions,
balanceAdjustment,
}: AccountStatementCardProps) {
const logoPath = resolveLogoSrc(logo);
const resultado = totalIncomes - totalExpenses;
@@ -84,10 +86,13 @@ export function AccountStatementCard({
<p className="text-sm text-muted-foreground ">
Saldo ao final do período
</p>
<MoneyValues
amount={currentBalance}
className="text-3xl leading-none tracking-tighter sm:text-2xl"
/>
<div className="flex items-center gap-2">
<MoneyValues
amount={currentBalance}
className="text-3xl leading-none tracking-tighter sm:text-2xl"
/>
{balanceAdjustment}
</div>
<div className="flex items-center gap-2">
<Badge
variant={getAccountStatusBadgeVariant(status)}

View File

@@ -8,7 +8,7 @@ import { toast } from "sonner";
import { deleteAccountAction } from "@/features/accounts/actions";
import { AccountCard } from "@/features/accounts/components/account-card";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/empty-state";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Button } from "@/shared/components/ui/button";
import { Card } from "@/shared/components/ui/card";
import {
@@ -186,14 +186,14 @@ export function AccountsPage({
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativos">Ativas</TabsTrigger>
<TabsTrigger value="arquivados">Arquivadas</TabsTrigger>
<TabsTrigger value="inativas">Inativas</TabsTrigger>
</TabsList>
<TabsContent value="ativos" className="mt-4">
{renderAccountList(orderedAccounts, false)}
</TabsContent>
<TabsContent value="arquivados" className="mt-4">
<TabsContent value="inativas" className="mt-4">
{renderAccountList(orderedArchivedAccounts, true)}
</TabsContent>
</Tabs>

View File

@@ -0,0 +1,135 @@
"use client";
import { RiEqualizerLine } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
import { toast } from "sonner";
import { adjustAccountBalanceAction } from "@/features/accounts/actions";
import { Button } from "@/shared/components/ui/button";
import { CurrencyInput } from "@/shared/components/ui/currency-input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/components/ui/dialog";
import { Label } from "@/shared/components/ui/label";
import { formatCurrency } from "@/shared/utils/currency";
type AdjustBalanceDialogProps = {
accountId: string;
period: string;
currentBalance: number;
};
export function AdjustBalanceDialog({
accountId,
period,
currentBalance,
}: AdjustBalanceDialogProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const [amount, setAmount] = useState<string>(currentBalance.toFixed(2));
useEffect(() => {
if (open) {
setAmount(currentBalance.toFixed(2));
}
}, [open, currentBalance]);
const targetBalance = Number(amount);
const diff = Number.isFinite(targetBalance)
? Math.round((targetBalance - currentBalance) * 100) / 100
: 0;
const diffLabel =
diff > 0
? `Será criado um lançamento de receita de ${formatCurrency(diff)}.`
: diff < 0
? `Será criado um lançamento de despesa de ${formatCurrency(Math.abs(diff))}.`
: "Nenhum ajuste será criado — o saldo já está correto.";
const handleSave = () => {
if (!Number.isFinite(targetBalance)) {
toast.error("Informe um valor válido.");
return;
}
startTransition(async () => {
const result = await adjustAccountBalanceAction({
accountId,
period,
currentBalance,
targetBalance,
});
if (result.success) {
toast.success(result.message);
setOpen(false);
router.refresh();
return;
}
toast.error(result.error);
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground hover:text-foreground"
aria-label="Ajustar saldo"
>
<RiEqualizerLine className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Ajustar saldo</DialogTitle>
<DialogDescription>
Informe o saldo correto da conta ao final do período. A diferença em
relação ao saldo atual será lançada como um ajuste.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-md border bg-muted/30 px-3 py-2 text-sm">
<p className="text-muted-foreground">Saldo atual no sistema</p>
<p className="font-medium text-foreground">
{formatCurrency(currentBalance)}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="adjust-balance-target">Saldo correto</Label>
<CurrencyInput
id="adjust-balance-target"
value={amount}
onValueChange={setAmount}
autoFocus
/>
<p className="text-xs text-muted-foreground">{diffLabel}</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isPending}
>
Cancelar
</Button>
<Button type="button" onClick={handleSave} disabled={isPending}>
{isPending ? "Salvando..." : "Salvar"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -99,13 +99,13 @@ async function fetchAccountsByStatus(
return { accounts, logoOptions };
}
export async function fetchAccountsForUser(
async function fetchAccountsForUser(
userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
return fetchAccountsByStatus(userId, false);
}
export async function fetchInactiveForUser(
async function fetchInactiveForUser(
userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
return fetchAccountsByStatus(userId, true);

View File

@@ -4,7 +4,10 @@ import {
fetchTransactionsPageWithRelations,
fetchTransactionsWithRelations,
} from "@/features/transactions/queries";
import { INITIAL_BALANCE_NOTE } from "@/shared/lib/accounts/constants";
import {
INITIAL_BALANCE_NOTE,
REFUND_NOTE_PREFIX,
} from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db";
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
@@ -74,7 +77,9 @@ export async function fetchAccountSummary(
sum(
case
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
when ${transactions.note} ilike ${`${REFUND_NOTE_PREFIX}%`} then 0
when ${transactions.transactionType} = 'Receita' then ${transactions.amount}
when ${transactions.transactionType} = 'Transferência' and ${transactions.amount} > 0 then ${transactions.amount}
else 0
end
),
@@ -86,7 +91,9 @@ export async function fetchAccountSummary(
sum(
case
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
when ${transactions.note} ilike ${`${REFUND_NOTE_PREFIX}%`} then abs(${transactions.amount})
when ${transactions.transactionType} = 'Despesa' then ${transactions.amount}
when ${transactions.transactionType} = 'Transferência' and ${transactions.amount} < 0 then ${transactions.amount}
else 0
end
),
@@ -135,7 +142,8 @@ export async function fetchAccountSummary(
const openingBalance = initialBalance + previousMovements;
const netAmount = Number(periodSummary?.netAmount ?? 0);
const totalIncomes = Number(periodSummary?.incomes ?? 0);
const totalExpenses = Math.abs(Number(periodSummary?.expenses ?? 0));
const expenseNet = Number(periodSummary?.expenses ?? 0);
const totalExpenses = Math.max(0, -expenseNet);
const currentBalance = openingBalance + netAmount;
return {
@@ -146,7 +154,7 @@ export async function fetchAccountSummary(
};
}
export async function fetchAccountLancamentos(
export async function fetchAccountTransactions(
filters: SQL[],
settledOnly = true,
) {
@@ -159,7 +167,7 @@ export async function fetchAccountLancamentos(
});
}
export async function fetchAccountLancamentosPage(
export async function fetchAccountTransactionsPage(
filters: SQL[],
{
page,

View File

@@ -18,7 +18,7 @@ import { fetchTransactionDialogOptionsAction } from "@/features/transactions/act
import { TransactionDetailsDialog } from "@/features/transactions/components/dialogs/transaction-details-dialog";
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
import type { TransactionItem } from "@/features/transactions/components/types";
import { EmptyState } from "@/shared/components/empty-state";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Card, CardContent } from "@/shared/components/ui/card";
import { cn } from "@/shared/utils/ui";

View File

@@ -2,11 +2,11 @@
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { fetchJson } from "@/shared/lib/fetch-json";
import { fetchJson } from "@/shared/utils/fetch-json";
const ATTACHMENT_URL_STALE_TIME = 4 * 60 * 1000;
export const attachmentUrlQueryKey = (attachmentId: string) =>
const attachmentUrlQueryKey = (attachmentId: string) =>
["attachments", "url", attachmentId] as const;
export function useAttachmentUrlQuery(attachmentId: string, enabled: boolean) {

View File

@@ -3,7 +3,7 @@ import {
RiBarChart2Line,
RiShieldCheckLine,
} from "@remixicon/react";
import { Logo } from "@/shared/components/logo";
import { Logo } from "@/shared/components/brand/logo";
import { DotPattern } from "@/shared/components/ui/dot-pattern";
import { AuthSidebarInvoicesMock } from "./auth-sidebar-invoices-mock";

View File

@@ -90,6 +90,7 @@ export function BudgetCard({ budget, onEdit, onRemove }: BudgetCardProps) {
<Progress
value={usagePercent}
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
indicatorClassName={cn(exceeded && "bg-destructive")}
aria-label={`${usagePercent.toFixed(1)}% do orçamento utilizado`}
/>
<span className="text-xs text-muted-foreground">

View File

@@ -8,7 +8,7 @@ import {
duplicatePreviousMonthBudgetsAction,
} from "@/features/budgets/actions";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/empty-state";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Button } from "@/shared/components/ui/button";
import { Card } from "@/shared/components/ui/card";
import { BudgetCard } from "./budget-card";

View File

@@ -26,7 +26,7 @@ type BudgetData = {
} | null;
};
export type CategoryOption = {
type CategoryOption = {
id: string;
name: string;
icon: string | null;

View File

@@ -16,7 +16,7 @@ export function CalendarGrid({
onCreateDay,
}: CalendarGridProps) {
return (
<div className="overflow-hidden rounded-lg border p-2">
<div className="overflow-hidden">
<div className="grid grid-cols-7 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{WEEK_DAYS_SHORT.map((dayName) => (
<span key={dayName} className="text-center">

View File

@@ -1,6 +1,7 @@
"use client";
import { EVENT_TYPE_STYLES } from "@/features/calendar/components/day-cell";
import { Card } from "@/shared/components/ui/card";
import { cn } from "@/shared/utils/ui";
const LEGEND_ITEMS = [
@@ -12,22 +13,24 @@ const LEGEND_ITEMS = [
export function CalendarLegend() {
return (
<ul className="flex items-center justify-start gap-2 px-1">
{LEGEND_ITEMS.map((item) => (
<li
key={item.label}
className={cn(
"flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium",
item.wrapper,
)}
>
<span
className={cn("size-1.5 shrink-0 rounded-full", item.dot)}
aria-hidden
/>
{item.label}
</li>
))}
</ul>
<Card className="px-4 py-2">
<ul className="flex flex-row items-center gap-2">
{LEGEND_ITEMS.map((item) => (
<li
key={item.label}
className={cn(
"flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium",
item.wrapper,
)}
>
<span
className={cn("size-1.5 shrink-0 rounded-full", item.dot)}
aria-hidden
/>
{item.label}
</li>
))}
</ul>
</Card>
);
}

View File

@@ -129,7 +129,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
onClick={() => onSelect(day)}
onKeyDown={handleKeyDown}
className={cn(
"group flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border bg-card/80 p-2 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent",
"group flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border bg-card/70 p-2 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent",
!day.isCurrentMonth && "bg-muted/20 opacity-60",
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
)}

View File

@@ -4,7 +4,7 @@ import {
buildOptionSets,
buildSluggedFilters,
mapTransactionsData,
} from "@/features/transactions/page-helpers";
} from "@/features/transactions/lib/page-helpers";
import {
fetchRecentEstablishments,
fetchTransactionFilterSources,

View File

@@ -13,10 +13,10 @@ import { db } from "@/shared/lib/db";
import {
dayOfMonthSchema,
noteSchema,
optionalDecimalSchema,
requiredDecimalSchema,
uuidSchema,
} from "@/shared/lib/schemas/common";
import { formatDecimalForDb } from "@/shared/utils/currency";
import { formatDecimalForDbRequired } from "@/shared/utils/currency";
import { normalizeFilePath } from "@/shared/utils/string";
const cardBaseSchema = z.object({
@@ -35,7 +35,7 @@ const cardBaseSchema = z.object({
closingDay: dayOfMonthSchema,
dueDay: dayOfMonthSchema,
note: noteSchema,
limit: optionalDecimalSchema,
limit: requiredDecimalSchema("limite"),
logo: z
.string({ message: "Selecione um logo." })
.trim()
@@ -87,7 +87,7 @@ export async function createCardAction(
closingDay: data.closingDay,
dueDay: data.dueDay,
note: data.note ?? null,
limit: formatDecimalForDb(data.limit),
limit: formatDecimalForDbRequired(data.limit),
logo: logoFile,
accountId: data.accountId,
userId: user.id,
@@ -121,7 +121,7 @@ export async function updateCardAction(
closingDay: data.closingDay,
dueDay: data.dueDay,
note: data.note ?? null,
limit: formatDecimalForDb(data.limit),
limit: formatDecimalForDbRequired(data.limit),
logo: logoFile,
accountId: data.accountId,
})

View File

@@ -154,13 +154,21 @@ export function CardDialog({
}
const rawLimit = normalizeDecimalInput(formState.limit);
const limitValue = rawLimit ? Number(rawLimit) : 0;
if (!Number.isFinite(limitValue) || limitValue <= 0) {
const message = "Informe um limite maior que zero.";
setErrorMessage(message);
toast.error(message);
return;
}
const payload: CardCreatePayload = {
name: formState.name.trim(),
brand: formState.brand,
status: formState.status,
closingDay: formState.closingDay,
dueDay: formState.dueDay,
limit: rawLimit ? Number(rawLimit) : null,
limit: limitValue,
note: formState.note.trim() || null,
logo: formState.logo,
accountId: formState.accountId,

View File

@@ -112,12 +112,13 @@ export function CardFormFields({
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-limit">Limite (R$)</Label>
<Label htmlFor="card-limit">Limite</Label>
<CurrencyInput
id="card-limit"
value={values.limit}
onValueChange={(value) => onChange("limit", value)}
placeholder="R$ 0,00"
required
/>
</div>

View File

@@ -30,9 +30,9 @@ interface CardItemProps {
status: string;
closingDay: string;
dueDay: string;
limit: number | null;
limitInUse?: number | null;
limitAvailable?: number | null;
limit: number;
limitInUse?: number;
limitAvailable?: number;
accountName: string;
logo?: string | null;
note?: string | null;
@@ -61,30 +61,19 @@ export function CardItem({
}: CardItemProps) {
void _accountName;
const limitTotal = limit ?? null;
const used =
limitInUse ??
(limitTotal !== null && limitAvailable != null
? Math.max(limitTotal - limitAvailable, 0)
: limitTotal !== null
? 0
: null);
(limitAvailable !== undefined ? Math.max(limit - limitAvailable, 0) : 0);
const available =
limitAvailable ??
(limitTotal !== null && used !== null
? Math.max(limitTotal - used, 0)
: null);
const available = limitAvailable ?? Math.max(limit - used, 0);
const usagePercent =
limitTotal && limitTotal > 0 && used !== null
? Math.min(Math.max((used / limitTotal) * 100, 0), 100)
: 0;
limit > 0 ? Math.min(Math.max((used / limit) * 100, 0), 100) : 0;
const exceeded = usagePercent >= 100;
const logoPath = resolveLogoSrc(logo);
const brandAsset = resolveCardBrandAsset(brand);
const isInactive = status?.toLowerCase() === "inativo";
const hasMetrics = limitTotal !== null && used !== null && available !== null;
return (
<Card className="flex flex-col p-6 w-full">
@@ -174,54 +163,46 @@ export function CardItem({
</CardHeader>
<CardContent className="flex flex-1 flex-col gap-4 px-0">
{hasMetrics &&
available !== null &&
used !== null &&
limitTotal !== null ? (
<>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">Disponível</span>
<MoneyValues
amount={available}
className="text-xl font-semibold text-success"
/>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">
Limite disponível
</span>
<MoneyValues
amount={available}
className="text-xl font-semibold text-success"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">
Limite total
</span>
<MoneyValues
amount={limitTotal}
className="text-sm font-semibold text-foreground"
/>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">Em uso</span>
<MoneyValues
amount={used}
className="text-sm font-semibold text-primary"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">Limite total</span>
<MoneyValues
amount={limit}
className="text-sm font-semibold text-foreground"
/>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">
Limite utilizado
</span>
<MoneyValues
amount={used}
className="text-sm font-semibold text-destructive"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Progress
value={usagePercent}
className="h-2.5"
aria-label={`${usagePercent.toFixed(0)}% do limite utilizado`}
/>
<span className="text-xs text-muted-foreground">
{usagePercent.toFixed(1)}% utilizado
</span>
</div>
</>
) : (
<p className="text-sm text-muted-foreground">
Ainda não limite registrado para este cartão.
</p>
)}
<div className="flex flex-col gap-2">
<Progress
value={usagePercent}
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
indicatorClassName={cn(exceeded && "bg-destructive")}
aria-label={`${usagePercent.toFixed(0)}% do limite utilizado`}
/>
<span className="text-xs text-muted-foreground">
{usagePercent.toFixed(1)}% utilizado
</span>
</div>
</CardContent>
<CardFooter className="mt-auto flex flex-wrap gap-4 px-0 pt-2 text-sm">

View File

@@ -2,7 +2,7 @@
import { RiBankLine } from "@remixicon/react";
import Image from "next/image";
import StatusDot from "@/shared/components/status-dot";
import StatusDot from "@/shared/components/feedback/status-dot";
import { resolveCardBrandLogoSrc } from "@/shared/lib/cards/brand-assets";
import { resolveLogoSrc } from "@/shared/lib/logo";

View File

@@ -6,7 +6,7 @@ import { useMemo, useState } from "react";
import { toast } from "sonner";
import { deleteCardAction } from "@/features/cards/actions";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/empty-state";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Button } from "@/shared/components/ui/button";
import { Card as UiCard } from "@/shared/components/ui/card";
import {
@@ -174,14 +174,14 @@ export function CardsPage({
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativos">Ativos</TabsTrigger>
<TabsTrigger value="arquivados">Arquivados</TabsTrigger>
<TabsTrigger value="inativos">Inativos</TabsTrigger>
</TabsList>
<TabsContent value="ativos" className="mt-4">
{renderCardList(orderedCards, false)}
</TabsContent>
<TabsContent value="arquivados" className="mt-4">
<TabsContent value="inativos" className="mt-4">
{renderCardList(orderedArchivedCards, true)}
</TabsContent>
</Tabs>

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