docs: expandir documentação do README e adicionar importação em massa de lançamentos

- Expande README.md com estatísticas detalhadas do projeto (200 componentes, 15+ tabelas, 20+ widgets)
  - Adiciona descrição completa da stack técnica e versões
  - Documenta estrutura de diretórios de forma abrangente
  - Inclui diagramas de schema de banco de dados e fluxos de dados
  - Adiciona seção de destaques e funcionalidades recentes
  - Implementa diálogo de importação em massa de lançamentos (bulk-import-dialog.tsx)
  - Adiciona fontes AISans (Regular e Semibold) ao projeto
  - Remove classe bg-muted das páginas de autenticação
  - Adiciona /docs ao .gitignore
  - Limpa código não utilizado em componentes de lançamentos e páginas do dashboard
  - Atualiza dependências no package.json
This commit is contained in:
Felipe Coutinho
2026-01-05 13:01:18 +00:00
parent 4237062bde
commit 901e423959
24 changed files with 1342 additions and 199 deletions

1
.gitignore vendored
View File

@@ -133,3 +133,4 @@ yarn.lock # Se usa pnpm, não precisa do yarn lock
local/
scratch/
playground/
/docs

586
README.md
View File

@@ -48,6 +48,17 @@
A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartões, gastos e receitas de forma clara. Se isso for útil pra você também, fique à vontade para usar e contribuir.
### 📊 Estatísticas do Projeto
- **~200 componentes React** organizados por feature
- **15+ tabelas de banco de dados** com relações complexas
- **20+ widgets** no dashboard principal
- **18+ queries paralelas** otimizadas para performance
- **736 linhas** de schema Drizzle ORM
- **Docker multi-stage** com imagem final de ~200MB
- **100% TypeScript** com strict mode
- **Self-hosted** - seus dados, seu controle
> 💡 **Licença Não-Comercial:** Este projeto é gratuito para uso pessoal, mas não pode ser usado comercialmente. Veja mais detalhes na seção [Licença](#-licença).
### ⚠️ Avisos importantes
@@ -78,13 +89,15 @@ Se você não se importa em dedicar alguns minutos por dia (ou semana) para mant
- Registre suas contas bancárias, cartões e dinheiro em espécie
- Adicione receitas, despesas e transferências entre contas
- Organize tudo por categorias (moradia, alimentação, transporte, etc.)
- Veja o saldo atual de cada conta
- Veja o saldo atual de cada conta e extratos detalhados
- Importação em massa de lançamentos via texto
📊 **Relatórios e gráficos**
- Dashboard com resumo mensal das suas finanças
- Gráficos de evolução do patrimônio
- Comparação de gastos por categoria
- Relatórios detalhados de categorias com histórico
- Entenda pra onde seu dinheiro está indo
💳 **Faturas de cartão de crédito**
@@ -92,11 +105,54 @@ Se você não se importa em dedicar alguns minutos por dia (ou semana) para mant
- Cadastre seus cartões e acompanhe as faturas
- Veja o que ainda não foi fechado na fatura atual
- Controle de limites e vencimentos
- Visualização de faturas por período
🎯 **Orçamentos**
- Defina quanto quer gastar por categoria no mês
- Acompanhe se está dentro do planejado
- Indicadores visuais de progresso do orçamento
💸 **Parcelamentos avançados**
- Controle completo de compras parceladas
- Antecipação de parcelas com cálculo de desconto
- Análise consolidada de parcelas em aberto
- Rastreamento de séries de parcelas
🤖 **Insights com IA**
- Análises financeiras geradas por IA (Claude, GPT, Gemini)
- Insights personalizados sobre seus gastos
- Recomendações e alertas inteligentes
- Histórico de insights salvos por período
👥 **Gestão colaborativa**
- Cadastro de pagadores/recebedores
- Sistema de compartilhamento com permissões (admin/viewer)
- Notificações automáticas por e-mail
- Colaboração em lançamentos compartilhados
📝 **Anotações e tarefas**
- Notas de texto para organização
- Listas de tarefas com checkboxes
- Sistema de arquivamento
- Anexação de anotações a lançamentos
📅 **Visualização em calendário**
- Visão mensal de todos os lançamentos
- Navegação intuitiva por data
- Filtros e organização temporal
⚙️ **Preferências e personalização**
- Tema claro/escuro
- Modo privacidade (oculta valores)
- Customização de comportamento (magnetlines, etc.)
- Configurações de usuário personalizadas
### Stack técnica
@@ -119,26 +175,82 @@ O projeto é open source, seus dados ficam no seu controle (pode rodar localment
### 🔐 Autenticação
- Better Auth integrado
- OAuth (Google, GitHub)
- Email magic links
- Session management
- Better Auth 1.4.10 integrado
- OAuth (Google)
- Autenticação por email/senha
- Session management com tokens
- Protected routes via middleware
- Verificação de email
### 🗄️ Banco de Dados
- PostgreSQL 18 (última versão estável)
- Drizzle ORM com TypeScript
- Drizzle ORM 0.45 com TypeScript
- Migrations automáticas
- Drizzle Studio (UI visual para DB)
- Suporte para banco local (Docker) ou remoto (Supabase, Neon, etc)
- Índices otimizados para performance
- Relações complexas e integridade referencial
### 💼 Gestão Financeira
- Controle completo de contas bancárias
- Gerenciamento de cartões de crédito
- Lançamentos com suporte a:
- Receitas e despesas
- Transferências entre contas
- Parcelamentos com séries
- Antecipação de parcelas
- Recorrências
- Categorização flexível
- Orçamentos mensais por categoria
- Faturas de cartão de crédito
### 🤖 Inteligência Artificial
- Integração com múltiplos providers:
- Anthropic Claude
- OpenAI GPT
- Google Gemini
- OpenRouter
- Análises financeiras personalizadas
- Insights salvos e histórico
### 👥 Colaboração
- Sistema de pagadores/recebedores
- Compartilhamento com permissões granulares
- Notificações por email (Resend)
- Códigos de compartilhamento únicos
- Multi-usuário com isolamento de dados
### 📊 Relatórios e Analytics
- Dashboard interativo com 20+ widgets
- Relatórios detalhados de categorias
- Histórico de transações
- Análise de parcelas consolidada
- Gráficos com Recharts
- Exportação de dados (PDF, Excel)
### 🎨 Interface
- shadcn/ui components
- shadcn/ui components (Radix UI)
- Tailwind CSS v4
- Dark mode suportado
- Animações com Framer Motion
- Dark mode com next-themes
- Animações fluidas com Motion
- Responsive design
- Modo privacidade (oculta valores)
- Componentes acessíveis (ARIA)
### 📝 Produtividade
- Sistema de anotações e tarefas
- Calendário de transações
- Importação em massa
- Calculadora integrada
- Preferências personalizáveis
- Changelog integrado
### 🐳 Docker
@@ -147,14 +259,18 @@ O projeto é open source, seus dados ficam no seu controle (pode rodar localment
- Volumes persistentes
- Network isolada
- Scripts npm facilitados
- Imagem final ~200MB
### 🧪 Desenvolvimento
- Next.js 16 com App Router
- Next.js 16.1 com App Router
- Turbopack (fast refresh)
- TypeScript 5.9
- ESLint + Prettier
- React 19
- TypeScript 5.9 (strict mode)
- ESLint 9
- React 19.2 (com Compiler)
- Server Actions
- Parallel data fetching
- Streaming SSR
---
@@ -162,34 +278,52 @@ O projeto é open source, seus dados ficam no seu controle (pode rodar localment
### Frontend
- **Framework:** Next.js 16 (App Router)
- **Linguagem:** TypeScript 5.9
- **UI Library:** React 19
- **Styling:** Tailwind CSS v4
- **Framework:** Next.js 16.1.1 (App Router)
- **Linguagem:** TypeScript 5.9.3
- **UI Library:** React 19.2.3
- **Styling:** Tailwind CSS 4.1.18
- **Components:** shadcn/ui (Radix UI)
- **Icons:** Remixicon
- **Animations:** Framer Motion
- **Icons:** Remixicon 4.8.0
- **Animations:** Motion 12.23.26
- **Tables:** TanStack React Table 8.21.3
- **Charts:** Recharts 3.6.0
- **Forms:** React Hook Form + Zod 4.3.4
- **Theme:** next-themes 0.4.6
### Backend
- **Runtime:** Node.js 22
- **Database:** PostgreSQL 18
- **ORM:** Drizzle ORM
- **Auth:** Better Auth
- **Email:** Resend
- **ORM:** Drizzle ORM 0.45.1
- **Database Driver:** pg 8.16.3
- **Auth:** Better Auth 1.4.10
- **Email:** Resend 6.6.0
- **Validation:** Zod 4.3.4
### AI Integration (Opcional)
- **AI SDK:** Vercel AI SDK 6.0.6
- **Anthropic:** Claude (via @ai-sdk/anthropic 3.0.2)
- **OpenAI:** GPT (via @ai-sdk/openai 3.0.2)
- **Google:** Gemini (via @ai-sdk/google 3.0.2)
- **OpenRouter:** via @openrouter/ai-sdk-provider 1.5.4
### Utilities
- **Date Handling:** date-fns 4.1.0
- **Class Management:** clsx 2.1.1 + tailwind-merge 3.4.0
- **PDF Export:** jspdf 4.0.0 + jspdf-autotable 5.0.2
- **Excel Export:** xlsx 0.18.5
- **Toast Notifications:** sonner 2.0.7
- **Command Palette:** cmdk 1.1.1
### DevOps
- **Containerization:** Docker + Docker Compose
- **Package Manager:** pnpm
- **Build Tool:** Turbopack
### AI Integration (Opcional)
- Anthropic (Claude)
- OpenAI (GPT)
- Google Gemini
- OpenRouter
- **Linting:** ESLint 9.39.2
- **Analytics:** Vercel Analytics + Speed Insights
---
@@ -732,43 +866,253 @@ psql $DATABASE_URL < backup.sql
```
opensheets/
├── app/ # Next.js App Router
│ ├── api/ # API Routes
│ │ ├── auth/ # Better Auth endpoints
│ │ └── health/ # Health check
│ ├── (dashboard)/ # Protected routes (com auth)
└── layout.tsx # Root layout
├── app/ # Next.js App Router
│ ├── api/ # API Routes
│ │ ├── auth/[...all]/ # Better Auth endpoints
│ │ └── health/ # Health check endpoint
│ ├── (auth)/ # Rotas públicas de autenticação
│ ├── login/ # Página de login
│ │ └── signup/ # Página de cadastro
│ ├── (dashboard)/ # Rotas protegidas (requer auth)
│ │ ├── dashboard/ # Dashboard principal
│ │ │ └── analise-parcelas/ # Análise de parcelas
│ │ ├── lancamentos/ # Lançamentos/transações
│ │ ├── contas/ # Contas bancárias
│ │ │ └── [contaId]/extrato # Extrato da conta
│ │ ├── cartoes/ # Cartões de crédito
│ │ │ └── [cartaoId]/fatura # Fatura do cartão
│ │ ├── categorias/ # Categorias
│ │ │ ├── historico/ # Histórico de categorias
│ │ │ └── [categoryId]/ # Detalhes da categoria
│ │ ├── pagadores/ # Pagadores/recebedores
│ │ │ └── [pagadorId]/ # Detalhes do pagador
│ │ ├── orcamentos/ # Orçamentos mensais
│ │ ├── anotacoes/ # Anotações e tarefas
│ │ │ └── arquivadas/ # Anotações arquivadas
│ │ ├── insights/ # Insights de IA
│ │ ├── relatorios/ # Relatórios
│ │ │ └── categorias/ # Relatório de categorias
│ │ ├── calendario/ # Visão de calendário
│ │ ├── changelog/ # Histórico de mudanças
│ │ └── ajustes/ # Configurações
│ ├── (landing-page)/ # Página inicial pública
│ ├── layout.tsx # Root layout
│ └── globals.css # Estilos globais (Tailwind)
├── components/ # React Components
│ ├── ui/ # shadcn/ui components
└── ... # Feature components
├── components/ # React Components (~200 arquivos)
│ ├── ui/ # shadcn/ui base components
│ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── table.tsx
│ │ └── ... (40+ componentes)
│ ├── lancamentos/ # Componentes de lançamentos
│ │ ├── dialogs/ # Diálogos (criar, editar, detalhes)
│ │ ├── table/ # Tabela com filtros avançados
│ │ ├── shared/ # Componentes compartilhados
│ │ └── page/ # Página completa
│ ├── dashboard/ # Widgets do dashboard (20+ widgets)
│ │ ├── accounts-summary.tsx
│ │ ├── income-expense-chart.tsx
│ │ ├── category-breakdown.tsx
│ │ └── ...
│ ├── cartoes/ # Componentes de cartões
│ ├── contas/ # Componentes de contas
│ ├── categorias/ # Componentes de categorias
│ ├── pagadores/ # Componentes de pagadores
│ ├── orcamentos/ # Componentes de orçamentos
│ ├── anotacoes/ # Componentes de anotações
│ ├── insights/ # Componentes de insights IA
│ ├── relatorios/ # Componentes de relatórios
│ ├── calendario/ # Componentes de calendário
│ ├── calculadora/ # Calculadora integrada
│ ├── sidebar/ # Sidebar de navegação
│ ├── skeletons/ # Estados de loading
│ └── month-picker/ # Seletor de mês/período
├── lib/ # Shared utilities
│ ├── db.ts # Drizzle client
├── auth.ts # Better Auth server
└── auth-client.ts # Better Auth client
├── lib/ # Lógica de negócio e utilitários
│ ├── auth/
│ ├── config.ts # Configuração Better Auth
│ ├── server.ts # Auth helpers (servidor)
│ │ └── client.ts # Auth client
│ ├── db.ts # Conexão Drizzle ORM
│ ├── dashboard/ # Fetchers do dashboard
│ │ ├── fetch-dashboard-data.ts # Fetcher principal (18+ queries paralelas)
│ │ ├── accounts.ts
│ │ ├── metrics.ts
│ │ └── ... (15+ fetchers especializados)
│ ├── lancamentos/ # Lógica de lançamentos
│ │ ├── constants.ts
│ │ ├── form-helpers.ts
│ │ ├── categoria-helpers.ts
│ │ └── formatting-helpers.ts
│ ├── actions/ # Helpers de Server Actions
│ │ ├── helpers.ts # Error handling, revalidation
│ │ └── types.ts # ActionResult types
│ ├── schemas/ # Zod validation schemas
│ ├── utils/ # Utilitários gerais
│ │ ├── currency.ts # Formatação de moeda
│ │ ├── date.ts # Manipulação de datas
│ │ ├── period/ # Utilitários de período (YYYY-MM)
│ │ └── calculator.ts # Lógica da calculadora
│ └── ... # Outros helpers
├── db/ # Drizzle schema
│ └── schema.ts # Database schema
├── db/ # Banco de dados
│ └── schema.ts # Schema Drizzle (736 linhas)
│ # 15+ tabelas com relações complexas
├── drizzle/ # Generated migrations
── migrations/
├── drizzle/ # Migrations geradas
── migrations/
│ └── meta/
├── hooks/ # Custom React hooks
├── public/ # Static assets
├── scripts/ # Utility scripts
│ ├── setup-env.sh # Env setup automation
│ └── postgres/init.sql # PostgreSQL init script
├── hooks/ # React Hooks customizados
│ ├── use-month-period.ts # Gerenciamento de período
│ ├── use-form-state.ts # Estado de formulários
│ ├── use-calculator-state.ts # Estado da calculadora
│ └── use-mobile.ts # Detecção mobile
├── docker/ # Docker configs
── postgres/init.sql
├── public/ # Assets estáticos
── logos/ # Logos de bancos
│ ├── bandeiras/ # Bandeiras de cartões
│ ├── icones/ # Ícones de categorias
│ ├── avatares/ # Avatares de usuários
│ ├── providers/ # Logos de providers
│ └── fonts/ # Fontes customizadas
├── Dockerfile # Production build
├── docker-compose.yml # Docker orchestration
├── next.config.ts # Next.js config
├── drizzle.config.ts # Drizzle ORM config
├── tailwind.config.ts # Tailwind config
└── tsconfig.json # TypeScript config
├── scripts/ # Scripts utilitários
│ ├── setup-env.sh # Setup de variáveis de ambiente
│ └── postgres/
│ ├── init.sql # Script de inicialização do PostgreSQL
│ └── enable-extensions.ts # Habilita extensões do PostgreSQL
├── Dockerfile # Multi-stage build otimizado
├── docker-compose.yml # Orquestração Docker
├── next.config.ts # Configuração Next.js
├── drizzle.config.ts # Configuração Drizzle ORM
├── tailwind.config.ts # Configuração Tailwind CSS
├── postcss.config.mjs # PostCSS config
├── components.json # shadcn/ui config
├── eslint.config.mjs # ESLint config
├── tsconfig.json # TypeScript config
├── package.json # Dependências e scripts
├── .env.example # Template de variáveis de ambiente
├── CLAUDE.md # Guia completo para IA
└── README.md # Este arquivo
```
### Principais Diretórios
| Diretório | Descrição | Arquivos |
| ------------------ | ------------------------------------------- | -------- |
| `app/(dashboard)/` | Páginas protegidas da aplicação | ~50 |
| `components/` | Componentes React reutilizáveis | ~200 |
| `lib/` | Lógica de negócio, helpers e utilitários | ~80 |
| `db/` | Schema do banco de dados | 1 |
| `hooks/` | React hooks customizados | ~10 |
| `public/` | Assets estáticos (imagens, ícones, logos) | ~100 |
| `scripts/` | Scripts de automação | ~5 |
### Estrutura do Banco de Dados
O OpenSheets possui um schema robusto com 15+ tabelas e relações complexas:
```
┌─────────────────────────────────────────────────────────────────┐
│ TABELAS PRINCIPAIS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ user user_preferences │
│ ├── id ├── id │
│ ├── name ├── user_id → user.id │
│ ├── email ├── disable_magnetlines │
│ └── ... └── ... │
│ │
│ contas cartoes │
│ ├── id ├── id │
│ ├── user_id → user.id ├── user_id → user.id │
│ ├── nome ├── conta_id → contas.id │
│ ├── tipo_conta ├── nome │
│ ├── saldo_inicial ├── bandeira │
│ └── ... ├── dt_fechamento │
│ ├── dt_vencimento │
│ └── ... │
│ │
│ categorias pagadores │
│ ├── id ├── id │
│ ├── user_id → user.id ├── user_id → user.id │
│ ├── nome ├── nome │
│ ├── tipo ├── email │
│ ├── icone ├── share_code (único) │
│ └── ... ├── role │
│ └── ... │
│ │
│ pagador_shares │
│ ├── id │
│ ├── pagador_id → pagadores.id │
│ ├── shared_with_user_id → user.id │
│ ├── created_by_user_id → user.id │
│ ├── permission (read/write) │
│ └── ... │
│ │
│ lancamentos (TABELA PRINCIPAL) │
│ ├── id │
│ ├── user_id → user.id │
│ ├── conta_id → contas.id │
│ ├── cartao_id → cartoes.id │
│ ├── categoria_id → categorias.id │
│ ├── pagador_id → pagadores.id │
│ ├── nome │
│ ├── valor │
│ ├── tipo_transacao (receita/despesa/transferencia) │
│ ├── forma_pagamento │
│ ├── condicao (aberto/realizado/cancelado) │
│ ├── data_compra │
│ ├── periodo (YYYY-MM) │
│ ├── qtde_parcela │
│ ├── parcela_atual │
│ ├── series_id (agrupa parcelas) │
│ ├── transfer_id (agrupa transferências) │
│ ├── antecipado (boolean) │
│ ├── antecipacao_id → installment_anticipations.id │
│ └── ... │
│ │
│ installment_anticipations │
│ ├── id │
│ ├── user_id → user.id │
│ ├── series_id │
│ ├── lancamento_id → lancamentos.id │
│ ├── periodo_antecipacao │
│ ├── parcelas_antecipadas (JSONB array) │
│ ├── valor_total │
│ ├── desconto │
│ └── ... │
│ │
│ faturas orcamentos │
│ ├── id ├── id │
│ ├── user_id → user.id ├── user_id → user.id │
│ ├── cartao_id → cartoes ├── categoria_id → categorias.id │
│ ├── periodo ├── valor │
│ ├── status_pagamento ├── periodo │
│ └── ... └── ... │
│ │
│ anotacoes saved_insights │
│ ├── id ├── id │
│ ├── user_id → user.id ├── user_id → user.id │
│ ├── titulo ├── period │
│ ├── descricao ├── model_id │
│ ├── tipo (nota/tarefa) ├── data (JSON) │
│ ├── tasks (JSON) ├── created_at │
│ ├── arquivada └── updated_at │
│ └── ... │
│ │
└─────────────────────────────────────────────────────────────────┘
ÍNDICES OTIMIZADOS:
• user_id + period (queries do dashboard)
• user_id + purchase_date (ordenação por data)
• series_id (agrupamento de parcelas)
• cartao_id + period (faturas)
• user_id + condition (filtros de condição)
• share_code (compartilhamento)
```
### Fluxo de Autenticação
@@ -778,15 +1122,45 @@ opensheets/
2. middleware.ts verifica sessão (Better Auth)
3. Se não autenticado → redirect /auth
3. Se não autenticado → redirect /login
4. Usuário faz login (OAuth ou email)
4. Usuário faz login (OAuth Google ou email/senha)
5. Better Auth valida e cria sessão
5. Better Auth valida credenciais e cria sessão
6. Cookie de sessão é salvo
6. Cookie de sessão é salvo no navegador
7. Usuário acessa rota protegida ✅
7. Inicialização automática de dados do usuário:
- Categorias padrão criadas
- Preferências inicializadas
8. Usuário acessa dashboard ✅
```
### Fluxo de Dados (Dashboard)
```
1. Usuário acessa /dashboard
2. Server Component busca userId da sessão
3. fetchDashboardData() executa 18+ queries em paralelo:
- Métricas (receitas, despesas, saldo)
- Contas e seus saldos
- Cartões e faturas
- Lançamentos recentes
- Gráficos de categorias
- Parcelas em aberto
- Orçamentos vs. realizado
- ... e mais 10+ datasets
4. Dados retornados em ~200-500ms (otimizado)
5. Server Component renderiza com dados
6. Client Components hidratam com interatividade
7. Dashboard totalmente funcional ✅
```
### Fluxo de Build (Docker)
@@ -803,6 +1177,90 @@ opensheets/
---
## 🆕 Destaques e Funcionalidades Recentes
O OpenSheets está em desenvolvimento ativo. Aqui estão algumas das funcionalidades mais interessantes já implementadas:
### 💸 Sistema Avançado de Parcelamentos
O controle de parcelamentos vai além do básico:
- **Séries de parcelas:** Agrupa todas as parcelas de uma compra
- **Antecipação inteligente:** Antecipe parcelas com cálculo automático de desconto
- **Análise consolidada:** Veja todas as parcelas em aberto e o impacto nos próximos meses
- **Rastreamento completo:** Histórico de todas as operações de antecipação
### 🤖 Insights Financeiros com IA
Integração robusta com múltiplos providers de IA:
- **Multi-provider:** Escolha entre Claude, GPT, Gemini ou OpenRouter
- **Análises personalizadas:** IA analisa seus padrões de gastos e sugere melhorias
- **Histórico persistente:** Insights salvos por período para acompanhamento
- **Contextual:** A IA tem acesso aos seus dados financeiros para análises precisas
### 👥 Colaboração e Compartilhamento
Sistema completo para gestão colaborativa de finanças:
- **Pagadores compartilhados:** Compartilhe acesso a pagadores específicos
- **Permissões granulares:** Defina quem pode visualizar ou editar
- **Códigos únicos:** Cada pagador tem um código de compartilhamento exclusivo
- **Notificações automáticas:** E-mails enviados automaticamente via Resend
- **Multi-usuário seguro:** Isolamento completo de dados entre usuários
### 📊 Relatórios Detalhados
Analytics poderosos para entender suas finanças:
- **Dashboard interativo:** 20+ widgets com diferentes visualizações
- **Relatórios de categorias:** Análise profunda por categoria com histórico
- **Comparativos mensais:** Veja a evolução dos seus gastos ao longo do tempo
- **Exportações:** PDF e Excel para análise externa
- **Gráficos interativos:** Recharts com dados em tempo real
### 📝 Produtividade Integrada
Ferramentas para manter tudo organizado:
- **Anotações:** Notas de texto para lembretes e planejamentos
- **Tarefas:** Listas com checkboxes para acompanhamento
- **Arquivamento:** Mantenha o histórico sem poluir a interface
- **Calendário:** Visualize todos os lançamentos em um calendário mensal
- **Calculadora:** Calculadora integrada para planejamento rápido
### 🎨 Experiência do Usuário
Atenção aos detalhes que fazem diferença:
- **Modo privacidade:** Oculte valores sensíveis com um clique
- **Tema adaptável:** Dark/light mode com persistência
- **Preferências:** Customize o comportamento da aplicação
- **Importação em massa:** Cole múltiplos lançamentos de uma vez
- **Responsivo:** Funciona perfeitamente em desktop e mobile
### 🔒 Segurança e Performance
Construído com as melhores práticas:
- **Isolamento de dados:** Cada usuário vê apenas seus próprios dados
- **Índices otimizados:** Queries rápidas mesmo com milhares de registros
- **Server Actions:** Mutações seguras no servidor
- **Type-safety:** TypeScript strict em toda a codebase
- **Validação robusta:** Zod schemas para todos os inputs
### 📦 Developer Experience
Feito por desenvolvedores, para desenvolvedores:
- **Hot reload instantâneo:** Turbopack para desenvolvimento rápido
- **Type inference:** Drizzle ORM com tipos automáticos
- **Migrations automáticas:** Schema sync simplificado
- **Docker completo:** Ambiente reproduzível em qualquer lugar
- **Scripts facilitados:** Comandos npm para tudo
---
## 🤝 Contribuindo
Contribuições são muito bem-vindas!

View File

@@ -2,7 +2,7 @@ import { LoginForm } from "@/components/auth/login-form";
export default function LoginPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<LoginForm />
</div>

View File

@@ -2,7 +2,7 @@ import { SignupForm } from "@/components/auth/signup-form";
export default function Page() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<SignupForm />
</div>

View File

@@ -183,6 +183,7 @@ export default async function Page({ params, searchParams }: PageProps) {
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
lancamentos={lancamentosData}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}

View File

@@ -79,6 +79,7 @@ export default async function Page({ params, searchParams }: PageProps) {
transactionCount={detail.transactions.length}
/>
<LancamentosPage
currentUserId={userId}
lancamentos={detail.transactions}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}

View File

@@ -161,6 +161,7 @@ export default async function Page({ params, searchParams }: PageProps) {
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
lancamentos={lancamentosData}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}

View File

@@ -67,6 +67,7 @@ export default async function Page({ searchParams }: PageProps) {
<main className="flex flex-col gap-6">
<MonthNavigation />
<LancamentosPage
currentUserId={userId}
lancamentos={lancamentosData}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}

View File

@@ -99,6 +99,9 @@ export default async function Page({ params, searchParams }: PageProps) {
let filterSources: Awaited<
ReturnType<typeof fetchLancamentoFilterSources>
> | null = null;
let loggedUserFilterSources: Awaited<
ReturnType<typeof fetchLancamentoFilterSources>
> | null = null;
let sluggedFilters: SluggedFilters;
let slugMaps: SlugMaps;
@@ -107,6 +110,8 @@ export default async function Page({ params, searchParams }: PageProps) {
sluggedFilters = buildSluggedFilters(filterSources);
slugMaps = buildSlugMaps(sluggedFilters);
} else {
// Buscar opções do usuário logado para usar ao importar
loggedUserFilterSources = await fetchLancamentoFilterSources(userId);
sluggedFilters = {
pagadorFiltersRaw: [],
categoriaFiltersRaw: [],
@@ -170,6 +175,7 @@ export default async function Page({ params, searchParams }: PageProps) {
const pagadorSharesData = shareRows;
let optionSets: OptionSet;
let loggedUserOptionSets: OptionSet | null = null;
let effectiveSluggedFilters = sluggedFilters;
if (canEdit && filterSources) {
@@ -192,6 +198,15 @@ export default async function Page({ params, searchParams }: PageProps) {
cartaoFiltersRaw: [],
};
optionSets = buildReadOnlyOptionSets(lancamentosData, pagador);
// Construir opções do usuário logado para usar ao importar
if (loggedUserFilterSources) {
const loggedUserSluggedFilters = buildSluggedFilters(loggedUserFilterSources);
loggedUserOptionSets = buildOptionSets({
...loggedUserSluggedFilters,
pagadorRows: loggedUserFilterSources.pagadorRows,
});
}
}
const pagadorSlug =
@@ -286,6 +301,7 @@ export default async function Page({ params, searchParams }: PageProps) {
<TabsContent value="lancamentos">
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
lancamentos={lancamentosData}
pagadorOptions={optionSets.pagadorOptions}
splitPagadorOptions={optionSets.splitPagadorOptions}
@@ -299,6 +315,12 @@ export default async function Page({ params, searchParams }: PageProps) {
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
allowCreate={canEdit}
importPagadorOptions={loggedUserOptionSets?.pagadorOptions}
importSplitPagadorOptions={loggedUserOptionSets?.splitPagadorOptions}
importDefaultPagadorId={loggedUserOptionSets?.defaultPagadorId}
importContaOptions={loggedUserOptionSets?.contaOptions}
importCartaoOptions={loggedUserOptionSets?.cartaoOptions}
importCategoriaOptions={loggedUserOptionSets?.categoriaOptions}
/>
</section>
</TabsContent>

View File

@@ -20,6 +20,13 @@ import {
RiShieldCheckLine,
RiTimeLine,
RiWalletLine,
RiRobot2Line,
RiTeamLine,
RiFileTextLine,
RiDownloadCloudLine,
RiEyeOffLine,
RiFlashlightLine,
RiPercentLine,
} from "@remixicon/react";
import Image from "next/image";
import Link from "next/link";
@@ -209,7 +216,47 @@ export default async function Page() {
<p className="text-sm text-muted-foreground">
Registre suas contas bancárias, cartões e dinheiro.
Adicione receitas, despesas e transferências. Organize
por categorias.
por categorias. Extratos detalhados por conta.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiPercentLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Parcelamentos avançados
</h3>
<p className="text-sm text-muted-foreground">
Controle completo de compras parceladas. Antecipe parcelas
com cálculo automático de desconto. Veja análise
consolidada de todas as parcelas em aberto.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiRobot2Line size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Insights com IA
</h3>
<p className="text-sm text-muted-foreground">
Análises financeiras geradas por IA (Claude, GPT, Gemini).
Insights personalizados sobre seus padrões de gastos e
recomendações inteligentes.
</p>
</div>
</div>
@@ -227,8 +274,9 @@ export default async function Page() {
Relatórios e gráficos
</h3>
<p className="text-sm text-muted-foreground">
Dashboard com resumo mensal. Gráficos de evolução do
patrimônio. Entenda pra onde seu dinheiro está indo.
Dashboard com 20+ widgets interativos. Relatórios
detalhados por categoria. Gráficos de evolução e
comparativos. Exportação em PDF e Excel.
</p>
</div>
</div>
@@ -246,8 +294,29 @@ export default async function Page() {
Faturas de cartão
</h3>
<p className="text-sm text-muted-foreground">
Cadastre seus cartões e acompanhe as faturas. Veja o que
ainda não foi fechado. Controle limites e vencimentos.
Cadastre seus cartões e acompanhe as faturas por período.
Veja o que ainda não foi fechado. Controle limites,
vencimentos e fechamentos.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiTeamLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Gestão colaborativa
</h3>
<p className="text-sm text-muted-foreground">
Compartilhe pagadores com permissões granulares (admin/
viewer). Notificações automáticas por e-mail. Colabore em
lançamentos compartilhados.
</p>
</div>
</div>
@@ -262,12 +331,12 @@ export default async function Page() {
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Categorias personalizadas
Categorias e orçamentos
</h3>
<p className="text-sm text-muted-foreground">
Crie e organize suas próprias categorias. Moradia,
alimentação, transporte, ou o que fizer sentido pra
você.
Crie categorias personalizadas. Defina orçamentos mensais
e acompanhe o quanto gastou vs. planejado com indicadores
visuais.
</p>
</div>
</div>
@@ -278,16 +347,16 @@ export default async function Page() {
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiMoneyDollarCircleLine
size={24}
className="text-primary"
/>
<RiFileTextLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">Orçamentos</h3>
<h3 className="font-semibold text-lg mb-2">
Anotações e tarefas
</h3>
<p className="text-sm text-muted-foreground">
Defina quanto quer gastar por categoria no s.
Acompanhe se está dentro do planejado.
Crie notas de texto e listas de tarefas com checkboxes.
Sistema de arquivamento para manter histórico. Organize
seus planejamentos financeiros.
</p>
</div>
</div>
@@ -305,8 +374,69 @@ export default async function Page() {
Calendário financeiro
</h3>
<p className="text-sm text-muted-foreground">
Visualize suas transações em calendário mensal. Nunca
perca prazos importantes.
Visualize todas as transações em calendário mensal.
Navegação intuitiva por data. Nunca perca prazos de
pagamentos importantes.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiDownloadCloudLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Importação em massa
</h3>
<p className="text-sm text-muted-foreground">
Cole múltiplos lançamentos de uma vez. Economize tempo ao
registrar várias transações. Formatação inteligente para
facilitar a entrada de dados.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiEyeOffLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Modo privacidade
</h3>
<p className="text-sm text-muted-foreground">
Oculte valores sensíveis com um clique. Tema dark/light
adaptável. Preferências personalizáveis. Calculadora
integrada para planejamento.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border hover:border-primary/50 transition-colors">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<RiFlashlightLine size={24} className="text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">
Performance otimizada
</h3>
<p className="text-sm text-muted-foreground">
Dashboard carrega em ~200-500ms com 18+ queries paralelas.
Índices otimizados. Type-safe em toda codebase. Isolamento
completo de dados por usuário.
</p>
</div>
</div>

View File

@@ -0,0 +1,363 @@
"use client";
import { createLancamentoAction } from "@/app/(dashboard)/lancamentos/actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
import { useCallback, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import type { LancamentoItem, SelectOption } from "../types";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
} from "../select-items";
interface BulkImportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
items: LancamentoItem[];
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
defaultPagadorId?: string | null;
}
export function BulkImportDialog({
open,
onOpenChange,
items,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
defaultPagadorId,
}: BulkImportDialogProps) {
const [pagadorId, setPagadorId] = useState<string | undefined>(
defaultPagadorId ?? undefined
);
const [categoriaId, setCategoriaId] = useState<string | undefined>(undefined);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(undefined);
const [isPending, startTransition] = useTransition();
// Reset form when dialog opens/closes
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
}
onOpenChange(newOpen);
},
[onOpenChange, defaultPagadorId]
);
const categoriaGroups = useMemo(() => {
// Get unique transaction types from items
const transactionTypes = new Set(items.map((item) => item.transactionType));
// Filter categories based on transaction types
const filtered = categoriaOptions.filter((option) => {
if (!option.group) return false;
return Array.from(transactionTypes).some(
(type) => option.group?.toLowerCase() === type.toLowerCase()
);
});
return groupAndSortCategorias(filtered);
}, [categoriaOptions, items]);
const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!pagadorId) {
toast.error("Selecione o pagador.");
return;
}
if (!categoriaId) {
toast.error("Selecione a categoria.");
return;
}
startTransition(async () => {
let successCount = 0;
let errorCount = 0;
for (const item of items) {
const sanitizedAmount = Math.abs(item.amount);
// Determine payment method based on original item
const isCredit = item.paymentMethod === "Cartão de crédito";
// Validate payment method fields
if (isCredit && !cartaoId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (!isCredit && !contaId) {
toast.error("Selecione uma conta.");
return;
}
const payload = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType: item.transactionType as "Despesa" | "Receita" | "Transferência",
amount: sanitizedAmount,
condition: item.condition as "À vista" | "Parcelado" | "Recorrente",
paymentMethod: item.paymentMethod as "Cartão de crédito" | "Cartão de débito" | "Pix" | "Dinheiro" | "Boleto" | "Pré-Pago | VR/VA" | "Transferência bancária",
pagadorId,
secondaryPagadorId: undefined,
isSplit: false,
contaId: isCredit ? undefined : contaId,
cartaoId: isCredit ? cartaoId : undefined,
categoriaId,
note: item.note || undefined,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
item.condition === "Parcelado" && item.installmentCount
? Number(item.installmentCount)
: undefined,
recurrenceCount:
item.condition === "Recorrente" && item.recurrenceCount
? Number(item.recurrenceCount)
: undefined,
dueDate:
item.paymentMethod === "Boleto" && item.dueDate
? item.dueDate
: undefined,
};
const result = await createLancamentoAction(payload as any);
if (result.success) {
successCount++;
} else {
errorCount++;
console.error(`Failed to import ${item.name}:`, result.error);
}
}
if (errorCount === 0) {
toast.success(
`${successCount} ${
successCount === 1 ? "lançamento importado" : "lançamentos importados"
} com sucesso!`
);
handleOpenChange(false);
} else if (successCount > 0) {
toast.warning(
`${successCount} importados, ${errorCount} falharam. Verifique o console para detalhes.`
);
} else {
toast.error("Falha ao importar lançamentos. Verifique o console.");
}
});
},
[items, pagadorId, categoriaId, contaId, cartaoId, handleOpenChange]
);
const itemCount = items.length;
const hasCredit = items.some((item) => item.paymentMethod === "Cartão de crédito");
const hasNonCredit = items.some((item) => item.paymentMethod !== "Cartão de crédito");
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Importar Lançamentos</DialogTitle>
<DialogDescription>
Importando {itemCount} {itemCount === 1 ? "lançamento" : "lançamentos"}.
Selecione o pagador, categoria e forma de pagamento para aplicar a todos.
</DialogDescription>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="pagador">Pagador *</Label>
<Select value={pagadorId} onValueChange={setPagadorId}>
<SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione o pagador">
{pagadorId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === pagadorId
);
return selectedOption ? (
<PagadorSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="categoria">Categoria *</Label>
<Select value={categoriaId} onValueChange={setCategoriaId}>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione a categoria">
{categoriaId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === categoriaId
);
return selectedOption ? (
<CategoriaSelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
label={option.label}
icon={option.icon}
/>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>
{hasNonCredit && (
<div className="space-y-2">
<Label htmlFor="conta">
Conta {hasCredit ? "(para não cartão)" : "*"}
</Label>
<Select value={contaId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione a conta">
{contaId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{hasCredit && (
<div className="space-y-2">
<Label htmlFor="cartao">
Cartão {hasNonCredit ? "(para cartão de crédito)" : "*"}
</Label>
<Select value={cartaoId} onValueChange={setCartaoId}>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione o cartão">
{cartaoId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<DialogFooter className="gap-3">
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isPending}
>
Cancelar
</Button>
<Button type="submit" disabled={isPending}>
{isPending ? "Importando..." : "Importar"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -22,6 +22,7 @@ export interface LancamentoDialogProps {
defaultPurchaseDate?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
onBulkEditRequest?: (data: {
id: string;
name: string;

View File

@@ -62,6 +62,7 @@ export function LancamentoDialog({
defaultPurchaseDate,
lockCartaoSelection,
lockPaymentMethod,
isImporting = false,
onBulkEditRequest,
}: LancamentoDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
@@ -75,6 +76,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
})
);
const [periodDirty, setPeriodDirty] = useState(false);
@@ -92,6 +94,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
}
)
);
@@ -106,6 +109,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
]);
const primaryPagador = formState.pagadorId;
@@ -301,15 +305,20 @@ export function LancamentoDialog({
]
);
const isCopyMode = mode === "create" && Boolean(lancamento);
const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting;
const isImportMode = mode === "create" && Boolean(lancamento) && isImporting;
const title = mode === "create"
? isCopyMode
? isImportMode
? "Importar para Minha Conta"
: isCopyMode
? "Copiar lançamento"
: "Novo lançamento"
: "Editar lançamento";
const description =
mode === "create"
? isCopyMode
? isImportMode
? "Importando lançamento de outro usuário. Ajuste a categoria, pagador e cartão/conta antes de salvar."
: isCopyMode
? "Os dados do lançamento foram copiados. Revise e ajuste conforme necessário antes de salvar."
: "Informe os dados abaixo para registrar um novo lançamento."
: "Atualize as informações do lançamento selecionado.";

View File

@@ -15,6 +15,7 @@ import { toast } from "sonner";
import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
import { BulkActionDialog, type BulkActionScope } from "../dialogs/bulk-action-dialog";
import { BulkImportDialog } from "../dialogs/bulk-import-dialog";
import { LancamentoDetailsDialog } from "../dialogs/lancamento-details-dialog";
import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog";
import { LancamentosTable } from "../table/lancamentos-table";
@@ -27,6 +28,7 @@ import type {
} from "../types";
interface LancamentosPageProps {
currentUserId: string;
lancamentos: LancamentoItem[];
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
@@ -44,9 +46,17 @@ interface LancamentosPageProps {
defaultPaymentMethod?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
// Opções específicas para o dialog de importação (quando visualizando dados de outro usuário)
importPagadorOptions?: SelectOption[];
importSplitPagadorOptions?: SelectOption[];
importDefaultPagadorId?: string | null;
importContaOptions?: SelectOption[];
importCartaoOptions?: SelectOption[];
importCategoriaOptions?: SelectOption[];
}
export function LancamentosPage({
currentUserId,
lancamentos,
pagadorOptions,
splitPagadorOptions,
@@ -64,6 +74,12 @@ export function LancamentosPage({
defaultPaymentMethod,
lockCartaoSelection,
lockPaymentMethod,
importPagadorOptions,
importSplitPagadorOptions,
importDefaultPagadorId,
importContaOptions,
importCartaoOptions,
importCategoriaOptions,
}: LancamentosPageProps) {
const [selectedLancamento, setSelectedLancamento] =
useState<LancamentoItem | null>(null);
@@ -72,6 +88,9 @@ export function LancamentosPage({
const [copyOpen, setCopyOpen] = useState(false);
const [lancamentoToCopy, setLancamentoToCopy] =
useState<LancamentoItem | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [lancamentoToImport, setLancamentoToImport] =
useState<LancamentoItem | null>(null);
const [massAddOpen, setMassAddOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [lancamentoToDelete, setLancamentoToDelete] =
@@ -105,6 +124,8 @@ export function LancamentosPage({
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
const [selectedForAnticipation, setSelectedForAnticipation] =
useState<LancamentoItem | null>(null);
const [bulkImportOpen, setBulkImportOpen] = useState(false);
const [lancamentosToImport, setLancamentosToImport] = useState<LancamentoItem[]>([]);
const handleToggleSettlement = useCallback(async (item: LancamentoItem) => {
if (item.paymentMethod === "Cartão de crédito") {
@@ -296,6 +317,16 @@ export function LancamentosPage({
setCopyOpen(true);
}, []);
const handleImport = useCallback((item: LancamentoItem) => {
setLancamentoToImport(item);
setImportOpen(true);
}, []);
const handleBulkImport = useCallback((items: LancamentoItem[]) => {
setLancamentosToImport(items);
setBulkImportOpen(true);
}, []);
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
if (item.seriesId) {
setPendingDeleteData(item);
@@ -325,6 +356,7 @@ export function LancamentosPage({
<>
<LancamentosTable
data={lancamentos}
currentUserId={currentUserId}
pagadorFilterOptions={pagadorFilterOptions}
categoriaFilterOptions={categoriaFilterOptions}
contaCartaoFilterOptions={contaCartaoFilterOptions}
@@ -332,8 +364,10 @@ export function LancamentosPage({
onMassAdd={allowCreate ? handleMassAdd : undefined}
onEdit={handleEdit}
onCopy={handleCopy}
onImport={handleImport}
onConfirmDelete={handleConfirmDelete}
onBulkDelete={handleMultipleBulkDelete}
onBulkImport={handleBulkImport}
onViewDetails={handleViewDetails}
onToggleSettlement={handleToggleSettlement}
onAnticipate={handleAnticipate}
@@ -381,6 +415,38 @@ export function LancamentosPage({
defaultPeriod={selectedPeriod}
/>
<LancamentoDialog
mode="create"
open={importOpen && !!lancamentoToImport}
onOpenChange={(open) => {
setImportOpen(open);
if (!open) {
setLancamentoToImport(null);
}
}}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
splitPagadorOptions={importSplitPagadorOptions ?? splitPagadorOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
estabelecimentos={estabelecimentos}
lancamento={lancamentoToImport ?? undefined}
defaultPeriod={selectedPeriod}
isImporting={true}
/>
<BulkImportDialog
open={bulkImportOpen && lancamentosToImport.length > 0}
onOpenChange={setBulkImportOpen}
items={lancamentosToImport}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
/>
<LancamentoDialog
mode="update"
open={editOpen && !!selectedLancamento}

View File

@@ -91,8 +91,10 @@ const resolveLogoSrc = (logo: string | null) => {
};
type BuildColumnsArgs = {
currentUserId: string;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
@@ -103,8 +105,10 @@ type BuildColumnsArgs = {
};
const buildColumns = ({
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -116,6 +120,7 @@ const buildColumns = ({
const noop = () => undefined;
const handleEdit = onEdit ?? noop;
const handleCopy = onCopy ?? noop;
const handleImport = onImport ?? noop;
const handleConfirmDelete = onConfirmDelete ?? noop;
const handleViewDetails = onViewDetails ?? noop;
const handleToggleSettlement = onToggleSettlement ?? noop;
@@ -419,6 +424,7 @@ const buildColumns = ({
contaLogo,
cartaoId,
contaId,
userId,
} = row.original;
const label = cartaoName ?? contaName;
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
@@ -428,20 +434,14 @@ const buildColumns = ({
? `/contas/${contaId}/extrato`
: null;
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
const isOwnData = userId === currentUserId;
if (!label) {
return "—";
}
return (
<Link
href={href ?? "#"}
className={cn(
"flex items-center gap-2",
href ? "underline " : "pointer-events-none"
)}
aria-disabled={!href}
>
const content = (
<>
{logoSrc ? (
<Image
src={logoSrc}
@@ -455,6 +455,23 @@ const buildColumns = ({
{Icon ? (
<Icon className="size-4 text-muted-foreground" aria-hidden />
) : null}
</>
);
if (!isOwnData) {
return <div className="flex items-center gap-2">{content}</div>;
}
return (
<Link
href={href ?? "#"}
className={cn(
"flex items-center gap-2",
href ? "underline " : "pointer-events-none"
)}
aria-disabled={!href}
>
{content}
</Link>
);
},
@@ -526,30 +543,41 @@ const buildColumns = ({
<RiEyeLine className="size-4" />
Detalhes
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => handleEdit(row.original)}
disabled={row.original.readonly}
>
<RiPencilLine className="size-4" />
Editar
</DropdownMenuItem>
{row.original.categoriaName !== "Pagamentos" && (
{row.original.userId === currentUserId && (
<DropdownMenuItem
onSelect={() => handleEdit(row.original)}
disabled={row.original.readonly}
>
<RiPencilLine className="size-4" />
Editar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && (
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
<RiFileCopyLine className="size-4" />
Copiar
</DropdownMenuItem>
)}
<DropdownMenuItem
variant="destructive"
onSelect={() => handleConfirmDelete(row.original)}
disabled={row.original.readonly}
>
<RiDeleteBin5Line className="size-4" />
Remover
</DropdownMenuItem>
{row.original.categoriaName !== "Pagamentos" && row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />
Importar para Minha Conta
</DropdownMenuItem>
)}
{row.original.userId === currentUserId && (
<DropdownMenuItem
variant="destructive"
onSelect={() => handleConfirmDelete(row.original)}
disabled={row.original.readonly}
>
<RiDeleteBin5Line className="size-4" />
Remover
</DropdownMenuItem>
)}
{/* Opções de Antecipação */}
{row.original.condition === "Parcelado" &&
{row.original.userId === currentUserId &&
row.original.condition === "Parcelado" &&
row.original.seriesId && (
<>
<DropdownMenuSeparator />
@@ -594,6 +622,7 @@ const buildColumns = ({
type LancamentosTableProps = {
data: LancamentoItem[];
currentUserId: string;
pagadorFilterOptions?: LancamentoFilterOption[];
categoriaFilterOptions?: LancamentoFilterOption[];
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
@@ -601,8 +630,10 @@ type LancamentosTableProps = {
onMassAdd?: () => void;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onBulkDelete?: (items: LancamentoItem[]) => void;
onBulkImport?: (items: LancamentoItem[]) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
onAnticipate?: (item: LancamentoItem) => void;
@@ -614,6 +645,7 @@ type LancamentosTableProps = {
export function LancamentosTable({
data,
currentUserId,
pagadorFilterOptions = [],
categoriaFilterOptions = [],
contaCartaoFilterOptions = [],
@@ -621,8 +653,10 @@ export function LancamentosTable({
onMassAdd,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onBulkDelete,
onBulkImport,
onViewDetails,
onToggleSettlement,
onAnticipate,
@@ -643,8 +677,10 @@ export function LancamentosTable({
const columns = useMemo(
() =>
buildColumns({
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -654,8 +690,10 @@ export function LancamentosTable({
showActions,
}),
[
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -693,6 +731,10 @@ export function LancamentosTable({
0
);
// Check if all data belongs to current user to determine if filters should be shown
const isOwnData = data.every((item) => item.userId === currentUserId);
const shouldShowFilters = showFilters && isOwnData;
const handleBulkDelete = () => {
if (onBulkDelete && selectedCount > 0) {
const selectedItems = selectedRows.map((row) => row.original);
@@ -701,8 +743,16 @@ export function LancamentosTable({
}
};
const handleBulkImport = () => {
if (onBulkImport && selectedCount > 0) {
const selectedItems = selectedRows.map((row) => row.original);
onBulkImport(selectedItems);
setRowSelection({});
}
};
const showTopControls =
Boolean(onCreate) || Boolean(onMassAdd) || showFilters;
Boolean(onCreate) || Boolean(onMassAdd) || shouldShowFilters;
return (
<TooltipProvider>
@@ -738,10 +788,10 @@ export function LancamentosTable({
) : null}
</div>
) : (
<span className={showFilters ? "hidden sm:block" : ""} />
<span className={shouldShowFilters ? "hidden sm:block" : ""} />
)}
{showFilters ? (
{shouldShowFilters ? (
<LancamentosFilters
pagadorOptions={pagadorFilterOptions}
categoriaOptions={categoriaFilterOptions}
@@ -752,7 +802,7 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkDelete ? (
{selectedCount > 0 && onBulkDelete && selectedRows.every(row => row.original.userId === currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
@@ -782,6 +832,36 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkImport && selectedRows.some(row => row.original.userId !== currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
{selectedCount}{" "}
{selectedCount === 1 ? "item selecionado" : "itens selecionados"}
</span>
<span className="hidden sm:inline" aria-hidden>
-
</span>
<span>
Total:{" "}
<MoneyValues
amount={selectedTotal}
className="inline font-medium text-foreground"
/>
</span>
</div>
<Button
onClick={handleBulkImport}
variant="default"
size="sm"
className="ml-auto"
>
<RiFileCopyLine className="size-4" />
Importar selecionados
</Button>
</div>
) : null}
<Card className="py-2">
<CardContent className="px-2 py-4 sm:px-4">
{hasRows ? (

View File

@@ -1,5 +1,6 @@
export type LancamentoItem = {
id: string;
userId: string;
name: string;
purchaseDate: string;
period: string;

View File

@@ -147,7 +147,7 @@ export function PagadoresPage({
value={shareCodeInput}
onChange={(event) => setShareCodeInput(event.target.value)}
disabled={joinPending}
className="w-56"
className="w-56 border-dashed"
/>
<Button type="submit" disabled={joinPending}>
{joinPending ? "Adicionando..." : "Adicionar por código"}

View File

@@ -39,6 +39,7 @@ export type LancamentoFormOverrides = {
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
isImporting?: boolean;
};
/**
@@ -64,7 +65,13 @@ export function buildLancamentoInitialState(
preferredPeriod && /^\d{4}-\d{2}$/.test(preferredPeriod)
? preferredPeriod
: derivedPeriod;
const fallbackPagadorId = lancamento?.pagadorId ?? defaultPagadorId ?? null;
// Quando importando, usar valores padrão do usuário logado ao invés dos valores do lançamento original
const isImporting = overrides?.isImporting ?? false;
const fallbackPagadorId = isImporting
? (defaultPagadorId ?? null)
: (lancamento?.pagadorId ?? defaultPagadorId ?? null);
const boletoPaymentDate =
lancamento?.boletoPaymentDate ??
(paymentMethod === "Boleto" && (lancamento?.isSettled ?? false)
@@ -92,12 +99,12 @@ export function buildLancamentoInitialState(
contaId:
paymentMethod === "Cartão de crédito"
? undefined
: lancamento?.contaId ?? undefined,
: isImporting ? undefined : (lancamento?.contaId ?? undefined),
cartaoId:
paymentMethod === "Cartão de crédito"
? lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined
? isImporting ? (overrides?.defaultCartaoId ?? undefined) : (lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined)
: undefined,
categoriaId: lancamento?.categoriaId ?? undefined,
categoriaId: isImporting ? undefined : (lancamento?.categoriaId ?? undefined),
installmentCount: lancamento?.installmentCount
? String(lancamento.installmentCount)
: "",

View File

@@ -388,6 +388,7 @@ type LancamentoRowWithRelations = typeof lancamentos.$inferSelect & {
export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) =>
rows.map((item) => ({
id: item.id,
userId: item.userId,
name: item.name,
purchaseDate: item.purchaseDate?.toISOString() ?? new Date().toISOString(),
period: item.period ?? "",

View File

@@ -56,7 +56,7 @@
"@tanstack/react-table": "8.21.3",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"ai": "^6.0.6",
"ai": "^6.0.7",
"babel-plugin-react-compiler": "^1.0.0",
"better-auth": "1.4.10",
"class-variance-authority": "0.7.1",
@@ -65,8 +65,8 @@
"date-fns": "^4.1.0",
"drizzle-orm": "0.45.1",
"jspdf": "^4.0.0",
"jspdf-autotable": "^5.0.2",
"motion": "^12.23.26",
"jspdf-autotable": "^5.0.7",
"motion": "^12.23.27",
"next": "16.1.1",
"next-themes": "0.4.6",
"pg": "8.16.3",
@@ -79,7 +79,7 @@
"tailwind-merge": "3.4.0",
"vaul": "1.1.2",
"xlsx": "^0.18.5",
"zod": "4.3.4"
"zod": "4.3.5"
},
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",

134
pnpm-lock.yaml generated
View File

@@ -10,16 +10,16 @@ importers:
dependencies:
'@ai-sdk/anthropic':
specifier: ^3.0.2
version: 3.0.2(zod@4.3.4)
version: 3.0.2(zod@4.3.5)
'@ai-sdk/google':
specifier: ^3.0.2
version: 3.0.2(zod@4.3.4)
version: 3.0.2(zod@4.3.5)
'@ai-sdk/openai':
specifier: ^3.0.2
version: 3.0.2(zod@4.3.4)
version: 3.0.2(zod@4.3.5)
'@openrouter/ai-sdk-provider':
specifier: ^1.5.4
version: 1.5.4(ai@6.0.6(zod@4.3.4))(zod@4.3.4)
version: 1.5.4(ai@6.0.7(zod@4.3.5))(zod@4.3.5)
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -96,8 +96,8 @@ importers:
specifier: ^1.3.1
version: 1.3.1(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
ai:
specifier: ^6.0.6
version: 6.0.6(zod@4.3.4)
specifier: ^6.0.7
version: 6.0.7(zod@4.3.5)
babel-plugin-react-compiler:
specifier: ^1.0.0
version: 1.0.0
@@ -123,11 +123,11 @@ importers:
specifier: ^4.0.0
version: 4.0.0
jspdf-autotable:
specifier: ^5.0.2
version: 5.0.2(jspdf@4.0.0)
specifier: ^5.0.7
version: 5.0.7(jspdf@4.0.0)
motion:
specifier: ^12.23.26
version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
specifier: ^12.23.27
version: 12.23.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next:
specifier: 16.1.1
version: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -165,8 +165,8 @@ importers:
specifier: ^0.18.5
version: 0.18.5
zod:
specifier: 4.3.4
version: 4.3.4
specifier: 4.3.5
version: 4.3.5
devDependencies:
'@tailwindcss/postcss':
specifier: 4.1.18
@@ -222,8 +222,8 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.5':
resolution: {integrity: sha512-AtxA1wcoKTHr9uFoC5KZEXqJP4SMW4j3VbcliUECUYssbWbePJ9+b3AaCny1lxf1xhDK9EIyAgBOKhXoQSr9nA==}
'@ai-sdk/gateway@3.0.6':
resolution: {integrity: sha512-oEpwjM0PIaSUErtZI8Ag+gQ+ZelysRWA96N5ahvOc5e9d7QkKJWF0POWx0nI1qBxvmUSw7ca0sLTVw+J5yn7Tg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -2148,8 +2148,8 @@ packages:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ai@6.0.6:
resolution: {integrity: sha512-LM0eAMWVn3RTj+0X5O1m/8g+7QiTeWG5aN5FsDbdmCkAQHVg93XxLbljFOLzi0NMjuJgf7fKLKmWoPsrdMyqfw==}
ai@6.0.7:
resolution: {integrity: sha512-kLzSXHdW6cAcb2mFSIfkbfzxYqqjrUnyhrB1sg855qlC+6XkLI8hmwFE8f/4SnjmtcTDOnkIaVjWoO5i5Ir0bw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -2952,8 +2952,8 @@ packages:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
framer-motion@12.23.26:
resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
framer-motion@12.23.27:
resolution: {integrity: sha512-EAcX8FS8jzZ4tSKpj+1GhwbVY+r1gfamPFwXZAsioPqu/ffRwU2otkKg6GEDCR41FVJv3RoBN7Aqep6drL9Itg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -3295,10 +3295,10 @@ packages:
engines: {node: '>=6'}
hasBin: true
jspdf-autotable@5.0.2:
resolution: {integrity: sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==}
jspdf-autotable@5.0.7:
resolution: {integrity: sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==}
peerDependencies:
jspdf: ^2 || ^3
jspdf: ^2 || ^3 || ^4
jspdf@4.0.0:
resolution: {integrity: sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==}
@@ -3450,8 +3450,8 @@ packages:
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion@12.23.26:
resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==}
motion@12.23.27:
resolution: {integrity: sha512-EDb0hAE6jNX8BHpmQK1GBf9Eizx9bg/Tz2KEAJBOGEnIJp8W77QweRpVb05U8R0L0/LXndHmS1Xv3fwXJh/kcQ==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -4240,42 +4240,42 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@4.3.4:
resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==}
zod@4.3.5:
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
snapshots:
'@ai-sdk/anthropic@3.0.2(zod@4.3.4)':
'@ai-sdk/anthropic@3.0.2(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 3.0.1
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.5)
zod: 4.3.5
'@ai-sdk/gateway@3.0.5(zod@4.3.4)':
'@ai-sdk/gateway@3.0.6(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 3.0.1
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.4)
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.5)
'@vercel/oidc': 3.0.5
zod: 4.3.4
zod: 4.3.5
'@ai-sdk/google@3.0.2(zod@4.3.4)':
'@ai-sdk/google@3.0.2(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 3.0.1
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.5)
zod: 4.3.5
'@ai-sdk/openai@3.0.2(zod@4.3.4)':
'@ai-sdk/openai@3.0.2(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 3.0.1
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.5)
zod: 4.3.5
'@ai-sdk/provider-utils@4.0.2(zod@4.3.4)':
'@ai-sdk/provider-utils@4.0.2(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 3.0.1
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 4.3.4
zod: 4.3.5
'@ai-sdk/provider@3.0.1':
dependencies:
@@ -4385,20 +4385,20 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.4))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)':
'@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)':
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@standard-schema/spec': 1.1.0
better-call: 1.1.7(zod@4.3.4)
better-call: 1.1.7(zod@4.3.5)
jose: 6.1.3
kysely: 0.28.9
nanostores: 1.1.0
zod: 4.3.4
zod: 4.3.5
'@better-auth/telemetry@1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.4))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))':
'@better-auth/telemetry@1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))':
dependencies:
'@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.4))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
@@ -4903,15 +4903,15 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@openrouter/ai-sdk-provider@1.5.4(ai@6.0.6(zod@4.3.4))(zod@4.3.4)':
'@openrouter/ai-sdk-provider@1.5.4(ai@6.0.7(zod@4.3.5))(zod@4.3.5)':
dependencies:
'@openrouter/sdk': 0.1.27
ai: 6.0.6(zod@4.3.4)
zod: 4.3.4
ai: 6.0.7(zod@4.3.5)
zod: 4.3.5
'@openrouter/sdk@0.1.27':
dependencies:
zod: 4.3.4
zod: 4.3.5
'@opentelemetry/api@1.9.0': {}
@@ -5886,13 +5886,13 @@ snapshots:
adler-32@1.3.1: {}
ai@6.0.6(zod@4.3.4):
ai@6.0.7(zod@4.3.5):
dependencies:
'@ai-sdk/gateway': 3.0.5(zod@4.3.4)
'@ai-sdk/gateway': 3.0.6(zod@4.3.5)
'@ai-sdk/provider': 3.0.1
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.4)
'@ai-sdk/provider-utils': 4.0.2(zod@4.3.5)
'@opentelemetry/api': 1.9.0
zod: 4.3.4
zod: 4.3.5
ajv@6.12.6:
dependencies:
@@ -6017,18 +6017,18 @@ snapshots:
better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.4))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.4))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
'@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.1.7(zod@4.3.4)
better-call: 1.1.7(zod@4.3.5)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.9
nanostores: 1.1.0
zod: 4.3.4
zod: 4.3.5
optionalDependencies:
drizzle-kit: 0.31.8
drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3)
@@ -6037,14 +6037,14 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
better-call@1.1.7(zod@4.3.4):
better-call@1.1.7(zod@4.3.5):
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
rou3: 0.7.12
set-cookie-parser: 2.7.2
optionalDependencies:
zod: 4.3.4
zod: 4.3.5
brace-expansion@1.1.12:
dependencies:
@@ -6661,8 +6661,8 @@ snapshots:
'@babel/parser': 7.28.5
eslint: 9.39.2(jiti@2.6.1)
hermes-parser: 0.25.1
zod: 4.3.4
zod-validation-error: 4.0.2(zod@4.3.4)
zod: 4.3.5
zod-validation-error: 4.0.2(zod@4.3.5)
transitivePeerDependencies:
- supports-color
@@ -6833,7 +6833,7 @@ snapshots:
frac@1.1.2: {}
framer-motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
framer-motion@12.23.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
motion-dom: 12.23.23
motion-utils: 12.23.6
@@ -7158,7 +7158,7 @@ snapshots:
json5@2.2.3: {}
jspdf-autotable@5.0.2(jspdf@4.0.0):
jspdf-autotable@5.0.7(jspdf@4.0.0):
dependencies:
jspdf: 4.0.0
@@ -7297,9 +7297,9 @@ snapshots:
motion-utils@12.23.6: {}
motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
motion@12.23.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
framer-motion: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
framer-motion: 12.23.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tslib: 2.8.1
optionalDependencies:
react: 19.2.3
@@ -8211,8 +8211,8 @@ snapshots:
yocto-queue@0.1.0: {}
zod-validation-error@4.0.2(zod@4.3.4):
zod-validation-error@4.0.2(zod@4.3.5):
dependencies:
zod: 4.3.4
zod: 4.3.5
zod@4.3.4: {}
zod@4.3.5: {}

Binary file not shown.

Binary file not shown.

View File

@@ -1,14 +1,14 @@
import localFont from "next/font/local";
const laranjinha = localFont({
const ai_sans = localFont({
src: [
{
path: "./LaranjinhaTextPro_Rg.woff2",
path: "./AISans-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "./LaranjinhaDisplayPro_Bd.woff2",
path: "./AISans-Semibold.woff2",
weight: "700",
style: "normal",
},
@@ -16,8 +16,8 @@ const laranjinha = localFont({
display: "swap",
});
const main_font = laranjinha;
const money_font = laranjinha;
const title_font = laranjinha;
const main_font = ai_sans;
const money_font = ai_sans;
const title_font = ai_sans;
export { main_font, money_font, title_font };