forked from git.gladyson/openmonetis
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/
|
local/
|
||||||
scratch/
|
scratch/
|
||||||
playground/
|
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.
|
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).
|
> 💡 **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
|
### ⚠️ 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
|
- Registre suas contas bancárias, cartões e dinheiro em espécie
|
||||||
- Adicione receitas, despesas e transferências entre contas
|
- Adicione receitas, despesas e transferências entre contas
|
||||||
- Organize tudo por categorias (moradia, alimentação, transporte, etc.)
|
- 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**
|
📊 **Relatórios e gráficos**
|
||||||
|
|
||||||
- Dashboard com resumo mensal das suas finanças
|
- Dashboard com resumo mensal das suas finanças
|
||||||
- Gráficos de evolução do patrimônio
|
- Gráficos de evolução do patrimônio
|
||||||
- Comparação de gastos por categoria
|
- Comparação de gastos por categoria
|
||||||
|
- Relatórios detalhados de categorias com histórico
|
||||||
- Entenda pra onde seu dinheiro está indo
|
- Entenda pra onde seu dinheiro está indo
|
||||||
|
|
||||||
💳 **Faturas de cartão de crédito**
|
💳 **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
|
- Cadastre seus cartões e acompanhe as faturas
|
||||||
- Veja o que ainda não foi fechado na fatura atual
|
- Veja o que ainda não foi fechado na fatura atual
|
||||||
- Controle de limites e vencimentos
|
- Controle de limites e vencimentos
|
||||||
|
- Visualização de faturas por período
|
||||||
|
|
||||||
🎯 **Orçamentos**
|
🎯 **Orçamentos**
|
||||||
|
|
||||||
- Defina quanto quer gastar por categoria no mês
|
- Defina quanto quer gastar por categoria no mês
|
||||||
- Acompanhe se está dentro do planejado
|
- 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
|
### Stack técnica
|
||||||
|
|
||||||
@@ -119,26 +175,82 @@ O projeto é open source, seus dados ficam no seu controle (pode rodar localment
|
|||||||
|
|
||||||
### 🔐 Autenticação
|
### 🔐 Autenticação
|
||||||
|
|
||||||
- Better Auth integrado
|
- Better Auth 1.4.10 integrado
|
||||||
- OAuth (Google, GitHub)
|
- OAuth (Google)
|
||||||
- Email magic links
|
- Autenticação por email/senha
|
||||||
- Session management
|
- Session management com tokens
|
||||||
- Protected routes via middleware
|
- Protected routes via middleware
|
||||||
|
- Verificação de email
|
||||||
|
|
||||||
### 🗄️ Banco de Dados
|
### 🗄️ Banco de Dados
|
||||||
|
|
||||||
- PostgreSQL 18 (última versão estável)
|
- PostgreSQL 18 (última versão estável)
|
||||||
- Drizzle ORM com TypeScript
|
- Drizzle ORM 0.45 com TypeScript
|
||||||
- Migrations automáticas
|
- Migrations automáticas
|
||||||
- Drizzle Studio (UI visual para DB)
|
- Drizzle Studio (UI visual para DB)
|
||||||
- Suporte para banco local (Docker) ou remoto (Supabase, Neon, etc)
|
- 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
|
### 🎨 Interface
|
||||||
|
|
||||||
- shadcn/ui components
|
- shadcn/ui components (Radix UI)
|
||||||
- Tailwind CSS v4
|
- Tailwind CSS v4
|
||||||
- Dark mode suportado
|
- Dark mode com next-themes
|
||||||
- Animações com Framer Motion
|
- 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
|
### 🐳 Docker
|
||||||
|
|
||||||
@@ -147,14 +259,18 @@ O projeto é open source, seus dados ficam no seu controle (pode rodar localment
|
|||||||
- Volumes persistentes
|
- Volumes persistentes
|
||||||
- Network isolada
|
- Network isolada
|
||||||
- Scripts npm facilitados
|
- Scripts npm facilitados
|
||||||
|
- Imagem final ~200MB
|
||||||
|
|
||||||
### 🧪 Desenvolvimento
|
### 🧪 Desenvolvimento
|
||||||
|
|
||||||
- Next.js 16 com App Router
|
- Next.js 16.1 com App Router
|
||||||
- Turbopack (fast refresh)
|
- Turbopack (fast refresh)
|
||||||
- TypeScript 5.9
|
- TypeScript 5.9 (strict mode)
|
||||||
- ESLint + Prettier
|
- ESLint 9
|
||||||
- React 19
|
- 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
|
### Frontend
|
||||||
|
|
||||||
- **Framework:** Next.js 16 (App Router)
|
- **Framework:** Next.js 16.1.1 (App Router)
|
||||||
- **Linguagem:** TypeScript 5.9
|
- **Linguagem:** TypeScript 5.9.3
|
||||||
- **UI Library:** React 19
|
- **UI Library:** React 19.2.3
|
||||||
- **Styling:** Tailwind CSS v4
|
- **Styling:** Tailwind CSS 4.1.18
|
||||||
- **Components:** shadcn/ui (Radix UI)
|
- **Components:** shadcn/ui (Radix UI)
|
||||||
- **Icons:** Remixicon
|
- **Icons:** Remixicon 4.8.0
|
||||||
- **Animations:** Framer Motion
|
- **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
|
### Backend
|
||||||
|
|
||||||
- **Runtime:** Node.js 22
|
- **Runtime:** Node.js 22
|
||||||
- **Database:** PostgreSQL 18
|
- **Database:** PostgreSQL 18
|
||||||
- **ORM:** Drizzle ORM
|
- **ORM:** Drizzle ORM 0.45.1
|
||||||
- **Auth:** Better Auth
|
- **Database Driver:** pg 8.16.3
|
||||||
- **Email:** Resend
|
- **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
|
### DevOps
|
||||||
|
|
||||||
- **Containerization:** Docker + Docker Compose
|
- **Containerization:** Docker + Docker Compose
|
||||||
- **Package Manager:** pnpm
|
- **Package Manager:** pnpm
|
||||||
- **Build Tool:** Turbopack
|
- **Build Tool:** Turbopack
|
||||||
|
- **Linting:** ESLint 9.39.2
|
||||||
### AI Integration (Opcional)
|
- **Analytics:** Vercel Analytics + Speed Insights
|
||||||
|
|
||||||
- Anthropic (Claude)
|
|
||||||
- OpenAI (GPT)
|
|
||||||
- Google Gemini
|
|
||||||
- OpenRouter
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -734,41 +868,251 @@ psql $DATABASE_URL < backup.sql
|
|||||||
opensheets/
|
opensheets/
|
||||||
├── app/ # Next.js App Router
|
├── app/ # Next.js App Router
|
||||||
│ ├── api/ # API Routes
|
│ ├── api/ # API Routes
|
||||||
│ │ ├── auth/ # Better Auth endpoints
|
│ │ ├── auth/[...all]/ # Better Auth endpoints
|
||||||
│ │ └── health/ # Health check
|
│ │ └── health/ # Health check endpoint
|
||||||
│ ├── (dashboard)/ # Protected routes (com auth)
|
│ ├── (auth)/ # Rotas públicas de autenticação
|
||||||
│ └── layout.tsx # Root layout
|
│ │ ├── 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
|
├── components/ # React Components (~200 arquivos)
|
||||||
│ ├── ui/ # shadcn/ui components
|
│ ├── ui/ # shadcn/ui base components
|
||||||
│ └── ... # Feature 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
|
├── lib/ # Lógica de negócio e utilitários
|
||||||
│ ├── db.ts # Drizzle client
|
│ ├── auth/
|
||||||
│ ├── auth.ts # Better Auth server
|
│ │ ├── config.ts # Configuração Better Auth
|
||||||
│ └── auth-client.ts # Better Auth client
|
│ │ ├── 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
|
├── db/ # Banco de dados
|
||||||
│ └── schema.ts # Database schema
|
│ └── schema.ts # Schema Drizzle (736 linhas)
|
||||||
|
│ # 15+ tabelas com relações complexas
|
||||||
│
|
│
|
||||||
├── drizzle/ # Generated migrations
|
├── drizzle/ # Migrations geradas
|
||||||
│ └── migrations/
|
│ ├── migrations/
|
||||||
|
│ └── meta/
|
||||||
│
|
│
|
||||||
├── hooks/ # Custom React hooks
|
├── hooks/ # React Hooks customizados
|
||||||
├── public/ # Static assets
|
│ ├── use-month-period.ts # Gerenciamento de período
|
||||||
├── scripts/ # Utility scripts
|
│ ├── use-form-state.ts # Estado de formulários
|
||||||
│ ├── setup-env.sh # Env setup automation
|
│ ├── use-calculator-state.ts # Estado da calculadora
|
||||||
│ └── postgres/init.sql # PostgreSQL init script
|
│ └── use-mobile.ts # Detecção mobile
|
||||||
│
|
│
|
||||||
├── docker/ # Docker configs
|
├── public/ # Assets estáticos
|
||||||
│ └── postgres/init.sql
|
│ ├── 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
|
├── scripts/ # Scripts utilitários
|
||||||
├── docker-compose.yml # Docker orchestration
|
│ ├── setup-env.sh # Setup de variáveis de ambiente
|
||||||
├── next.config.ts # Next.js config
|
│ └── postgres/
|
||||||
├── drizzle.config.ts # Drizzle ORM config
|
│ ├── init.sql # Script de inicialização do PostgreSQL
|
||||||
├── tailwind.config.ts # Tailwind config
|
│ └── enable-extensions.ts # Habilita extensões do PostgreSQL
|
||||||
└── tsconfig.json # TypeScript config
|
│
|
||||||
|
├── 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
|
### Fluxo de Autenticação
|
||||||
@@ -778,15 +1122,45 @@ opensheets/
|
|||||||
↓
|
↓
|
||||||
2. middleware.ts verifica sessão (Better Auth)
|
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)
|
### 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
|
## 🤝 Contribuindo
|
||||||
|
|
||||||
Contribuições são muito bem-vindas!
|
Contribuições são muito bem-vindas!
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { LoginForm } from "@/components/auth/login-form";
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
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">
|
<div className="w-full max-w-sm md:max-w-4xl">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SignupForm } from "@/components/auth/signup-form";
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
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">
|
<div className="w-full max-w-sm md:max-w-4xl">
|
||||||
<SignupForm />
|
<SignupForm />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<LancamentosSection
|
<LancamentosSection
|
||||||
|
currentUserId={userId}
|
||||||
lancamentos={lancamentosData}
|
lancamentos={lancamentosData}
|
||||||
pagadorOptions={pagadorOptions}
|
pagadorOptions={pagadorOptions}
|
||||||
splitPagadorOptions={splitPagadorOptions}
|
splitPagadorOptions={splitPagadorOptions}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
transactionCount={detail.transactions.length}
|
transactionCount={detail.transactions.length}
|
||||||
/>
|
/>
|
||||||
<LancamentosPage
|
<LancamentosPage
|
||||||
|
currentUserId={userId}
|
||||||
lancamentos={detail.transactions}
|
lancamentos={detail.transactions}
|
||||||
pagadorOptions={pagadorOptions}
|
pagadorOptions={pagadorOptions}
|
||||||
splitPagadorOptions={splitPagadorOptions}
|
splitPagadorOptions={splitPagadorOptions}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<LancamentosSection
|
<LancamentosSection
|
||||||
|
currentUserId={userId}
|
||||||
lancamentos={lancamentosData}
|
lancamentos={lancamentosData}
|
||||||
pagadorOptions={pagadorOptions}
|
pagadorOptions={pagadorOptions}
|
||||||
splitPagadorOptions={splitPagadorOptions}
|
splitPagadorOptions={splitPagadorOptions}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
<LancamentosPage
|
<LancamentosPage
|
||||||
|
currentUserId={userId}
|
||||||
lancamentos={lancamentosData}
|
lancamentos={lancamentosData}
|
||||||
pagadorOptions={pagadorOptions}
|
pagadorOptions={pagadorOptions}
|
||||||
splitPagadorOptions={splitPagadorOptions}
|
splitPagadorOptions={splitPagadorOptions}
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
let filterSources: Awaited<
|
let filterSources: Awaited<
|
||||||
ReturnType<typeof fetchLancamentoFilterSources>
|
ReturnType<typeof fetchLancamentoFilterSources>
|
||||||
> | null = null;
|
> | null = null;
|
||||||
|
let loggedUserFilterSources: Awaited<
|
||||||
|
ReturnType<typeof fetchLancamentoFilterSources>
|
||||||
|
> | null = null;
|
||||||
let sluggedFilters: SluggedFilters;
|
let sluggedFilters: SluggedFilters;
|
||||||
let slugMaps: SlugMaps;
|
let slugMaps: SlugMaps;
|
||||||
|
|
||||||
@@ -107,6 +110,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
sluggedFilters = buildSluggedFilters(filterSources);
|
sluggedFilters = buildSluggedFilters(filterSources);
|
||||||
slugMaps = buildSlugMaps(sluggedFilters);
|
slugMaps = buildSlugMaps(sluggedFilters);
|
||||||
} else {
|
} else {
|
||||||
|
// Buscar opções do usuário logado para usar ao importar
|
||||||
|
loggedUserFilterSources = await fetchLancamentoFilterSources(userId);
|
||||||
sluggedFilters = {
|
sluggedFilters = {
|
||||||
pagadorFiltersRaw: [],
|
pagadorFiltersRaw: [],
|
||||||
categoriaFiltersRaw: [],
|
categoriaFiltersRaw: [],
|
||||||
@@ -170,6 +175,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
const pagadorSharesData = shareRows;
|
const pagadorSharesData = shareRows;
|
||||||
|
|
||||||
let optionSets: OptionSet;
|
let optionSets: OptionSet;
|
||||||
|
let loggedUserOptionSets: OptionSet | null = null;
|
||||||
let effectiveSluggedFilters = sluggedFilters;
|
let effectiveSluggedFilters = sluggedFilters;
|
||||||
|
|
||||||
if (canEdit && filterSources) {
|
if (canEdit && filterSources) {
|
||||||
@@ -192,6 +198,15 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
cartaoFiltersRaw: [],
|
cartaoFiltersRaw: [],
|
||||||
};
|
};
|
||||||
optionSets = buildReadOnlyOptionSets(lancamentosData, pagador);
|
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 =
|
const pagadorSlug =
|
||||||
@@ -286,6 +301,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
<TabsContent value="lancamentos">
|
<TabsContent value="lancamentos">
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<LancamentosSection
|
<LancamentosSection
|
||||||
|
currentUserId={userId}
|
||||||
lancamentos={lancamentosData}
|
lancamentos={lancamentosData}
|
||||||
pagadorOptions={optionSets.pagadorOptions}
|
pagadorOptions={optionSets.pagadorOptions}
|
||||||
splitPagadorOptions={optionSets.splitPagadorOptions}
|
splitPagadorOptions={optionSets.splitPagadorOptions}
|
||||||
@@ -299,6 +315,12 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
selectedPeriod={selectedPeriod}
|
selectedPeriod={selectedPeriod}
|
||||||
estabelecimentos={estabelecimentos}
|
estabelecimentos={estabelecimentos}
|
||||||
allowCreate={canEdit}
|
allowCreate={canEdit}
|
||||||
|
importPagadorOptions={loggedUserOptionSets?.pagadorOptions}
|
||||||
|
importSplitPagadorOptions={loggedUserOptionSets?.splitPagadorOptions}
|
||||||
|
importDefaultPagadorId={loggedUserOptionSets?.defaultPagadorId}
|
||||||
|
importContaOptions={loggedUserOptionSets?.contaOptions}
|
||||||
|
importCartaoOptions={loggedUserOptionSets?.cartaoOptions}
|
||||||
|
importCategoriaOptions={loggedUserOptionSets?.categoriaOptions}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ import {
|
|||||||
RiShieldCheckLine,
|
RiShieldCheckLine,
|
||||||
RiTimeLine,
|
RiTimeLine,
|
||||||
RiWalletLine,
|
RiWalletLine,
|
||||||
|
RiRobot2Line,
|
||||||
|
RiTeamLine,
|
||||||
|
RiFileTextLine,
|
||||||
|
RiDownloadCloudLine,
|
||||||
|
RiEyeOffLine,
|
||||||
|
RiFlashlightLine,
|
||||||
|
RiPercentLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -209,7 +216,47 @@ export default async function Page() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Registre suas contas bancárias, cartões e dinheiro.
|
Registre suas contas bancárias, cartões e dinheiro.
|
||||||
Adicione receitas, despesas e transferências. Organize
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,8 +274,9 @@ export default async function Page() {
|
|||||||
Relatórios e gráficos
|
Relatórios e gráficos
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Dashboard com resumo mensal. Gráficos de evolução do
|
Dashboard com 20+ widgets interativos. Relatórios
|
||||||
patrimônio. Entenda pra onde seu dinheiro está indo.
|
detalhados por categoria. Gráficos de evolução e
|
||||||
|
comparativos. Exportação em PDF e Excel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,8 +294,29 @@ export default async function Page() {
|
|||||||
Faturas de cartão
|
Faturas de cartão
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Cadastre seus cartões e acompanhe as faturas. Veja o que
|
Cadastre seus cartões e acompanhe as faturas por período.
|
||||||
ainda não foi fechado. Controle limites e vencimentos.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,12 +331,12 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-lg mb-2">
|
<h3 className="font-semibold text-lg mb-2">
|
||||||
Categorias personalizadas
|
Categorias e orçamentos
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Crie e organize suas próprias categorias. Moradia,
|
Crie categorias personalizadas. Defina orçamentos mensais
|
||||||
alimentação, transporte, ou o que fizer sentido pra
|
e acompanhe o quanto gastou vs. planejado com indicadores
|
||||||
você.
|
visuais.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,16 +347,16 @@ export default async function Page() {
|
|||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
||||||
<RiMoneyDollarCircleLine
|
<RiFileTextLine size={24} className="text-primary" />
|
||||||
size={24}
|
|
||||||
className="text-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<p className="text-sm text-muted-foreground">
|
||||||
Defina quanto quer gastar por categoria no mês.
|
Crie notas de texto e listas de tarefas com checkboxes.
|
||||||
Acompanhe se está dentro do planejado.
|
Sistema de arquivamento para manter histórico. Organize
|
||||||
|
seus planejamentos financeiros.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,8 +374,69 @@ export default async function Page() {
|
|||||||
Calendário financeiro
|
Calendário financeiro
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Visualize suas transações em calendário mensal. Nunca
|
Visualize todas as transações em calendário mensal.
|
||||||
perca prazos importantes.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
defaultPurchaseDate?: string | null;
|
||||||
lockCartaoSelection?: boolean;
|
lockCartaoSelection?: boolean;
|
||||||
lockPaymentMethod?: boolean;
|
lockPaymentMethod?: boolean;
|
||||||
|
isImporting?: boolean;
|
||||||
onBulkEditRequest?: (data: {
|
onBulkEditRequest?: (data: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function LancamentoDialog({
|
|||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
lockCartaoSelection,
|
lockCartaoSelection,
|
||||||
lockPaymentMethod,
|
lockPaymentMethod,
|
||||||
|
isImporting = false,
|
||||||
onBulkEditRequest,
|
onBulkEditRequest,
|
||||||
}: LancamentoDialogProps) {
|
}: LancamentoDialogProps) {
|
||||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||||
@@ -75,6 +76,7 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
isImporting,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const [periodDirty, setPeriodDirty] = useState(false);
|
const [periodDirty, setPeriodDirty] = useState(false);
|
||||||
@@ -92,6 +94,7 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
isImporting,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -106,6 +109,7 @@ export function LancamentoDialog({
|
|||||||
defaultCartaoId,
|
defaultCartaoId,
|
||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
defaultPurchaseDate,
|
defaultPurchaseDate,
|
||||||
|
isImporting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const primaryPagador = formState.pagadorId;
|
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"
|
const title = mode === "create"
|
||||||
? isCopyMode
|
? isImportMode
|
||||||
|
? "Importar para Minha Conta"
|
||||||
|
: isCopyMode
|
||||||
? "Copiar lançamento"
|
? "Copiar lançamento"
|
||||||
: "Novo lançamento"
|
: "Novo lançamento"
|
||||||
: "Editar lançamento";
|
: "Editar lançamento";
|
||||||
const description =
|
const description =
|
||||||
mode === "create"
|
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."
|
? "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."
|
: "Informe os dados abaixo para registrar um novo lançamento."
|
||||||
: "Atualize as informações do lançamento selecionado.";
|
: "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 { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
|
||||||
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
|
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
|
||||||
import { BulkActionDialog, type BulkActionScope } from "../dialogs/bulk-action-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 { LancamentoDetailsDialog } from "../dialogs/lancamento-details-dialog";
|
||||||
import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog";
|
import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog";
|
||||||
import { LancamentosTable } from "../table/lancamentos-table";
|
import { LancamentosTable } from "../table/lancamentos-table";
|
||||||
@@ -27,6 +28,7 @@ import type {
|
|||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
interface LancamentosPageProps {
|
interface LancamentosPageProps {
|
||||||
|
currentUserId: string;
|
||||||
lancamentos: LancamentoItem[];
|
lancamentos: LancamentoItem[];
|
||||||
pagadorOptions: SelectOption[];
|
pagadorOptions: SelectOption[];
|
||||||
splitPagadorOptions: SelectOption[];
|
splitPagadorOptions: SelectOption[];
|
||||||
@@ -44,9 +46,17 @@ interface LancamentosPageProps {
|
|||||||
defaultPaymentMethod?: string | null;
|
defaultPaymentMethod?: string | null;
|
||||||
lockCartaoSelection?: boolean;
|
lockCartaoSelection?: boolean;
|
||||||
lockPaymentMethod?: 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({
|
export function LancamentosPage({
|
||||||
|
currentUserId,
|
||||||
lancamentos,
|
lancamentos,
|
||||||
pagadorOptions,
|
pagadorOptions,
|
||||||
splitPagadorOptions,
|
splitPagadorOptions,
|
||||||
@@ -64,6 +74,12 @@ export function LancamentosPage({
|
|||||||
defaultPaymentMethod,
|
defaultPaymentMethod,
|
||||||
lockCartaoSelection,
|
lockCartaoSelection,
|
||||||
lockPaymentMethod,
|
lockPaymentMethod,
|
||||||
|
importPagadorOptions,
|
||||||
|
importSplitPagadorOptions,
|
||||||
|
importDefaultPagadorId,
|
||||||
|
importContaOptions,
|
||||||
|
importCartaoOptions,
|
||||||
|
importCategoriaOptions,
|
||||||
}: LancamentosPageProps) {
|
}: LancamentosPageProps) {
|
||||||
const [selectedLancamento, setSelectedLancamento] =
|
const [selectedLancamento, setSelectedLancamento] =
|
||||||
useState<LancamentoItem | null>(null);
|
useState<LancamentoItem | null>(null);
|
||||||
@@ -72,6 +88,9 @@ export function LancamentosPage({
|
|||||||
const [copyOpen, setCopyOpen] = useState(false);
|
const [copyOpen, setCopyOpen] = useState(false);
|
||||||
const [lancamentoToCopy, setLancamentoToCopy] =
|
const [lancamentoToCopy, setLancamentoToCopy] =
|
||||||
useState<LancamentoItem | null>(null);
|
useState<LancamentoItem | null>(null);
|
||||||
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
|
const [lancamentoToImport, setLancamentoToImport] =
|
||||||
|
useState<LancamentoItem | null>(null);
|
||||||
const [massAddOpen, setMassAddOpen] = useState(false);
|
const [massAddOpen, setMassAddOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [lancamentoToDelete, setLancamentoToDelete] =
|
const [lancamentoToDelete, setLancamentoToDelete] =
|
||||||
@@ -105,6 +124,8 @@ export function LancamentosPage({
|
|||||||
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
|
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
|
||||||
const [selectedForAnticipation, setSelectedForAnticipation] =
|
const [selectedForAnticipation, setSelectedForAnticipation] =
|
||||||
useState<LancamentoItem | null>(null);
|
useState<LancamentoItem | null>(null);
|
||||||
|
const [bulkImportOpen, setBulkImportOpen] = useState(false);
|
||||||
|
const [lancamentosToImport, setLancamentosToImport] = useState<LancamentoItem[]>([]);
|
||||||
|
|
||||||
const handleToggleSettlement = useCallback(async (item: LancamentoItem) => {
|
const handleToggleSettlement = useCallback(async (item: LancamentoItem) => {
|
||||||
if (item.paymentMethod === "Cartão de crédito") {
|
if (item.paymentMethod === "Cartão de crédito") {
|
||||||
@@ -296,6 +317,16 @@ export function LancamentosPage({
|
|||||||
setCopyOpen(true);
|
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) => {
|
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
|
||||||
if (item.seriesId) {
|
if (item.seriesId) {
|
||||||
setPendingDeleteData(item);
|
setPendingDeleteData(item);
|
||||||
@@ -325,6 +356,7 @@ export function LancamentosPage({
|
|||||||
<>
|
<>
|
||||||
<LancamentosTable
|
<LancamentosTable
|
||||||
data={lancamentos}
|
data={lancamentos}
|
||||||
|
currentUserId={currentUserId}
|
||||||
pagadorFilterOptions={pagadorFilterOptions}
|
pagadorFilterOptions={pagadorFilterOptions}
|
||||||
categoriaFilterOptions={categoriaFilterOptions}
|
categoriaFilterOptions={categoriaFilterOptions}
|
||||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||||
@@ -332,8 +364,10 @@ export function LancamentosPage({
|
|||||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
|
onImport={handleImport}
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onBulkDelete={handleMultipleBulkDelete}
|
onBulkDelete={handleMultipleBulkDelete}
|
||||||
|
onBulkImport={handleBulkImport}
|
||||||
onViewDetails={handleViewDetails}
|
onViewDetails={handleViewDetails}
|
||||||
onToggleSettlement={handleToggleSettlement}
|
onToggleSettlement={handleToggleSettlement}
|
||||||
onAnticipate={handleAnticipate}
|
onAnticipate={handleAnticipate}
|
||||||
@@ -381,6 +415,38 @@ export function LancamentosPage({
|
|||||||
defaultPeriod={selectedPeriod}
|
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
|
<LancamentoDialog
|
||||||
mode="update"
|
mode="update"
|
||||||
open={editOpen && !!selectedLancamento}
|
open={editOpen && !!selectedLancamento}
|
||||||
|
|||||||
@@ -91,8 +91,10 @@ const resolveLogoSrc = (logo: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type BuildColumnsArgs = {
|
type BuildColumnsArgs = {
|
||||||
|
currentUserId: string;
|
||||||
onEdit?: (item: LancamentoItem) => void;
|
onEdit?: (item: LancamentoItem) => void;
|
||||||
onCopy?: (item: LancamentoItem) => void;
|
onCopy?: (item: LancamentoItem) => void;
|
||||||
|
onImport?: (item: LancamentoItem) => void;
|
||||||
onConfirmDelete?: (item: LancamentoItem) => void;
|
onConfirmDelete?: (item: LancamentoItem) => void;
|
||||||
onViewDetails?: (item: LancamentoItem) => void;
|
onViewDetails?: (item: LancamentoItem) => void;
|
||||||
onToggleSettlement?: (item: LancamentoItem) => void;
|
onToggleSettlement?: (item: LancamentoItem) => void;
|
||||||
@@ -103,8 +105,10 @@ type BuildColumnsArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildColumns = ({
|
const buildColumns = ({
|
||||||
|
currentUserId,
|
||||||
onEdit,
|
onEdit,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
onImport,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
@@ -116,6 +120,7 @@ const buildColumns = ({
|
|||||||
const noop = () => undefined;
|
const noop = () => undefined;
|
||||||
const handleEdit = onEdit ?? noop;
|
const handleEdit = onEdit ?? noop;
|
||||||
const handleCopy = onCopy ?? noop;
|
const handleCopy = onCopy ?? noop;
|
||||||
|
const handleImport = onImport ?? noop;
|
||||||
const handleConfirmDelete = onConfirmDelete ?? noop;
|
const handleConfirmDelete = onConfirmDelete ?? noop;
|
||||||
const handleViewDetails = onViewDetails ?? noop;
|
const handleViewDetails = onViewDetails ?? noop;
|
||||||
const handleToggleSettlement = onToggleSettlement ?? noop;
|
const handleToggleSettlement = onToggleSettlement ?? noop;
|
||||||
@@ -419,6 +424,7 @@ const buildColumns = ({
|
|||||||
contaLogo,
|
contaLogo,
|
||||||
cartaoId,
|
cartaoId,
|
||||||
contaId,
|
contaId,
|
||||||
|
userId,
|
||||||
} = row.original;
|
} = row.original;
|
||||||
const label = cartaoName ?? contaName;
|
const label = cartaoName ?? contaName;
|
||||||
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
|
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
|
||||||
@@ -428,20 +434,14 @@ const buildColumns = ({
|
|||||||
? `/contas/${contaId}/extrato`
|
? `/contas/${contaId}/extrato`
|
||||||
: null;
|
: null;
|
||||||
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
|
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
|
||||||
|
const isOwnData = userId === currentUserId;
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<Link
|
<>
|
||||||
href={href ?? "#"}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2",
|
|
||||||
href ? "underline " : "pointer-events-none"
|
|
||||||
)}
|
|
||||||
aria-disabled={!href}
|
|
||||||
>
|
|
||||||
{logoSrc ? (
|
{logoSrc ? (
|
||||||
<Image
|
<Image
|
||||||
src={logoSrc}
|
src={logoSrc}
|
||||||
@@ -455,6 +455,23 @@ const buildColumns = ({
|
|||||||
{Icon ? (
|
{Icon ? (
|
||||||
<Icon className="size-4 text-muted-foreground" aria-hidden />
|
<Icon className="size-4 text-muted-foreground" aria-hidden />
|
||||||
) : null}
|
) : 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>
|
</Link>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -526,6 +543,7 @@ const buildColumns = ({
|
|||||||
<RiEyeLine className="size-4" />
|
<RiEyeLine className="size-4" />
|
||||||
Detalhes
|
Detalhes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{row.original.userId === currentUserId && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => handleEdit(row.original)}
|
onSelect={() => handleEdit(row.original)}
|
||||||
disabled={row.original.readonly}
|
disabled={row.original.readonly}
|
||||||
@@ -533,12 +551,20 @@ const buildColumns = ({
|
|||||||
<RiPencilLine className="size-4" />
|
<RiPencilLine className="size-4" />
|
||||||
Editar
|
Editar
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{row.original.categoriaName !== "Pagamentos" && (
|
)}
|
||||||
|
{row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && (
|
||||||
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
||||||
<RiFileCopyLine className="size-4" />
|
<RiFileCopyLine className="size-4" />
|
||||||
Copiar
|
Copiar
|
||||||
</DropdownMenuItem>
|
</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
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onSelect={() => handleConfirmDelete(row.original)}
|
onSelect={() => handleConfirmDelete(row.original)}
|
||||||
@@ -547,9 +573,11 @@ const buildColumns = ({
|
|||||||
<RiDeleteBin5Line className="size-4" />
|
<RiDeleteBin5Line className="size-4" />
|
||||||
Remover
|
Remover
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Opções de Antecipação */}
|
{/* Opções de Antecipação */}
|
||||||
{row.original.condition === "Parcelado" &&
|
{row.original.userId === currentUserId &&
|
||||||
|
row.original.condition === "Parcelado" &&
|
||||||
row.original.seriesId && (
|
row.original.seriesId && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -594,6 +622,7 @@ const buildColumns = ({
|
|||||||
|
|
||||||
type LancamentosTableProps = {
|
type LancamentosTableProps = {
|
||||||
data: LancamentoItem[];
|
data: LancamentoItem[];
|
||||||
|
currentUserId: string;
|
||||||
pagadorFilterOptions?: LancamentoFilterOption[];
|
pagadorFilterOptions?: LancamentoFilterOption[];
|
||||||
categoriaFilterOptions?: LancamentoFilterOption[];
|
categoriaFilterOptions?: LancamentoFilterOption[];
|
||||||
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
|
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
|
||||||
@@ -601,8 +630,10 @@ type LancamentosTableProps = {
|
|||||||
onMassAdd?: () => void;
|
onMassAdd?: () => void;
|
||||||
onEdit?: (item: LancamentoItem) => void;
|
onEdit?: (item: LancamentoItem) => void;
|
||||||
onCopy?: (item: LancamentoItem) => void;
|
onCopy?: (item: LancamentoItem) => void;
|
||||||
|
onImport?: (item: LancamentoItem) => void;
|
||||||
onConfirmDelete?: (item: LancamentoItem) => void;
|
onConfirmDelete?: (item: LancamentoItem) => void;
|
||||||
onBulkDelete?: (items: LancamentoItem[]) => void;
|
onBulkDelete?: (items: LancamentoItem[]) => void;
|
||||||
|
onBulkImport?: (items: LancamentoItem[]) => void;
|
||||||
onViewDetails?: (item: LancamentoItem) => void;
|
onViewDetails?: (item: LancamentoItem) => void;
|
||||||
onToggleSettlement?: (item: LancamentoItem) => void;
|
onToggleSettlement?: (item: LancamentoItem) => void;
|
||||||
onAnticipate?: (item: LancamentoItem) => void;
|
onAnticipate?: (item: LancamentoItem) => void;
|
||||||
@@ -614,6 +645,7 @@ type LancamentosTableProps = {
|
|||||||
|
|
||||||
export function LancamentosTable({
|
export function LancamentosTable({
|
||||||
data,
|
data,
|
||||||
|
currentUserId,
|
||||||
pagadorFilterOptions = [],
|
pagadorFilterOptions = [],
|
||||||
categoriaFilterOptions = [],
|
categoriaFilterOptions = [],
|
||||||
contaCartaoFilterOptions = [],
|
contaCartaoFilterOptions = [],
|
||||||
@@ -621,8 +653,10 @@ export function LancamentosTable({
|
|||||||
onMassAdd,
|
onMassAdd,
|
||||||
onEdit,
|
onEdit,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
onImport,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onBulkDelete,
|
onBulkDelete,
|
||||||
|
onBulkImport,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
onAnticipate,
|
onAnticipate,
|
||||||
@@ -643,8 +677,10 @@ export function LancamentosTable({
|
|||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildColumns({
|
buildColumns({
|
||||||
|
currentUserId,
|
||||||
onEdit,
|
onEdit,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
onImport,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
@@ -654,8 +690,10 @@ export function LancamentosTable({
|
|||||||
showActions,
|
showActions,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
currentUserId,
|
||||||
onEdit,
|
onEdit,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
onImport,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onToggleSettlement,
|
onToggleSettlement,
|
||||||
@@ -693,6 +731,10 @@ export function LancamentosTable({
|
|||||||
0
|
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 = () => {
|
const handleBulkDelete = () => {
|
||||||
if (onBulkDelete && selectedCount > 0) {
|
if (onBulkDelete && selectedCount > 0) {
|
||||||
const selectedItems = selectedRows.map((row) => row.original);
|
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 =
|
const showTopControls =
|
||||||
Boolean(onCreate) || Boolean(onMassAdd) || showFilters;
|
Boolean(onCreate) || Boolean(onMassAdd) || shouldShowFilters;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -738,10 +788,10 @@ export function LancamentosTable({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className={showFilters ? "hidden sm:block" : ""} />
|
<span className={shouldShowFilters ? "hidden sm:block" : ""} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showFilters ? (
|
{shouldShowFilters ? (
|
||||||
<LancamentosFilters
|
<LancamentosFilters
|
||||||
pagadorOptions={pagadorFilterOptions}
|
pagadorOptions={pagadorFilterOptions}
|
||||||
categoriaOptions={categoriaFilterOptions}
|
categoriaOptions={categoriaFilterOptions}
|
||||||
@@ -752,7 +802,7 @@ export function LancamentosTable({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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-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">
|
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
|
||||||
<span>
|
<span>
|
||||||
@@ -782,6 +832,36 @@ export function LancamentosTable({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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">
|
<Card className="py-2">
|
||||||
<CardContent className="px-2 py-4 sm:px-4">
|
<CardContent className="px-2 py-4 sm:px-4">
|
||||||
{hasRows ? (
|
{hasRows ? (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type LancamentoItem = {
|
export type LancamentoItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
purchaseDate: string;
|
purchaseDate: string;
|
||||||
period: string;
|
period: string;
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function PagadoresPage({
|
|||||||
value={shareCodeInput}
|
value={shareCodeInput}
|
||||||
onChange={(event) => setShareCodeInput(event.target.value)}
|
onChange={(event) => setShareCodeInput(event.target.value)}
|
||||||
disabled={joinPending}
|
disabled={joinPending}
|
||||||
className="w-56"
|
className="w-56 border-dashed"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={joinPending}>
|
<Button type="submit" disabled={joinPending}>
|
||||||
{joinPending ? "Adicionando..." : "Adicionar por código"}
|
{joinPending ? "Adicionando..." : "Adicionar por código"}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type LancamentoFormOverrides = {
|
|||||||
defaultCartaoId?: string | null;
|
defaultCartaoId?: string | null;
|
||||||
defaultPaymentMethod?: string | null;
|
defaultPaymentMethod?: string | null;
|
||||||
defaultPurchaseDate?: string | null;
|
defaultPurchaseDate?: string | null;
|
||||||
|
isImporting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +65,13 @@ export function buildLancamentoInitialState(
|
|||||||
preferredPeriod && /^\d{4}-\d{2}$/.test(preferredPeriod)
|
preferredPeriod && /^\d{4}-\d{2}$/.test(preferredPeriod)
|
||||||
? preferredPeriod
|
? preferredPeriod
|
||||||
: derivedPeriod;
|
: 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 =
|
const boletoPaymentDate =
|
||||||
lancamento?.boletoPaymentDate ??
|
lancamento?.boletoPaymentDate ??
|
||||||
(paymentMethod === "Boleto" && (lancamento?.isSettled ?? false)
|
(paymentMethod === "Boleto" && (lancamento?.isSettled ?? false)
|
||||||
@@ -92,12 +99,12 @@ export function buildLancamentoInitialState(
|
|||||||
contaId:
|
contaId:
|
||||||
paymentMethod === "Cartão de crédito"
|
paymentMethod === "Cartão de crédito"
|
||||||
? undefined
|
? undefined
|
||||||
: lancamento?.contaId ?? undefined,
|
: isImporting ? undefined : (lancamento?.contaId ?? undefined),
|
||||||
cartaoId:
|
cartaoId:
|
||||||
paymentMethod === "Cartão de crédito"
|
paymentMethod === "Cartão de crédito"
|
||||||
? lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined
|
? isImporting ? (overrides?.defaultCartaoId ?? undefined) : (lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined)
|
||||||
: undefined,
|
: undefined,
|
||||||
categoriaId: lancamento?.categoriaId ?? undefined,
|
categoriaId: isImporting ? undefined : (lancamento?.categoriaId ?? undefined),
|
||||||
installmentCount: lancamento?.installmentCount
|
installmentCount: lancamento?.installmentCount
|
||||||
? String(lancamento.installmentCount)
|
? String(lancamento.installmentCount)
|
||||||
: "",
|
: "",
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ type LancamentoRowWithRelations = typeof lancamentos.$inferSelect & {
|
|||||||
export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) =>
|
export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) =>
|
||||||
rows.map((item) => ({
|
rows.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
userId: item.userId,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
purchaseDate: item.purchaseDate?.toISOString() ?? new Date().toISOString(),
|
purchaseDate: item.purchaseDate?.toISOString() ?? new Date().toISOString(),
|
||||||
period: item.period ?? "",
|
period: item.period ?? "",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
"@vercel/speed-insights": "^1.3.1",
|
"@vercel/speed-insights": "^1.3.1",
|
||||||
"ai": "^6.0.6",
|
"ai": "^6.0.7",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"better-auth": "1.4.10",
|
"better-auth": "1.4.10",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"jspdf": "^4.0.0",
|
"jspdf": "^4.0.0",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.27",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "4.3.4"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
|
|||||||
134
pnpm-lock.yaml
generated
134
pnpm-lock.yaml
generated
@@ -10,16 +10,16 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/anthropic':
|
'@ai-sdk/anthropic':
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(zod@4.3.4)
|
version: 3.0.2(zod@4.3.5)
|
||||||
'@ai-sdk/google':
|
'@ai-sdk/google':
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(zod@4.3.4)
|
version: 3.0.2(zod@4.3.5)
|
||||||
'@ai-sdk/openai':
|
'@ai-sdk/openai':
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(zod@4.3.4)
|
version: 3.0.2(zod@4.3.5)
|
||||||
'@openrouter/ai-sdk-provider':
|
'@openrouter/ai-sdk-provider':
|
||||||
specifier: ^1.5.4
|
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':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.12
|
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)
|
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
|
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)
|
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:
|
ai:
|
||||||
specifier: ^6.0.6
|
specifier: ^6.0.7
|
||||||
version: 6.0.6(zod@4.3.4)
|
version: 6.0.7(zod@4.3.5)
|
||||||
babel-plugin-react-compiler:
|
babel-plugin-react-compiler:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -123,11 +123,11 @@ importers:
|
|||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
jspdf-autotable:
|
jspdf-autotable:
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.7
|
||||||
version: 5.0.2(jspdf@4.0.0)
|
version: 5.0.7(jspdf@4.0.0)
|
||||||
motion:
|
motion:
|
||||||
specifier: ^12.23.26
|
specifier: ^12.23.27
|
||||||
version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 12.23.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.1
|
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)
|
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
|
specifier: ^0.18.5
|
||||||
version: 0.18.5
|
version: 0.18.5
|
||||||
zod:
|
zod:
|
||||||
specifier: 4.3.4
|
specifier: 4.3.5
|
||||||
version: 4.3.4
|
version: 4.3.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: 4.1.18
|
specifier: 4.1.18
|
||||||
@@ -222,8 +222,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
|
|
||||||
'@ai-sdk/gateway@3.0.5':
|
'@ai-sdk/gateway@3.0.6':
|
||||||
resolution: {integrity: sha512-AtxA1wcoKTHr9uFoC5KZEXqJP4SMW4j3VbcliUECUYssbWbePJ9+b3AaCny1lxf1xhDK9EIyAgBOKhXoQSr9nA==}
|
resolution: {integrity: sha512-oEpwjM0PIaSUErtZI8Ag+gQ+ZelysRWA96N5ahvOc5e9d7QkKJWF0POWx0nI1qBxvmUSw7ca0sLTVw+J5yn7Tg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
@@ -2148,8 +2148,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
ai@6.0.6:
|
ai@6.0.7:
|
||||||
resolution: {integrity: sha512-LM0eAMWVn3RTj+0X5O1m/8g+7QiTeWG5aN5FsDbdmCkAQHVg93XxLbljFOLzi0NMjuJgf7fKLKmWoPsrdMyqfw==}
|
resolution: {integrity: sha512-kLzSXHdW6cAcb2mFSIfkbfzxYqqjrUnyhrB1sg855qlC+6XkLI8hmwFE8f/4SnjmtcTDOnkIaVjWoO5i5Ir0bw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4.1.8
|
zod: ^3.25.76 || ^4.1.8
|
||||||
@@ -2952,8 +2952,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
framer-motion@12.23.26:
|
framer-motion@12.23.27:
|
||||||
resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
|
resolution: {integrity: sha512-EAcX8FS8jzZ4tSKpj+1GhwbVY+r1gfamPFwXZAsioPqu/ffRwU2otkKg6GEDCR41FVJv3RoBN7Aqep6drL9Itg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emotion/is-prop-valid': '*'
|
'@emotion/is-prop-valid': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
@@ -3295,10 +3295,10 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jspdf-autotable@5.0.2:
|
jspdf-autotable@5.0.7:
|
||||||
resolution: {integrity: sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==}
|
resolution: {integrity: sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
jspdf: ^2 || ^3
|
jspdf: ^2 || ^3 || ^4
|
||||||
|
|
||||||
jspdf@4.0.0:
|
jspdf@4.0.0:
|
||||||
resolution: {integrity: sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==}
|
resolution: {integrity: sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==}
|
||||||
@@ -3450,8 +3450,8 @@ packages:
|
|||||||
motion-utils@12.23.6:
|
motion-utils@12.23.6:
|
||||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
motion@12.23.26:
|
motion@12.23.27:
|
||||||
resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==}
|
resolution: {integrity: sha512-EDb0hAE6jNX8BHpmQK1GBf9Eizx9bg/Tz2KEAJBOGEnIJp8W77QweRpVb05U8R0L0/LXndHmS1Xv3fwXJh/kcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emotion/is-prop-valid': '*'
|
'@emotion/is-prop-valid': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
@@ -4240,42 +4240,42 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.0 || ^4.0.0
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
|
||||||
zod@4.3.4:
|
zod@4.3.5:
|
||||||
resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==}
|
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ai-sdk/anthropic@3.0.2(zod@4.3.4)':
|
'@ai-sdk/anthropic@3.0.2(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 3.0.1
|
'@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)
|
||||||
zod: 4.3.4
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 3.0.1
|
'@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
|
'@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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 3.0.1
|
'@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)
|
||||||
zod: 4.3.4
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 3.0.1
|
'@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)
|
||||||
zod: 4.3.4
|
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:
|
dependencies:
|
||||||
'@ai-sdk/provider': 3.0.1
|
'@ai-sdk/provider': 3.0.1
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
eventsource-parser: 3.0.6
|
eventsource-parser: 3.0.6
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
|
|
||||||
'@ai-sdk/provider@3.0.1':
|
'@ai-sdk/provider@3.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4385,20 +4385,20 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@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:
|
dependencies:
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.21
|
'@better-fetch/fetch': 1.1.21
|
||||||
'@standard-schema/spec': 1.1.0
|
'@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
|
jose: 6.1.3
|
||||||
kysely: 0.28.9
|
kysely: 0.28.9
|
||||||
nanostores: 1.1.0
|
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:
|
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-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.21
|
'@better-fetch/fetch': 1.1.21
|
||||||
|
|
||||||
@@ -4903,15 +4903,15 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/is-core-module@1.0.39': {}
|
'@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:
|
dependencies:
|
||||||
'@openrouter/sdk': 0.1.27
|
'@openrouter/sdk': 0.1.27
|
||||||
ai: 6.0.6(zod@4.3.4)
|
ai: 6.0.7(zod@4.3.5)
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
|
|
||||||
'@openrouter/sdk@0.1.27':
|
'@openrouter/sdk@0.1.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
@@ -5886,13 +5886,13 @@ snapshots:
|
|||||||
|
|
||||||
adler-32@1.3.1: {}
|
adler-32@1.3.1: {}
|
||||||
|
|
||||||
ai@6.0.6(zod@4.3.4):
|
ai@6.0.7(zod@4.3.5):
|
||||||
dependencies:
|
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': 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
|
'@opentelemetry/api': 1.9.0
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
dependencies:
|
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):
|
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:
|
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/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))
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.21
|
'@better-fetch/fetch': 1.1.21
|
||||||
'@noble/ciphers': 2.1.1
|
'@noble/ciphers': 2.1.1
|
||||||
'@noble/hashes': 2.0.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
|
defu: 6.1.4
|
||||||
jose: 6.1.3
|
jose: 6.1.3
|
||||||
kysely: 0.28.9
|
kysely: 0.28.9
|
||||||
nanostores: 1.1.0
|
nanostores: 1.1.0
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-kit: 0.31.8
|
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)
|
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: 19.2.3
|
||||||
react-dom: 19.2.3(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:
|
dependencies:
|
||||||
'@better-auth/utils': 0.3.0
|
'@better-auth/utils': 0.3.0
|
||||||
'@better-fetch/fetch': 1.1.21
|
'@better-fetch/fetch': 1.1.21
|
||||||
rou3: 0.7.12
|
rou3: 0.7.12
|
||||||
set-cookie-parser: 2.7.2
|
set-cookie-parser: 2.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6661,8 +6661,8 @@ snapshots:
|
|||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
hermes-parser: 0.25.1
|
hermes-parser: 0.25.1
|
||||||
zod: 4.3.4
|
zod: 4.3.5
|
||||||
zod-validation-error: 4.0.2(zod@4.3.4)
|
zod-validation-error: 4.0.2(zod@4.3.5)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6833,7 +6833,7 @@ snapshots:
|
|||||||
|
|
||||||
frac@1.1.2: {}
|
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:
|
dependencies:
|
||||||
motion-dom: 12.23.23
|
motion-dom: 12.23.23
|
||||||
motion-utils: 12.23.6
|
motion-utils: 12.23.6
|
||||||
@@ -7158,7 +7158,7 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
jspdf-autotable@5.0.2(jspdf@4.0.0):
|
jspdf-autotable@5.0.7(jspdf@4.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
jspdf: 4.0.0
|
jspdf: 4.0.0
|
||||||
|
|
||||||
@@ -7297,9 +7297,9 @@ snapshots:
|
|||||||
|
|
||||||
motion-utils@12.23.6: {}
|
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:
|
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
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
@@ -8211,8 +8211,8 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
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:
|
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";
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
const laranjinha = localFont({
|
const ai_sans = localFont({
|
||||||
src: [
|
src: [
|
||||||
{
|
{
|
||||||
path: "./LaranjinhaTextPro_Rg.woff2",
|
path: "./AISans-Regular.woff2",
|
||||||
weight: "400",
|
weight: "400",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "./LaranjinhaDisplayPro_Bd.woff2",
|
path: "./AISans-Semibold.woff2",
|
||||||
weight: "700",
|
weight: "700",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
},
|
},
|
||||||
@@ -16,8 +16,8 @@ const laranjinha = localFont({
|
|||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const main_font = laranjinha;
|
const main_font = ai_sans;
|
||||||
const money_font = laranjinha;
|
const money_font = ai_sans;
|
||||||
const title_font = laranjinha;
|
const title_font = ai_sans;
|
||||||
|
|
||||||
export { main_font, money_font, title_font };
|
export { main_font, money_font, title_font };
|
||||||
|
|||||||
Reference in New Issue
Block a user