Run SQL for credit card and expenses

This commit executes the provided SQL script to create tables for credit cards and their associated expenses in the Supabase database.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-20 19:57:53 +00:00
parent c461f132fb
commit ea18d3488d
10 changed files with 1040 additions and 150 deletions

View File

@ -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 (
<Router>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Index />
</ProtectedRoute>
} />
<Route path="/transacoes" element={
<ProtectedRoute>
<Transacoes />
</ProtectedRoute>
} />
<Route path="/categorias" element={
<ProtectedRoute>
<Categorias />
</ProtectedRoute>
} />
<Route path="/calendario" element={
<ProtectedRoute>
<Calendario />
</ProtectedRoute>
} />
<Route path="/metas" element={
<ProtectedRoute>
<Metas />
</ProtectedRoute>
} />
<Route path="/whatsapp" element={
<ProtectedRoute>
<WhatsApp />
</ProtectedRoute>
} />
<Route path="/grupos-whatsapp" element={
<ProtectedRoute>
<GruposWhatsApp />
</ProtectedRoute>
} />
<Route path="/login" element={<Auth />} />
<Route path="/auth" element={<Auth />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />
<Routes>
<Route path="/auth" element={isLoggedIn ? <Navigate to="/" /> : <Auth />} />
<Route path="/" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Dashboard /></Suspense></ProtectedRoute>} />
<Route path="/transacoes" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Transacoes /></Suspense></ProtectedRoute>} />
<Route path="/cartoes" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><CartoesCredito /></Suspense></ProtectedRoute>} />
<Route path="/categorias" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Categorias /></Suspense></ProtectedRoute>} />
<Route path="/metas" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Metas /></Suspense></ProtectedRoute>} />
<Route path="/calendario" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Calendario /></Suspense></ProtectedRoute>} />
<Route path="/whatsapp" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><WhatsApp /></Suspense></ProtectedRoute>} />
<Route path="/grupos-whatsapp" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><GruposWhatsApp /></Suspense></ProtectedRoute>} />
<Route path="*" element={<Suspense fallback={<div>Carregando...</div>}><NotFound /></Suspense>} />
</Routes>
</Router>
);
}

View File

@ -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<typeof cartaoSchema>;
interface CartaoCreditoFormProps {
onSuccess: () => void;
onCancel: () => void;
}
export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
const form = useForm<CartaoFormValues>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="nome"
render={({ field }) => (
<FormItem>
<FormLabel>Nome do Cartão</FormLabel>
<FormControl>
<Input placeholder="Ex: Nubank, Itaú, Banco do Brasil..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-2">
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>
Cancelar
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Salvando..." : "Salvar"}
</Button>
</div>
</form>
</Form>
);
}

View File

@ -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 (
<div className="flex justify-center p-8">
<div className="animate-pulse flex space-x-4">
<div className="h-10 w-10 rounded-full bg-gray-200"></div>
<div className="flex-1 space-y-2">
<div className="h-4 rounded bg-gray-200"></div>
<div className="h-4 rounded bg-gray-200 w-5/6"></div>
</div>
</div>
</div>
);
}
if (cartoes.length === 0) {
return (
<div className="text-center p-8">
<CreditCard className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium">Nenhum cartão de crédito cadastrado</h3>
<p className="text-muted-foreground">
Adicione seu primeiro cartão para começar a registrar despesas.
</p>
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{cartoes.map((cartao) => (
<Card
key={cartao.id}
className="hover:shadow-md transition-shadow cursor-pointer"
onClick={() => onCartaoClick(cartao)}
>
<CardHeader className="pb-2">
<CardTitle className="flex items-center text-lg">
<CreditCard className="w-5 h-5 mr-2" />
{cartao.nome}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-semibold">
{formatCurrency(cartao.total_despesas)}
</p>
<p className="text-xs text-muted-foreground">
Total em despesas
</p>
</CardContent>
</Card>
))}
</div>
);
}

View File

@ -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<typeof despesaSchema>;
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<DespesaFormValues>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="mb-4">
<h3 className="font-medium">Cartão: {cartao.nome}</h3>
</div>
<FormField
control={form.control}
name="valor"
render={({ field }) => (
<FormItem>
<FormLabel>Valor</FormLabel>
<FormControl>
<Input
placeholder="0,00"
{...field}
type="text"
inputMode="decimal"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="data_despesa"
render={({ field }) => (
<FormItem>
<FormLabel>Data</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="descricao"
render={({ field }) => (
<FormItem>
<FormLabel>Descrição</FormLabel>
<FormControl>
<Input placeholder="Detalhes da despesa" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-2">
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>
Cancelar
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Salvando..." : "Salvar"}
</Button>
</div>
</form>
</Form>
);
}

View File

@ -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 <div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-1/4"></div>
<div className="h-10 bg-gray-200 rounded"></div>
<div className="h-10 bg-gray-200 rounded"></div>
<div className="h-10 bg-gray-200 rounded"></div>
</div>;
}
if (despesas.length === 0) {
return <div className="text-center py-10 text-muted-foreground">
Este cartão ainda não possui despesas registradas.
</div>;
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Data</TableHead>
<TableHead>Descrição</TableHead>
<TableHead className="text-right">Valor</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{despesas.map((despesa) => (
<TableRow key={despesa.id}>
<TableCell className="font-medium">
{formatDate(despesa.data_despesa)}
</TableCell>
<TableCell>{despesa.descricao}</TableCell>
<TableCell className="text-right">
{formatCurrency(despesa.valor)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@ -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: <Home className="mr-2 h-5 w-5" />
},
{
name: 'Transações',
path: '/transacoes',
icon: <DollarSign className="mr-2 h-5 w-5" />
},
{
name: 'Categorias',
path: '/categorias',
icon: <PieChart className="mr-2 h-5 w-5" />
},
{
name: 'Metas',
path: '/metas',
icon: <Flag className="mr-2 h-5 w-5" />
},
{
name: 'Calendário',
path: '/calendario',
icon: <CalendarDays className="mr-2 h-5 w-5" />
},
{
name: 'Conectar WhatsApp',
path: '/whatsapp',
icon: <MessageCircle className="mr-2 h-5 w-5" />
},
{
name: 'Grupos WhatsApp',
path: '/grupos-whatsapp',
icon: <Users className="mr-2 h-5 w-5" />
},
];
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 (
<div
className={cn(
"flex flex-col h-full bg-sidebar border-r border-border transition-all duration-300 ease-in-out",
collapsed ? "w-[60px]" : "w-[250px]",
className
)}
>
<div className="flex items-center justify-between p-4">
{!collapsed && (
<h1 className="font-bold text-xl text-primary">FinDash</h1>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setCollapsed(!collapsed)}
className="ml-auto"
>
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
</Button>
</div>
<nav className="flex-1 py-4 space-y-1">
{navItems.map((item) => (
<Link
key={item.name}
to={item.path}
className={cn(
"flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors",
isItemActive(item.path)
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent/50",
collapsed ? "justify-center" : "justify-start"
)}
>
<span className={collapsed ? "mr-0" : "mr-2"}>{item.icon}</span>
{!collapsed && <span>{item.name}</span>}
</Link>
))}
</nav>
</div>
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 (
<aside className={cn(
"fixed left-0 top-0 z-50 flex h-full w-64 flex-col border-r bg-background shadow-sm transition-all duration-300 ease-in-out",
isOpen ? 'translate-x-0' : '-translate-x-full',
isMobile ? 'w-full' : 'w-64'
)}>
<div className="flex items-center justify-between px-4 py-2">
<Link to="/" className="flex items-center space-x-2 font-semibold">
<span>FinDash</span>
</Link>
{isMobile && (
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-5 w-5" />
</Button>
)}
</div>
<div className="scrollbar-thin h-full space-y-4 py-4 overflow-y-auto">
<div className="px-3 py-2">
<div className="space-y-1">
<NavLink
to="/"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Home className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</NavLink>
<NavLink
to="/transacoes"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Receipt className="mr-2 h-4 w-4" />
<span>Transações</span>
</NavLink>
<NavLink
to="/cartoes"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<CreditCard className="mr-2 h-4 w-4" />
<span>Cartões de Crédito</span>
</NavLink>
<NavLink
to="/categorias"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<ListFilter className="mr-2 h-4 w-4" />
<span>Categorias</span>
</NavLink>
<NavLink
to="/metas"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Target className="mr-2 h-4 w-4" />
<span>Metas</span>
</NavLink>
<NavLink
to="/calendario"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Calendar className="mr-2 h-4 w-4" />
<span>Calendário</span>
</NavLink>
</div>
</div>
<div className="px-3 py-2">
<h2 className="mb-2 px-3 text-sm font-semibold tracking-tight">
WhatsApp
</h2>
<div className="space-y-1">
<NavLink
to="/whatsapp"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<MessageSquareText className="mr-2 h-4 w-4" />
<span>Enviar Mensagem</span>
</NavLink>
<NavLink
to="/gruposwhatsapp"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Users className="mr-2 h-4 w-4" />
<span>Grupos</span>
</NavLink>
</div>
</div>
</div>
</aside>
);
}

View File

@ -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

View File

@ -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<CartaoCredito[]>([]);
const [cartaoSelecionado, setCartaoSelecionado] = useState<CartaoCredito | null>(null);
const [despesas, setDespesas] = useState<DespesaCartao[]>([]);
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 (
<Layout>
<div className="space-y-6">
{!detalheCartaoAberto ? (
// Tela de listagem de cartões
<>
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold tracking-tight">Cartões de Crédito</h1>
<Button
className="flex items-center gap-2"
onClick={() => setIsDialogNovoCartaoOpen(true)}
>
<PlusCircle className="h-4 w-4" />
Novo Cartão
</Button>
</div>
<CartaoCreditoList
cartoes={cartoes}
isLoading={isLoading}
onCartaoClick={handleCartaoClick}
/>
</>
) : (
// Tela de detalhes de um cartão específico
<>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handleVoltarParaLista}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold tracking-tight">
{cartaoSelecionado?.nome}
</h1>
</div>
<Button
className="flex items-center gap-2"
onClick={() => setIsDialogNovaDespesaOpen(true)}
>
<PlusCircle className="h-4 w-4" />
Nova Despesa
</Button>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Resumo</CardTitle>
<CardDescription>Total de despesas do cartão</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{formatCurrency(cartaoSelecionado?.total_despesas)}
</p>
</CardContent>
</Card>
<div className="space-y-4">
<h2 className="text-xl font-semibold">Despesas do Cartão</h2>
<DespesasList despesas={despesas} isLoading={isLoading} />
</div>
</>
)}
{/* Dialog para adicionar novo cartão */}
<Dialog open={isDialogNovoCartaoOpen} onOpenChange={setIsDialogNovoCartaoOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Novo Cartão de Crédito</DialogTitle>
<DialogDescription>
Preencha as informações para cadastrar um novo cartão de crédito.
</DialogDescription>
</DialogHeader>
<CartaoCreditoForm
onSuccess={handleCartaoSuccess}
onCancel={() => setIsDialogNovoCartaoOpen(false)}
/>
</DialogContent>
</Dialog>
{/* Dialog para adicionar nova despesa */}
<Dialog open={isDialogNovaDespesaOpen} onOpenChange={setIsDialogNovaDespesaOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Nova Despesa do Cartão</DialogTitle>
<DialogDescription>
Preencha as informações para registrar uma nova despesa.
</DialogDescription>
</DialogHeader>
{cartaoSelecionado && (
<DespesaCartaoForm
cartao={cartaoSelecionado}
onSuccess={handleDespesaSuccess}
onCancel={() => setIsDialogNovaDespesaOpen(false)}
/>
)}
</DialogContent>
</Dialog>
</div>
</Layout>
);
};
export default CartoesCreditoPage;

View File

@ -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<CartaoCredito[]> {
// 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<CartaoCredito | null> {
// 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<CartaoCredito | null> {
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<DespesaCartao[]> {
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<DespesaCartao | null> {
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<number> {
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;
}
}

18
src/types/cartaoTypes.ts Normal file
View File

@ -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;
}