diff --git a/src/App.tsx b/src/App.tsx index a4295f7..fa6b6e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,63 +1,40 @@ - import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import Index from '@/pages/Index'; -import Transacoes from '@/pages/Transacoes'; -import Categorias from '@/pages/Categorias'; -import Calendario from '@/pages/Calendario'; -import NotFound from '@/pages/NotFound'; -import Auth from '@/pages/Auth'; -import Metas from '@/pages/Metas'; -import WhatsApp from '@/pages/WhatsApp'; -import GruposWhatsApp from '@/pages/GruposWhatsApp'; -import { Toaster } from '@/components/ui/toaster'; -import './App.css'; -import ProtectedRoute from '@/components/auth/ProtectedRoute'; +import { Suspense, lazy } from 'react'; +import { Toaster } from "@/components/ui/sonner"; +import Auth from './pages/Auth'; +import ProtectedRoute from './components/auth/ProtectedRoute'; +import { authStore } from './stores/authStore'; + +// Lazy-loaded components +const Dashboard = lazy(() => import('./pages/Index')); +const Transacoes = lazy(() => import('./pages/Transacoes')); +const Categorias = lazy(() => import('./pages/Categorias')); +const Metas = lazy(() => import('./pages/Metas')); +const Calendario = lazy(() => import('./pages/Calendario')); +const WhatsApp = lazy(() => import('./pages/WhatsApp')); +const GruposWhatsApp = lazy(() => import('./pages/GruposWhatsApp')); +const NotFound = lazy(() => import('./pages/NotFound')); +const CartoesCredito = lazy(() => import('./pages/CartoesCredito')); function App() { + const isLoggedIn = authStore((state) => state.isLoggedIn); + return ( - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - - - - } /> - } /> - } /> - } /> - + + + : } /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + Carregando...}>} /> + ); } diff --git a/src/components/credito/CartaoCreditoForm.tsx b/src/components/credito/CartaoCreditoForm.tsx new file mode 100644 index 0000000..4d8e473 --- /dev/null +++ b/src/components/credito/CartaoCreditoForm.tsx @@ -0,0 +1,100 @@ + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { useToast } from '@/components/ui/use-toast'; +import { criarCartao } from '@/services/cartaoCreditoService'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; + +const cartaoSchema = z.object({ + nome: z.string().min(1, { message: 'Nome do cartão é obrigatório' }), +}); + +type CartaoFormValues = z.infer; + +interface CartaoCreditoFormProps { + onSuccess: () => void; + onCancel: () => void; +} + +export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const { toast } = useToast(); + + const form = useForm({ + resolver: zodResolver(cartaoSchema), + defaultValues: { + nome: '', + } + }); + + async function onSubmit(data: CartaoFormValues) { + setIsSubmitting(true); + + try { + const resultado = await criarCartao(data.nome); + + if (resultado) { + toast({ + title: "Cartão adicionado", + description: "Cartão de crédito cadastrado com sucesso", + }); + onSuccess(); + } else { + toast({ + title: "Erro ao salvar", + description: "Não foi possível salvar o cartão de crédito", + variant: "destructive" + }); + } + } catch (error) { + console.error('Erro ao processar formulário:', error); + toast({ + title: "Erro inesperado", + description: "Ocorreu um erro ao processar sua solicitação", + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + } + + return ( +
+ + ( + + Nome do Cartão + + + + + + )} + /> + +
+ + +
+ + + ); +} diff --git a/src/components/credito/CartaoCreditoList.tsx b/src/components/credito/CartaoCreditoList.tsx new file mode 100644 index 0000000..053b98b --- /dev/null +++ b/src/components/credito/CartaoCreditoList.tsx @@ -0,0 +1,76 @@ + +import { useState } from 'react'; +import { CartaoCredito } from '@/types/cartaoTypes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useToast } from '@/components/ui/use-toast'; +import { CreditCard } from 'lucide-react'; + +interface CartaoCreditoListProps { + cartoes: CartaoCredito[]; + isLoading: boolean; + onCartaoClick: (cartao: CartaoCredito) => void; +} + +export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoCreditoListProps) { + const { toast } = useToast(); + + const formatCurrency = (value: number = 0) => { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); + }; + + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + + if (cartoes.length === 0) { + return ( +
+ +

Nenhum cartão de crédito cadastrado

+

+ Adicione seu primeiro cartão para começar a registrar despesas. +

+
+ ); + } + + return ( +
+ {cartoes.map((cartao) => ( + onCartaoClick(cartao)} + > + + + + {cartao.nome} + + + +

+ {formatCurrency(cartao.total_despesas)} +

+

+ Total em despesas +

+
+
+ ))} +
+ ); +} diff --git a/src/components/credito/DespesaCartaoForm.tsx b/src/components/credito/DespesaCartaoForm.tsx new file mode 100644 index 0000000..c6d1e46 --- /dev/null +++ b/src/components/credito/DespesaCartaoForm.tsx @@ -0,0 +1,160 @@ + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { useToast } from '@/components/ui/use-toast'; +import { criarDespesa } from '@/services/cartaoCreditoService'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { CartaoCredito } from '@/types/cartaoTypes'; + +const despesaSchema = z.object({ + valor: z.string().min(1, { message: 'Valor é obrigatório' }), + data_despesa: z.string().min(1, { message: 'Data é obrigatória' }), + descricao: z.string().min(1, { message: 'Descrição é obrigatória' }), +}); + +type DespesaFormValues = z.infer; + +interface DespesaCartaoFormProps { + cartao: CartaoCredito; + onSuccess: () => void; + onCancel: () => void; +} + +export function DespesaCartaoForm({ cartao, onSuccess, onCancel }: DespesaCartaoFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const { toast } = useToast(); + + const form = useForm({ + resolver: zodResolver(despesaSchema), + defaultValues: { + valor: '', + data_despesa: new Date().toISOString().split('T')[0], + descricao: '', + } + }); + + async function onSubmit(data: DespesaFormValues) { + setIsSubmitting(true); + + try { + const valorNumerico = parseFloat(data.valor.replace(',', '.')); + + if (isNaN(valorNumerico)) { + toast({ + title: "Erro no formulário", + description: "O valor precisa ser um número válido", + variant: "destructive" + }); + setIsSubmitting(false); + return; + } + + const resultado = await criarDespesa( + cartao.id, + valorNumerico, + data.data_despesa, + data.descricao + ); + + if (resultado) { + toast({ + title: "Despesa adicionada", + description: "Despesa do cartão cadastrada com sucesso", + }); + onSuccess(); + } else { + toast({ + title: "Erro ao salvar", + description: "Não foi possível salvar a despesa do cartão", + variant: "destructive" + }); + } + } catch (error) { + console.error('Erro ao processar formulário:', error); + toast({ + title: "Erro inesperado", + description: "Ocorreu um erro ao processar sua solicitação", + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + } + + return ( +
+ +
+

Cartão: {cartao.nome}

+
+ + ( + + Valor + + + + + + )} + /> + + ( + + Data + + + + + + )} + /> + + ( + + Descrição + + + + + + )} + /> + +
+ + +
+ + + ); +} diff --git a/src/components/credito/DespesasList.tsx b/src/components/credito/DespesasList.tsx new file mode 100644 index 0000000..56c20d1 --- /dev/null +++ b/src/components/credito/DespesasList.tsx @@ -0,0 +1,81 @@ + +import { useState } from 'react'; +import { DespesaCartao } from '@/types/cartaoTypes'; +import { useToast } from "@/components/ui/use-toast"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { formatDistanceToNow } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +interface DespesasListProps { + despesas: DespesaCartao[]; + isLoading: boolean; +} + +export function DespesasList({ despesas, isLoading }: DespesasListProps) { + const { toast } = useToast(); + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); + }; + + const formatDate = (dateString: string) => { + const options: Intl.DateTimeFormatOptions = { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }; + return new Date(dateString).toLocaleDateString('pt-BR', options); + }; + + if (isLoading) { + return
+
+
+
+
+
; + } + + if (despesas.length === 0) { + return
+ Este cartão ainda não possui despesas registradas. +
; + } + + return ( +
+ + + + Data + Descrição + Valor + + + + {despesas.map((despesa) => ( + + + {formatDate(despesa.data_despesa)} + + {despesa.descricao} + + {formatCurrency(despesa.valor)} + + + ))} + +
+
+ ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index ebdd7f2..a116456 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,104 +1,131 @@ - -import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, NavLink, useLocation } from 'react-router-dom'; +import { useMobile } from '@/hooks/use-mobile'; +import { + Home, + ListFilter, + Receipt, + Target, + Calendar, + CreditCard, + MessageSquareText, + Users, + Menu, + X +} from 'lucide-react'; import { cn } from '@/lib/utils'; -import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart, Flag, MessageCircle, Users } from 'lucide-react'; - -// Atualizado para incluir a página de grupos do WhatsApp -const navItems = [ - { - name: 'Dashboard', - path: '/dashboard', - icon: - }, - { - name: 'Transações', - path: '/transacoes', - icon: - }, - { - name: 'Categorias', - path: '/categorias', - icon: - }, - { - name: 'Metas', - path: '/metas', - icon: - }, - { - name: 'Calendário', - path: '/calendario', - icon: - }, - { - name: 'Conectar WhatsApp', - path: '/whatsapp', - icon: - }, - { - name: 'Grupos WhatsApp', - path: '/grupos-whatsapp', - icon: - }, -]; +import { Button } from '@/components/ui/button'; interface SidebarProps { - className?: string; + isOpen: boolean; + onClose: () => void; } -const Sidebar = ({ className }: SidebarProps) => { - const location = useLocation(); - const [collapsed, setCollapsed] = React.useState(false); - - // Improved function to verify if the item is active based on exact path - const isItemActive = (path: string) => { - return location.pathname === path; - }; - - return ( -
-
- {!collapsed && ( -

FinDash

- )} - -
- - -
+const getNavLinkClass = (isActive: boolean) => { + return cn( + "group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-secondary hover:text-foreground", + isActive ? "bg-secondary text-foreground" : "text-muted-foreground" ); }; -export default Sidebar; +export default function Sidebar({ isOpen, onClose }: SidebarProps) { + const isMobile = useMobile(); + const location = useLocation(); + + return ( + + ); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 36517b6..0adc9dc 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -99,6 +99,30 @@ export type Database = { } Relationships: [] } + cartoes_credito: { + Row: { + created_at: string + id: string + login: string | null + nome: string + user_id: string + } + Insert: { + created_at?: string + id?: string + login?: string | null + nome: string + user_id: string + } + Update: { + created_at?: string + id?: string + login?: string | null + nome?: string + user_id?: string + } + Relationships: [] + } conselheiraamore: { Row: { content: string | null @@ -120,6 +144,41 @@ export type Database = { } Relationships: [] } + despesas_cartao: { + Row: { + cartao_id: string + created_at: string + data_despesa: string + descricao: string + id: string + valor: number + } + Insert: { + cartao_id: string + created_at?: string + data_despesa: string + descricao: string + id?: string + valor: number + } + Update: { + cartao_id?: string + created_at?: string + data_despesa?: string + descricao?: string + id?: string + valor?: number + } + Relationships: [ + { + foreignKeyName: "despesas_cartao_cartao_id_fkey" + columns: ["cartao_id"] + isOneToOne: false + referencedRelation: "cartoes_credito" + referencedColumns: ["id"] + }, + ] + } grupos_whatsapp: { Row: { created_at: string | null diff --git a/src/pages/CartoesCredito.tsx b/src/pages/CartoesCredito.tsx new file mode 100644 index 0000000..53e55c3 --- /dev/null +++ b/src/pages/CartoesCredito.tsx @@ -0,0 +1,224 @@ + +import { useState, useEffect } from 'react'; +import Layout from '@/components/layout/Layout'; +import { CartaoCreditoForm } from '@/components/credito/CartaoCreditoForm'; +import { DespesaCartaoForm } from '@/components/credito/DespesaCartaoForm'; +import { CartaoCreditoList } from '@/components/credito/CartaoCreditoList'; +import { DespesasList } from '@/components/credito/DespesasList'; +import { CartaoCredito, DespesaCartao } from '@/types/cartaoTypes'; +import { getCartoes, getDespesasCartao } from '@/services/cartaoCreditoService'; +import { Button } from '@/components/ui/button'; +import { useToast } from "@/components/ui/use-toast"; +import { PlusCircle, ArrowLeft } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +const CartoesCreditoPage = () => { + const [cartoes, setCartoes] = useState([]); + const [cartaoSelecionado, setCartaoSelecionado] = useState(null); + const [despesas, setDespesas] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogNovoCartaoOpen, setIsDialogNovoCartaoOpen] = useState(false); + const [isDialogNovaDespesaOpen, setIsDialogNovaDespesaOpen] = useState(false); + const [detalheCartaoAberto, setDetalheCartaoAberto] = useState(false); + const { toast } = useToast(); + + const loadCartoes = async () => { + try { + setIsLoading(true); + const data = await getCartoes(); + setCartoes(data); + } catch (error) { + console.error("Erro ao carregar cartões:", error); + toast({ + title: "Erro ao carregar cartões", + description: "Não foi possível obter os dados dos cartões", + variant: "destructive" + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadCartoes(); + }, []); + + const loadDespesasCartao = async (cartaoId: string) => { + try { + setIsLoading(true); + const data = await getDespesasCartao(cartaoId); + setDespesas(data); + } catch (error) { + console.error("Erro ao carregar despesas:", error); + toast({ + title: "Erro ao carregar despesas", + description: "Não foi possível obter as despesas do cartão", + variant: "destructive" + }); + } finally { + setIsLoading(false); + } + }; + + const handleCartaoSuccess = () => { + setIsDialogNovoCartaoOpen(false); + loadCartoes(); + toast({ + title: "Cartão adicionado", + description: "Cartão de crédito cadastrado com sucesso", + }); + }; + + const handleDespesaSuccess = () => { + setIsDialogNovaDespesaOpen(false); + // Recarregar detalhes do cartão após adicionar despesa + if (cartaoSelecionado) { + loadDespesasCartao(cartaoSelecionado.id); + // Recarregar também a lista de cartões para atualizar os totais + loadCartoes(); + } + toast({ + title: "Despesa adicionada", + description: "Despesa do cartão registrada com sucesso", + }); + }; + + const handleCartaoClick = (cartao: CartaoCredito) => { + setCartaoSelecionado(cartao); + loadDespesasCartao(cartao.id); + setDetalheCartaoAberto(true); + }; + + const handleVoltarParaLista = () => { + setDetalheCartaoAberto(false); + setCartaoSelecionado(null); + setDespesas([]); + }; + + const formatCurrency = (value: number = 0) => { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); + }; + + return ( + +
+ {!detalheCartaoAberto ? ( + // Tela de listagem de cartões + <> +
+

Cartões de Crédito

+ +
+ + + + ) : ( + // Tela de detalhes de um cartão específico + <> +
+
+ +

+ {cartaoSelecionado?.nome} +

+
+ +
+ + + + Resumo + Total de despesas do cartão + + +

+ {formatCurrency(cartaoSelecionado?.total_despesas)} +

+
+
+ +
+

Despesas do Cartão

+ +
+ + )} + + {/* Dialog para adicionar novo cartão */} + + + + Novo Cartão de Crédito + + Preencha as informações para cadastrar um novo cartão de crédito. + + + setIsDialogNovoCartaoOpen(false)} + /> + + + + {/* Dialog para adicionar nova despesa */} + + + + Nova Despesa do Cartão + + Preencha as informações para registrar uma nova despesa. + + + {cartaoSelecionado && ( + setIsDialogNovaDespesaOpen(false)} + /> + )} + + +
+
+ ); +}; + +export default CartoesCreditoPage; diff --git a/src/services/cartaoCreditoService.ts b/src/services/cartaoCreditoService.ts new file mode 100644 index 0000000..943a56f --- /dev/null +++ b/src/services/cartaoCreditoService.ts @@ -0,0 +1,168 @@ + +import { supabase } from "@/integrations/supabase/client"; +import { CartaoCredito, DespesaCartao } from "@/types/cartaoTypes"; + +// Função para buscar todos os cartões de crédito do usuário +export async function getCartoes(): Promise { + // Obter o email do usuário do localStorage + const userEmail = localStorage.getItem('userEmail'); + + if (!userEmail) { + console.error('Email do usuário não encontrado no localStorage'); + return []; + } + + // Normalizar o email (minúsculo e sem espaços) + const normalizedEmail = userEmail.trim().toLowerCase(); + + try { + const { data, error } = await supabase + .from('cartoes_credito') + .select('*') + .eq('login', normalizedEmail); + + if (error) { + console.error('Erro ao buscar cartões:', error); + throw new Error('Não foi possível carregar os cartões de crédito'); + } + + // Buscar o total de despesas para cada cartão + const cartoesComTotal = await Promise.all( + data.map(async (cartao: CartaoCredito) => { + const total = await getTotalDespesasCartao(cartao.id); + return { ...cartao, total_despesas: total }; + }) + ); + + return cartoesComTotal; + } catch (error) { + console.error('Erro ao buscar cartões:', error); + return []; + } +} + +// Função para criar um novo cartão de crédito +export async function criarCartao(nome: string): Promise { + // Obter o email do usuário do localStorage + const userEmail = localStorage.getItem('userEmail'); + // Obter o ID do usuário do localStorage (mantido por compatibilidade) + const userId = localStorage.getItem('userId'); + + if (!userEmail || !userId) { + console.error('Dados do usuário não encontrados no localStorage'); + return null; + } + + // Normalizar o email (minúsculo e sem espaços) + const normalizedEmail = userEmail.trim().toLowerCase(); + + try { + const { data, error } = await supabase + .from('cartoes_credito') + .insert([{ + nome: nome, + user_id: userId, + login: normalizedEmail + }]) + .select(); + + if (error) { + console.error('Erro ao criar cartão:', error); + throw new Error('Não foi possível criar o cartão de crédito'); + } + + return data[0]; + } catch (error) { + console.error('Erro ao criar cartão:', error); + return null; + } +} + +// Função para buscar detalhes de um cartão específico +export async function getCartao(cartaoId: string): Promise { + try { + const { data, error } = await supabase + .from('cartoes_credito') + .select('*') + .eq('id', cartaoId) + .single(); + + if (error) { + console.error('Erro ao buscar cartão:', error); + throw new Error('Não foi possível carregar os detalhes do cartão'); + } + + return data; + } catch (error) { + console.error('Erro ao buscar cartão:', error); + return null; + } +} + +// Função para buscar despesas de um cartão +export async function getDespesasCartao(cartaoId: string): Promise { + try { + const { data, error } = await supabase + .from('despesas_cartao') + .select('*') + .eq('cartao_id', cartaoId) + .order('data_despesa', { ascending: false }); + + if (error) { + console.error('Erro ao buscar despesas:', error); + throw new Error('Não foi possível carregar as despesas do cartão'); + } + + return data; + } catch (error) { + console.error('Erro ao buscar despesas:', error); + return []; + } +} + +// Função para criar uma nova despesa no cartão +export async function criarDespesa(cartaoId: string, valor: number, data_despesa: string, descricao: string): Promise { + try { + const { data, error } = await supabase + .from('despesas_cartao') + .insert([{ + cartao_id: cartaoId, + valor: valor, + data_despesa: data_despesa, + descricao: descricao + }]) + .select(); + + if (error) { + console.error('Erro ao criar despesa:', error); + throw new Error('Não foi possível adicionar a despesa ao cartão'); + } + + return data[0]; + } catch (error) { + console.error('Erro ao criar despesa:', error); + return null; + } +} + +// Função para calcular o total de despesas de um cartão +export async function getTotalDespesasCartao(cartaoId: string): Promise { + try { + const { data, error } = await supabase + .from('despesas_cartao') + .select('valor') + .eq('cartao_id', cartaoId); + + if (error) { + console.error('Erro ao calcular total de despesas:', error); + throw new Error('Não foi possível calcular o total de despesas'); + } + + // Somar todos os valores das despesas + const total = data.reduce((acc, item) => acc + Number(item.valor), 0); + return total; + } catch (error) { + console.error('Erro ao calcular total de despesas:', error); + return 0; + } +} diff --git a/src/types/cartaoTypes.ts b/src/types/cartaoTypes.ts new file mode 100644 index 0000000..23f100d --- /dev/null +++ b/src/types/cartaoTypes.ts @@ -0,0 +1,18 @@ + +export interface CartaoCredito { + id: string; + nome: string; + user_id: string; + login?: string; + created_at: string; + total_despesas?: number; +} + +export interface DespesaCartao { + id: string; + cartao_id: string; + valor: number; + data_despesa: string; + descricao: string; + created_at: string; +}