diff --git a/src/App.tsx b/src/App.tsx index 3d7ccbb..50d494e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,154 +1,31 @@ -import React, { useEffect, useState } from "react"; -import { Toaster } from "@/components/ui/toaster"; -import { Toaster as Sonner } from "@/components/ui/sonner"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, 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 Auth from "./pages/Auth"; -import NotFound from "./pages/NotFound"; -import ProtectedRoute from "./components/auth/ProtectedRoute"; -import { supabase } from "@/integrations/supabase/client"; -// Create a client -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - retry: 1, - }, - }, -}); - -const App = () => { - const [isAuthenticated, setIsAuthenticated] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const checkAuthentication = async () => { - try { - // Get stored authentication status - const storedAuth = localStorage.getItem('autenticado') === 'true'; - - if (storedAuth) { - console.log('Using stored authentication: authenticated'); - setIsAuthenticated(true); - setIsLoading(false); - return; - } - - // If no stored status, check session - const { data: sessionData } = await supabase.auth.getSession(); - - if (sessionData?.session) { - console.log('Session found in App.tsx'); - setIsAuthenticated(true); - localStorage.setItem('autenticado', 'true'); - localStorage.setItem('userId', sessionData.session.user.id); - } else { - // For RLS disabled mode, we default to authenticated - console.log('No session found, but setting as authenticated for RLS disabled'); - setIsAuthenticated(true); - localStorage.setItem('autenticado', 'true'); - localStorage.setItem('userId', 'default'); - } - } catch (error) { - console.error('Error checking authentication in App.tsx:', error); - // For RLS disabled, we default to authenticated - setIsAuthenticated(true); - localStorage.setItem('autenticado', 'true'); - localStorage.setItem('userId', 'default'); - } finally { - setIsLoading(false); - } - }; - - checkAuthentication(); - - const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => { - console.log('Auth event in App.tsx:', event); - - // For RLS disabled mode, we keep authenticated state regardless - setIsAuthenticated(true); - localStorage.setItem('autenticado', 'true'); - localStorage.setItem('userId', session?.user?.id || 'default'); - }); - - return () => { - authListener.subscription.unsubscribe(); - }; - }, []); - - if (isLoading) { - return ( -
-

Carregando...

-
- ); - } +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 { Toaster } from '@/components/ui/toaster'; +import './App.css'; +function App() { return ( - - - - - - - {/* Root path redirects to dashboard */} - } - /> - - {/* Dashboard route using Index component */} - - - - } - /> - - {/* Auth page is still available, but we should never need to redirect here */} - } /> - - {/* Protected routes - with RLS disabled, they're always accessible */} - - - - } - /> - - - - } - /> - - - - } - /> - - {/* 404 route */} - } /> - - - - + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); -}; +} export default App; diff --git a/src/components/dashboard/MetaProgressCard.tsx b/src/components/dashboard/MetaProgressCard.tsx new file mode 100644 index 0000000..2f7428d --- /dev/null +++ b/src/components/dashboard/MetaProgressCard.tsx @@ -0,0 +1,90 @@ + +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; +import { Trophy, Flag, FlagOff } from 'lucide-react'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from '@/components/ui/progress'; +import { ResultadoMeta } from '@/types/financialTypes'; + +interface MetaProgressCardProps { + meta: ResultadoMeta; + onEditClick?: () => void; +} + +export function MetaProgressCard({ meta, onEditClick }: MetaProgressCardProps) { + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL', + }).format(value); + }; + + const getMesNome = (mes: number) => { + const date = new Date(2000, mes - 1, 1); + return format(date, 'MMMM', { locale: ptBR }); + }; + + const getStatusIcon = () => { + if (meta.meta_batida) { + return ; + } else { + return ; + } + }; + + const getProgressColor = () => { + if (meta.meta_batida) { + return 'bg-green-500'; + } else if (meta.percentual_atingido >= 75) { + return 'bg-yellow-500'; + } else { + return 'bg-red-500'; + } + }; + + return ( + + + + {getMesNome(meta.mes)} {meta.ano} +
+ {getStatusIcon()} +
+
+
+ + +
+ Meta: {formatCurrency(meta.valor_meta)} + Economia: {formatCurrency(meta.economia_real)} +
+ +
+
+ +
+
+ 0% + {meta.percentual_atingido.toFixed(0)}% atingido + 100% +
+
+
+ + {onEditClick && ( + + + + )} +
+ ); +} diff --git a/src/components/dashboard/MetasForm.tsx b/src/components/dashboard/MetasForm.tsx new file mode 100644 index 0000000..c3078bc --- /dev/null +++ b/src/components/dashboard/MetasForm.tsx @@ -0,0 +1,214 @@ + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useToast } from '@/components/ui/use-toast'; +import { salvarMeta } from '@/services/metasService'; + +const formSchema = z.object({ + mes: z.string(), + ano: z.string(), + valor_meta: z.string().refine(value => !isNaN(Number(value)) && Number(value) > 0, { + message: 'Valor da meta deve ser um número positivo', + }), +}); + +interface MetaFormProps { + onSuccess: () => void; + onCancel: () => void; + userId: string; + valorInicial?: number; + mesInicial?: number; + anoInicial?: number; +} + +export function MetasForm({ onSuccess, onCancel, userId, valorInicial = 0, mesInicial, anoInicial }: MetaFormProps) { + const { toast } = useToast(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const meses = Array.from({ length: 12 }, (_, i) => { + const date = new Date(2000, i, 1); + return { + value: (i + 1).toString(), + label: format(date, 'MMMM', { locale: ptBR }), + }; + }); + + const anos = Array.from({ length: 6 }, (_, i) => { + const ano = new Date().getFullYear() - 2 + i; + return { + value: ano.toString(), + label: ano.toString(), + }; + }); + + // Valor atual do mês e ano + const currentDate = new Date(); + const mesAtual = (mesInicial || currentDate.getMonth() + 1).toString(); + const anoAtual = (anoInicial || currentDate.getFullYear()).toString(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + mes: mesAtual, + ano: anoAtual, + valor_meta: valorInicial > 0 ? valorInicial.toString() : '', + }, + }); + + async function onSubmit(values: z.infer) { + try { + setIsSubmitting(true); + const novaMeta = { + user_id: userId, + mes: parseInt(values.mes), + ano: parseInt(values.ano), + valor_meta: parseFloat(values.valor_meta) + }; + + await salvarMeta(novaMeta); + + toast({ + title: "Meta registrada com sucesso", + description: `Meta de economia para ${meses.find(m => m.value === values.mes)?.label} de ${values.ano} salva.`, + }); + + onSuccess(); + } catch (error) { + console.error("Erro ao salvar meta:", error); + toast({ + title: "Erro ao salvar", + description: "Não foi possível salvar a meta. Tente novamente.", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); + } + } + + return ( +
+ +
+ ( + + Mês + + + + )} + /> + + ( + + Ano + + + + )} + /> +
+ + ( + + Valor da Meta (R$) + + + + + + )} + /> + +
+ + +
+ + + ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 930a95f..10dffde 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Link, useLocation } from 'react-router-dom'; import { cn } from '@/lib/utils'; import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart, Flag } from 'lucide-react'; // Updated navItems with correct paths const navItems = [ @@ -22,6 +22,11 @@ const navItems = [ path: '/categorias', icon: }, + { + name: 'Metas', + path: '/metas', + icon: + }, { name: 'Calendário', path: '/calendario', diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ce9e354..69116ba 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -120,6 +120,33 @@ export type Database = { } Relationships: [] } + metas: { + Row: { + ano: number + created_at: string | null + id: string + mes: number + user_id: string + valor_meta: number + } + Insert: { + ano: number + created_at?: string | null + id?: string + mes: number + user_id: string + valor_meta: number + } + Update: { + ano?: number + created_at?: string | null + id?: string + mes?: number + user_id?: string + valor_meta?: number + } + Relationships: [] + } transacoes: { Row: { categoria: string | null diff --git a/src/pages/Metas.tsx b/src/pages/Metas.tsx new file mode 100644 index 0000000..3844215 --- /dev/null +++ b/src/pages/Metas.tsx @@ -0,0 +1,188 @@ + +import { useState, useEffect } from 'react'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; +import Layout from '@/components/layout/Layout'; +import { MetasForm } from '@/components/dashboard/MetasForm'; +import { MetaProgressCard } from '@/components/dashboard/MetaProgressCard'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { ResultadoMeta } from '@/types/financialTypes'; +import { useToast } from '@/components/ui/use-toast'; +import { calcularResultadosMetas, getMeta } from '@/services/metasService'; +import { PlusCircle, Check } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +const MetasPage = () => { + const { toast } = useToast(); + const [userId, setUserId] = useState(''); + const [resultados, setResultados] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [metaParaEditar, setMetaParaEditar] = useState<{ mes: number, ano: number } | null>(null); + const [valorMetaAtual, setValorMetaAtual] = useState(0); + + useEffect(() => { + // Obter userId do localStorage (temporário até termos autenticação completa) + const user = localStorage.getItem('finDashUser'); + if (user) { + setUserId(user); + loadData(user); + } else { + setIsLoading(false); + } + }, []); + + const loadData = async (user: string) => { + try { + setIsLoading(true); + const resultado = await calcularResultadosMetas(user); + setResultados(resultado); + } catch (error) { + console.error('Erro ao carregar metas:', error); + toast({ + title: 'Erro ao carregar metas', + description: 'Não foi possível obter os dados de metas', + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }; + + const handleEditarMeta = async (mes: number, ano: number) => { + try { + const meta = await getMeta(userId, mes, ano); + if (meta) { + setValorMetaAtual(meta.valor_meta); + setMetaParaEditar({ mes, ano }); + setIsDialogOpen(true); + } + } catch (error) { + console.error('Erro ao buscar meta para edição:', error); + } + }; + + const handleNovaMeta = () => { + setMetaParaEditar(null); + setValorMetaAtual(0); + setIsDialogOpen(true); + }; + + const handleSuccess = () => { + setIsDialogOpen(false); + loadData(userId); + }; + + const getMesAtual = () => { + const now = new Date(); + return { + mes: now.getMonth() + 1, + ano: now.getFullYear(), + nome: format(now, 'MMMM', { locale: ptBR }), + }; + }; + + const mesAtual = getMesAtual(); + + // Agrupar por ano (objeto com chave = ano e valor = array de metas daquele ano) + const resultadosPorAno = resultados.reduce((acc, meta) => { + if (!acc[meta.ano]) { + acc[meta.ano] = []; + } + acc[meta.ano].push(meta); + return acc; + }, {} as Record); + + // Ordenar os anos de forma decrescente + const anosOrdenados = Object.keys(resultadosPorAno) + .map(Number) + .sort((a, b) => b - a); + + return ( + +
+
+

Metas de Economia

+ +
+ + {isLoading ? ( +
+
+
+

Carregando suas metas...

+
+
+ ) : resultados.length === 0 ? ( + +

Nenhuma meta definida

+

+ Defina sua primeira meta de economia para {mesAtual.nome} {mesAtual.ano} +

+ +
+ ) : ( + <> + {anosOrdenados.map(ano => ( +
+

{ano}

+
+ {resultadosPorAno[ano] + .sort((a, b) => b.mes - a.mes) + .map(meta => ( + handleEditarMeta(meta.mes, meta.ano)} + /> + ))} +
+
+ ))} + + )} + + + + + + {metaParaEditar ? 'Editar Meta' : 'Nova Meta'} + + + {metaParaEditar + ? 'Atualize os valores da sua meta de economia.' + : 'Defina uma nova meta de economia mensal.'} + + + setIsDialogOpen(false)} + userId={userId} + valorInicial={metaParaEditar ? valorMetaAtual : 0} + mesInicial={metaParaEditar?.mes} + anoInicial={metaParaEditar?.ano} + /> + + +
+
+ ); +}; + +export default MetasPage; diff --git a/src/pages/Transacoes.tsx b/src/pages/Transacoes.tsx index bcb1537..1ada3a8 100644 --- a/src/pages/Transacoes.tsx +++ b/src/pages/Transacoes.tsx @@ -128,25 +128,14 @@ const TransacoesPage = () => { - {/* Split da tabela em duas colunas */} -
-
-

Receitas

- -
- -
-

Despesas

- -
+ {/* Tabela completa de transações */} +
+

Todas as Transações

+
diff --git a/src/services/metasService.ts b/src/services/metasService.ts new file mode 100644 index 0000000..837cfa3 --- /dev/null +++ b/src/services/metasService.ts @@ -0,0 +1,159 @@ + +import { supabase } from '@/integrations/supabase/client'; + +export interface Meta { + id: string; + user_id: string; + mes: number; + ano: number; + valor_meta: number; + created_at?: string; +} + +export interface ResultadoMeta { + mes: number; + ano: number; + valor_meta: number; + economia_real: number; + percentual_atingido: number; + meta_batida: boolean; +} + +// Obter todas as metas do usuário +export const getMetas = async (userId: string): Promise => { + const { data, error } = await supabase + .from('metas') + .select('*') + .eq('user_id', userId) + .order('ano', { ascending: false }) + .order('mes', { ascending: false }); + + if (error) { + console.error('Erro ao buscar metas:', error); + throw new Error(error.message); + } + + return data || []; +}; + +// Obter meta específica +export const getMeta = async (userId: string, mes: number, ano: number): Promise => { + const { data, error } = await supabase + .from('metas') + .select('*') + .eq('user_id', userId) + .eq('mes', mes) + .eq('ano', ano) + .maybeSingle(); + + if (error) { + console.error('Erro ao buscar meta:', error); + throw new Error(error.message); + } + + return data; +}; + +// Criar ou atualizar meta +export const salvarMeta = async (meta: Partial): Promise => { + // Verificar se já existe uma meta para este mês/ano + const existingMeta = await getMeta(meta.user_id as string, meta.mes as number, meta.ano as number); + + if (existingMeta) { + // Atualizar meta existente + const { data, error } = await supabase + .from('metas') + .update({ valor_meta: meta.valor_meta }) + .eq('id', existingMeta.id) + .select() + .single(); + + if (error) { + console.error('Erro ao atualizar meta:', error); + throw new Error(error.message); + } + + return data; + } else { + // Criar nova meta + const { data, error } = await supabase + .from('metas') + .insert(meta) + .select() + .single(); + + if (error) { + console.error('Erro ao criar meta:', error); + throw new Error(error.message); + } + + return data; + } +}; + +// Deletar meta +export const deletarMeta = async (metaId: string): Promise => { + const { error } = await supabase + .from('metas') + .delete() + .eq('id', metaId); + + if (error) { + console.error('Erro ao deletar meta:', error); + throw new Error(error.message); + } +}; + +// Calcular resultados das metas +export const calcularResultadosMetas = async (userId: string): Promise => { + const metas = await getMetas(userId); + const resultados: ResultadoMeta[] = []; + + for (const meta of metas) { + // Buscar transações do mês da meta + const startDate = new Date(meta.ano, meta.mes - 1, 1); + const endDate = new Date(meta.ano, meta.mes, 0); + + const { data: transacoes, error } = await supabase + .from('transacoes') + .select('*') + .eq('user', userId) + .gte('quando', startDate.toISOString().split('T')[0]) + .lte('quando', endDate.toISOString().split('T')[0]); + + if (error) { + console.error('Erro ao buscar transações para cálculo de metas:', error); + continue; + } + + // Calcular receitas e despesas do mês + const receitas = transacoes + .filter(t => t.tipo === 'receita') + .reduce((sum, t) => sum + Number(t.valor || 0), 0); + + const despesas = transacoes + .filter(t => t.tipo === 'despesa') + .reduce((sum, t) => sum + Number(t.valor || 0), 0); + + // Calcular economia real + const economiaReal = receitas - despesas; + + // Calcular percentual atingido e se a meta foi batida + const percentualAtingido = meta.valor_meta > 0 + ? Math.min(100, (economiaReal / meta.valor_meta) * 100) + : 0; + + const metaBatida = economiaReal >= meta.valor_meta; + + resultados.push({ + mes: meta.mes, + ano: meta.ano, + valor_meta: meta.valor_meta, + economia_real: economiaReal, + percentual_atingido: percentualAtingido, + meta_batida: metaBatida + }); + } + + return resultados; +}; diff --git a/src/types/financialTypes.ts b/src/types/financialTypes.ts index 7d5af6a..674f75a 100644 --- a/src/types/financialTypes.ts +++ b/src/types/financialTypes.ts @@ -37,3 +37,21 @@ export interface TransactionFilters { tipo: string; categoria: string; } + +export interface Meta { + id: string; + user_id: string; + mes: number; + ano: number; + valor_meta: number; + created_at?: string; +} + +export interface ResultadoMeta { + mes: number; + ano: number; + valor_meta: number; + economia_real: number; + percentual_atingido: number; + meta_batida: boolean; +}