diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b5ef5..f99c958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/), e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/). +## [2.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. @@ -220,6 +238,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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) @@ -240,6 +260,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Estabelecimentos: integração com Logo.dev — logos automáticos de marcas exibidos na coluna de estabelecimentos nos lançamentos @@ -259,6 +281,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Docker: `docker-compose.yml` refatorado — removidos profiles, build e dependência de arquivo externo; compose agora é standalone (basta `curl` + `docker compose up -d`) @@ -268,6 +292,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Dashboard: novos widgets configuráveis — Anexos (resumo de arquivos do período), Inbox (snapshot de pré-lançamentos pendentes) e Tendências de Categoria @@ -304,24 +330,32 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 +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 - 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 +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 - Anexos: corrigido upload que falhava com `NetworkError` — CSP `connect-src` bloqueava fetch para o Storage ## [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 - Tokens: corrigido `/api/auth/device/verify` que rejeitava tokens criados via Settings (revertido de JWT para hash lookup) @@ -334,6 +368,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Tokens: removido aceite de tokens sem expiração (`expiresAt NULL`); tokens criados via settings agora expiram em 1 ano @@ -349,12 +385,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 +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 - Dependências: adiciona `@tanstack/react-query` e um provider global para padronizar cache, deduplicação e invalidação de leituras client-side @@ -390,12 +430,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 +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 - 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 @@ -416,6 +460,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -432,6 +478,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page @@ -453,6 +501,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -468,12 +518,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 +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 - Scripts: novo comando `mockup` no `package.json` para executar `scripts/mock-data.ts` @@ -502,6 +556,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -512,6 +568,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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`) @@ -568,6 +626,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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`. @@ -582,6 +642,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Suporte completo a Passkeys (WebAuthn) com plugin `@better-auth/passkey` no servidor e `passkeyClient` no cliente de autenticação @@ -616,6 +678,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Inbox de pré-lançamentos: ações para excluir item individual (processado/descartado) e limpar itens em lote por status @@ -634,6 +698,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -645,6 +711,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Prop `compact` no DatePicker para formato abreviado "28 fev" (sem "de" e sem ano) @@ -656,6 +724,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Dialogs padronizados: padding maior (p-10), largura max-w-xl, botões do footer com largura igual (flex-1) @@ -679,6 +749,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Topbar de navegação substituindo o header fixo: backdrop blur, links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas) @@ -702,6 +774,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -713,12 +787,16 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 +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 - 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}" @@ -726,6 +804,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -746,6 +826,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -767,6 +849,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Landing page reformulada: visual modernizado, melhor experiência mobile e novas seções @@ -789,6 +873,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -803,6 +889,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário @@ -822,6 +910,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes) @@ -843,6 +933,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - 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 @@ -873,6 +965,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Calculadora arrastável via drag handle no header do dialog @@ -888,6 +982,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Indexes compostos em `lancamentos`: `(userId, period, transactionType)` e `(pagadorId, period)` @@ -908,6 +1004,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Refatoração para otimização do React 19 compiler @@ -936,6 +1034,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [1.2.5] - 2025-02-01 +Versão pequena: novo widget de pagadores no dashboard com avatares atualizados. + ### Adicionado - Widget de pagadores no dashboard @@ -943,6 +1043,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Preservar formatação nas anotações @@ -950,6 +1052,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Versão exibida na sidebar @@ -957,6 +1061,8 @@ Esta versão é quase toda sobre organização e polimento. O código interno do ## [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 - Atualização de dependências diff --git a/README.md b/README.md index a1c1d9b..2c895f2 100644 --- a/README.md +++ b/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. -[![Version](https://img.shields.io/badge/version-2.5.4-blue?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-2.5.5-blue?style=flat-square)](CHANGELOG.md) [![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/) @@ -61,7 +61,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ ### 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. @@ -85,7 +85,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ OpenMonetis Companion

-⚙️ **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 diff --git a/package.json b/package.json index e7fcb39..783af0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmonetis", - "version": "2.5.4", + "version": "2.5.5", "private": true, "packageManager": "pnpm@10.33.0", "scripts": { @@ -88,6 +88,7 @@ "resend": "^6.12.2", "sonner": "2.0.7", "tailwind-merge": "3.5.0", + "tw-animate-css": "^1.4.0", "vaul": "1.1.2", "zod": "4.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31b33dc..0d94622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,9 @@ importers: tailwind-merge: specifier: 3.5.0 version: 3.5.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 vaul: specifier: 1.1.2 version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -4211,6 +4214,9 @@ packages: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -8398,6 +8404,8 @@ snapshots: dependencies: tslib: 1.14.1 + tw-animate-css@1.4.0: {} + typescript@6.0.3: {} unbash@3.0.0: {} diff --git a/src/app/(dashboard)/payers/[payerId]/page.tsx b/src/app/(dashboard)/payers/[payerId]/page.tsx index 4f90362..540902d 100644 --- a/src/app/(dashboard)/payers/[payerId]/page.tsx +++ b/src/app/(dashboard)/payers/[payerId]/page.tsx @@ -76,11 +76,11 @@ const capitalize = (value: string) => const EMPTY_FILTERS: TransactionSearchFilters = { transactionFilter: null, - conditionFilter: null, - paymentFilter: null, - payerFilter: null, - categoryFilter: null, - accountCardFilter: null, + conditionFilters: [], + paymentFilters: [], + payerFilters: [], + categoryFilters: [], + accountCardFilters: [], searchFilter: null, settledFilter: null, attachmentFilter: null, diff --git a/src/app/(landing-page)/page.tsx b/src/app/(landing-page)/page.tsx index 5b442d1..44346f7 100644 --- a/src/app/(landing-page)/page.tsx +++ b/src/app/(landing-page)/page.tsx @@ -208,7 +208,7 @@ export default async function Page() { {/* Features Section */} -
+
@@ -447,7 +447,7 @@ export default async function Page() {
{/* Tech Stack Section */} -
+
@@ -535,7 +535,7 @@ export default async function Page() {
{/* Who is this for Section */} -
+
diff --git a/src/app/globals.css b/src/app/globals.css index c2c2d57..e556d4a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); @@ -269,54 +270,6 @@ 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 { 0%, 40% { opacity: 1; } 50%, 90% { opacity: 0; } diff --git a/src/features/settings/components/changelog-tab.tsx b/src/features/settings/components/changelog-tab.tsx index 9a18f15..1a4b3b4 100644 --- a/src/features/settings/components/changelog-tab.tsx +++ b/src/features/settings/components/changelog-tab.tsx @@ -1,20 +1,26 @@ -import Link from "next/link"; -import type { ChangelogVersion } from "@/features/settings/lib/parse-changelog"; -import { Badge } from "@/shared/components/ui/badge"; -import { Card } from "@/shared/components/ui/card"; +"use client"; -/** Converte "[texto](url)" em link; texto simples fica como está */ -function parseContributorLine(content: string) { - const linkMatch = content.match(/^\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/); - if (linkMatch) { - return { label: linkMatch[1], url: linkMatch[2] }; - } - return { label: content, url: null }; -} +import { RiArrowDownSLine } from "@remixicon/react"; +import { format, parseISO } from "date-fns"; +import { ptBR } from "date-fns/locale"; +import { useEffect, useMemo, useState } from "react"; +import type { + BumpType, + ChangelogVersion, +} from "@/features/settings/lib/changelog-types"; +import { Badge } from "@/shared/components/ui/badge"; +import { Button } from "@/shared/components/ui/button"; +import { Card } from "@/shared/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/shared/components/ui/collapsible"; +import { cn } from "@/shared/utils/ui"; const sectionBadgeVariant: Record< string, - "success" | "info" | "destructive" | "secondary" | "outline" + "success" | "info" | "destructive" | "outline" | "secondary" > = { Adicionado: "success", Alterado: "info", @@ -22,75 +28,211 @@ const sectionBadgeVariant: Record< Removido: "destructive", }; -function getSectionVariant(type: string) { - return sectionBadgeVariant[type] ?? "secondary"; +const dotByBump: Record = { + major: "size-4 bg-primary", + minor: "size-3 bg-primary/80", + patch: "size-2.5 bg-muted-foreground/40", +}; + +const bumpLabel: Record = { + major: "Major", + minor: "Minor", + patch: "Patch", +}; + +function versionAnchorId(version: string) { + return `v${version.replace(/\./g, "-")}`; +} + +function anchorIdToVersion(id: string): string | null { + if (!id.startsWith("v")) return null; + return id.slice(1).replace(/-/g, "."); +} + +function groupByMonth(versions: ChangelogVersion[]) { + const groups: { key: string; label: string; items: ChangelogVersion[] }[] = + []; + for (const v of versions) { + const date = parseISO(v.isoDate); + const key = Number.isNaN(date.getTime()) + ? v.isoDate.slice(0, 7) + : format(date, "yyyy-MM"); + const label = Number.isNaN(date.getTime()) + ? key + : format(date, "MMMM 'de' yyyy", { locale: ptBR }); + const last = groups.at(-1); + if (last?.key === key) last.items.push(v); + else groups.push({ key, label, items: [v] }); + } + return groups; +} + +function VersionDetails({ version }: { version: ChangelogVersion }) { + return ( + + {version.sections.map((section) => ( +
+ + {section.type} + +
    + {section.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ ); +} + +type TimelineItemProps = { + version: ChangelogVersion; + open: boolean; + onOpenChange: (open: boolean) => void; + isLatest: boolean; +}; + +function TimelineItem({ + version, + open, + onOpenChange, + isLatest, +}: TimelineItemProps) { + const hasDetails = version.sections.length > 0; + const date = parseISO(version.isoDate); + const validDate = !Number.isNaN(date.getTime()); + + return ( +
+
+ + +
+ +
+
+

v{version.version}

+ {isLatest ? ( + + Atual + + ) : null} + +
+ + {version.summary ? ( + +
+ {version.summary} +
+
+ ) : null} + + {hasDetails ? ( + + + + + + + + + ) : null} +
+
+ ); } export function ChangelogTab({ versions }: { versions: ChangelogVersion[] }) { + const [openVersions, setOpenVersions] = useState>(() => { + const initial = new Set(); + const first = versions[0]?.version; + if (first) initial.add(first); + return initial; + }); + + useEffect(() => { + if (typeof window === "undefined") return; + const hash = window.location.hash.slice(1); + if (!hash) return; + const target = anchorIdToVersion(hash); + if (target) { + setOpenVersions((prev) => { + if (prev.has(target)) return prev; + const next = new Set(prev); + next.add(target); + return next; + }); + } + requestAnimationFrame(() => { + const el = document.getElementById(hash); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + }, []); + + const groups = useMemo(() => groupByMonth(versions), [versions]); + const latestVersion = versions[0]?.version; + const setVersionOpen = (version: string, isOpen: boolean) => { + setOpenVersions((prev) => { + const next = new Set(prev); + if (isOpen) next.add(version); + else next.delete(version); + return next; + }); + }; + return ( -
- {versions.map((version) => ( - -
-

v{version.version}

- - {version.date} - -
-
- {version.summary && ( -

- {version.summary} -

- )} - {version.sections.map((section) => ( -
- - {section.type} - -
    - {section.items.map((item) => ( -
  • - - {item} -
  • - ))} -
-
+
+ {groups.map((group) => ( +
+

+ {group.label} +

+
+ {group.items.map((version) => ( + setVersionOpen(version.version, o)} + /> ))} - {version.contributor && ( -
- - Contribuições: {(() => { - const { label, url } = parseContributorLine( - version.contributor, - ); - if (url) { - return ( - - {label} - - ); - } - return ( - - {label} - - ); - })()} - -
- )}
- +
))}
); diff --git a/src/features/settings/lib/changelog-types.ts b/src/features/settings/lib/changelog-types.ts new file mode 100644 index 0000000..dfe8027 --- /dev/null +++ b/src/features/settings/lib/changelog-types.ts @@ -0,0 +1,30 @@ +export type SectionType = "Adicionado" | "Alterado" | "Corrigido" | "Removido"; + +export const SECTION_TYPES: readonly SectionType[] = [ + "Adicionado", + "Alterado", + "Corrigido", + "Removido", +]; + +export type ChangelogSection = { + type: SectionType; + items: string[]; +}; + +export type BumpType = "major" | "minor" | "patch"; + +export type ChangelogVersion = { + version: string; + /** Formato exibido "DD/MM/YYYY". */ + date: string; + /** Data ISO crua "YYYY-MM-DD" para ordenação e formatação client-side. */ + isoDate: string; + bump: BumpType; + summary?: string; + sections: ChangelogSection[]; +}; + +export function isSectionType(value: string): value is SectionType { + return (SECTION_TYPES as readonly string[]).includes(value); +} diff --git a/src/features/settings/lib/parse-changelog.ts b/src/features/settings/lib/parse-changelog.ts index c9a7694..74e7ac5 100644 --- a/src/features/settings/lib/parse-changelog.ts +++ b/src/features/settings/lib/parse-changelog.ts @@ -1,21 +1,27 @@ +import "server-only"; import fs from "node:fs"; import path from "node:path"; +import { + type BumpType, + type ChangelogSection, + type ChangelogVersion, + isSectionType, +} from "@/features/settings/lib/changelog-types"; -type ChangelogSection = { - type: string; - items: string[]; -}; +function diffBump(current: string, previous: string | undefined): BumpType { + if (!previous) return "minor"; + const [aMajor = 0, aMinor = 0] = current.split(".").map(Number); + const [bMajor = 0, bMinor = 0] = previous.split(".").map(Number); + if (aMajor !== bMajor) return "major"; + if (aMinor !== bMinor) return "minor"; + return "patch"; +} -export type ChangelogVersion = { - version: string; - date: string; - summary?: string; - sections: ChangelogSection[]; - /** Linha de contribuições/autor (pode conter markdown, ex: [Nome](url)) */ - contributor?: string; -}; +let cached: ChangelogVersion[] | null = null; export function parseChangelog(): ChangelogVersion[] { + if (cached) return cached; + const filePath = path.join(process.cwd(), "CHANGELOG.md"); const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); @@ -31,10 +37,14 @@ export function parseChangelog(): ChangelogVersion[] { if (currentSection && currentVersion) { currentVersion.sections.push(currentSection); } - const [y, m, d] = versionMatch[2].split("-"); + const version = versionMatch[1] ?? ""; + const isoDate = versionMatch[2] ?? ""; + const [y, m, d] = isoDate.split("-"); currentVersion = { - version: versionMatch[1], - date: d && m && y ? `${d}/${m}/${y}` : versionMatch[2], + version, + isoDate, + date: d && m && y ? `${d}/${m}/${y}` : isoDate, + bump: "patch", sections: [], }; versions.push(currentVersion); @@ -52,33 +62,35 @@ export function parseChangelog(): ChangelogVersion[] { if (currentSection) { currentVersion.sections.push(currentSection); } - currentSection = { type: sectionMatch[1], items: [] }; + const type = sectionMatch[1] ?? ""; + currentSection = isSectionType(type) ? { type, items: [] } : null; continue; } const itemMatch = line.match(/^- (.+)$/); if (itemMatch && currentSection) { - currentSection.items.push(itemMatch[1]); + currentSection.items.push(itemMatch[1] ?? ""); continue; } if (currentVersion && !currentSection && line.trim()) { summaryLines.push(line.trim()); - continue; - } - - // **Contribuições:** ou **Autor:** com texto/link opcional - const contributorMatch = line.match( - /^\*\*(?:Contribuições|Autor):\*\*\s*(.+)$/, - ); - if (contributorMatch && currentVersion) { - currentVersion.contributor = contributorMatch[1].trim() || undefined; } } if (currentSection && currentVersion) { currentVersion.sections.push(currentSection); } + if (currentVersion && !currentVersion.summary && summaryLines.length > 0) { + currentVersion.summary = summaryLines.join(" ").trim(); + } + for (let i = 0; i < versions.length; i++) { + const current = versions[i]; + if (!current) continue; + current.bump = diffBump(current.version, versions[i + 1]?.version); + } + + cached = versions; return versions; } diff --git a/src/features/transactions/actions/export-actions.ts b/src/features/transactions/actions/export-actions.ts index 2ab65ce..37d01a8 100644 --- a/src/features/transactions/actions/export-actions.ts +++ b/src/features/transactions/actions/export-actions.ts @@ -25,11 +25,11 @@ const exportTransactionsSchema: z.ZodType = z.object( period: z.string().regex(/^\d{4}-\d{2}$/), filters: z.object({ transactionFilter: z.string().nullable(), - conditionFilter: z.string().nullable(), - paymentFilter: z.string().nullable(), - payerFilter: z.string().nullable(), - categoryFilter: z.string().nullable(), - accountCardFilter: z.string().nullable(), + conditionFilters: z.array(z.string()), + paymentFilters: z.array(z.string()), + payerFilters: z.array(z.string()), + categoryFilters: z.array(z.string()), + accountCardFilters: z.array(z.string()), searchFilter: z.string().nullable(), settledFilter: z.string().nullable(), attachmentFilter: z.string().nullable(), diff --git a/src/features/transactions/components/dialogs/bulk-action-dialog.tsx b/src/features/transactions/components/dialogs/bulk-action-dialog.tsx index c3dfa32..f066908 100644 --- a/src/features/transactions/components/dialogs/bulk-action-dialog.tsx +++ b/src/features/transactions/components/dialogs/bulk-action-dialog.tsx @@ -116,10 +116,10 @@ export function BulkActionDialog({ htmlFor="period" className="text-sm cursor-pointer font-medium" > - Todas as pessoas deste período + {`Todas as pessoas desta parcela (${currentNumber}/${totalCount})`}

- Aplica a todos os lançamentos deste mesmo mês na série + Aplica a alteração para todas as pessoas que dividem esta parcela

{scope === "period" && actionType === "edit" && (
diff --git a/src/features/transactions/components/page/transactions-page.tsx b/src/features/transactions/components/page/transactions-page.tsx index e9c8842..f3c541d 100644 --- a/src/features/transactions/components/page/transactions-page.tsx +++ b/src/features/transactions/components/page/transactions-page.tsx @@ -1,5 +1,6 @@ "use client"; +import { RiAddFill } from "@remixicon/react"; import { useState } from "react"; import { toast } from "sonner"; import { @@ -18,6 +19,7 @@ import { getPresignedUploadUrlAction, } from "@/features/transactions/actions/attachments"; import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog"; +import { Button } from "@/shared/components/ui/button"; import type { TransactionsExportContext, TransactionsPaginationState, @@ -115,7 +117,6 @@ export function TransactionsPage({ const [selectedTransaction, setSelectedTransaction] = useState(null); const [editOpen, setEditOpen] = useState(false); - const [createOpen, setCreateOpen] = useState(false); const [copyOpen, setCopyOpen] = useState(false); const [transactionToCopy, setTransactionToCopy] = useState(null); @@ -411,15 +412,6 @@ export function TransactionsPage({ setPendingMultipleDeleteData([]); }; - const [transactionTypeForCreate, setTransactionTypeForCreate] = useState< - "Despesa" | "Receita" | null - >(null); - - const handleCreate = (type: "Despesa" | "Receita") => { - setTransactionTypeForCreate(type); - setCreateOpen(true); - }; - const handleMassAdd = () => { setMassAddOpen(true); }; @@ -558,6 +550,57 @@ export function TransactionsPage({ setAnticipationHistoryOpen(true); }; + const createSlot = allowCreate ? ( + <> + + + Nova Receita + + } + /> + + + Nova Despesa + + } + /> + + ) : null; + return ( <> settlementLoadingId === id} /> - {allowCreate ? ( - - ) : null} - void; + widthClass?: string; + disabled?: boolean; + searchable?: boolean; + searchPlaceholder?: string; + groupOrder?: string[]; +} + +function MultiSelectFilter({ + placeholder, + options, + selected, + onChange, + widthClass = "w-full", + disabled, + searchable = false, + searchPlaceholder = "Buscar...", + groupOrder, +}: MultiSelectFilterProps) { + const [open, setOpen] = useState(false); + + const groupedOptions = useMemo(() => { + const map = new Map(); + for (const option of options) { + const key = option.group ?? ""; + const list = map.get(key) ?? []; + list.push(option); + map.set(key, list); + } + const orderedKeys = groupOrder + ? [ + ...groupOrder, + ...Array.from(map.keys()).filter((k) => !groupOrder.includes(k)), + ] + : Array.from(map.keys()); + return orderedKeys + .filter((key) => map.has(key)) + .map((key) => ({ name: key, items: map.get(key) ?? [] })); + }, [options, groupOrder]); + + const selectedSet = new Set(selected); + const selectedOptions = options.filter((option) => + selectedSet.has(option.value), + ); + + const toggle = (value: string) => { + if (selectedSet.has(value)) { + onChange(selected.filter((v) => v !== value)); + } else { + onChange([...selected, value]); + } + }; + + const clear = () => { + onChange([]); + }; + + const triggerLabel: ReactNode = + selectedOptions.length === 0 ? ( + placeholder + ) : selectedOptions.length === 1 ? ( + (selectedOptions[0]?.render ?? selectedOptions[0]?.label) + ) : ( + + + {selectedOptions.length} selecionados + + + ); + + return ( + + + + + + + {searchable ? : null} + + Nada encontrado. + + clear()} + disabled={selectedOptions.length === 0} + className="text-muted-foreground data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none" + > + Limpar seleção + + + {groupedOptions.map((group) => ( + + {group.items.map((option) => { + const isSelected = selectedSet.has(option.value); + return ( + toggle(option.value)} + className="gap-2" + > + + + {option.render ?? option.label} + + {isSelected ? ( + + ) : null} + + ); + })} + + ))} + + + + + ); +} + interface TransactionsFiltersProps { payerOptions: TransactionFilterOption[]; categoryOptions: TransactionFilterOption[]; @@ -152,6 +305,11 @@ export function TransactionsFilters({ const getParamValue = (key: string) => searchParams.get(key) ?? FILTER_EMPTY_VALUE; + const getParamValues = useCallback( + (key: string) => searchParams.getAll(key), + [searchParams], + ); + const handleFilterChange = useCallback( (key: string, value: string | null) => { const nextParams = new URLSearchParams(searchParams.toString()); @@ -174,6 +332,27 @@ export function TransactionsFilters({ [searchParams, pathname, router], ); + const handleMultiFilterChange = useCallback( + (key: string, values: string[]) => { + const nextParams = new URLSearchParams(searchParams.toString()); + nextParams.delete(key); + for (const value of values) { + if (value) { + nextParams.append(key, value); + } + } + nextParams.delete("page"); + + startTransition(() => { + const target = nextParams.toString() + ? `${pathname}?${nextParams.toString()}` + : pathname; + router.replace(target, { scroll: false }); + }); + }, + [searchParams, pathname, router], + ); + const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? ""); const currentSearchParam = searchParams.get("q") ?? ""; @@ -205,7 +384,6 @@ export function TransactionsFilters({ nextParams.set("pageSize", pageSizeValue); } setSearchValue(""); - setCategoryOpen(false); startTransition(() => { const target = nextParams.toString() ? `${pathname}?${nextParams.toString()}` @@ -214,56 +392,79 @@ export function TransactionsFilters({ }); }; - const payerSelectOptions = payerOptions.map((option) => ({ - value: option.slug, - label: option.label, - avatarUrl: option.avatarUrl, - })); + const conditionOptions = useMemo( + () => + TRANSACTION_CONDITIONS.map((value) => ({ + value: slugify(value), + label: value, + render: , + })), + [], + ); - const accountOptions = accountCardOptions - .filter((option) => option.kind === "conta") - .map((option) => ({ - value: option.slug, - label: option.label, - logo: option.logo, - })); + const paymentOptions = useMemo( + () => + PAYMENT_METHODS.map((value) => ({ + value: slugify(value), + label: value, + render: , + })), + [], + ); - const cardOptions = accountCardOptions - .filter((option) => option.kind === "cartao") - .map((option) => ({ - value: option.slug, - label: option.label, - logo: option.logo, - })); + const payerMultiOptions = useMemo( + () => + payerOptions.map((option) => ({ + value: option.slug, + label: option.label, + render: ( + + ), + })), + [payerOptions], + ); - const categoryValue = getParamValue("category"); - const selectedCategory = - categoryValue !== FILTER_EMPTY_VALUE - ? categoryOptions.find((option) => option.slug === categoryValue) - : null; + const categoryMultiOptions = useMemo( + () => + categoryOptions.map((option) => ({ + value: option.slug, + label: option.label, + render: ( + + ), + })), + [categoryOptions], + ); - const payerValue = getParamValue("payer"); - const selectedPayer = - payerValue !== FILTER_EMPTY_VALUE - ? payerOptions.find((option) => option.slug === payerValue) - : null; + const accountCardMultiOptions = useMemo( + () => + accountCardOptions.map((option) => ({ + value: option.slug, + label: option.label, + group: option.kind === "cartao" ? "Cartões" : "Contas", + render: ( + + ), + })), + [accountCardOptions], + ); - const accountCardValue = getParamValue("accountCard"); - const selectedAccountCard = - accountCardValue !== FILTER_EMPTY_VALUE - ? accountCardOptions.find((option) => option.slug === accountCardValue) - : null; - - const [categoryOpen, setCategoryOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const hasActiveFilters = searchParams.get("type") || - searchParams.get("condition") || - searchParams.get("payment") || - searchParams.get("payer") || - searchParams.get("category") || - searchParams.get("accountCard") || + searchParams.getAll("condition").length > 0 || + searchParams.getAll("payment").length > 0 || + searchParams.getAll("payer").length > 0 || + searchParams.getAll("category").length > 0 || + searchParams.getAll("accountCard").length > 0 || searchParams.get("settled") || searchParams.get("hasAttachment") || searchParams.get("isDivided"); @@ -280,13 +481,28 @@ export function TransactionsFilters({ className, )} > - setSearchValue(event.target.value)} - placeholder="Buscar" - aria-label="Buscar lançamentos" - className="w-full md:w-[250px] text-sm border-dashed" - /> +
+ setSearchValue(event.target.value)} + placeholder="Buscar" + aria-label="Buscar lançamentos" + className={cn( + "w-full text-sm border-dashed", + searchValue.length > 0 && "pr-8", + )} + /> + {searchValue.length > 0 ? ( + + ) : null} +
{exportButton && ( @@ -348,20 +564,14 @@ export function TransactionsFilters({ - ({ - value: slugify(v), - label: v, - }))} - widthClass="w-full border-dashed" + options={conditionOptions} + selected={getParamValues("condition")} + onChange={(values) => + handleMultiFilterChange("condition", values) + } disabled={isPending} - getParamValue={getParamValue} - onChange={handleFilterChange} - renderContent={(label) => ( - - )} />
@@ -369,195 +579,61 @@ export function TransactionsFilters({ - ({ - value: slugify(v), - label: v, - }))} - widthClass="w-full border-dashed" + + handleMultiFilterChange("payment", values) + } disabled={isPending} - getParamValue={getParamValue} - onChange={handleFilterChange} - renderContent={(label) => ( - - )} />
- + searchable + searchPlaceholder="Buscar pessoa..." + />
- - - - - - - - - Nada encontrado. - - { - handleFilterChange("category", null); - setCategoryOpen(false); - }} - > - Todas - {categoryValue === FILTER_EMPTY_VALUE ? ( - - ) : null} - - {categoryOptions.map((option) => ( - { - handleFilterChange("category", option.slug); - setCategoryOpen(false); - }} - > - - {categoryValue === option.slug ? ( - - ) : null} - - ))} - - - - - + + handleMultiFilterChange("category", values) + } + disabled={isPending} + searchable + searchPlaceholder="Buscar categoria..." + />
- + searchable + searchPlaceholder="Buscar conta ou cartão..." + groupOrder={["Contas", "Cartões"]} + />
diff --git a/src/features/transactions/components/table/transactions-table.tsx b/src/features/transactions/components/table/transactions-table.tsx index c893496..b3493c4 100644 --- a/src/features/transactions/components/table/transactions-table.tsx +++ b/src/features/transactions/components/table/transactions-table.tsx @@ -1,6 +1,5 @@ "use client"; import { - RiAddFill, RiArrowLeftRightLine, RiFileExcel2Line, RiFlashlightFill, @@ -16,7 +15,7 @@ import { type VisibilityState, } from "@tanstack/react-table"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useMemo, useState } from "react"; +import { type ReactNode, useMemo, useState } from "react"; import type { TransactionsExportContext, TransactionsPaginationState, @@ -61,7 +60,7 @@ type TransactionsTableProps = { selectedPeriod?: string; pagination?: TransactionsPaginationState; exportContext?: TransactionsExportContext; - onCreate?: (type: "Despesa" | "Receita") => void; + createSlot?: ReactNode; onMassAdd?: () => void; onEdit?: (item: TransactionItem) => void; onCopy?: (item: TransactionItem) => void; @@ -90,7 +89,7 @@ export function TransactionsTable({ selectedPeriod, pagination: serverPagination, exportContext, - onCreate, + createSlot, onMassAdd, onEdit, onCopy, @@ -253,32 +252,15 @@ export function TransactionsTable({ }; const showTopControls = - Boolean(onCreate) || Boolean(onMassAdd) || showFilters; + Boolean(createSlot) || Boolean(onMassAdd) || showFilters; return ( {showTopControls ? (
- {onCreate || onMassAdd ? ( + {createSlot || onMassAdd ? (
- {onCreate ? ( - <> - - - - ) : null} + {createSlot} {onMassAdd ? ( diff --git a/src/features/transactions/lib/export-types.ts b/src/features/transactions/lib/export-types.ts index bf3f672..c776f2b 100644 --- a/src/features/transactions/lib/export-types.ts +++ b/src/features/transactions/lib/export-types.ts @@ -1,10 +1,10 @@ type TransactionExportFilters = { transactionFilter: string | null; - conditionFilter: string | null; - paymentFilter: string | null; - payerFilter: string | null; - categoryFilter: string | null; - accountCardFilter: string | null; + conditionFilters: string[]; + paymentFilters: string[]; + payerFilters: string[]; + categoryFilters: string[]; + accountCardFilters: string[]; searchFilter: string | null; settledFilter: string | null; attachmentFilter: string | null; diff --git a/src/features/transactions/lib/page-helpers.ts b/src/features/transactions/lib/page-helpers.ts index 2f8e767..ea9deb0 100644 --- a/src/features/transactions/lib/page-helpers.ts +++ b/src/features/transactions/lib/page-helpers.ts @@ -1,5 +1,5 @@ import type { SQL } from "drizzle-orm"; -import { and, eq, ilike, isNotNull, or, sql } from "drizzle-orm"; +import { and, eq, ilike, inArray, isNotNull, or, sql } from "drizzle-orm"; import { cards, type categories, @@ -37,11 +37,11 @@ const TRANSACTIONS_PAGE_SIZE_OPTIONS = [5, 10, 20, 30, 40, 50, 100]; export type TransactionSearchFilters = { transactionFilter: string | null; - conditionFilter: string | null; - paymentFilter: string | null; - payerFilter: string | null; - categoryFilter: string | null; - accountCardFilter: string | null; + conditionFilters: string[]; + paymentFilters: string[]; + payerFilters: string[]; + categoryFilters: string[]; + accountCardFilters: string[]; searchFilter: string | null; settledFilter: string | null; attachmentFilter: string | null; @@ -123,15 +123,27 @@ export const getSingleParam = ( return Array.isArray(value) ? (value[0] ?? null) : value; }; +export const getMultiParam = ( + params: ResolvedSearchParams, + key: string, +): string[] => { + const value = params?.[key]; + if (!value) { + return []; + } + const list = Array.isArray(value) ? value : [value]; + return list.filter((item): item is string => Boolean(item)); +}; + export const extractTransactionSearchFilters = ( params: ResolvedSearchParams, ): TransactionSearchFilters => ({ transactionFilter: getSingleParam(params, "type"), - conditionFilter: getSingleParam(params, "condition"), - paymentFilter: getSingleParam(params, "payment"), - payerFilter: getSingleParam(params, "payer"), - categoryFilter: getSingleParam(params, "category"), - accountCardFilter: getSingleParam(params, "accountCard"), + conditionFilters: getMultiParam(params, "condition"), + paymentFilters: getMultiParam(params, "payment"), + payerFilters: getMultiParam(params, "payer"), + categoryFilters: getMultiParam(params, "category"), + accountCardFilters: getMultiParam(params, "accountCard"), searchFilter: getSingleParam(params, "q"), settledFilter: getSingleParam(params, "settled"), attachmentFilter: getSingleParam(params, "hasAttachment"), @@ -354,41 +366,63 @@ export const buildTransactionWhere = ({ where.push(eq(transactions.transactionType, typeValue)); } - const conditionValue = - conditionSlugToValue[filters.conditionFilter ?? ""] ?? null; - if (isValidCondition(conditionValue)) { - where.push(eq(transactions.condition, conditionValue)); + const conditionValues = filters.conditionFilters + .map((slug) => conditionSlugToValue[slug] ?? null) + .filter(isValidCondition); + if (conditionValues.length > 0) { + where.push(inArray(transactions.condition, conditionValues)); } - const paymentValue = paymentSlugToValue[filters.paymentFilter ?? ""] ?? null; - if (isValidPaymentMethod(paymentValue)) { - where.push(eq(transactions.paymentMethod, paymentValue)); + const paymentValues = filters.paymentFilters + .map((slug) => paymentSlugToValue[slug] ?? null) + .filter(isValidPaymentMethod); + if (paymentValues.length > 0) { + where.push(inArray(transactions.paymentMethod, paymentValues)); } - if (!payerId && filters.payerFilter) { - const id = slugMaps.payer.get(filters.payerFilter); - if (id) { - where.push(eq(transactions.payerId, id)); + if (!payerId && filters.payerFilters.length > 0) { + const ids = filters.payerFilters + .map((slug) => slugMaps.payer.get(slug)) + .filter((id): id is string => Boolean(id)); + if (ids.length > 0) { + where.push(inArray(transactions.payerId, ids)); } } - if (filters.categoryFilter) { - const id = slugMaps.category.get(filters.categoryFilter); - if (id) { - where.push(eq(transactions.categoryId, id)); + if (filters.categoryFilters.length > 0) { + const ids = filters.categoryFilters + .map((slug) => slugMaps.category.get(slug)) + .filter((id): id is string => Boolean(id)); + if (ids.length > 0) { + where.push(inArray(transactions.categoryId, ids)); } } - if (filters.accountCardFilter) { - const accountId = slugMaps.financialAccount.get(filters.accountCardFilter); - const relatedCardId = accountId - ? null - : slugMaps.card.get(filters.accountCardFilter); - if (accountId) { - where.push(eq(transactions.accountId, accountId)); + if (filters.accountCardFilters.length > 0) { + const accountIds: string[] = []; + const cardIds: string[] = []; + for (const slug of filters.accountCardFilters) { + const accountId = slugMaps.financialAccount.get(slug); + if (accountId) { + accountIds.push(accountId); + continue; + } + const cardId = slugMaps.card.get(slug); + if (cardId) { + cardIds.push(cardId); + } } - if (!accountId && relatedCardId) { - where.push(eq(transactions.cardId, relatedCardId)); + if (accountIds.length > 0 && cardIds.length > 0) { + where.push( + or( + inArray(transactions.accountId, accountIds), + inArray(transactions.cardId, cardIds), + ) as SQL, + ); + } else if (accountIds.length > 0) { + where.push(inArray(transactions.accountId, accountIds)); + } else if (cardIds.length > 0) { + where.push(inArray(transactions.cardId, cardIds)); } } diff --git a/src/shared/components/navigation/navbar/navbar-user.tsx b/src/shared/components/navigation/navbar/navbar-user.tsx index 20a9098..3156888 100644 --- a/src/shared/components/navigation/navbar/navbar-user.tsx +++ b/src/shared/components/navigation/navbar/navbar-user.tsx @@ -190,7 +190,7 @@ export function NavbarUser({ > - Atualização {updateCheck.latestVersion} disponível + Versão {updateCheck.latestVersion} disponível )} diff --git a/src/shared/components/ui/checkbox.tsx b/src/shared/components/ui/checkbox.tsx index c7eee60..4011d04 100644 --- a/src/shared/components/ui/checkbox.tsx +++ b/src/shared/components/ui/checkbox.tsx @@ -22,8 +22,8 @@ function Checkbox({ data-slot="checkbox-indicator" className="grid place-content-center text-current transition-none" > - - + + ); diff --git a/src/shared/components/ui/dialog.tsx b/src/shared/components/ui/dialog.tsx index 962e96a..f8b12b4 100644 --- a/src/shared/components/ui/dialog.tsx +++ b/src/shared/components/ui/dialog.tsx @@ -36,7 +36,10 @@ function DialogOverlay({ return ( ); @@ -56,7 +59,7 @@ function DialogContent({