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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -133,3 +133,4 @@ yarn.lock # Se usa pnpm, não precisa do yarn lock
|
||||
local/
|
||||
scratch/
|
||||
playground/
|
||||
/docs
|
||||
582
README.md
582
README.md
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -734,41 +868,251 @@ 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
|
||||
│ │ ├── 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!
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 mê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>
|
||||
|
||||
363
components/lancamentos/dialogs/bulk-import-dialog.tsx
Normal file
363
components/lancamentos/dialogs/bulk-import-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export interface LancamentoDialogProps {
|
||||
defaultPurchaseDate?: string | null;
|
||||
lockCartaoSelection?: boolean;
|
||||
lockPaymentMethod?: boolean;
|
||||
isImporting?: boolean;
|
||||
onBulkEditRequest?: (data: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,6 +543,7 @@ const buildColumns = ({
|
||||
<RiEyeLine className="size-4" />
|
||||
Detalhes
|
||||
</DropdownMenuItem>
|
||||
{row.original.userId === currentUserId && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => handleEdit(row.original)}
|
||||
disabled={row.original.readonly}
|
||||
@@ -533,12 +551,20 @@ const buildColumns = ({
|
||||
<RiPencilLine className="size-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
{row.original.categoriaName !== "Pagamentos" && (
|
||||
)}
|
||||
{row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && (
|
||||
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Copiar
|
||||
</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)}
|
||||
@@ -547,9 +573,11 @@ const buildColumns = ({
|
||||
<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 ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type LancamentoItem = {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
purchaseDate: string;
|
||||
period: string;
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
: "",
|
||||
|
||||
@@ -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 ?? "",
|
||||
|
||||
@@ -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
134
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
BIN
public/fonts/AISans-Regular.woff2
Normal file
BIN
public/fonts/AISans-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/AISans-Semibold.woff2
Normal file
BIN
public/fonts/AISans-Semibold.woff2
Normal file
Binary file not shown.
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user