Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6fba5f953 | ||
|
|
18893bfe02 | ||
|
|
7fdf9e2876 | ||
|
|
7d0781b035 | ||
|
|
b9b843b9db | ||
|
|
01215b3124 | ||
|
|
d70223e7b3 | ||
|
|
6ea064e1bd | ||
|
|
9c0669a152 | ||
|
|
b2d4b29cb5 | ||
|
|
1df2ba787d | ||
|
|
e5d9b66cca | ||
|
|
37edb1b76d | ||
|
|
6288f5f8d4 | ||
|
|
57ac326c2a | ||
|
|
dccc18b1c1 | ||
|
|
0cb01a1d4c | ||
|
|
51652da4f8 | ||
|
|
7a74f9405e | ||
|
|
94bf93194f | ||
|
|
d55173e8c1 | ||
|
|
4a73088c09 | ||
|
|
eaa20448a8 | ||
|
|
367d78d43d | ||
|
|
2fc6d11d78 | ||
|
|
0f5c735be0 | ||
|
|
4bea6330bf | ||
|
|
8389752172 |
216
CHANGELOG.md
@@ -5,7 +5,131 @@ 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/),
|
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/).
|
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [2.5.5] - 2026-05-06
|
||||||
|
|
||||||
|
Esta versão melhora a navegação por históricos e lançamentos. O changelog ganhou uma linha do tempo mais leve, colapsável e fácil de escanear; os filtros de lançamentos passam a aceitar múltiplas pessoas, categorias, formas de pagamento, condições e contas/cartões na mesma busca; e os diálogos adotam as animações compartilhadas do design system. Também há pequenos polimentos de texto e layout para deixar a interface mais consistente.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Lançamentos: filtros multi-seleção para condição, forma de pagamento, pessoa, categoria e conta/cartão, permitindo combinar vários valores no mesmo filtro (query string passa a aceitar múltiplos valores por chave).
|
||||||
|
- Changelog: parser passou a inferir o tipo de bump (major/minor/patch) a partir da numeração e a extrair o parágrafo de resumo abaixo do cabeçalho de versão; novo arquivo `src/features/settings/lib/changelog-types.ts` consolidando os tipos compartilhados.
|
||||||
|
- UI: dependência `tw-animate-css` para usar as mesmas animações utilitárias já presentes nos componentes shadcn/ui.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- Changelog: visual da página reformulado para linha do tempo com resumo sempre visível, detalhes colapsáveis por versão, agrupamento por mês e marcadores visuais por tipo de bump; componente migrado para `"use client"` com `Collapsible` e abertura via âncora (`#vX-Y-Z`).
|
||||||
|
- Lançamentos: botões "Nova Receita" e "Nova Despesa" agora usam os próprios triggers do `TransactionDialog` (via prop `createSlot`), reduzindo estado manual na página e eliminando o fluxo `setCreateOpen` + `transactionTypeForCreate`.
|
||||||
|
- Diálogos: animações customizadas em CSS (`@keyframes dialog-in/out` e `overlay-in/out`) substituídas pelas classes utilitárias compartilhadas em `Dialog`/`DialogOverlay` (`data-[state=open]:animate-in`, `zoom-in-95`, `fade-in-0`).
|
||||||
|
- BulkActionDialog: label do escopo "Todas as pessoas" passa a indicar a parcela atual (`Todas as pessoas desta parcela (N/Total)`) com descrição mais clara sobre o efeito da ação.
|
||||||
|
- Checkbox: `RiCheckLine`/`RiSubtractLine` agora herdam `text-current` para alinhar com a cor do indicator nativo.
|
||||||
|
- Landing page: remoção de fundos alternados (`bg-muted/40`) nas seções "Funcionalidades", "Stack" e "Para quem é" para uma leitura visual mais limpa.
|
||||||
|
- Navbar: aviso de atualização passa a usar o texto "Versão X disponível".
|
||||||
|
|
||||||
|
## [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
|
## [2.4.4] - 2026-04-27
|
||||||
|
|
||||||
@@ -97,7 +221,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: 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)
|
- 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
|
- 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)
|
- 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
|
- 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
|
- 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
|
||||||
@@ -114,6 +238,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.4.1] - 2026-04-16
|
## [2.4.1] - 2026-04-16
|
||||||
|
|
||||||
|
Versão pequena com refresh visual nas telas de autenticação (efeito blob com três círculos coloridos em movimento e card com glassmorphism), capitalização dos labels da navbar para melhor legibilidade e otimização do banco com 17 índices novos em foreign keys — evitando sequential scans em deletes em tabelas grandes como `lancamentos`. Corrigida regressão no `postgres:18-alpine` que recusava iniciar em instalações existentes; adicionada variável `PGDATA` no compose para preservar dados de quem já tinha o volume populado.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- UI/Auth: layout animado nas páginas de login e signup com efeito blob (3 círculos coloridos em movimento) e card com glassmorphism; layout compartilhado extraído para `app/(auth)/layout.tsx` eliminando duplicação (PR #42)
|
- UI/Auth: layout animado nas páginas de login e signup com efeito blob (3 círculos coloridos em movimento) e card com glassmorphism; layout compartilhado extraído para `app/(auth)/layout.tsx` eliminando duplicação (PR #42)
|
||||||
@@ -134,6 +260,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.4.0] - 2026-04-13
|
## [2.4.0] - 2026-04-13
|
||||||
|
|
||||||
|
Esta versão integra o serviço Logo.dev para exibir automaticamente logos de marcas na coluna de estabelecimentos dos lançamentos, com picker manual para fixar o domínio quando a sugestão automática não acerta. As consultas vão por novas rotas de API (`/api/logo/search` e `/api/logo/mapping`) que servem como proxy seguro — a secret key fica server-side. Inclui também tabela própria `establishment_logos` com PK composta `(user_id, name_key)` para persistir as preferências por usuário.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Estabelecimentos: integração com Logo.dev — logos automáticos de marcas exibidos na coluna de estabelecimentos nos lançamentos
|
- Estabelecimentos: integração com Logo.dev — logos automáticos de marcas exibidos na coluna de estabelecimentos nos lançamentos
|
||||||
@@ -153,6 +281,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.3.8] - 2026-04-12
|
## [2.3.8] - 2026-04-12
|
||||||
|
|
||||||
|
Refatoração do `docker-compose.yml` para virar standalone — agora basta um `curl` + `docker compose up -d`, sem dependências de arquivos externos ou profiles complexos. README reescrito em dois perfis claros (Usar com Docker e Desenvolver com hot-reload) e scripts npm reduzidos de 10 para 5.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Docker: `docker-compose.yml` refatorado — removidos profiles, build e dependência de arquivo externo; compose agora é standalone (basta `curl` + `docker compose up -d`)
|
- Docker: `docker-compose.yml` refatorado — removidos profiles, build e dependência de arquivo externo; compose agora é standalone (basta `curl` + `docker compose up -d`)
|
||||||
@@ -162,6 +292,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.3.7] - 2026-04-11
|
## [2.3.7] - 2026-04-11
|
||||||
|
|
||||||
|
Esta versão amplia significativamente o dashboard com três novos widgets configuráveis (Anexos, Inbox, Tendências de Categoria), adiciona filtros úteis na tabela de lançamentos (por status de pagamento e por presença de anexo) e moderniza a tipografia substituindo a fonte local por Inter (Google Fonts, self-hosted pelo Next.js) — eliminando arquivos `.woff2` do repositório. Pesos tipográficos foram padronizados para `font-semibold` em títulos, rótulos e valores monetários, e o card de grupo de parcelas foi redesenhado expandindo num dialog de detalhes com parcelas pagas/pendentes separadas. No backend, a CSP foi expandida para permitir preview de anexos PDF via S3, e o setup ganhou script `install-deps.sh` pra preparar servidores Ubuntu 24.04 limpos.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Dashboard: novos widgets configuráveis — Anexos (resumo de arquivos do período), Inbox (snapshot de pré-lançamentos pendentes) e Tendências de Categoria
|
- Dashboard: novos widgets configuráveis — Anexos (resumo de arquivos do período), Inbox (snapshot de pré-lançamentos pendentes) e Tendências de Categoria
|
||||||
@@ -198,24 +330,32 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.3.6] - 2026-04-09
|
## [2.3.6] - 2026-04-09
|
||||||
|
|
||||||
|
Correção pontual no Docker — adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para o `drizzle-kit` resolver corretamente o `drizzle-orm` ao executar as migrations no container.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Docker: adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para que o `drizzle-kit` consiga resolver `drizzle-orm` ao executar as migrations no container
|
- Docker: adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para que o `drizzle-kit` consiga resolver `drizzle-orm` ao executar as migrations no container
|
||||||
|
|
||||||
## [2.3.5] - 2026-04-07
|
## [2.3.5] - 2026-04-07
|
||||||
|
|
||||||
|
Correção crítica na CSP: regra movida do `next.config.ts` (build time) para `proxy.ts` (runtime), desbloqueando uploads de anexos quando o `S3_ENDPOINT` ainda não estava disponível durante o build da imagem Docker.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- CSP: movido `Content-Security-Policy` do `next.config.ts` (build time) para `proxy.ts` (runtime), corrigindo bloqueio de upload de anexos quando `S3_ENDPOINT` não estava disponível durante o build do Docker
|
- CSP: movido `Content-Security-Policy` do `next.config.ts` (build time) para `proxy.ts` (runtime), corrigindo bloqueio de upload de anexos quando `S3_ENDPOINT` não estava disponível durante o build do Docker
|
||||||
|
|
||||||
## [2.3.4] - 2026-04-05
|
## [2.3.4] - 2026-04-05
|
||||||
|
|
||||||
|
Correção pontual no upload de anexos — a CSP `connect-src` bloqueava o fetch para o storage, gerando `NetworkError` na hora de subir o arquivo.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Anexos: corrigido upload que falhava com `NetworkError` — CSP `connect-src` bloqueava fetch para o Storage
|
- Anexos: corrigido upload que falhava com `NetworkError` — CSP `connect-src` bloqueava fetch para o Storage
|
||||||
|
|
||||||
## [2.3.3] - 2026-04-05
|
## [2.3.3] - 2026-04-05
|
||||||
|
|
||||||
|
Correção do fluxo de tokens da API: `/api/auth/device/verify` voltou a aceitar tokens criados pela tela de Settings (revertido de JWT para hash lookup). O prefixo dos tokens também foi renomeado de `os_` para `opm_` (OpenMonetis) e rotas JWT não utilizadas foram removidas — usuários precisam recriar os tokens existentes.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Tokens: corrigido `/api/auth/device/verify` que rejeitava tokens criados via Settings (revertido de JWT para hash lookup)
|
- Tokens: corrigido `/api/auth/device/verify` que rejeitava tokens criados via Settings (revertido de JWT para hash lookup)
|
||||||
@@ -228,6 +368,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.3.2] - 2026-04-04
|
## [2.3.2] - 2026-04-04
|
||||||
|
|
||||||
|
Esta versão concentra hardening de segurança. Tokens da API ganharam expiração obrigatória de 1 ano (sem mais tokens eternos) e o refresh foi corrigido para validar JWT por assinatura. A CSP foi expandida com `default-src`, `script-src`, `style-src`, `img-src`, `font-src` e `connect-src` (no lugar de uma regra única ampla), e foi adicionada mitigação para CVE-2024-44294 desabilitando parsing de fórmulas em `xlsx`. Inclui ainda novos headers (`Referrer-Policy`, `X-Permitted-Cross-Domain-Policies`), respostas `401 JSON` em vez de redirect 302 em rotas autenticadas, `security.txt` (RFC 9116) e correção de URL com protocolo duplicado no sitemap.
|
||||||
|
|
||||||
### Segurança
|
### Segurança
|
||||||
|
|
||||||
- Tokens: removido aceite de tokens sem expiração (`expiresAt NULL`); tokens criados via settings agora expiram em 1 ano
|
- Tokens: removido aceite de tokens sem expiração (`expiresAt NULL`); tokens criados via settings agora expiram em 1 ano
|
||||||
@@ -243,12 +385,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.3.1] - 2026-04-03
|
## [2.3.1] - 2026-04-03
|
||||||
|
|
||||||
|
Correção pontual de infraestrutura — dependências do `drizzle-kit` passaram a ser instaladas em `/app/migrate/` separadamente do `node_modules` do build standalone, corrigindo o erro `Cannot find module 'next'` no startup do container.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Infraestrutura: deps do drizzle-kit agora são instaladas em `/app/migrate/` separado do `node_modules` do standalone, corrigindo erro `Cannot find module 'next'` no startup do container
|
- Infraestrutura: deps do drizzle-kit agora são instaladas em `/app/migrate/` separado do `node_modules` do standalone, corrigindo erro `Cannot find module 'next'` no startup do container
|
||||||
|
|
||||||
## [2.3.0] - 2026-04-03
|
## [2.3.0] - 2026-04-03
|
||||||
|
|
||||||
|
Esta versão introduz `@tanstack/react-query` no projeto, padronizando cache, deduplicação e invalidação de leituras client-side. Várias features (anexos, insights, antecipação de parcelas) passaram a usar React Query no lugar de `useEffect` manual sobre rotas GET dedicadas. O dashboard ganhou ajuda contextual em cada métrica e configuração persistida pra ocultar contas marcadas como não consideradas no saldo total; o menu do usuário na navbar passou a avisar quando há release nova publicada no GitHub; e o Docker passou a rodar migrations automaticamente no startup via `docker-entrypoint.sh`. Internamente, o `knip` foi adicionado pra auditar arquivos/exports/tipos sem uso, várias rotas e actions ganharam validações extras (filtros por `userId` em joins, rate limits explícitos no Better Auth, headers `Cache-Control: private, no-store` em rotas privadas) e o projeto foi atualizado para Next.js 16.2.2 e Biome 2.4.10.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Dependências: adiciona `@tanstack/react-query` e um provider global para padronizar cache, deduplicação e invalidação de leituras client-side
|
- Dependências: adiciona `@tanstack/react-query` e um provider global para padronizar cache, deduplicação e invalidação de leituras client-side
|
||||||
@@ -284,12 +430,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.2.1] - 2026-04-01
|
## [2.2.1] - 2026-04-01
|
||||||
|
|
||||||
|
Correção pontual no build da imagem Docker — removido `chown -R /app` do stage final (que travava o build/push da GitHub Action por lentidão excessiva); permissões agora definidas via `COPY --chown` direto.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Docker: imagem de produção deixa de executar `chown -R /app` no stage final; as permissões passam a ser definidas nos `COPY --chown`, reduzindo o risco de travamento e lentidão excessiva no build/push da GitHub Action
|
- Docker: imagem de produção deixa de executar `chown -R /app` no stage final; as permissões passam a ser definidas nos `COPY --chown`, reduzindo o risco de travamento e lentidão excessiva no build/push da GitHub Action
|
||||||
|
|
||||||
## [2.2.0] - 2026-04-01
|
## [2.2.0] - 2026-04-01
|
||||||
|
|
||||||
|
Esta versão entrega uma nova página dedicada de galeria de anexos em `/attachments` com miniaturas, visualização inline (incluindo PDF via `pdfjs-dist`), download direto e acesso a partir do lançamento. As páginas de login e cadastro foram redesenhadas com sidebar mockup de faturas, três blocos de funcionalidade e gradiente decorativo. O dashboard passou a notificar boletos e faturas com vencimento dentro de 5 dias, e o cache do dashboard migrou de `unstable_cache` para a diretiva `use cache` (com `cacheTag` e `cacheLife`), com `cacheComponents: true` no `next.config.ts` e `connection()` em todas as páginas para forçar render dinâmico. A tipografia ganhou peso 500 (Medium) padronizado em títulos, valores e rótulos.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Anexos: nova página de galeria em `/attachments` com miniaturas, visualização inline de imagem e PDF, download direto e acesso a partir do lançamento
|
- Anexos: nova página de galeria em `/attachments` com miniaturas, visualização inline de imagem e PDF, download direto e acesso a partir do lançamento
|
||||||
@@ -310,6 +460,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.1.2] - 2026-03-30
|
## [2.1.2] - 2026-03-30
|
||||||
|
|
||||||
|
Pequena versão de polimento: novo escopo `"period"` na ação em lote de lançamentos (aplica alteração a todos os lançamentos do período sem sobrescrever o pagador individual de cada um), preferência de tamanho máximo por arquivo de anexo (5/10/25/50/100 MB) persistida no banco e respeitada em todos os pontos de upload, e redesign visual da página de Configurações com separadores entre seções e títulos maiores.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Preferências: nova configuração de tamanho máximo por arquivo de anexo (5, 10, 25, 50 ou 100 MB), persistida no banco e respeitada em todos os pontos de upload
|
- Preferências: nova configuração de tamanho máximo por arquivo de anexo (5, 10, 25, 50 ou 100 MB), persistida no banco e respeitada em todos os pontos de upload
|
||||||
@@ -326,6 +478,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.1.1] - 2026-03-29
|
## [2.1.1] - 2026-03-29
|
||||||
|
|
||||||
|
Esta versão extrai a navbar pra um componente `NavbarShell` compartilhado entre app e landing page e cria uma variante `navbar` no Button pra centralizar os estilos antes duplicados em `nav-styles.ts`. A integração com `@vercel/analytics`/`@vercel/speed-insights` foi substituída por Umami self-hosted via script tag no layout raiz.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page
|
- Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page
|
||||||
@@ -347,6 +501,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.1.0] - 2026-03-28
|
## [2.1.0] - 2026-03-28
|
||||||
|
|
||||||
|
Esta versão adiciona suporte a anexos em transações, com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento. O upload exige token assinado por arquivo, valida ownership da transação na leitura/remoção e confere tamanho/tipo do objeto no storage antes de persistir o vínculo no banco. Inclui também novo workflow `release.yml` que cria tag e GitHub Release automaticamente a partir da versão do `package.json` e da entrada correspondente no `CHANGELOG.md`.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Lançamentos: suporte a anexos em transações com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento
|
- Lançamentos: suporte a anexos em transações com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento
|
||||||
@@ -362,12 +518,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.0.3] - 2026-03-26
|
## [2.0.3] - 2026-03-26
|
||||||
|
|
||||||
|
Correção pontual em `/transactions` — removida dependência de `crypto.randomUUID()` no carregamento inicial, que falhava em ambientes self-hosted sem HTTPS (a API só está disponível em contextos seguros).
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Lançamentos: `/transactions` deixa de depender de `crypto.randomUUID()` no carregamento inicial, corrigindo a falha em ambientes self-hosted sem HTTPS ao abrir a página
|
- Lançamentos: `/transactions` deixa de depender de `crypto.randomUUID()` no carregamento inicial, corrigindo a falha em ambientes self-hosted sem HTTPS ao abrir a página
|
||||||
|
|
||||||
## [2.0.2] - 2026-03-25
|
## [2.0.2] - 2026-03-25
|
||||||
|
|
||||||
|
Versão focada nas notificações da navbar: novo estado persistido permite marcar alertas de fatura, boleto e orçamento como lidos ou arquivados por usuário; o snapshot global passa a usar o período corrente do negócio (não mais o `periodo` da URL), itens lidos saem do badge e arquivados somem da lista padrão do sino. O filtro foi refinado para um seletor explícito entre `Ativas` e `Arquivadas`. Inclui ajustes pontuais no detalhamento por categoria do dashboard (oculta categorias sem movimentação no período), na arte decorativa do cabeçalho de boas-vindas e na edição em lote de lançamentos em série (que agora propaga também o status de pagamento para transações fora do cartão).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Scripts: novo comando `mockup` no `package.json` para executar `scripts/mock-data.ts`
|
- Scripts: novo comando `mockup` no `package.json` para executar `scripts/mock-data.ts`
|
||||||
@@ -396,6 +556,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.0.1] - 2026-03-21
|
## [2.0.1] - 2026-03-21
|
||||||
|
|
||||||
|
Versão de correções na inbox de pré-lançamentos: filtro por app passa a montar a lista completa a partir de todos os itens do status atual (sem depender da página carregada), notificações de cartões/apps sem logo cadastrado passam a usar `default_icon.png` como fallback, e o select de apps exibe os logos. Inclui também correção de divergência entre a versão exibida no UI e a reportada pelo `/api/health` (que agora reporta a versão atual do `package.json`).
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Inbox: filtro por app em `/inbox` agora monta a lista completa de apps da aba a partir de todos os itens do status atual, sem depender apenas da página carregada, e o SSR deixa de quebrar quando `sourceApps` vier inconsistente
|
- Inbox: filtro por app em `/inbox` agora monta a lista completa de apps da aba a partir de todos os itens do status atual, sem depender apenas da página carregada, e o SSR deixa de quebrar quando `sourceApps` vier inconsistente
|
||||||
@@ -406,6 +568,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [2.0.0] - 2026-03-21
|
## [2.0.0] - 2026-03-21
|
||||||
|
|
||||||
|
Marco importante do projeto. Esta versão consolida ganhos de performance, segurança e organização interna. No backend, paginação server-side real foi implementada em transações, extrato e inbox; o dashboard reduziu de 19 fetchers para 7 blocos com agregações compartilhadas; exportações de PDF/Excel passaram a carregar libs sob demanda apenas no clique; e o cache de dashboard/insights ganhou invalidação segmentada por `userId` (sem fallback global). Internamente, identificadores foram migrados de PT-BR para inglês (`lancamento` → `transaction`, `pagador` → `payer`, `conta` → `account`, etc.) e helpers foram consolidados em módulos de domínio. Visualmente, a navbar e os cards de auth ganharam dot pattern + brilho em primary, faturas tiveram refinamento na hierarquia visual, e a tipografia foi unificada na família America. Inclui ainda script `scripts/backup.sh` para backup automático do PostgreSQL, importação de extratos OFX e XLS/XLSX com tela de revisão e dedup por FITID, e nova opção de zerar dados financeiros sem excluir o usuário.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Infraestrutura: script `scripts/backup.sh` para backup automático do banco PostgreSQL; configuração de destino (rclone, cron, retenção) feita separadamente; passa a gerar também `*.data.sql.gz` com dados puros de todas as tabelas públicas (`--data-only --schema=public`)
|
- Infraestrutura: script `scripts/backup.sh` para backup automático do banco PostgreSQL; configuração de destino (rclone, cron, retenção) feita separadamente; passa a gerar também `*.data.sql.gz` com dados puros de todas as tabelas públicas (`--data-only --schema=public`)
|
||||||
@@ -462,6 +626,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.7] - 2026-03-05
|
## [1.7.7] - 2026-03-05
|
||||||
|
|
||||||
|
Versão de organização interna sem mudanças visíveis grandes. Períodos e navegação mensal passaram a usar os helpers centrais de período (`YYYY-MM`), hooks locais (calculadora, month-picker, logo picker) foram movidos pra perto das respectivas features e `components/navbar`/`sidebar` foram consolidados em `components/navigation/*`. Análise de parcelas migrou para `/relatorios/analise-parcelas`, exportações em PDF/CSV/Excel ganharam melhor branding e apresentação, e a calculadora teve ajustes de estabilidade no arrasto.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`.
|
- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`.
|
||||||
@@ -476,6 +642,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.6] - 2026-03-02
|
## [1.7.6] - 2026-03-02
|
||||||
|
|
||||||
|
Esta versão adiciona suporte completo a Passkeys (WebAuthn) via `@better-auth/passkey`: nova aba em `/ajustes` permite listar, adicionar, renomear e remover credenciais, e a tela de login ganhou ação dedicada para passkey. O dashboard ganhou widget de Anotações e atalhos rápidos na toolbar de widgets pra criar Receita, Despesa ou Anotação direto. Top Estabelecimentos foi unificado num único widget com abas, e o widget "Lançamentos recentes" foi substituído por "Progresso de metas" com lista de orçamentos do período (gasto, limite e percentual de uso por categoria).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Suporte completo a Passkeys (WebAuthn) com plugin `@better-auth/passkey` no servidor e `passkeyClient` no cliente de autenticação
|
- Suporte completo a Passkeys (WebAuthn) com plugin `@better-auth/passkey` no servidor e `passkeyClient` no cliente de autenticação
|
||||||
@@ -510,6 +678,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.5] - 2026-02-28
|
## [1.7.5] - 2026-02-28
|
||||||
|
|
||||||
|
Versão pequena de polimento: ações para excluir item individual (processado/descartado) e limpar itens em lote por status na inbox de pré-lançamentos, redesign dos cards e diálogos dos widgets de boletos e faturas com indicação "Atrasado / Pagar" quando vencidos e não pagos, e migração da página de categorias de cards pra layout em tabela com link direto para detalhe e ações inline.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Inbox de pré-lançamentos: ações para excluir item individual (processado/descartado) e limpar itens em lote por status
|
- Inbox de pré-lançamentos: ações para excluir item individual (processado/descartado) e limpar itens em lote por status
|
||||||
@@ -528,6 +698,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.4] - 2026-02-28
|
## [1.7.4] - 2026-02-28
|
||||||
|
|
||||||
|
Versão de polimento de responsividade no mobile: 26 componentes ajustados (navbar, filtros, skeletons, widgets, dialogs), card de análise de parcelas empilhado verticalmente em telas pequenas e cards do top estabelecimentos reorganizados em coluna única no mobile. Inclui também regra mais inteligente em "Remover selecionados" — quando todos os itens pertencem à mesma série, abre dialog de escopo com 3 opções; e ajuste no consumo de limite por despesa recorrente no cartão (só consome quando a data já passou).
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Card de análise de parcelas (`/dashboard/analise-parcelas`): layout empilhado no mobile — nome/cartão e valores Total/Pendente em linhas separadas ao invés de lado-a-lado, evitando truncamento
|
- Card de análise de parcelas (`/dashboard/analise-parcelas`): layout empilhado no mobile — nome/cartão e valores Total/Pendente em linhas separadas ao invés de lado-a-lado, evitando truncamento
|
||||||
@@ -539,6 +711,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.3] - 2026-02-27
|
## [1.7.3] - 2026-02-27
|
||||||
|
|
||||||
|
Versão pequena com nova prop `compact` no DatePicker (formato abreviado "28 fev", sem "de" e sem ano) e modal de múltiplos lançamentos reformulado: selects de conta e cartão separados por forma de pagamento, InlinePeriodPicker ao escolher cartão de crédito e DatePicker compacto.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Prop `compact` no DatePicker para formato abreviado "28 fev" (sem "de" e sem ano)
|
- Prop `compact` no DatePicker para formato abreviado "28 fev" (sem "de" e sem ano)
|
||||||
@@ -550,6 +724,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.2] - 2026-02-26
|
## [1.7.2] - 2026-02-26
|
||||||
|
|
||||||
|
Versão de polimento dos diálogos: padding maior (p-10), largura padronizada em `max-w-xl` e botões do footer com largura igual; o lançamento dialog ganhou seção colapsável "Condições e anotações" e cálculo automático do período da fatura via `deriveCreditCardPeriod()`. Inclui também uma faxina de tipos (non-null assertions removidas, `any` substituído por tipos explícitos em 15+ arquivos) e remoção de 6 componentes e 20+ funções/tipos sem uso.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Dialogs padronizados: padding maior (p-10), largura max-w-xl, botões do footer com largura igual (flex-1)
|
- Dialogs padronizados: padding maior (p-10), largura max-w-xl, botões do footer com largura igual (flex-1)
|
||||||
@@ -573,6 +749,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.7.1] - 2026-02-24
|
## [1.7.1] - 2026-02-24
|
||||||
|
|
||||||
|
Esta versão substitui o header lateral por uma topbar de navegação com backdrop blur e links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas), expande o sino de notificações pra exibir orçamentos estourados e pré-lançamentos pendentes em seções separadas, e cria página dedicada de changelog em `/changelog` (acessível pelo menu do usuário com a versão atual exibida ao lado).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Topbar de navegação substituindo o header fixo: backdrop blur, links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas)
|
- Topbar de navegação substituindo o header fixo: backdrop blur, links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas)
|
||||||
@@ -596,6 +774,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.6.3] - 2026-02-19
|
## [1.6.3] - 2026-02-19
|
||||||
|
|
||||||
|
Correção pontual: variável `RESEND_FROM_EMAIL` não era lida corretamente do `.env` quando o valor continha espaços (precisa estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- E-mail Resend: variável `RESEND_FROM_EMAIL` não era lida do `.env` (valores com espaço precisam estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions
|
- E-mail Resend: variável `RESEND_FROM_EMAIL` não era lida do `.env` (valores com espaço precisam estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions
|
||||||
@@ -607,12 +787,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.6.2] - 2026-02-19
|
## [1.6.2] - 2026-02-19
|
||||||
|
|
||||||
|
Correção pontual no mobile: ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente. Adicionado `stopPropagation` nos eventos de click/touch dos botões e delay com `requestAnimationFrame` antes de fechar o seletor.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Bug no mobile onde, ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente: adicionado `stopPropagation` nos eventos de click/touch dos botões de logo e delay com `requestAnimationFrame` antes de fechar o seletor de logo
|
- Bug no mobile onde, ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente: adicionado `stopPropagation` nos eventos de click/touch dos botões de logo e delay com `requestAnimationFrame` antes de fechar o seletor de logo
|
||||||
|
|
||||||
## [1.6.1] - 2026-02-18
|
## [1.6.1] - 2026-02-18
|
||||||
|
|
||||||
|
Versão pequena: nome do estabelecimento padronizado para transferências entre contas ("Saída - Transf. entre contas" e "Entrada - Transf. entre contas") com anotação no formato "de {origem} -> {destino}", e correção de avisos `width(-1) and height(-1)` do `ChartContainer` no console.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Transferências entre contas: nome do estabelecimento passa a ser "Saída - Transf. entre contas" na saída e "Entrada - Transf. entre contas" na entrada e adicionando em anotação no formato "de {conta origem} -> {conta destino}"
|
- Transferências entre contas: nome do estabelecimento passa a ser "Saída - Transf. entre contas" na saída e "Entrada - Transf. entre contas" na entrada e adicionando em anotação no formato "de {conta origem} -> {conta destino}"
|
||||||
@@ -620,6 +804,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.6.0] - 2026-02-18
|
## [1.6.0] - 2026-02-18
|
||||||
|
|
||||||
|
Versão de personalização da tabela de lançamentos. Duas novas preferências em Ajustes > Extrato e lançamentos: "Anotações em coluna" (controla se a anotação aparece como coluna ou tooltip no ícone) e "Ordem das colunas" (lista ordenável por arrasto pra reordenar Estabelecimento, Transação, Valor etc.). Inclui ajustes mobile no header do dashboard (fixo só no mobile) e na rolagem horizontal de tabs e botões de ação.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Preferência "Anotações em coluna" em Ajustes > Extrato e lançamentos: quando ativa, a anotação dos lançamentos aparece em coluna na tabela; quando inativa, permanece no balão (tooltip) no ícone
|
- Preferência "Anotações em coluna" em Ajustes > Extrato e lançamentos: quando ativa, a anotação dos lançamentos aparece em coluna na tabela; quando inativa, permanece no balão (tooltip) no ícone
|
||||||
@@ -640,6 +826,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.5.3] - 2026-02-21
|
## [1.5.3] - 2026-02-21
|
||||||
|
|
||||||
|
Versão focada no painel do pagador (novo card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status), além de SEO completo na landing page (Open Graph, Twitter Card, JSON-LD Schema.org, sitemap.xml e robots.txt) e layout específico com metadados ricos. Imagens da landing convertidas de PNG para WebP para melhor performance.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Painel do pagador: card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status
|
- Painel do pagador: card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status
|
||||||
@@ -661,6 +849,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.5.2] - 2026-02-16
|
## [1.5.2] - 2026-02-16
|
||||||
|
|
||||||
|
Reforma visual da landing page: hero com gradient sutil e tipografia responsiva, dashboard preview sem bordas pra visual mais limpo, seção "Funcionalidades" reorganizada em 6 cards principais + 6 extras compactos, seção "Como usar" com tabs Docker (Recomendado) vs Manual e footer simplificado em 3 colunas. Inclui menu hamburger mobile com Sheet drawer, animações fade-in via Intersection Observer e seção dedicada ao OpenMonetis Companion com screenshots e fluxo de captura.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Landing page reformulada: visual modernizado, melhor experiência mobile e novas seções
|
- Landing page reformulada: visual modernizado, melhor experiência mobile e novas seções
|
||||||
@@ -683,6 +873,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.5.1] - 2026-02-16
|
## [1.5.1] - 2026-02-16
|
||||||
|
|
||||||
|
Esta versão renomeia o projeto de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos: package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page). URLs do repositório atualizados de `opensheets-app` para `openmonetis`, image Docker renomeada para `felipegcoutinho/openmonetis` e logo textual atualizado. Inclui também suporte a multi-domínio via `PUBLIC_DOMAIN` (domínio público serve apenas a landing page, com middleware bloqueando rotas do app).
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Projeto renomeado de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos): package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page
|
- Projeto renomeado de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos): package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page
|
||||||
@@ -697,6 +889,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.5.0] - 2026-02-15
|
## [1.5.0] - 2026-02-15
|
||||||
|
|
||||||
|
Versão de personalização tipográfica: 13 fontes disponíveis (incluindo SF Pro Display, SF Pro Rounded, Inter, Geist Sans, Roboto, Reddit Sans, JetBrains Mono e outras) configuráveis por usuário tanto pra interface quanto pros valores monetários, com FontProvider que aplica a troca instantaneamente via CSS variables sem necessidade de reload. Fontes Apple SF Pro carregadas localmente com 4 pesos (Regular, Medium, Semibold, Bold) e novas colunas `system_font` e `money_font` na tabela `preferencias_usuario`.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário
|
- Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário
|
||||||
@@ -716,6 +910,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.4.1] - 2026-02-15
|
## [1.4.1] - 2026-02-15
|
||||||
|
|
||||||
|
Versão focada na inbox de pré-lançamentos: novas abas "Pendentes", "Processados" e "Descartados" (antes só pendentes), logo do cartão/conta exibido automaticamente nos cards via matching por nome do app, pre-fill automático do cartão de crédito ao processar e badges de status com data nos itens já processados/descartados em modo readonly. Cor `--warning` ajustada para melhor contraste (mais alaranjada).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes)
|
- Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes)
|
||||||
@@ -737,6 +933,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.4.0] - 2026-02-07
|
## [1.4.0] - 2026-02-07
|
||||||
|
|
||||||
|
Reforma do design system: ~60+ componentes migrados de cores hardcoded do Tailwind (`green-500`, `red-600`, `amber-500`, `blue-500` etc.) pra tokens semânticos (`success`, `destructive`, `warning`, `info`); adicionados novos tokens `--success`, `--warning`, `--info` (com foregrounds) tanto em light quanto dark mode, novas variantes `success` e `info` no Badge, e cores de chart estendidas de 6 para 10. Inclui também correção do bug de invalidação de cache do dashboard que impedia widgets de boleto/fatura de atualizar após pagamento, e fix de scroll em listas Popover+Command (estabelecimento, categorias, filtros) com a prop `modal`.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Widgets de boleto/fatura não atualizavam após pagamento: actions de fatura (`updateInvoicePaymentStatusAction`, `updatePaymentDateAction`) e antecipação de parcelas não invalidavam o cache do dashboard
|
- Widgets de boleto/fatura não atualizavam após pagamento: actions de fatura (`updateInvoicePaymentStatusAction`, `updatePaymentDateAction`) e antecipação de parcelas não invalidavam o cache do dashboard
|
||||||
@@ -767,6 +965,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.3.1] - 2026-02-06
|
## [1.3.1] - 2026-02-06
|
||||||
|
|
||||||
|
Versão pequena: calculadora arrastável via drag handle no header do dialog, callback `onSelectValue` pra inserir valor diretamente no campo de lançamento, e nova aba "Changelog" em Ajustes com histórico parseado do `CHANGELOG.md`. As páginas de itens ativos e arquivados em Cartões, Contas e Anotações foram unificadas com sistema de tabs (mesmo padrão de Categorias), eliminando rotas separadas e nomenclatura inconsistente.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Calculadora arrastável via drag handle no header do dialog
|
- Calculadora arrastável via drag handle no header do dialog
|
||||||
@@ -782,6 +982,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.3.0] - 2026-02-06
|
## [1.3.0] - 2026-02-06
|
||||||
|
|
||||||
|
Versão de performance no dashboard: indexes compostos em `lancamentos`, cache cross-request via `unstable_cache` com tag `"dashboard"` e TTL de 120s, e invalidação automática em mutations financeiras via `revalidateTag`. Eliminados ~20 JOINs com a tabela `pagadores` (substituídos por filtro direto via `pagadorId`) e queries consolidadas (income-expense-balance: 12→1 com GROUP BY; payment-status: 2→1; expenses/income por categoria: 4→2). Auth session deduplicada por request via `React.cache()` e scan de métricas limitado a 24 meses.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Indexes compostos em `lancamentos`: `(userId, period, transactionType)` e `(pagadorId, period)`
|
- Indexes compostos em `lancamentos`: `(userId, period, transactionType)` e `(pagadorId, period)`
|
||||||
@@ -802,6 +1004,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.2.6] - 2025-02-04
|
## [1.2.6] - 2025-02-04
|
||||||
|
|
||||||
|
Versão de adaptação ao React 19 compiler: removidos ~60 `useCallback`/`useMemo` desnecessários, wrappers `React.memo` redundantes e simplificação de padrões de hidratação com `useSyncExternalStore`. Sem mudanças visíveis ao usuário — só faxina interna alinhada às novas otimizações automáticas do compilador.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Refatoração para otimização do React 19 compiler
|
- Refatoração para otimização do React 19 compiler
|
||||||
@@ -830,6 +1034,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.2.5] - 2025-02-01
|
## [1.2.5] - 2025-02-01
|
||||||
|
|
||||||
|
Versão pequena: novo widget de pagadores no dashboard com avatares atualizados.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Widget de pagadores no dashboard
|
- Widget de pagadores no dashboard
|
||||||
@@ -837,6 +1043,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.2.4] - 2025-01-22
|
## [1.2.4] - 2025-01-22
|
||||||
|
|
||||||
|
Correção pontual: preservação de formatação nas anotações e ajuste no layout do card de anotações.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Preservar formatação nas anotações
|
- Preservar formatação nas anotações
|
||||||
@@ -844,6 +1052,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.2.3] - 2025-01-22
|
## [1.2.3] - 2025-01-22
|
||||||
|
|
||||||
|
Versão pequena: versão do app passa a aparecer na sidebar e atualização da documentação.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Versão exibida na sidebar
|
- Versão exibida na sidebar
|
||||||
@@ -851,6 +1061,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do
|
|||||||
|
|
||||||
## [1.2.2] - 2025-01-22
|
## [1.2.2] - 2025-01-22
|
||||||
|
|
||||||
|
Versão de manutenção: atualização de dependências e formatação aplicada em todo o código.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Atualização de dependências
|
- Atualização de dependências
|
||||||
|
|||||||
57
CLAUDE.md
@@ -97,7 +97,7 @@ src/
|
|||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ ├── globals.css
|
│ ├── globals.css
|
||||||
│ └── layout.tsx
|
│ └── layout.tsx
|
||||||
├── features/
|
├── features/ # cada feature segue: actions.ts, queries.ts, actions/, components/, hooks/, lib/
|
||||||
│ ├── auth/
|
│ ├── auth/
|
||||||
│ ├── landing/
|
│ ├── landing/
|
||||||
│ ├── dashboard/
|
│ ├── dashboard/
|
||||||
@@ -117,9 +117,12 @@ src/
|
|||||||
│ └── settings/
|
│ └── settings/
|
||||||
├── shared/
|
├── shared/
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ui/
|
│ │ ├── ui/ # shadcn/ui primitives
|
||||||
│ │ ├── navigation/
|
│ │ ├── navigation/ # navbar, sidebar, breadcrumbs
|
||||||
│ │ ├── providers/
|
│ │ ├── 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/
|
│ │ ├── month-picker/
|
||||||
│ │ ├── logo-picker/
|
│ │ ├── logo-picker/
|
||||||
│ │ ├── calculator/
|
│ │ ├── calculator/
|
||||||
@@ -134,34 +137,56 @@ src/
|
|||||||
│ │ ├── calculator/
|
│ │ ├── calculator/
|
||||||
│ │ ├── categories/
|
│ │ ├── categories/
|
||||||
│ │ ├── email/
|
│ │ ├── email/
|
||||||
|
│ │ ├── import/
|
||||||
│ │ ├── installments/
|
│ │ ├── installments/
|
||||||
│ │ ├── invoices/
|
│ │ ├── invoices/
|
||||||
│ │ ├── logo/
|
│ │ ├── logo/
|
||||||
|
│ │ ├── notifications/
|
||||||
│ │ ├── payers/
|
│ │ ├── payers/
|
||||||
│ │ ├── schemas/
|
│ │ ├── schemas/
|
||||||
|
│ │ ├── storage/
|
||||||
│ │ ├── transfers/
|
│ │ ├── transfers/
|
||||||
│ │ ├── types/
|
│ │ ├── types/
|
||||||
|
│ │ ├── version/
|
||||||
│ │ └── db.ts
|
│ │ └── db.ts
|
||||||
│ └── utils/
|
│ └── utils/
|
||||||
│ ├── period/
|
│ ├── period/
|
||||||
|
│ ├── calculator.ts
|
||||||
|
│ ├── calendar.ts
|
||||||
|
│ ├── category-colors.ts
|
||||||
│ ├── currency.ts
|
│ ├── currency.ts
|
||||||
│ ├── date.ts
|
│ ├── date.ts
|
||||||
|
│ ├── export-branding.ts
|
||||||
|
│ ├── fetch-json.ts
|
||||||
│ ├── financial-dates.ts
|
│ ├── financial-dates.ts
|
||||||
│ ├── percentage.ts
|
│ ├── icons.tsx
|
||||||
│ ├── category-colors.ts
|
│ ├── id.ts
|
||||||
│ ├── calendar.ts
|
│ ├── initials.ts
|
||||||
│ ├── math.ts
|
│ ├── math.ts
|
||||||
│ ├── number.ts
|
│ ├── number.ts
|
||||||
|
│ ├── percentage.ts
|
||||||
│ ├── string.ts
|
│ ├── string.ts
|
||||||
│ ├── initials.ts
|
│ └── ui.ts
|
||||||
│ ├── icons.tsx
|
|
||||||
│ ├── export-branding.ts
|
|
||||||
│ ├── ui.ts
|
|
||||||
│ └── calculator.ts
|
|
||||||
└── db/
|
└── db/
|
||||||
└── schema.ts
|
└── 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
|
## Import Patterns
|
||||||
@@ -299,9 +324,11 @@ export async function fetchData(userId: string, period: string) {
|
|||||||
2. Criar a feature em `src/features/<feature>/`
|
2. Criar a feature em `src/features/<feature>/`
|
||||||
3. Separar:
|
3. Separar:
|
||||||
- `components/`
|
- `components/`
|
||||||
- `queries.ts`
|
- `queries.ts` (entry point de leitura)
|
||||||
- `actions.ts`
|
- `actions.ts` (entry point de Server Actions; vira barrel quando crescer e migrar para `actions/`)
|
||||||
- `types.ts` ou `schemas.ts` quando fizer sentido
|
- `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
|
4. Extrair para `src/shared/` tudo que for reutilizavel
|
||||||
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
|
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
|
||||||
6. Rodar:
|
6. Rodar:
|
||||||
|
|||||||
389
DESIGN.md
Normal 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:**
|
||||||
|
- **4–8px:** Tight spacing within compact components (icon-text pairs, inline elements)
|
||||||
|
- **12–16px:** Standard padding inside cards, inputs, and buttons
|
||||||
|
- **24–32px:** Section gaps, spacing between components on a page
|
||||||
|
- **48–64px:** Large section separations, hero spacing
|
||||||
|
- **80–128px:** 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.06–0.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 (`24px–48px`) around sections and inside cards for breathing room
|
||||||
|
- Stack elements vertically with `24–32px` 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 | `375px–599px` | Single column; container padding `16px`; font sizes reduce 1–2 sizes; gap scale halved |
|
||||||
|
| Tablet | `600px–1023px` | 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 25–33% 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
|
||||||
64
README.md
@@ -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.
|
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
|
||||||
|
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
[](https://nextjs.org/)
|
[](https://nextjs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
@@ -20,11 +20,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./public/images/dashboard-preview-light.webp" alt="Dashboard Preview" width="800" />
|
<img src="./public/images/dashboard-preview-light.png" alt="Dashboard Preview" width="800" />
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="./public/images/preview-lancamentos-light.webp" alt="Lançamentos" width="800" />
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -65,7 +61,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
|
|||||||
|
|
||||||
### Funcionalidades
|
### Funcionalidades
|
||||||
|
|
||||||
💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas e transferências. Categorização, extratos detalhados e importação de extratos OFX e XLS/XLSX com detecção automática de categoria.
|
💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas e transferências. Categorização, filtros combináveis, extratos detalhados e importação de extratos OFX e XLS/XLSX com detecção automática de categoria.
|
||||||
|
|
||||||
📊 **Dashboard e relatórios** — Widgets interativos de métricas, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
|
📊 **Dashboard e relatórios** — Widgets interativos de métricas, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
|
||||||
|
|
||||||
@@ -89,7 +85,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
|
|||||||
<img src="./public/images/companion-preview-light.webp" alt="OpenMonetis Companion" width="300" height="600" />
|
<img src="./public/images/companion-preview-light.webp" alt="OpenMonetis Companion" width="300" height="600" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
⚙️ **Personalização** — Tema dark/light e modo privacidade.
|
⚙️ **Personalização** — Tema dark/light, modo privacidade e changelog visual para acompanhar as novidades do app.
|
||||||
|
|
||||||
### Stack técnica
|
### Stack técnica
|
||||||
|
|
||||||
@@ -131,21 +127,20 @@ Só quer rodar o OpenMonetis. **Não precisa clonar o repositório nem instalar
|
|||||||
# 1. Baixe o compose
|
# 1. Baixe o compose
|
||||||
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
|
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
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Acesse em: `http://localhost:3000`
|
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
|
Mais sobre .env em [Variáveis de Ambiente](#-variáveis-de-ambiente).
|
||||||
# .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).
|
|
||||||
|
|
||||||
**Banco remoto (Supabase, Neon, Railway...):** defina `DATABASE_URL` no `.env` e suba só o app:
|
**Banco remoto (Supabase, Neon, Railway...):** defina `DATABASE_URL` no `.env` e suba só o app:
|
||||||
|
|
||||||
@@ -512,7 +507,18 @@ openmonetis/
|
|||||||
│ │ └── auth/ # Formulários de autenticação
|
│ │ └── auth/ # Formulários de autenticação
|
||||||
│ │
|
│ │
|
||||||
│ ├── shared/ # Código reutilizado entre features
|
│ ├── 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
|
│ │ ├── hooks/ # React hooks globais
|
||||||
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
|
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
|
||||||
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
|
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
|
||||||
@@ -528,6 +534,22 @@ openmonetis/
|
|||||||
└── proxy.ts # Middleware (auth + multi-domínio)
|
└── 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
|
## 🤝 Contribuindo
|
||||||
@@ -567,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)
|
**Desenvolvido por:** Felipe Coutinho — [@felipegcoutinho](https://github.com/felipegcoutinho)
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|||||||
@@ -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": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
3
drizzle/0029_friendly_spitfire.sql
Normal 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;
|
||||||
2916
drizzle/meta/0029_snapshot.json
Normal file
@@ -197,6 +197,13 @@
|
|||||||
"when": 1777153372633,
|
"when": 1777153372633,
|
||||||
"tag": "0028_fancy_reaper",
|
"tag": "0028_fancy_reaper",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777648189399,
|
||||||
|
"tag": "0029_friendly_spitfire",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
],
|
],
|
||||||
// PostCSS is inferred from the config file, but the project only depends on
|
// PostCSS is inferred from the config file, but the project only depends on
|
||||||
// the Tailwind PostCSS plugin directly.
|
// the Tailwind PostCSS plugin directly.
|
||||||
|
// `server-only` is provided implicitly by Next.js — no install needed.
|
||||||
"ignoreDependencies": [
|
"ignoreDependencies": [
|
||||||
"postcss"
|
"postcss",
|
||||||
|
"server-only"
|
||||||
],
|
],
|
||||||
"next": true,
|
"next": true,
|
||||||
"postcss": true,
|
"postcss": true,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const nextConfig: NextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
prefetchInlining: true,
|
prefetchInlining: true,
|
||||||
turbopackFileSystemCacheForDev: true,
|
turbopackFileSystemCacheForDev: true,
|
||||||
|
optimizePackageImports: ["@remixicon/react"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Headers for Safari compatibility
|
// Headers for Safari compatibility
|
||||||
|
|||||||
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openmonetis",
|
"name": "openmonetis",
|
||||||
"version": "2.4.4",
|
"version": "2.5.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.33.0",
|
"packageManager": "pnpm@10.33.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -31,16 +31,16 @@
|
|||||||
"mockup": "tsx scripts/mock-data.ts"
|
"mockup": "tsx scripts/mock-data.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.71",
|
"@ai-sdk/anthropic": "^3.0.74",
|
||||||
"@ai-sdk/google": "^3.0.64",
|
"@ai-sdk/google": "^3.0.67",
|
||||||
"@ai-sdk/openai": "^3.0.53",
|
"@ai-sdk/openai": "^3.0.60",
|
||||||
"@aws-sdk/client-s3": "^3.1037.0",
|
"@aws-sdk/client-s3": "^3.1042.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1037.0",
|
"@aws-sdk/s3-request-presigner": "^3.1042.0",
|
||||||
"@better-auth/passkey": "^1.6.9",
|
"@better-auth/passkey": "^1.6.9",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@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-alert-dialog": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
@@ -63,10 +63,10 @@
|
|||||||
"@radix-ui/react-toggle-group": "1.1.11",
|
"@radix-ui/react-toggle-group": "1.1.11",
|
||||||
"@radix-ui/react-tooltip": "1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@remixicon/react": "4.9.0",
|
"@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-table": "8.21.3",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"ai": "^6.0.168",
|
"ai": "^6.0.175",
|
||||||
"better-auth": "1.6.9",
|
"better-auth": "1.6.9",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pdfjs-dist": "^5.6.205",
|
"pdfjs-dist": "^5.7.284",
|
||||||
"pg": "8.20.0",
|
"pg": "8.20.0",
|
||||||
"react": "19.2.5",
|
"react": "19.2.5",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
@@ -88,8 +88,9 @@
|
|||||||
"resend": "^6.12.2",
|
"resend": "^6.12.2",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.5.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"zod": "4.3.6"
|
"zod": "4.4.3"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.13",
|
"@biomejs/biome": "2.4.14",
|
||||||
"@tailwindcss/postcss": "4.2.4",
|
"@tailwindcss/postcss": "4.2.4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "25.6.0",
|
"@types/node": "25.6.0",
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.10",
|
||||||
"knip": "^6.7.0",
|
"knip": "^6.11.0",
|
||||||
"tailwindcss": "4.2.4",
|
"tailwindcss": "4.2.4",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "6.0.3"
|
"typescript": "6.0.3"
|
||||||
|
|||||||
906
pnpm-lock.yaml
generated
BIN
public/images/dashboard-preview-dark.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 183 KiB |
BIN
public/images/dashboard-preview-light.png
Normal file
|
After Width: | Height: | Size: 562 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 176 KiB |
BIN
public/logos/dinheiro.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
1089
scripts/mock-data.ts
@@ -1,4 +1,4 @@
|
|||||||
import { Logo } from "@/shared/components/logo";
|
import { Logo } from "@/shared/components/brand/logo";
|
||||||
|
|
||||||
export default function AuthLayout({
|
export default function AuthLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { notFound } from "next/navigation";
|
|||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import { AccountDialog } from "@/features/accounts/components/account-dialog";
|
import { AccountDialog } from "@/features/accounts/components/account-dialog";
|
||||||
import { AccountStatementCard } from "@/features/accounts/components/account-statement-card";
|
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 type { Account } from "@/features/accounts/components/types";
|
||||||
import {
|
import {
|
||||||
fetchAccountData,
|
fetchAccountData,
|
||||||
fetchAccountLancamentosPage,
|
|
||||||
fetchAccountSummary,
|
fetchAccountSummary,
|
||||||
|
fetchAccountTransactionsPage,
|
||||||
} from "@/features/accounts/statement-queries";
|
} from "@/features/accounts/statement-queries";
|
||||||
import { fetchUserPreferences } from "@/features/settings/queries";
|
import { fetchUserPreferences } from "@/features/settings/queries";
|
||||||
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
resolveTransactionPagination,
|
resolveTransactionPagination,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
@@ -88,7 +89,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const transactionsPage = await fetchAccountLancamentosPage(
|
const transactionsPage = await fetchAccountTransactionsPage(
|
||||||
filters,
|
filters,
|
||||||
pagination,
|
pagination,
|
||||||
);
|
);
|
||||||
@@ -141,6 +142,13 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
totalIncomes={totalIncomes}
|
totalIncomes={totalIncomes}
|
||||||
totalExpenses={totalExpenses}
|
totalExpenses={totalExpenses}
|
||||||
logo={account.logo}
|
logo={account.logo}
|
||||||
|
balanceAdjustment={
|
||||||
|
<AdjustBalanceDialog
|
||||||
|
accountId={account.id}
|
||||||
|
period={selectedPeriod}
|
||||||
|
currentBalance={currentBalance}
|
||||||
|
/>
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
<AccountDialog
|
<AccountDialog
|
||||||
mode="update"
|
mode="update"
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default function RootLayout({
|
|||||||
subtitle="Gerencie os anexos das suas transações"
|
subtitle="Gerencie os anexos das suas transações"
|
||||||
/>
|
/>
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||||
|
|
||||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||||
|
|
||||||
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
|
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
|
||||||
userId,
|
userId,
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { fetchCalendarData } from "@/features/calendar/queries";
|
|||||||
import {
|
import {
|
||||||
getSingleParam,
|
getSingleParam,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
import type { CalendarPeriod } from "@/shared/lib/types/calendar";
|
import type { CalendarPeriod } from "@/shared/lib/types/calendar";
|
||||||
@@ -36,7 +36,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-3">
|
<main className="flex flex-col gap-4">
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
<MonthlyCalendar
|
<MonthlyCalendar
|
||||||
period={calendarPeriod}
|
period={calendarPeriod}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
getSingleParam,
|
getSingleParam,
|
||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
@@ -118,6 +118,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
financialAccount.id === card.accountId,
|
financialAccount.id === card.accountId,
|
||||||
)?.name ?? "Conta";
|
)?.name ?? "Conta";
|
||||||
|
|
||||||
|
const limitAmount = Number(card.limit);
|
||||||
|
|
||||||
const cardDialogData: Card = {
|
const cardDialogData: Card = {
|
||||||
id: card.id,
|
id: card.id,
|
||||||
name: card.name,
|
name: card.name,
|
||||||
@@ -127,19 +129,14 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
dueDay: card.dueDay,
|
dueDay: card.dueDay,
|
||||||
note: card.note ?? null,
|
note: card.note ?? null,
|
||||||
logo: card.logo,
|
logo: card.logo,
|
||||||
limit:
|
limit: limitAmount,
|
||||||
card.limit !== null && card.limit !== undefined
|
|
||||||
? Number(card.limit)
|
|
||||||
: null,
|
|
||||||
accountId: card.accountId,
|
accountId: card.accountId,
|
||||||
accountName,
|
accountName,
|
||||||
limitInUse: 0,
|
limitInUse: 0,
|
||||||
limitAvailable: null,
|
limitAvailable: limitAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
|
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(
|
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
|
||||||
1,
|
1,
|
||||||
@@ -163,6 +160,12 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
limitAmount={limitAmount}
|
limitAmount={limitAmount}
|
||||||
invoiceStatus={invoiceStatus}
|
invoiceStatus={invoiceStatus}
|
||||||
paymentDate={paymentDate}
|
paymentDate={paymentDate}
|
||||||
|
defaultPaymentAccountId={card.accountId}
|
||||||
|
paymentAccountOptions={accountOptions.map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
logo: option.logo ?? null,
|
||||||
|
}))}
|
||||||
logo={card.logo}
|
logo={card.logo}
|
||||||
actions={
|
actions={
|
||||||
<CardDialog
|
<CardDialog
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default async function Page() {
|
|||||||
await fetchAllCardsForUser(userId);
|
await fetchAllCardsForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<CardsPage
|
<CardsPage
|
||||||
cards={activeCards}
|
cards={activeCards}
|
||||||
archivedCards={archivedCards}
|
archivedCards={archivedCards}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TransactionsPage } from "@/features/transactions/components/page/transa
|
|||||||
import {
|
import {
|
||||||
buildOptionSets,
|
buildOptionSets,
|
||||||
buildSluggedFilters,
|
buildSluggedFilters,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function Page() {
|
|||||||
const categories = await fetchCategoriesForUser(userId);
|
const categories = await fetchCategoriesForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<CategoriesPage categories={categories} />
|
<CategoriesPage categories={categories} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { connection } from "next/server";
|
|||||||
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
|
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
|
||||||
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
|
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
|
||||||
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
|
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 { 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 { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
|
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<InboxPage
|
<InboxPage
|
||||||
activeStatus={activeStatus}
|
activeStatus={activeStatus}
|
||||||
activeApp={activeApp}
|
activeApp={activeApp}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { connection } from "next/server";
|
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 { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
||||||
import { LogoDevProvider } from "@/shared/components/providers/logo-dev-provider";
|
import { LogoDevProvider } from "@/shared/components/providers/logo-dev-provider";
|
||||||
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
||||||
@@ -21,8 +21,8 @@ export default async function DashboardLayout({
|
|||||||
<PrivacyProvider>
|
<PrivacyProvider>
|
||||||
<AppNavbar
|
<AppNavbar
|
||||||
user={{ ...session.user, image: session.user.image ?? null }}
|
user={{ ...session.user, image: session.user.image ?? null }}
|
||||||
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
|
payerAvatarUrl={navbarData.payerAvatarUrl}
|
||||||
preLancamentosCount={navbarData.preLancamentosCount}
|
inboxPendingCount={navbarData.inboxPendingCount}
|
||||||
notificationsSnapshot={navbarData.notificationsSnapshot}
|
notificationsSnapshot={navbarData.notificationsSnapshot}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex flex-1 flex-col pt-16">
|
<div className="relative flex flex-1 flex-col pt-16">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function Page() {
|
|||||||
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
|
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
|
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
|
|||||||
* Loading state para a página de detalhes do pagador.
|
* Loading state para a página de detalhes do pagador.
|
||||||
* Layout: navegação mensal + tabs com card compartilhado do pagador.
|
* Layout: navegação mensal + tabs com card compartilhado do pagador.
|
||||||
*/
|
*/
|
||||||
export default function PagadorDetailsLoading() {
|
export default function PayerDetailsLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<div className="h-[60px] animate-pulse rounded-md bg-foreground/10" />
|
<div className="h-[60px] animate-pulse rounded-md bg-foreground/10" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { connection } from "next/server";
|
|||||||
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
|
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
|
||||||
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
|
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
|
||||||
import { PayerHistoryCard } from "@/features/payers/components/details/payer-history-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 { PayerLeaveShareCard } from "@/features/payers/components/details/payer-leave-share-card";
|
||||||
import { PayerMonthlySummaryCard } from "@/features/payers/components/details/payer-monthly-summary-card";
|
import { PayerMonthlySummaryCard } from "@/features/payers/components/details/payer-monthly-summary-card";
|
||||||
import {
|
import {
|
||||||
@@ -16,12 +16,12 @@ import {
|
|||||||
PayerPaymentStatusCard,
|
PayerPaymentStatusCard,
|
||||||
} from "@/features/payers/components/details/payer-payment-method-cards";
|
} from "@/features/payers/components/details/payer-payment-method-cards";
|
||||||
import { PayerSharingCard } from "@/features/payers/components/details/payer-sharing-card";
|
import { PayerSharingCard } from "@/features/payers/components/details/payer-sharing-card";
|
||||||
|
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
|
||||||
import {
|
import {
|
||||||
fetchCurrentUserShare,
|
fetchCurrentUserShare,
|
||||||
fetchPagadorLancamentos,
|
|
||||||
fetchPayerShares,
|
fetchPayerShares,
|
||||||
} from "@/features/payers/detail-queries";
|
fetchPayerTransactions,
|
||||||
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
|
} from "@/features/payers/lib/detail-queries";
|
||||||
import { fetchUserPreferences } from "@/features/settings/queries";
|
import { fetchUserPreferences } from "@/features/settings/queries";
|
||||||
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
||||||
import {
|
import {
|
||||||
@@ -36,13 +36,12 @@ import {
|
|||||||
type SluggedFilters,
|
type SluggedFilters,
|
||||||
type SlugMaps,
|
type SlugMaps,
|
||||||
type TransactionSearchFilters,
|
type TransactionSearchFilters,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
} from "@/features/transactions/queries";
|
} from "@/features/transactions/queries";
|
||||||
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
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 MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -50,16 +49,17 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/shared/components/ui/tabs";
|
} from "@/shared/components/ui/tabs";
|
||||||
|
import { ExpandableWidgetCard } from "@/shared/components/widgets/expandable-widget-card";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||||
import { getPayerAccess } from "@/shared/lib/payers/access";
|
import { getPayerAccess } from "@/shared/lib/payers/access";
|
||||||
import {
|
import {
|
||||||
fetchPagadorBoletoItems,
|
fetchPayerBoletoItems,
|
||||||
fetchPagadorBoletoStats,
|
fetchPayerBoletoStats,
|
||||||
fetchPagadorCardUsage,
|
fetchPayerCardUsage,
|
||||||
fetchPagadorPaymentStatus,
|
|
||||||
fetchPayerHistory,
|
fetchPayerHistory,
|
||||||
fetchPayerMonthlyBreakdown,
|
fetchPayerMonthlyBreakdown,
|
||||||
|
fetchPayerPaymentStatus,
|
||||||
type PayerCardUsageItem,
|
type PayerCardUsageItem,
|
||||||
} from "@/shared/lib/payers/details";
|
} from "@/shared/lib/payers/details";
|
||||||
import { parsePeriodParam } from "@/shared/utils/period";
|
import { parsePeriodParam } from "@/shared/utils/period";
|
||||||
@@ -76,11 +76,11 @@ const capitalize = (value: string) =>
|
|||||||
|
|
||||||
const EMPTY_FILTERS: TransactionSearchFilters = {
|
const EMPTY_FILTERS: TransactionSearchFilters = {
|
||||||
transactionFilter: null,
|
transactionFilter: null,
|
||||||
conditionFilter: null,
|
conditionFilters: [],
|
||||||
paymentFilter: null,
|
paymentFilters: [],
|
||||||
payerFilter: null,
|
payerFilters: [],
|
||||||
categoryFilter: null,
|
categoryFilters: [],
|
||||||
accountCardFilter: null,
|
accountCardFilters: [],
|
||||||
searchFilter: null,
|
searchFilter: null,
|
||||||
settledFilter: null,
|
settledFilter: null,
|
||||||
attachmentFilter: null,
|
attachmentFilter: null,
|
||||||
@@ -182,7 +182,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
estabelecimentos,
|
estabelecimentos,
|
||||||
userPreferences,
|
userPreferences,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchPagadorLancamentos(filters),
|
fetchPayerTransactions(filters),
|
||||||
fetchPayerMonthlyBreakdown({
|
fetchPayerMonthlyBreakdown({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
@@ -193,22 +193,22 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorCardUsage({
|
fetchPayerCardUsage({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorBoletoStats({
|
fetchPayerBoletoStats({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorBoletoItems({
|
fetchPayerBoletoItems({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorPaymentStatus({
|
fetchPayerPaymentStatus({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
@@ -333,7 +333,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TabsContent value="profile" className="space-y-4">
|
<TabsContent value="profile" className="space-y-4">
|
||||||
<PagadorInfoCard payer={payerData} />
|
<PayerInfoCard payer={payerData} />
|
||||||
{canEdit && payerData.shareCode ? (
|
{canEdit && payerData.shareCode ? (
|
||||||
<PayerSharingCard
|
<PayerSharingCard
|
||||||
payerId={pagador.id}
|
payerId={pagador.id}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
|
|||||||
* Loading state para a página de pessoas
|
* Loading state para a página de pessoas
|
||||||
* Layout: Header + Input de compartilhamento + Grid de cards
|
* Layout: Header + Input de compartilhamento + Grid de cards
|
||||||
*/
|
*/
|
||||||
export default function PagadoresLoading() {
|
export default function PayersLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col items-start gap-6">
|
||||||
<div className="w-full space-y-6">
|
<div className="w-full space-y-6">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function Page() {
|
|||||||
const { payers, avatarOptions } = await fetchPayersForUser(userId);
|
const { payers, avatarOptions } = await fetchPayersForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<PayersPage payers={payers} avatarOptions={avatarOptions} />
|
<PayersPage payers={payers} avatarOptions={avatarOptions} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { RiBankCard2Line } from "@remixicon/react";
|
import { RiBankCard2Line } from "@remixicon/react";
|
||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import { fetchCartoesReportData } from "@/features/reports/cards-report-queries";
|
|
||||||
import { CardCategoryBreakdown } from "@/features/reports/components/cards/card-category-breakdown";
|
import { CardCategoryBreakdown } from "@/features/reports/components/cards/card-category-breakdown";
|
||||||
import { CardInvoiceStatus } from "@/features/reports/components/cards/card-invoice-status";
|
import { CardInvoiceStatus } from "@/features/reports/components/cards/card-invoice-status";
|
||||||
import { CardTopExpenses } from "@/features/reports/components/cards/card-top-expenses";
|
import { CardTopExpenses } from "@/features/reports/components/cards/card-top-expenses";
|
||||||
import { CardUsageChart } from "@/features/reports/components/cards/card-usage-chart";
|
import { CardUsageChart } from "@/features/reports/components/cards/card-usage-chart";
|
||||||
import { CardsOverview } from "@/features/reports/components/cards/cards-overview";
|
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 MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import { Card } from "@/shared/components/ui/card";
|
import { Card } from "@/shared/components/ui/card";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import type { Category } from "@/db/schema";
|
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 { CategoryReportPage } from "@/features/reports/components/category-report-page";
|
||||||
import type {
|
import type {
|
||||||
CategoryOption,
|
CategoryOption,
|
||||||
FilterState,
|
FilterState,
|
||||||
} from "@/features/reports/components/types";
|
} 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 { getUserId } from "@/shared/lib/auth/server";
|
||||||
import type { CategoryReportFilters } from "@/shared/lib/types/reports";
|
import type { CategoryReportFilters } from "@/shared/lib/types/reports";
|
||||||
import { addMonthsToPeriod, getCurrentPeriod } from "@/shared/utils/period";
|
import { addMonthsToPeriod, getCurrentPeriod } from "@/shared/utils/period";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const validatePeriodFilter = (value: string | null): PeriodFilter => {
|
|||||||
return "6";
|
return "6";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function TopEstabelecimentosPage({
|
export default async function TopEstablishmentsPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PageProps) {
|
}: PageProps) {
|
||||||
await connection();
|
await connection();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default async function Page() {
|
|||||||
<TabsTrigger value="passkeys">Passkeys</TabsTrigger>
|
<TabsTrigger value="passkeys">Passkeys</TabsTrigger>
|
||||||
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
|
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
|
||||||
<TabsTrigger value="deletar" className="text-destructive">
|
<TabsTrigger value="deletar" className="text-destructive">
|
||||||
Deletar conta
|
Ações perigosas
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +190,6 @@ export default async function Page() {
|
|||||||
ou excluir sua conta inteira de forma irreversível.
|
ou excluir sua conta inteira de forma irreversível.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
|
||||||
<DeleteAccountForm />
|
<DeleteAccountForm />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ImportPage } from "@/features/transactions/components/import/import-pag
|
|||||||
import {
|
import {
|
||||||
buildOptionSets,
|
buildOptionSets,
|
||||||
buildSluggedFilters,
|
buildSluggedFilters,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import { fetchTransactionFilterSources } from "@/features/transactions/queries";
|
import { fetchTransactionFilterSources } from "@/features/transactions/queries";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
|
|||||||
* Loading state para a página de lançamentos
|
* Loading state para a página de lançamentos
|
||||||
* Mantém o mesmo layout da página final
|
* Mantém o mesmo layout da página final
|
||||||
*/
|
*/
|
||||||
export default function LancamentosLoading() {
|
export default function TransactionsLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
{/* Month Picker placeholder */}
|
{/* Month Picker placeholder */}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
resolveTransactionPagination,
|
resolveTransactionPagination,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
|
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
|
||||||
import { MobileNav } from "@/features/landing/components/mobile-nav";
|
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 { SetupTabs } from "@/features/landing/components/setup-tabs";
|
||||||
import {
|
import {
|
||||||
companionBanks,
|
companionBanks,
|
||||||
@@ -25,7 +24,7 @@ import {
|
|||||||
import { landingImages } from "@/features/landing/images";
|
import { landingImages } from "@/features/landing/images";
|
||||||
import { fetchGitHubStats } from "@/features/landing/queries";
|
import { fetchGitHubStats } from "@/features/landing/queries";
|
||||||
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
|
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 { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
@@ -51,19 +50,19 @@ export default async function Page() {
|
|||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<NavbarShell>
|
<NavbarShell>
|
||||||
{/* Center Navigation Links */}
|
{/* 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 }) => (
|
{navLinks.map(({ href, label }) => (
|
||||||
<a
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
href={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}
|
{label}
|
||||||
</a>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</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" />
|
<AnimatedThemeToggler variant="navbar" />
|
||||||
{!isPublicDomain &&
|
{!isPublicDomain &&
|
||||||
(session?.user ? (
|
(session?.user ? (
|
||||||
@@ -71,26 +70,27 @@ export default async function Page() {
|
|||||||
<Button
|
<Button
|
||||||
variant="navbar"
|
variant="navbar"
|
||||||
size="sm"
|
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
|
Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="hidden md:flex items-center gap-2">
|
<div className="hidden md:flex items-center gap-1">
|
||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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
|
Entrar
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
size="sm"
|
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
|
Começar
|
||||||
</Button>
|
</Button>
|
||||||
@@ -207,33 +207,8 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 */}
|
{/* Features Section */}
|
||||||
<section id="funcionalidades" className="py-12 md:py-24 bg-muted/40">
|
<section id="funcionalidades" className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
@@ -472,7 +447,7 @@ export default async function Page() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Tech Stack Section */}
|
{/* Tech Stack Section */}
|
||||||
<section id="stack" className="py-12 md:py-24 bg-muted/40">
|
<section id="stack" className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
@@ -560,7 +535,7 @@ export default async function Page() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Who is this for Section */}
|
{/* Who is this for Section */}
|
||||||
<section id="para-quem-e" className="py-12 md:py-24 bg-muted/40">
|
<section id="para-quem-e" className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
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";
|
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
const PRIVATE_RESPONSE_HEADERS = {
|
const PRIVATE_RESPONSE_HEADERS = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
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";
|
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
const PRIVATE_RESPONSE_HEADERS = {
|
const PRIVATE_RESPONSE_HEADERS = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(97.412% 0.00332 67.032);
|
--background: oklch(95.99% 0.00411 55.512);
|
||||||
--foreground: oklch(27% 0.008 45);
|
--foreground: oklch(27% 0.008 45);
|
||||||
--card: oklch(100% 0 0);
|
--card: oklch(100% 0 0);
|
||||||
--card-foreground: var(--foreground);
|
--card-foreground: var(--foreground);
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
--destructive: oklch(55% 0.22 27);
|
--destructive: oklch(55% 0.22 27);
|
||||||
--destructive-foreground: oklch(98% 0.005 30);
|
--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);
|
--input: var(--border);
|
||||||
--ring: var(--primary);
|
--ring: var(--primary);
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
--destructive: oklch(62% 0.2 28);
|
--destructive: oklch(62% 0.2 28);
|
||||||
--destructive-foreground: oklch(98% 0.005 30);
|
--destructive-foreground: oklch(98% 0.005 30);
|
||||||
|
|
||||||
--border: oklch(28% 0.0035 55);
|
--border: oklch(24.957% 0.00355 48.274);
|
||||||
--input: var(--border);
|
--input: var(--border);
|
||||||
--ring: var(--primary);
|
--ring: var(--primary);
|
||||||
|
|
||||||
@@ -269,54 +270,6 @@
|
|||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes dialog-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dialog-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes overlay-in {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes overlay-out {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="dialog-overlay"][data-state="open"] {
|
|
||||||
animation: overlay-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="dialog-overlay"][data-state="closed"] {
|
|
||||||
animation: overlay-out 0.15s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="dialog-content"][data-state="open"] {
|
|
||||||
animation: dialog-in 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="dialog-content"][data-state="closed"] {
|
|
||||||
animation: dialog-out 0.15s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink-in {
|
@keyframes blink-in {
|
||||||
0%, 40% { opacity: 1; }
|
0%, 40% { opacity: 1; }
|
||||||
50%, 90% { opacity: 0; }
|
50%, 90% { opacity: 0; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { inter } from "@/public/fonts/font_index";
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: "OpenMonetis | Suas finanças, do seu jeito",
|
default: "OpenMonetis | Suas finanças, do seu jeito",
|
||||||
template: "%s | OpenMonetis",
|
template: "OpenMonetis | %s",
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
"Controle suas finanças pessoais de forma simples e transparente.",
|
"Controle suas finanças pessoais de forma simples e transparente.",
|
||||||
@@ -40,7 +40,7 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body className="subpixel-antialiased" suppressHydrationWarning>
|
<body className="antialiased" suppressHydrationWarning>
|
||||||
<ThemeProvider attribute="class" defaultTheme="light">
|
<ThemeProvider attribute="class" defaultTheme="light">
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<Suspense>{children}</Suspense>
|
<Suspense>{children}</Suspense>
|
||||||
|
|||||||
@@ -301,7 +301,9 @@ export const cards = pgTable(
|
|||||||
closingDay: text("dt_fechamento").notNull(),
|
closingDay: text("dt_fechamento").notNull(),
|
||||||
dueDay: text("dt_vencimento").notNull(),
|
dueDay: text("dt_vencimento").notNull(),
|
||||||
note: text("anotacao"),
|
note: text("anotacao"),
|
||||||
limit: numeric("limite", { precision: 10, scale: 2 }),
|
limit: numeric("limite", { precision: 10, scale: 2 })
|
||||||
|
.notNull()
|
||||||
|
.default("0"),
|
||||||
brand: text("bandeira"),
|
brand: text("bandeira"),
|
||||||
logo: text("logo"),
|
logo: text("logo"),
|
||||||
status: text("status").notNull(),
|
status: text("status").notNull(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { categories, financialAccounts, transactions } from "@/db/schema";
|
import { categories, financialAccounts, transactions } from "@/db/schema";
|
||||||
import {
|
import {
|
||||||
|
ACCOUNT_BALANCE_ADJUSTMENT_NAME,
|
||||||
INITIAL_BALANCE_CATEGORY_NAME,
|
INITIAL_BALANCE_CATEGORY_NAME,
|
||||||
INITIAL_BALANCE_CONDITION,
|
INITIAL_BALANCE_CONDITION,
|
||||||
INITIAL_BALANCE_NOTE,
|
INITIAL_BALANCE_NOTE,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "@/shared/lib/actions/helpers";
|
} from "@/shared/lib/actions/helpers";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
import { db } from "@/shared/lib/db";
|
import { db } from "@/shared/lib/db";
|
||||||
|
import { PERIOD_FORMAT_REGEX } from "@/shared/lib/invoices";
|
||||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
|
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
|
||||||
import {
|
import {
|
||||||
@@ -26,8 +28,11 @@ import {
|
|||||||
TRANSFER_ESTABLISHMENT_SAIDA,
|
TRANSFER_ESTABLISHMENT_SAIDA,
|
||||||
TRANSFER_PAYMENT_METHOD,
|
TRANSFER_PAYMENT_METHOD,
|
||||||
} from "@/shared/lib/transfers/constants";
|
} from "@/shared/lib/transfers/constants";
|
||||||
import { formatDecimalForDbRequired } from "@/shared/utils/currency";
|
import {
|
||||||
import { getTodayInfo } from "@/shared/utils/date";
|
formatCurrency,
|
||||||
|
formatDecimalForDbRequired,
|
||||||
|
} from "@/shared/utils/currency";
|
||||||
|
import { getBusinessTodayDate, getTodayInfo } from "@/shared/utils/date";
|
||||||
import { normalizeFilePath } from "@/shared/utils/string";
|
import { normalizeFilePath } from "@/shared/utils/string";
|
||||||
|
|
||||||
const accountBaseSchema = z.object({
|
const accountBaseSchema = z.object({
|
||||||
@@ -99,7 +104,7 @@ export async function createAccountAction(
|
|||||||
|
|
||||||
if (hasInitialBalance && !adminPayerId) {
|
if (hasInitialBalance && !adminPayerId) {
|
||||||
throw new Error(
|
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) {
|
if (!adminPayerId) {
|
||||||
throw new Error(
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/shared/components/ui/tooltip";
|
} from "@/shared/components/ui/tooltip";
|
||||||
|
import { isAccountInactive } from "@/shared/lib/accounts/constants";
|
||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
|
||||||
interface AccountCardProps {
|
interface AccountCardProps {
|
||||||
@@ -46,7 +47,7 @@ export function AccountCard({
|
|||||||
onTransfer,
|
onTransfer,
|
||||||
className,
|
className,
|
||||||
}: AccountCardProps) {
|
}: AccountCardProps) {
|
||||||
const isInactive = status?.toLowerCase() === "inativa";
|
const isInactive = isAccountInactive(status);
|
||||||
|
|
||||||
const balanceColor =
|
const balanceColor =
|
||||||
balance > 0
|
balance > 0
|
||||||
@@ -145,6 +146,7 @@ export function AccountCard({
|
|||||||
<span className="text-xs text-muted-foreground">Saldo</span>
|
<span className="text-xs text-muted-foreground">Saldo</span>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={balance}
|
amount={balance}
|
||||||
|
showPositiveSign
|
||||||
className={cn("text-2xl font-semibold", balanceColor)}
|
className={cn("text-2xl font-semibold", balanceColor)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ const DEFAULT_ACCOUNT_TYPES = [
|
|||||||
"Conta Poupança",
|
"Conta Poupança",
|
||||||
"Carteira Digital",
|
"Carteira Digital",
|
||||||
"Conta Investimento",
|
"Conta Investimento",
|
||||||
|
"Dinheiro",
|
||||||
"Pré-Pago | VR/VA",
|
"Pré-Pago | VR/VA",
|
||||||
|
"Outros",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const DEFAULT_ACCOUNT_STATUS = ["Ativa", "Inativa"] as const;
|
const DEFAULT_ACCOUNT_STATUS = ["Ativa", "Inativa"] as const;
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shared/components/ui/select";
|
} from "@/shared/components/ui/select";
|
||||||
import { Textarea } from "@/shared/components/ui/textarea";
|
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";
|
import type { AccountFormValues } from "./types";
|
||||||
|
|
||||||
@@ -54,12 +57,16 @@ export function AccountFormFields({
|
|||||||
onValueChange={(value) => onChange("accountType", value)}
|
onValueChange={(value) => onChange("accountType", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="account-type" className="w-full">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{accountTypes.map((type) => (
|
{accountTypes.map((type) => (
|
||||||
<SelectItem key={type} value={type}>
|
<SelectItem key={type} value={type}>
|
||||||
{type}
|
<AccountTypeSelectContent label={type} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
"use client";
|
"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 }) {
|
export function StatusSelectContent({ label }: { label: string }) {
|
||||||
const isActive = label === "Ativa";
|
const isActive = label === "Ativa";
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type AccountStatementCardProps = {
|
|||||||
totalExpenses: number;
|
totalExpenses: number;
|
||||||
logo?: string | null;
|
logo?: string | null;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
|
balanceAdjustment?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccountStatusBadgeVariant = (
|
const getAccountStatusBadgeVariant = (
|
||||||
@@ -45,6 +46,7 @@ export function AccountStatementCard({
|
|||||||
totalExpenses,
|
totalExpenses,
|
||||||
logo,
|
logo,
|
||||||
actions,
|
actions,
|
||||||
|
balanceAdjustment,
|
||||||
}: AccountStatementCardProps) {
|
}: AccountStatementCardProps) {
|
||||||
const logoPath = resolveLogoSrc(logo);
|
const logoPath = resolveLogoSrc(logo);
|
||||||
const resultado = totalIncomes - totalExpenses;
|
const resultado = totalIncomes - totalExpenses;
|
||||||
@@ -84,10 +86,13 @@ export function AccountStatementCard({
|
|||||||
<p className="text-sm text-muted-foreground ">
|
<p className="text-sm text-muted-foreground ">
|
||||||
Saldo ao final do período
|
Saldo ao final do período
|
||||||
</p>
|
</p>
|
||||||
<MoneyValues
|
<div className="flex items-center gap-2">
|
||||||
amount={currentBalance}
|
<MoneyValues
|
||||||
className="text-3xl leading-none tracking-tighter sm:text-2xl"
|
amount={currentBalance}
|
||||||
/>
|
className="text-3xl leading-none tracking-tighter sm:text-2xl"
|
||||||
|
/>
|
||||||
|
{balanceAdjustment}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
variant={getAccountStatusBadgeVariant(status)}
|
variant={getAccountStatusBadgeVariant(status)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toast } from "sonner";
|
|||||||
import { deleteAccountAction } from "@/features/accounts/actions";
|
import { deleteAccountAction } from "@/features/accounts/actions";
|
||||||
import { AccountCard } from "@/features/accounts/components/account-card";
|
import { AccountCard } from "@/features/accounts/components/account-card";
|
||||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
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 { Button } from "@/shared/components/ui/button";
|
||||||
import { Card } from "@/shared/components/ui/card";
|
import { Card } from "@/shared/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -186,14 +186,14 @@ export function AccountsPage({
|
|||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="ativos">Ativas</TabsTrigger>
|
<TabsTrigger value="ativos">Ativas</TabsTrigger>
|
||||||
<TabsTrigger value="arquivados">Arquivadas</TabsTrigger>
|
<TabsTrigger value="inativas">Inativas</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="ativos" className="mt-4">
|
<TabsContent value="ativos" className="mt-4">
|
||||||
{renderAccountList(orderedAccounts, false)}
|
{renderAccountList(orderedAccounts, false)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="arquivados" className="mt-4">
|
<TabsContent value="inativas" className="mt-4">
|
||||||
{renderAccountList(orderedArchivedAccounts, true)}
|
{renderAccountList(orderedArchivedAccounts, true)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
135
src/features/accounts/components/adjust-balance-dialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -99,13 +99,13 @@ async function fetchAccountsByStatus(
|
|||||||
return { accounts, logoOptions };
|
return { accounts, logoOptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountsForUser(
|
async function fetchAccountsForUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
|
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
|
||||||
return fetchAccountsByStatus(userId, false);
|
return fetchAccountsByStatus(userId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchInactiveForUser(
|
async function fetchInactiveForUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
|
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
|
||||||
return fetchAccountsByStatus(userId, true);
|
return fetchAccountsByStatus(userId, true);
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import {
|
|||||||
fetchTransactionsPageWithRelations,
|
fetchTransactionsPageWithRelations,
|
||||||
fetchTransactionsWithRelations,
|
fetchTransactionsWithRelations,
|
||||||
} from "@/features/transactions/queries";
|
} 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 { db } from "@/shared/lib/db";
|
||||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
|
|
||||||
@@ -74,7 +77,9 @@ export async function fetchAccountSummary(
|
|||||||
sum(
|
sum(
|
||||||
case
|
case
|
||||||
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
|
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} = 'Receita' then ${transactions.amount}
|
||||||
|
when ${transactions.transactionType} = 'Transferência' and ${transactions.amount} > 0 then ${transactions.amount}
|
||||||
else 0
|
else 0
|
||||||
end
|
end
|
||||||
),
|
),
|
||||||
@@ -86,7 +91,9 @@ export async function fetchAccountSummary(
|
|||||||
sum(
|
sum(
|
||||||
case
|
case
|
||||||
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
|
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} = 'Despesa' then ${transactions.amount}
|
||||||
|
when ${transactions.transactionType} = 'Transferência' and ${transactions.amount} < 0 then ${transactions.amount}
|
||||||
else 0
|
else 0
|
||||||
end
|
end
|
||||||
),
|
),
|
||||||
@@ -135,7 +142,8 @@ export async function fetchAccountSummary(
|
|||||||
const openingBalance = initialBalance + previousMovements;
|
const openingBalance = initialBalance + previousMovements;
|
||||||
const netAmount = Number(periodSummary?.netAmount ?? 0);
|
const netAmount = Number(periodSummary?.netAmount ?? 0);
|
||||||
const totalIncomes = Number(periodSummary?.incomes ?? 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;
|
const currentBalance = openingBalance + netAmount;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -146,7 +154,7 @@ export async function fetchAccountSummary(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountLancamentos(
|
export async function fetchAccountTransactions(
|
||||||
filters: SQL[],
|
filters: SQL[],
|
||||||
settledOnly = true,
|
settledOnly = true,
|
||||||
) {
|
) {
|
||||||
@@ -159,7 +167,7 @@ export async function fetchAccountLancamentos(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountLancamentosPage(
|
export async function fetchAccountTransactionsPage(
|
||||||
filters: SQL[],
|
filters: SQL[],
|
||||||
{
|
{
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { fetchTransactionDialogOptionsAction } from "@/features/transactions/act
|
|||||||
import { TransactionDetailsDialog } from "@/features/transactions/components/dialogs/transaction-details-dialog";
|
import { TransactionDetailsDialog } from "@/features/transactions/components/dialogs/transaction-details-dialog";
|
||||||
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
|
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
|
||||||
import type { TransactionItem } from "@/features/transactions/components/types";
|
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 { Card, CardContent } from "@/shared/components/ui/card";
|
||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useRef, useState } from "react";
|
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;
|
const ATTACHMENT_URL_STALE_TIME = 4 * 60 * 1000;
|
||||||
|
|
||||||
export const attachmentUrlQueryKey = (attachmentId: string) =>
|
const attachmentUrlQueryKey = (attachmentId: string) =>
|
||||||
["attachments", "url", attachmentId] as const;
|
["attachments", "url", attachmentId] as const;
|
||||||
|
|
||||||
export function useAttachmentUrlQuery(attachmentId: string, enabled: boolean) {
|
export function useAttachmentUrlQuery(attachmentId: string, enabled: boolean) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
RiBarChart2Line,
|
RiBarChart2Line,
|
||||||
RiShieldCheckLine,
|
RiShieldCheckLine,
|
||||||
} from "@remixicon/react";
|
} 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 { DotPattern } from "@/shared/components/ui/dot-pattern";
|
||||||
import { AuthSidebarInvoicesMock } from "./auth-sidebar-invoices-mock";
|
import { AuthSidebarInvoicesMock } from "./auth-sidebar-invoices-mock";
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function BudgetCard({ budget, onEdit, onRemove }: BudgetCardProps) {
|
|||||||
<Progress
|
<Progress
|
||||||
value={usagePercent}
|
value={usagePercent}
|
||||||
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
|
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
|
||||||
|
indicatorClassName={cn(exceeded && "bg-destructive")}
|
||||||
aria-label={`${usagePercent.toFixed(1)}% do orçamento utilizado`}
|
aria-label={`${usagePercent.toFixed(1)}% do orçamento utilizado`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
duplicatePreviousMonthBudgetsAction,
|
duplicatePreviousMonthBudgetsAction,
|
||||||
} from "@/features/budgets/actions";
|
} from "@/features/budgets/actions";
|
||||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
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 { Button } from "@/shared/components/ui/button";
|
||||||
import { Card } from "@/shared/components/ui/card";
|
import { Card } from "@/shared/components/ui/card";
|
||||||
import { BudgetCard } from "./budget-card";
|
import { BudgetCard } from "./budget-card";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type BudgetData = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryOption = {
|
type CategoryOption = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function CalendarGrid({
|
|||||||
onCreateDay,
|
onCreateDay,
|
||||||
}: CalendarGridProps) {
|
}: CalendarGridProps) {
|
||||||
return (
|
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">
|
<div className="grid grid-cols-7 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
{WEEK_DAYS_SHORT.map((dayName) => (
|
{WEEK_DAYS_SHORT.map((dayName) => (
|
||||||
<span key={dayName} className="text-center">
|
<span key={dayName} className="text-center">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { EVENT_TYPE_STYLES } from "@/features/calendar/components/day-cell";
|
import { EVENT_TYPE_STYLES } from "@/features/calendar/components/day-cell";
|
||||||
|
import { Card } from "@/shared/components/ui/card";
|
||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
|
||||||
const LEGEND_ITEMS = [
|
const LEGEND_ITEMS = [
|
||||||
@@ -12,22 +13,24 @@ const LEGEND_ITEMS = [
|
|||||||
|
|
||||||
export function CalendarLegend() {
|
export function CalendarLegend() {
|
||||||
return (
|
return (
|
||||||
<ul className="flex items-center justify-start gap-2 px-1">
|
<Card className="px-4 py-2">
|
||||||
{LEGEND_ITEMS.map((item) => (
|
<ul className="flex flex-row items-center gap-2">
|
||||||
<li
|
{LEGEND_ITEMS.map((item) => (
|
||||||
key={item.label}
|
<li
|
||||||
className={cn(
|
key={item.label}
|
||||||
"flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium",
|
className={cn(
|
||||||
item.wrapper,
|
"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)}
|
<span
|
||||||
aria-hidden
|
className={cn("size-1.5 shrink-0 rounded-full", item.dot)}
|
||||||
/>
|
aria-hidden
|
||||||
{item.label}
|
/>
|
||||||
</li>
|
{item.label}
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
|
|||||||
onClick={() => onSelect(day)}
|
onClick={() => onSelect(day)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className={cn(
|
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.isCurrentMonth && "bg-muted/20 opacity-60",
|
||||||
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
|
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
buildOptionSets,
|
buildOptionSets,
|
||||||
buildSluggedFilters,
|
buildSluggedFilters,
|
||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import { db } from "@/shared/lib/db";
|
|||||||
import {
|
import {
|
||||||
dayOfMonthSchema,
|
dayOfMonthSchema,
|
||||||
noteSchema,
|
noteSchema,
|
||||||
optionalDecimalSchema,
|
requiredDecimalSchema,
|
||||||
uuidSchema,
|
uuidSchema,
|
||||||
} from "@/shared/lib/schemas/common";
|
} from "@/shared/lib/schemas/common";
|
||||||
import { formatDecimalForDb } from "@/shared/utils/currency";
|
import { formatDecimalForDbRequired } from "@/shared/utils/currency";
|
||||||
import { normalizeFilePath } from "@/shared/utils/string";
|
import { normalizeFilePath } from "@/shared/utils/string";
|
||||||
|
|
||||||
const cardBaseSchema = z.object({
|
const cardBaseSchema = z.object({
|
||||||
@@ -35,7 +35,7 @@ const cardBaseSchema = z.object({
|
|||||||
closingDay: dayOfMonthSchema,
|
closingDay: dayOfMonthSchema,
|
||||||
dueDay: dayOfMonthSchema,
|
dueDay: dayOfMonthSchema,
|
||||||
note: noteSchema,
|
note: noteSchema,
|
||||||
limit: optionalDecimalSchema,
|
limit: requiredDecimalSchema("limite"),
|
||||||
logo: z
|
logo: z
|
||||||
.string({ message: "Selecione um logo." })
|
.string({ message: "Selecione um logo." })
|
||||||
.trim()
|
.trim()
|
||||||
@@ -87,7 +87,7 @@ export async function createCardAction(
|
|||||||
closingDay: data.closingDay,
|
closingDay: data.closingDay,
|
||||||
dueDay: data.dueDay,
|
dueDay: data.dueDay,
|
||||||
note: data.note ?? null,
|
note: data.note ?? null,
|
||||||
limit: formatDecimalForDb(data.limit),
|
limit: formatDecimalForDbRequired(data.limit),
|
||||||
logo: logoFile,
|
logo: logoFile,
|
||||||
accountId: data.accountId,
|
accountId: data.accountId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -121,7 +121,7 @@ export async function updateCardAction(
|
|||||||
closingDay: data.closingDay,
|
closingDay: data.closingDay,
|
||||||
dueDay: data.dueDay,
|
dueDay: data.dueDay,
|
||||||
note: data.note ?? null,
|
note: data.note ?? null,
|
||||||
limit: formatDecimalForDb(data.limit),
|
limit: formatDecimalForDbRequired(data.limit),
|
||||||
logo: logoFile,
|
logo: logoFile,
|
||||||
accountId: data.accountId,
|
accountId: data.accountId,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -154,13 +154,21 @@ export function CardDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rawLimit = normalizeDecimalInput(formState.limit);
|
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 = {
|
const payload: CardCreatePayload = {
|
||||||
name: formState.name.trim(),
|
name: formState.name.trim(),
|
||||||
brand: formState.brand,
|
brand: formState.brand,
|
||||||
status: formState.status,
|
status: formState.status,
|
||||||
closingDay: formState.closingDay,
|
closingDay: formState.closingDay,
|
||||||
dueDay: formState.dueDay,
|
dueDay: formState.dueDay,
|
||||||
limit: rawLimit ? Number(rawLimit) : null,
|
limit: limitValue,
|
||||||
note: formState.note.trim() || null,
|
note: formState.note.trim() || null,
|
||||||
logo: formState.logo,
|
logo: formState.logo,
|
||||||
accountId: formState.accountId,
|
accountId: formState.accountId,
|
||||||
|
|||||||
@@ -112,12 +112,13 @@ export function CardFormFields({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="card-limit">Limite (R$)</Label>
|
<Label htmlFor="card-limit">Limite</Label>
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
id="card-limit"
|
id="card-limit"
|
||||||
value={values.limit}
|
value={values.limit}
|
||||||
onValueChange={(value) => onChange("limit", value)}
|
onValueChange={(value) => onChange("limit", value)}
|
||||||
placeholder="R$ 0,00"
|
placeholder="R$ 0,00"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ interface CardItemProps {
|
|||||||
status: string;
|
status: string;
|
||||||
closingDay: string;
|
closingDay: string;
|
||||||
dueDay: string;
|
dueDay: string;
|
||||||
limit: number | null;
|
limit: number;
|
||||||
limitInUse?: number | null;
|
limitInUse?: number;
|
||||||
limitAvailable?: number | null;
|
limitAvailable?: number;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
logo?: string | null;
|
logo?: string | null;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
@@ -61,30 +61,19 @@ export function CardItem({
|
|||||||
}: CardItemProps) {
|
}: CardItemProps) {
|
||||||
void _accountName;
|
void _accountName;
|
||||||
|
|
||||||
const limitTotal = limit ?? null;
|
|
||||||
const used =
|
const used =
|
||||||
limitInUse ??
|
limitInUse ??
|
||||||
(limitTotal !== null && limitAvailable != null
|
(limitAvailable !== undefined ? Math.max(limit - limitAvailable, 0) : 0);
|
||||||
? Math.max(limitTotal - limitAvailable, 0)
|
|
||||||
: limitTotal !== null
|
|
||||||
? 0
|
|
||||||
: null);
|
|
||||||
|
|
||||||
const available =
|
const available = limitAvailable ?? Math.max(limit - used, 0);
|
||||||
limitAvailable ??
|
|
||||||
(limitTotal !== null && used !== null
|
|
||||||
? Math.max(limitTotal - used, 0)
|
|
||||||
: null);
|
|
||||||
|
|
||||||
const usagePercent =
|
const usagePercent =
|
||||||
limitTotal && limitTotal > 0 && used !== null
|
limit > 0 ? Math.min(Math.max((used / limit) * 100, 0), 100) : 0;
|
||||||
? Math.min(Math.max((used / limitTotal) * 100, 0), 100)
|
const exceeded = usagePercent >= 100;
|
||||||
: 0;
|
|
||||||
|
|
||||||
const logoPath = resolveLogoSrc(logo);
|
const logoPath = resolveLogoSrc(logo);
|
||||||
const brandAsset = resolveCardBrandAsset(brand);
|
const brandAsset = resolveCardBrandAsset(brand);
|
||||||
const isInactive = status?.toLowerCase() === "inativo";
|
const isInactive = status?.toLowerCase() === "inativo";
|
||||||
const hasMetrics = limitTotal !== null && used !== null && available !== null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col p-6 w-full">
|
<Card className="flex flex-col p-6 w-full">
|
||||||
@@ -174,54 +163,46 @@ export function CardItem({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-1 flex-col gap-4 px-0">
|
<CardContent className="flex flex-1 flex-col gap-4 px-0">
|
||||||
{hasMetrics &&
|
<div className="flex flex-col gap-0.5">
|
||||||
available !== null &&
|
<span className="text-xs text-muted-foreground">
|
||||||
used !== null &&
|
Limite disponível
|
||||||
limitTotal !== null ? (
|
</span>
|
||||||
<>
|
<MoneyValues
|
||||||
<div className="flex flex-col gap-0.5">
|
amount={available}
|
||||||
<span className="text-xs text-muted-foreground">Disponível</span>
|
className="text-xl font-semibold text-success"
|
||||||
<MoneyValues
|
/>
|
||||||
amount={available}
|
</div>
|
||||||
className="text-xl font-semibold text-success"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">Limite total</span>
|
||||||
Limite total
|
<MoneyValues
|
||||||
</span>
|
amount={limit}
|
||||||
<MoneyValues
|
className="text-sm font-semibold text-foreground"
|
||||||
amount={limitTotal}
|
/>
|
||||||
className="text-sm font-semibold text-foreground"
|
</div>
|
||||||
/>
|
<div className="flex flex-col gap-0.5">
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
<div className="flex flex-col gap-0.5">
|
Limite utilizado
|
||||||
<span className="text-xs text-muted-foreground">Em uso</span>
|
</span>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={used}
|
amount={used}
|
||||||
className="text-sm font-semibold text-primary"
|
className="text-sm font-semibold text-destructive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Progress
|
<Progress
|
||||||
value={usagePercent}
|
value={usagePercent}
|
||||||
className="h-2.5"
|
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
|
||||||
aria-label={`${usagePercent.toFixed(0)}% do limite utilizado`}
|
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 className="text-xs text-muted-foreground">
|
||||||
</span>
|
{usagePercent.toFixed(1)}% utilizado
|
||||||
</div>
|
</span>
|
||||||
</>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Ainda não há limite registrado para este cartão.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="mt-auto flex flex-wrap gap-4 px-0 pt-2 text-sm">
|
<CardFooter className="mt-auto flex flex-wrap gap-4 px-0 pt-2 text-sm">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { RiBankLine } from "@remixicon/react";
|
import { RiBankLine } from "@remixicon/react";
|
||||||
import Image from "next/image";
|
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 { resolveCardBrandLogoSrc } from "@/shared/lib/cards/brand-assets";
|
||||||
import { resolveLogoSrc } from "@/shared/lib/logo";
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { deleteCardAction } from "@/features/cards/actions";
|
import { deleteCardAction } from "@/features/cards/actions";
|
||||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
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 { Button } from "@/shared/components/ui/button";
|
||||||
import { Card as UiCard } from "@/shared/components/ui/card";
|
import { Card as UiCard } from "@/shared/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -174,14 +174,14 @@ export function CardsPage({
|
|||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="ativos">Ativos</TabsTrigger>
|
<TabsTrigger value="ativos">Ativos</TabsTrigger>
|
||||||
<TabsTrigger value="arquivados">Arquivados</TabsTrigger>
|
<TabsTrigger value="inativos">Inativos</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="ativos" className="mt-4">
|
<TabsContent value="ativos" className="mt-4">
|
||||||
{renderCardList(orderedCards, false)}
|
{renderCardList(orderedCards, false)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="arquivados" className="mt-4">
|
<TabsContent value="inativos" className="mt-4">
|
||||||
{renderCardList(orderedArchivedCards, true)}
|
{renderCardList(orderedArchivedCards, true)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ export type Card = {
|
|||||||
dueDay: string;
|
dueDay: string;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
logo: string | null;
|
logo: string | null;
|
||||||
limit: number | null;
|
limit: number;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
limitInUse: number;
|
limitInUse: number;
|
||||||
limitAvailable: number | null;
|
limitAvailable: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CardFormValues = {
|
export type CardFormValues = {
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ type CardData = {
|
|||||||
dueDay: string;
|
dueDay: string;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
logo: string | null;
|
logo: string | null;
|
||||||
limit: number | null;
|
limit: number;
|
||||||
limitInUse: number;
|
limitInUse: number;
|
||||||
limitAvailable: number | null;
|
limitAvailable: number;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountSimple = {
|
type AccountSimple = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
logo: string | null;
|
logo: string | null;
|
||||||
@@ -96,15 +96,12 @@ async function fetchCardsByStatus(
|
|||||||
dueDay: card.dueDay,
|
dueDay: card.dueDay,
|
||||||
note: card.note,
|
note: card.note,
|
||||||
logo: card.logo,
|
logo: card.logo,
|
||||||
limit: card.limit ? Number(card.limit) : null,
|
limit: Number(card.limit),
|
||||||
limitInUse: (() => {
|
limitInUse: (() => {
|
||||||
const total = usageMap.get(card.id) ?? 0;
|
const total = usageMap.get(card.id) ?? 0;
|
||||||
return total < 0 ? Math.abs(total) : 0;
|
return total < 0 ? Math.abs(total) : 0;
|
||||||
})(),
|
})(),
|
||||||
limitAvailable: (() => {
|
limitAvailable: (() => {
|
||||||
if (!card.limit) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const total = usageMap.get(card.id) ?? 0;
|
const total = usageMap.get(card.id) ?? 0;
|
||||||
const inUse = total < 0 ? Math.abs(total) : 0;
|
const inUse = total < 0 ? Math.abs(total) : 0;
|
||||||
return Math.max(Number(card.limit) - inUse, 0);
|
return Math.max(Number(card.limit) - inUse, 0);
|
||||||
@@ -124,7 +121,7 @@ async function fetchCardsByStatus(
|
|||||||
return { cards: cardList, accounts, logoOptions };
|
return { cards: cardList, accounts, logoOptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCardsForUser(userId: string): Promise<{
|
async function fetchCardsForUser(userId: string): Promise<{
|
||||||
cards: CardData[];
|
cards: CardData[];
|
||||||
accounts: AccountSimple[];
|
accounts: AccountSimple[];
|
||||||
logoOptions: string[];
|
logoOptions: string[];
|
||||||
@@ -132,7 +129,7 @@ export async function fetchCardsForUser(userId: string): Promise<{
|
|||||||
return fetchCardsByStatus(userId, false);
|
return fetchCardsByStatus(userId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchInactiveForUser(userId: string): Promise<{
|
async function fetchInactiveForUser(userId: string): Promise<{
|
||||||
cards: CardData[];
|
cards: CardData[];
|
||||||
accounts: AccountSimple[];
|
accounts: AccountSimple[];
|
||||||
logoOptions: string[];
|
logoOptions: string[];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import StatusDot from "@/shared/components/status-dot";
|
import StatusDot from "@/shared/components/feedback/status-dot";
|
||||||
|
|
||||||
export function TypeSelectContent({ label }: { label: string }) {
|
export function TypeSelectContent({ label }: { label: string }) {
|
||||||
const isReceita = label === "Receita";
|
const isReceita = label === "Receita";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type Category, categories } from "@/db/schema";
|
|||||||
import type { CategoryType } from "@/shared/lib/categories/constants";
|
import type { CategoryType } from "@/shared/lib/categories/constants";
|
||||||
import { db } from "@/shared/lib/db";
|
import { db } from "@/shared/lib/db";
|
||||||
|
|
||||||
export type CategoryData = {
|
type CategoryData = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: CategoryType;
|
type: CategoryType;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/shared/utils/financial-dates";
|
} from "@/shared/utils/financial-dates";
|
||||||
|
|
||||||
export type BillDialogState = PaymentDialogState;
|
export type BillDialogState = PaymentDialogState;
|
||||||
export type BillStatusDateItem = Pick<
|
type BillStatusDateItem = Pick<
|
||||||
DashboardBill,
|
DashboardBill,
|
||||||
"dueDate" | "boletoPaymentDate" | "isSettled"
|
"dueDate" | "boletoPaymentDate" | "isSettled"
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export type DashboardBill = {
|
|||||||
dueDate: string | null;
|
dueDate: string | null;
|
||||||
boletoPaymentDate: string | null;
|
boletoPaymentDate: string | null;
|
||||||
isSettled: boolean;
|
isSettled: boolean;
|
||||||
|
accountId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BillPaymentAccountOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
logo: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardBillsSnapshot = {
|
export type DashboardBillsSnapshot = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type BillDialogState,
|
type BillDialogState,
|
||||||
getCurrentBillDateString,
|
getCurrentBillDateString,
|
||||||
@@ -20,12 +21,26 @@ type BillWidgetController = Omit<
|
|||||||
> & {
|
> & {
|
||||||
selectedBill: DashboardBill | null;
|
selectedBill: DashboardBill | null;
|
||||||
modalState: BillDialogState;
|
modalState: BillDialogState;
|
||||||
|
paymentAccountId: string;
|
||||||
|
setPaymentAccountId: (accountId: string) => void;
|
||||||
|
paymentDate: Date;
|
||||||
|
setPaymentDate: (date: Date) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toIsoDate = (date: Date) => date.toISOString().split("T")[0] ?? "";
|
||||||
|
|
||||||
export function useBillWidgetController(
|
export function useBillWidgetController(
|
||||||
bills?: DashboardBill[],
|
bills?: DashboardBill[],
|
||||||
): BillWidgetController {
|
): BillWidgetController {
|
||||||
const safeBills = bills ?? EMPTY_BILLS;
|
const safeBills = bills ?? EMPTY_BILLS;
|
||||||
|
const [paymentAccountId, setPaymentAccountId] = useState<string>("");
|
||||||
|
const [paymentDate, setPaymentDate] = useState<Date>(() => new Date());
|
||||||
|
|
||||||
|
const paymentAccountIdRef = useRef(paymentAccountId);
|
||||||
|
const paymentDateRef = useRef(paymentDate);
|
||||||
|
paymentAccountIdRef.current = paymentAccountId;
|
||||||
|
paymentDateRef.current = paymentDate;
|
||||||
|
|
||||||
const controller = usePaymentDialogController({
|
const controller = usePaymentDialogController({
|
||||||
items: safeBills,
|
items: safeBills,
|
||||||
getItemId: (bill) => bill.id,
|
getItemId: (bill) => bill.id,
|
||||||
@@ -34,13 +49,36 @@ export function useBillWidgetController(
|
|||||||
toggleTransactionSettlementAction({
|
toggleTransactionSettlementAction({
|
||||||
id: bill.id,
|
id: bill.id,
|
||||||
value: true,
|
value: true,
|
||||||
|
paymentAccountId: paymentAccountIdRef.current || null,
|
||||||
|
paymentDate: toIsoDate(paymentDateRef.current),
|
||||||
}),
|
}),
|
||||||
applyConfirmedState: (bill) =>
|
applyConfirmedState: (bill) =>
|
||||||
markBillAsSettled(bill, getCurrentBillDateString()),
|
markBillAsSettled(
|
||||||
|
{
|
||||||
|
...bill,
|
||||||
|
accountId: paymentAccountIdRef.current || bill.accountId,
|
||||||
|
},
|
||||||
|
toIsoDate(paymentDateRef.current) || getCurrentBillDateString(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedBillId = controller.selectedItem?.id ?? null;
|
||||||
|
const selectedBillAccountId = controller.selectedItem?.accountId ?? "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedBillId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPaymentAccountId(selectedBillAccountId ?? "");
|
||||||
|
setPaymentDate(new Date());
|
||||||
|
}, [selectedBillId, selectedBillAccountId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...controller,
|
...controller,
|
||||||
selectedBill: controller.selectedItem,
|
selectedBill: controller.selectedItem,
|
||||||
|
paymentAccountId,
|
||||||
|
setPaymentAccountId,
|
||||||
|
paymentDate,
|
||||||
|
setPaymentDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { and, desc, eq, isNull, ne, or, sql } from "drizzle-orm";
|
import { and, desc, eq, isNull, ne, or, sql } from "drizzle-orm";
|
||||||
import { categories, financialAccounts, transactions } from "@/db/schema";
|
import { categories, financialAccounts, transactions } from "@/db/schema";
|
||||||
import { mapTransactionsData } from "@/features/transactions/page-helpers";
|
import { mapTransactionsData } from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||||
INITIAL_BALANCE_NOTE,
|
INITIAL_BALANCE_NOTE,
|
||||||
@@ -17,7 +17,7 @@ import { getPreviousPeriod } from "@/shared/utils/period";
|
|||||||
|
|
||||||
type MappedLancamentos = ReturnType<typeof mapTransactionsData>;
|
type MappedLancamentos = ReturnType<typeof mapTransactionsData>;
|
||||||
|
|
||||||
export type CategoryDetailData = {
|
type CategoryDetailData = {
|
||||||
category: {
|
category: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import {
|
|||||||
formatPeriodMonthShort,
|
formatPeriodMonthShort,
|
||||||
} from "@/shared/utils/period";
|
} from "@/shared/utils/period";
|
||||||
|
|
||||||
export type CategoryOption = {
|
type CategoryOption = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
type: "receita" | "despesa";
|
type: "receita" | "despesa";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryHistoryItem = {
|
type CategoryHistoryItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
buildDashboardAdminFilters,
|
buildDashboardAdminFilters,
|
||||||
excludeAutoInvoiceEntries,
|
excludeAutoInvoiceEntries,
|
||||||
excludeInitialBalanceWhenConfigured,
|
excludeInitialBalanceWhenConfigured,
|
||||||
|
excludeRefundEntries,
|
||||||
excludeTransactionsFromExcludedAccounts,
|
excludeTransactionsFromExcludedAccounts,
|
||||||
} from "@/features/dashboard/transaction-filters";
|
} from "@/features/dashboard/lib/transaction-filters";
|
||||||
import { db } from "@/shared/lib/db";
|
import { db } from "@/shared/lib/db";
|
||||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||||
@@ -168,6 +169,7 @@ export async function fetchDashboardCategoryOverview(
|
|||||||
eq(transactions.transactionType, "Receita"),
|
eq(transactions.transactionType, "Receita"),
|
||||||
eq(categories.type, "receita"),
|
eq(categories.type, "receita"),
|
||||||
excludeAutoInvoiceEntries(),
|
excludeAutoInvoiceEntries(),
|
||||||
|
excludeRefundEntries(),
|
||||||
excludeInitialBalanceWhenConfigured(),
|
excludeInitialBalanceWhenConfigured(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export type CategoryOption = {
|
type CategoryOption = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryTransaction = {
|
type CategoryTransaction = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|||||||