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

Registre suas contas bancárias, cartões e dinheiro. Adicione receitas, despesas e transferências. Organize - por categorias. + por categorias. Extratos detalhados por conta. +

+
+
+ + + + + +
+
+ +
+
+

+ Parcelamentos avançados +

+

+ Controle completo de compras parceladas. Antecipe parcelas + com cálculo automático de desconto. Veja análise + consolidada de todas as parcelas em aberto. +

+
+
+
+
+ + + +
+
+ +
+
+

+ Insights com IA +

+

+ Análises financeiras geradas por IA (Claude, GPT, Gemini). + Insights personalizados sobre seus padrões de gastos e + recomendações inteligentes.

@@ -227,8 +274,9 @@ export default async function Page() { Relatórios e gráficos

- Dashboard com resumo mensal. Gráficos de evolução do - patrimônio. Entenda pra onde seu dinheiro está indo. + Dashboard com 20+ widgets interativos. Relatórios + detalhados por categoria. Gráficos de evolução e + comparativos. Exportação em PDF e Excel.

@@ -246,8 +294,29 @@ export default async function Page() { Faturas de cartão

- Cadastre seus cartões e acompanhe as faturas. Veja o que - ainda não foi fechado. Controle limites e vencimentos. + Cadastre seus cartões e acompanhe as faturas por período. + Veja o que ainda não foi fechado. Controle limites, + vencimentos e fechamentos. +

+ + + + + + + +
+
+ +
+
+

+ Gestão colaborativa +

+

+ Compartilhe pagadores com permissões granulares (admin/ + viewer). Notificações automáticas por e-mail. Colabore em + lançamentos compartilhados.

@@ -262,12 +331,12 @@ export default async function Page() {

- Categorias personalizadas + Categorias e orçamentos

- Crie e organize suas próprias categorias. Moradia, - alimentação, transporte, ou o que fizer sentido pra - você. + Crie categorias personalizadas. Defina orçamentos mensais + e acompanhe o quanto gastou vs. planejado com indicadores + visuais.

@@ -278,16 +347,16 @@ export default async function Page() {
- +
-

Orçamentos

+

+ Anotações e tarefas +

- Defina quanto quer gastar por categoria no mês. - Acompanhe se está dentro do planejado. + Crie notas de texto e listas de tarefas com checkboxes. + Sistema de arquivamento para manter histórico. Organize + seus planejamentos financeiros.

@@ -305,8 +374,69 @@ export default async function Page() { Calendário financeiro

- Visualize suas transações em calendário mensal. Nunca - perca prazos importantes. + Visualize todas as transações em calendário mensal. + Navegação intuitiva por data. Nunca perca prazos de + pagamentos importantes. +

+ + +
+
+ + + +
+
+ +
+
+

+ Importação em massa +

+

+ 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. +

+
+
+
+
+ + + +
+
+ +
+
+

+ Modo privacidade +

+

+ Oculte valores sensíveis com um clique. Tema dark/light + adaptável. Preferências personalizáveis. Calculadora + integrada para planejamento. +

+
+
+
+
+ + + +
+
+ +
+
+

+ Performance otimizada +

+

+ Dashboard carrega em ~200-500ms com 18+ queries paralelas. + Índices otimizados. Type-safe em toda codebase. Isolamento + completo de dados por usuário.

diff --git a/components/lancamentos/dialogs/bulk-import-dialog.tsx b/components/lancamentos/dialogs/bulk-import-dialog.tsx new file mode 100644 index 0000000..eaaed87 --- /dev/null +++ b/components/lancamentos/dialogs/bulk-import-dialog.tsx @@ -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( + defaultPagadorId ?? undefined + ); + const [categoriaId, setCategoriaId] = useState(undefined); + const [contaId, setContaId] = useState(undefined); + const [cartaoId, setCartaoId] = useState(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) => { + 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 ( + + + + Importar Lançamentos + + Importando {itemCount} {itemCount === 1 ? "lançamento" : "lançamentos"}. + Selecione o pagador, categoria e forma de pagamento para aplicar a todos. + + + +
+
+ + +
+ +
+ + +
+ + {hasNonCredit && ( +
+ + +
+ )} + + {hasCredit && ( +
+ + +
+ )} + + + + + +
+
+
+ ); +} diff --git a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog-types.ts b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog-types.ts index f3399fb..f73c353 100644 --- a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog-types.ts +++ b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog-types.ts @@ -22,6 +22,7 @@ export interface LancamentoDialogProps { defaultPurchaseDate?: string | null; lockCartaoSelection?: boolean; lockPaymentMethod?: boolean; + isImporting?: boolean; onBulkEditRequest?: (data: { id: string; name: string; diff --git a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx index fe5f78d..c417d78 100644 --- a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx +++ b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx @@ -62,6 +62,7 @@ export function LancamentoDialog({ defaultPurchaseDate, lockCartaoSelection, lockPaymentMethod, + isImporting = false, onBulkEditRequest, }: LancamentoDialogProps) { const [dialogOpen, setDialogOpen] = useControlledState( @@ -75,6 +76,7 @@ export function LancamentoDialog({ defaultCartaoId, defaultPaymentMethod, defaultPurchaseDate, + isImporting, }) ); const [periodDirty, setPeriodDirty] = useState(false); @@ -92,6 +94,7 @@ export function LancamentoDialog({ defaultCartaoId, defaultPaymentMethod, defaultPurchaseDate, + isImporting, } ) ); @@ -106,6 +109,7 @@ export function LancamentoDialog({ defaultCartaoId, defaultPaymentMethod, defaultPurchaseDate, + isImporting, ]); const primaryPagador = formState.pagadorId; @@ -301,15 +305,20 @@ export function LancamentoDialog({ ] ); - const isCopyMode = mode === "create" && Boolean(lancamento); + const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting; + const isImportMode = mode === "create" && Boolean(lancamento) && isImporting; const title = mode === "create" - ? isCopyMode + ? isImportMode + ? "Importar para Minha Conta" + : isCopyMode ? "Copiar lançamento" : "Novo lançamento" : "Editar lançamento"; const description = mode === "create" - ? isCopyMode + ? isImportMode + ? "Importando lançamento de outro usuário. Ajuste a categoria, pagador e cartão/conta antes de salvar." + : isCopyMode ? "Os dados do lançamento foram copiados. Revise e ajuste conforme necessário antes de salvar." : "Informe os dados abaixo para registrar um novo lançamento." : "Atualize as informações do lançamento selecionado."; diff --git a/components/lancamentos/page/lancamentos-page.tsx b/components/lancamentos/page/lancamentos-page.tsx index d894803..6d327f0 100644 --- a/components/lancamentos/page/lancamentos-page.tsx +++ b/components/lancamentos/page/lancamentos-page.tsx @@ -15,6 +15,7 @@ import { toast } from "sonner"; import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog"; import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog"; import { BulkActionDialog, type BulkActionScope } from "../dialogs/bulk-action-dialog"; +import { BulkImportDialog } from "../dialogs/bulk-import-dialog"; import { LancamentoDetailsDialog } from "../dialogs/lancamento-details-dialog"; import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog"; import { LancamentosTable } from "../table/lancamentos-table"; @@ -27,6 +28,7 @@ import type { } from "../types"; interface LancamentosPageProps { + currentUserId: string; lancamentos: LancamentoItem[]; pagadorOptions: SelectOption[]; splitPagadorOptions: SelectOption[]; @@ -44,9 +46,17 @@ interface LancamentosPageProps { defaultPaymentMethod?: string | null; lockCartaoSelection?: boolean; lockPaymentMethod?: boolean; + // Opções específicas para o dialog de importação (quando visualizando dados de outro usuário) + importPagadorOptions?: SelectOption[]; + importSplitPagadorOptions?: SelectOption[]; + importDefaultPagadorId?: string | null; + importContaOptions?: SelectOption[]; + importCartaoOptions?: SelectOption[]; + importCategoriaOptions?: SelectOption[]; } export function LancamentosPage({ + currentUserId, lancamentos, pagadorOptions, splitPagadorOptions, @@ -64,6 +74,12 @@ export function LancamentosPage({ defaultPaymentMethod, lockCartaoSelection, lockPaymentMethod, + importPagadorOptions, + importSplitPagadorOptions, + importDefaultPagadorId, + importContaOptions, + importCartaoOptions, + importCategoriaOptions, }: LancamentosPageProps) { const [selectedLancamento, setSelectedLancamento] = useState(null); @@ -72,6 +88,9 @@ export function LancamentosPage({ const [copyOpen, setCopyOpen] = useState(false); const [lancamentoToCopy, setLancamentoToCopy] = useState(null); + const [importOpen, setImportOpen] = useState(false); + const [lancamentoToImport, setLancamentoToImport] = + useState(null); const [massAddOpen, setMassAddOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [lancamentoToDelete, setLancamentoToDelete] = @@ -105,6 +124,8 @@ export function LancamentosPage({ const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false); const [selectedForAnticipation, setSelectedForAnticipation] = useState(null); + const [bulkImportOpen, setBulkImportOpen] = useState(false); + const [lancamentosToImport, setLancamentosToImport] = useState([]); const handleToggleSettlement = useCallback(async (item: LancamentoItem) => { if (item.paymentMethod === "Cartão de crédito") { @@ -296,6 +317,16 @@ export function LancamentosPage({ setCopyOpen(true); }, []); + const handleImport = useCallback((item: LancamentoItem) => { + setLancamentoToImport(item); + setImportOpen(true); + }, []); + + const handleBulkImport = useCallback((items: LancamentoItem[]) => { + setLancamentosToImport(items); + setBulkImportOpen(true); + }, []); + const handleConfirmDelete = useCallback((item: LancamentoItem) => { if (item.seriesId) { setPendingDeleteData(item); @@ -325,6 +356,7 @@ export function LancamentosPage({ <> + { + 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} + /> + + 0} + onOpenChange={setBulkImportOpen} + items={lancamentosToImport} + pagadorOptions={importPagadorOptions ?? pagadorOptions} + contaOptions={importContaOptions ?? contaOptions} + cartaoOptions={importCartaoOptions ?? cartaoOptions} + categoriaOptions={importCategoriaOptions ?? categoriaOptions} + defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId} + /> + { }; type BuildColumnsArgs = { + currentUserId: string; onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; + onImport?: (item: LancamentoItem) => void; onConfirmDelete?: (item: LancamentoItem) => void; onViewDetails?: (item: LancamentoItem) => void; onToggleSettlement?: (item: LancamentoItem) => void; @@ -103,8 +105,10 @@ type BuildColumnsArgs = { }; const buildColumns = ({ + currentUserId, onEdit, onCopy, + onImport, onConfirmDelete, onViewDetails, onToggleSettlement, @@ -116,6 +120,7 @@ const buildColumns = ({ const noop = () => undefined; const handleEdit = onEdit ?? noop; const handleCopy = onCopy ?? noop; + const handleImport = onImport ?? noop; const handleConfirmDelete = onConfirmDelete ?? noop; const handleViewDetails = onViewDetails ?? noop; const handleToggleSettlement = onToggleSettlement ?? noop; @@ -419,6 +424,7 @@ const buildColumns = ({ contaLogo, cartaoId, contaId, + userId, } = row.original; const label = cartaoName ?? contaName; const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); @@ -428,20 +434,14 @@ const buildColumns = ({ ? `/contas/${contaId}/extrato` : null; const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null; + const isOwnData = userId === currentUserId; if (!label) { return "—"; } - return ( - + const content = ( + <> {logoSrc ? ( ) : null} + + ); + + if (!isOwnData) { + return
{content}
; + } + + return ( + + {content} ); }, @@ -526,30 +543,41 @@ const buildColumns = ({ Detalhes - handleEdit(row.original)} - disabled={row.original.readonly} - > - - Editar - - {row.original.categoriaName !== "Pagamentos" && ( + {row.original.userId === currentUserId && ( + handleEdit(row.original)} + disabled={row.original.readonly} + > + + Editar + + )} + {row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && ( handleCopy(row.original)}> Copiar )} - handleConfirmDelete(row.original)} - disabled={row.original.readonly} - > - - Remover - + {row.original.categoriaName !== "Pagamentos" && row.original.userId !== currentUserId && ( + handleImport(row.original)}> + + Importar para Minha Conta + + )} + {row.original.userId === currentUserId && ( + handleConfirmDelete(row.original)} + disabled={row.original.readonly} + > + + Remover + + )} {/* Opções de Antecipação */} - {row.original.condition === "Parcelado" && + {row.original.userId === currentUserId && + row.original.condition === "Parcelado" && row.original.seriesId && ( <> @@ -594,6 +622,7 @@ const buildColumns = ({ type LancamentosTableProps = { data: LancamentoItem[]; + currentUserId: string; pagadorFilterOptions?: LancamentoFilterOption[]; categoriaFilterOptions?: LancamentoFilterOption[]; contaCartaoFilterOptions?: ContaCartaoFilterOption[]; @@ -601,8 +630,10 @@ type LancamentosTableProps = { onMassAdd?: () => void; onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; + onImport?: (item: LancamentoItem) => void; onConfirmDelete?: (item: LancamentoItem) => void; onBulkDelete?: (items: LancamentoItem[]) => void; + onBulkImport?: (items: LancamentoItem[]) => void; onViewDetails?: (item: LancamentoItem) => void; onToggleSettlement?: (item: LancamentoItem) => void; onAnticipate?: (item: LancamentoItem) => void; @@ -614,6 +645,7 @@ type LancamentosTableProps = { export function LancamentosTable({ data, + currentUserId, pagadorFilterOptions = [], categoriaFilterOptions = [], contaCartaoFilterOptions = [], @@ -621,8 +653,10 @@ export function LancamentosTable({ onMassAdd, onEdit, onCopy, + onImport, onConfirmDelete, onBulkDelete, + onBulkImport, onViewDetails, onToggleSettlement, onAnticipate, @@ -643,8 +677,10 @@ export function LancamentosTable({ const columns = useMemo( () => buildColumns({ + currentUserId, onEdit, onCopy, + onImport, onConfirmDelete, onViewDetails, onToggleSettlement, @@ -654,8 +690,10 @@ export function LancamentosTable({ showActions, }), [ + currentUserId, onEdit, onCopy, + onImport, onConfirmDelete, onViewDetails, onToggleSettlement, @@ -693,6 +731,10 @@ export function LancamentosTable({ 0 ); + // Check if all data belongs to current user to determine if filters should be shown + const isOwnData = data.every((item) => item.userId === currentUserId); + const shouldShowFilters = showFilters && isOwnData; + const handleBulkDelete = () => { if (onBulkDelete && selectedCount > 0) { const selectedItems = selectedRows.map((row) => row.original); @@ -701,8 +743,16 @@ export function LancamentosTable({ } }; + const handleBulkImport = () => { + if (onBulkImport && selectedCount > 0) { + const selectedItems = selectedRows.map((row) => row.original); + onBulkImport(selectedItems); + setRowSelection({}); + } + }; + const showTopControls = - Boolean(onCreate) || Boolean(onMassAdd) || showFilters; + Boolean(onCreate) || Boolean(onMassAdd) || shouldShowFilters; return ( @@ -738,10 +788,10 @@ export function LancamentosTable({ ) : null} ) : ( - + )} - {showFilters ? ( + {shouldShowFilters ? ( ) : null} - {selectedCount > 0 && onBulkDelete ? ( + {selectedCount > 0 && onBulkDelete && selectedRows.every(row => row.original.userId === currentUserId) ? (
@@ -782,6 +832,36 @@ export function LancamentosTable({
) : null} + {selectedCount > 0 && onBulkImport && selectedRows.some(row => row.original.userId !== currentUserId) ? ( +
+
+ + {selectedCount}{" "} + {selectedCount === 1 ? "item selecionado" : "itens selecionados"} + + + - + + + Total:{" "} + + +
+ +
+ ) : null} + {hasRows ? ( diff --git a/components/lancamentos/types.ts b/components/lancamentos/types.ts index 7dd176d..cae9612 100644 --- a/components/lancamentos/types.ts +++ b/components/lancamentos/types.ts @@ -1,5 +1,6 @@ export type LancamentoItem = { id: string; + userId: string; name: string; purchaseDate: string; period: string; diff --git a/components/pagadores/pagadores-page.tsx b/components/pagadores/pagadores-page.tsx index 3af012c..2a90bf1 100644 --- a/components/pagadores/pagadores-page.tsx +++ b/components/pagadores/pagadores-page.tsx @@ -147,7 +147,7 @@ export function PagadoresPage({ value={shareCodeInput} onChange={(event) => setShareCodeInput(event.target.value)} disabled={joinPending} - className="w-56" + className="w-56 border-dashed" />