Implement credit card and expense features.

Add credit card registration form with required fields and auto-generated code. Implement expense registration form with card selection and required fields. Establish association logic between expenses and cards based on user and card codes. Ensure data privacy and validation.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-21 00:21:30 +00:00
parent 04ed74ac61
commit 0d58128969
6 changed files with 296 additions and 67 deletions

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod'; import * as z from 'zod';
@ -15,9 +15,31 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Opções para as listas suspensas
const bandeirasCartao = [
'Visa', 'Mastercard', 'Elo', 'American Express', 'Hipercard',
'Diners Club', 'Credicard', 'Sorocred', 'Cabal', 'Banescard',
'Sicoobcard', 'Sicredi', 'Alelo', 'Ticket', 'VR', 'ValeCard', 'Havan'
];
const bancos = [
'Itaú', 'Bradesco', 'Santander', 'Banco do Brasil', 'Caixa', 'Nubank',
'Inter', 'C6 Bank', 'Next', 'Original', 'Pan', 'Banco Bari',
'Sicoob', 'Sicredi', 'Neon', 'BTG Pactual', 'PagBank', 'Will Bank', 'XP'
];
const cartaoSchema = z.object({ const cartaoSchema = z.object({
nome: z.string().min(1, { message: 'Nome do cartão é obrigatório' }), nome: z.string().min(1, { message: 'Nome do cartão é obrigatório' }),
bandeira: z.string({ required_error: 'Selecione uma bandeira' }),
banco: z.string({ required_error: 'Selecione um banco' }),
}); });
type CartaoFormValues = z.infer<typeof cartaoSchema>; type CartaoFormValues = z.infer<typeof cartaoSchema>;
@ -35,14 +57,30 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
resolver: zodResolver(cartaoSchema), resolver: zodResolver(cartaoSchema),
defaultValues: { defaultValues: {
nome: '', nome: '',
bandeira: '',
banco: '',
} }
}); });
// Função para gerar um código único para o cartão
const gerarCartaoCodigo = (nome: string, banco: string) => {
const randomStr = Math.random().toString(36).substring(2, 8);
return `${banco.toLowerCase()}_${nome.toLowerCase().replace(/\s/g, '')}_${randomStr}`;
};
async function onSubmit(data: CartaoFormValues) { async function onSubmit(data: CartaoFormValues) {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const resultado = await criarCartao(data.nome); // Gerar código único para o cartão
const cartao_codigo = gerarCartaoCodigo(data.nome, data.banco);
const resultado = await criarCartao(
data.nome,
data.bandeira,
data.banco,
cartao_codigo
);
if (resultado) { if (resultado) {
toast({ toast({
@ -79,13 +117,63 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
<FormItem> <FormItem>
<FormLabel>Nome do Cartão</FormLabel> <FormLabel>Nome do Cartão</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Ex: Nubank, Itaú, Banco do Brasil..." {...field} /> <Input placeholder="Ex: Gold, Platinum, Internacional..." {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="bandeira"
render={({ field }) => (
<FormItem>
<FormLabel>Bandeira</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Selecione uma bandeira" />
</SelectTrigger>
</FormControl>
<SelectContent>
{bandeirasCartao.map((bandeira) => (
<SelectItem key={bandeira} value={bandeira}>
{bandeira}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="banco"
render={({ field }) => (
<FormItem>
<FormLabel>Banco Emissor</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Selecione um banco" />
</SelectTrigger>
</FormControl>
<SelectContent>
{bancos.map((banco) => (
<SelectItem key={banco} value={banco}>
{banco}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-2"> <div className="flex justify-end space-x-2 pt-2">
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}> <Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>
Cancelar Cancelar

View File

@ -60,6 +60,10 @@ export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoC
<CreditCard className="w-5 h-5 mr-2" /> <CreditCard className="w-5 h-5 mr-2" />
{cartao.nome} {cartao.nome}
</CardTitle> </CardTitle>
<div className="text-sm text-muted-foreground space-y-1">
{cartao.banco && <div>{cartao.banco}</div>}
{cartao.bandeira && <div>{cartao.bandeira}</div>}
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-2xl font-semibold"> <p className="text-2xl font-semibold">

View File

@ -4,7 +4,11 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod'; import * as z from 'zod';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { CartaoCredito } from '@/types/cartaoTypes';
import { criarDespesa } from '@/services/cartaoCreditoService'; import { criarDespesa } from '@/services/cartaoCreditoService';
import { CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { import {
Form, Form,
FormControl, FormControl,
@ -15,23 +19,31 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { CartaoCredito } from '@/types/cartaoTypes';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue SelectValue,
} from '@/components/ui/select'; } from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { cn } from '@/lib/utils';
const despesaSchema = z.object({ const despesaCartaoSchema = z.object({
cartaoId: z.string().min(1, { message: 'Cartão é obrigatório' }), cartao_id: z.string().min(1, { message: 'Selecione um cartão' }),
valor: z.string().min(1, { message: 'Valor é obrigatório' }), valor: z.number().positive({ message: 'O valor deve ser maior que zero' }),
data_despesa: z.string().min(1, { message: 'Data é obrigatória' }), data_despesa: z.date({
required_error: "Selecione uma data",
}),
descricao: z.string().min(1, { message: 'Descrição é obrigatória' }), descricao: z.string().min(1, { message: 'Descrição é obrigatória' }),
}); });
type DespesaFormValues = z.infer<typeof despesaSchema>; type DespesaCartaoFormValues = z.infer<typeof despesaCartaoSchema>;
interface DespesaCartaoFormSelectProps { interface DespesaCartaoFormSelectProps {
cartoes: CartaoCredito[]; cartoes: CartaoCredito[];
@ -42,44 +54,56 @@ interface DespesaCartaoFormSelectProps {
export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: DespesaCartaoFormSelectProps) { export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: DespesaCartaoFormSelectProps) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const [selectedCartao, setSelectedCartao] = useState<CartaoCredito | null>(null);
const form = useForm<DespesaFormValues>({ const form = useForm<DespesaCartaoFormValues>({
resolver: zodResolver(despesaSchema), resolver: zodResolver(despesaCartaoSchema),
defaultValues: { defaultValues: {
cartaoId: '', valor: undefined,
valor: '', data_despesa: new Date(),
data_despesa: new Date().toISOString().split('T')[0],
descricao: '', descricao: '',
} }
}); });
async function onSubmit(data: DespesaFormValues) { const handleCartaoChange = (cartaoId: string) => {
setIsSubmitting(true); const cartao = cartoes.find(c => c.id === cartaoId);
setSelectedCartao(cartao || null);
};
try { const formatCartaoLabel = (cartao: CartaoCredito) => {
const valorNumerico = parseFloat(data.valor.replace(',', '.')); let label = cartao.nome;
if (cartao.banco) label += ` - ${cartao.banco}`;
if (cartao.bandeira) label += ` (${cartao.bandeira})`;
return label;
};
if (isNaN(valorNumerico)) { async function onSubmit(data: DespesaCartaoFormValues) {
if (!selectedCartao || !selectedCartao.cartao_codigo) {
toast({ toast({
title: "Erro no formulário", title: "Erro",
description: "O valor precisa ser um número válido", description: "Informações do cartão incompletas",
variant: "destructive" variant: "destructive"
}); });
setIsSubmitting(false);
return; return;
} }
setIsSubmitting(true);
try {
const formattedDate = format(data.data_despesa, 'yyyy-MM-dd');
const resultado = await criarDespesa( const resultado = await criarDespesa(
data.cartaoId, data.cartao_id,
valorNumerico, selectedCartao.cartao_codigo,
data.data_despesa, data.valor,
formattedDate,
data.descricao data.descricao
); );
if (resultado) { if (resultado) {
toast({ toast({
title: "Despesa adicionada", title: "Despesa adicionada",
description: "Despesa do cartão cadastrada com sucesso", description: "Despesa do cartão registrada com sucesso",
}); });
onSuccess(); onSuccess();
} else { } else {
@ -101,21 +125,21 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
} }
} }
const selectedCartaoNome = form.watch('cartaoId') ?
cartoes.find(cartao => cartao.id === form.watch('cartaoId'))?.nome : '';
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="cartaoId" name="cartao_id"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Cartão de Crédito</FormLabel> <FormLabel>Cartão de Crédito</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={(value) => {
value={field.value} field.onChange(value);
handleCartaoChange(value);
}}
defaultValue={field.value}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@ -125,7 +149,7 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<SelectContent> <SelectContent>
{cartoes.map((cartao) => ( {cartoes.map((cartao) => (
<SelectItem key={cartao.id} value={cartao.id}> <SelectItem key={cartao.id} value={cartao.id}>
{cartao.nome} {formatCartaoLabel(cartao)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@ -135,12 +159,6 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
)} )}
/> />
{selectedCartaoNome && (
<div className="mb-4">
<p className="text-sm text-muted-foreground">Cartão selecionado: <span className="font-medium">{selectedCartaoNome}</span></p>
</div>
)}
<FormField <FormField
control={form.control} control={form.control}
name="valor" name="valor"
@ -149,10 +167,11 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<FormLabel>Valor</FormLabel> <FormLabel>Valor</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="0,00" type="number"
step="0.01"
placeholder="0.00"
{...field} {...field}
type="text" onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
inputMode="decimal"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -164,11 +183,40 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
control={form.control} control={form.control}
name="data_despesa" name="data_despesa"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-col">
<FormLabel>Data</FormLabel> <FormLabel>Data da Despesa</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl> <FormControl>
<Input type="date" {...field} /> <Button
variant={"outline"}
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "dd/MM/yyyy", { locale: ptBR })
) : (
<span>Selecione uma data</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl> </FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
initialFocus
locale={ptBR}
/>
</PopoverContent>
</Popover>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -181,7 +229,7 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<FormItem> <FormItem>
<FormLabel>Descrição</FormLabel> <FormLabel>Descrição</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Detalhes da despesa" {...field} /> <Input placeholder="Ex: Compras de supermercado, Restaurante..." {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -24,6 +24,7 @@ export const TransactionSummaryCards = ({
<Card> <Card>
<CardHeader className="pb-2 bg-green-50"> <CardHeader className="pb-2 bg-green-50">
<CardTitle className="text-finance-green flex items-center"> <CardTitle className="text-finance-green flex items-center">
<Wallet className="h-4 w-4 mr-2" />
Ganhos do mês Ganhos do mês
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -29,7 +29,7 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
// Buscar o total de despesas para cada cartão // Buscar o total de despesas para cada cartão
const cartoesComTotal = await Promise.all( const cartoesComTotal = await Promise.all(
data.map(async (cartao: CartaoCredito) => { data.map(async (cartao: CartaoCredito) => {
const total = await getTotalDespesasCartao(cartao.id); const total = await getTotalDespesasCartao(cartao.cartao_codigo);
return { ...cartao, total_despesas: total }; return { ...cartao, total_despesas: total };
}) })
); );
@ -41,8 +41,41 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
} }
} }
// Verificar se já existe um cartão com o mesmo nome e banco para o usuário
export async function verificarCartaoExistente(nome: string, banco: string): Promise<boolean> {
// 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 false;
}
// Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase();
try {
const { data, error, count } = await supabase
.from('cartoes_credito')
.select('*', { count: 'exact' })
.eq('login', normalizedEmail)
.eq('nome', nome)
.eq('banco', banco);
if (error) {
console.error('Erro ao verificar cartão existente:', error);
return false;
}
return count ? count > 0 : false;
} catch (error) {
console.error('Erro ao verificar cartão existente:', error);
return false;
}
}
// Função para criar um novo cartão de crédito // Função para criar um novo cartão de crédito
export async function criarCartao(nome: string): Promise<CartaoCredito | null> { export async function criarCartao(nome: string, bandeira: string, banco: string, cartao_codigo: string): Promise<CartaoCredito | null> {
// Obter o email do usuário do localStorage // Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail'); const userEmail = localStorage.getItem('userEmail');
// Obter o ID do usuário do localStorage (mantido por compatibilidade) // Obter o ID do usuário do localStorage (mantido por compatibilidade)
@ -56,11 +89,20 @@ export async function criarCartao(nome: string): Promise<CartaoCredito | null> {
// Normalizar o email (minúsculo e sem espaços) // Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase(); const normalizedEmail = userEmail.trim().toLowerCase();
// Verificar se já existe um cartão com o mesmo nome e banco
const cartaoExistente = await verificarCartaoExistente(nome, banco);
if (cartaoExistente) {
throw new Error(`Você já possui um cartão ${nome} do banco ${banco}.`);
}
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('cartoes_credito') .from('cartoes_credito')
.insert([{ .insert([{
nome: nome, nome: nome,
bandeira: bandeira,
banco: banco,
cartao_codigo: cartao_codigo,
user_id: userId, user_id: userId,
login: normalizedEmail login: normalizedEmail
}]) }])
@ -74,7 +116,7 @@ export async function criarCartao(nome: string): Promise<CartaoCredito | null> {
return data[0]; return data[0];
} catch (error) { } catch (error) {
console.error('Erro ao criar cartão:', error); console.error('Erro ao criar cartão:', error);
return null; throw error;
} }
} }
@ -100,12 +142,24 @@ export async function getCartao(cartaoId: string): Promise<CartaoCredito | null>
} }
// Função para buscar despesas de um cartão // Função para buscar despesas de um cartão
export async function getDespesasCartao(cartaoId: string): Promise<DespesaCartao[]> { export async function getDespesasCartao(cartaoCodigo: string): Promise<DespesaCartao[]> {
// 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 { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('despesas_cartao') .from('despesas_cartao')
.select('*') .select('*')
.eq('cartao_id', cartaoId) .eq('cartao_codigo', cartaoCodigo)
.eq('login', normalizedEmail)
.order('data_despesa', { ascending: false }); .order('data_despesa', { ascending: false });
if (error) { if (error) {
@ -121,15 +175,31 @@ export async function getDespesasCartao(cartaoId: string): Promise<DespesaCartao
} }
// Função para criar uma nova despesa no cartão // 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> { export async function criarDespesa(cartao_id: string, cartao_codigo: string, valor: number, data_despesa: string, descricao: string): Promise<DespesaCartao | 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 { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('despesas_cartao') .from('despesas_cartao')
.insert([{ .insert([{
cartao_id: cartaoId, cartao_id: cartao_id,
cartao_codigo: cartao_codigo,
valor: valor, valor: valor,
data_despesa: data_despesa, data_despesa: data_despesa,
descricao: descricao descricao: descricao,
login: normalizedEmail,
user_id: userId
}]) }])
.select(); .select();
@ -146,12 +216,24 @@ export async function criarDespesa(cartaoId: string, valor: number, data_despesa
} }
// Função para calcular o total de despesas de um cartão // Função para calcular o total de despesas de um cartão
export async function getTotalDespesasCartao(cartaoId: string): Promise<number> { export async function getTotalDespesasCartao(cartaoCodigo: string): Promise<number> {
// 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 0;
}
// Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase();
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('despesas_cartao') .from('despesas_cartao')
.select('valor') .select('valor')
.eq('cartao_id', cartaoId); .eq('cartao_codigo', cartaoCodigo)
.eq('login', normalizedEmail);
if (error) { if (error) {
console.error('Erro ao calcular total de despesas:', error); console.error('Erro ao calcular total de despesas:', error);

View File

@ -2,6 +2,9 @@
export interface CartaoCredito { export interface CartaoCredito {
id: string; id: string;
nome: string; nome: string;
bandeira?: string;
banco?: string;
cartao_codigo: string;
user_id: string; user_id: string;
login?: string; login?: string;
created_at: string; created_at: string;
@ -11,6 +14,9 @@ export interface CartaoCredito {
export interface DespesaCartao { export interface DespesaCartao {
id: string; id: string;
cartao_id: string; cartao_id: string;
cartao_codigo: string;
user_id?: string;
login?: string;
valor: number; valor: number;
data_despesa: string; data_despesa: string;
descricao: string; descricao: string;