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 { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
@ -15,9 +15,31 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
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({
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>;
@ -35,14 +57,30 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
resolver: zodResolver(cartaoSchema),
defaultValues: {
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) {
setIsSubmitting(true);
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) {
toast({
@ -79,13 +117,63 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
<FormItem>
<FormLabel>Nome do Cartão</FormLabel>
<FormControl>
<Input placeholder="Ex: Nubank, Itaú, Banco do Brasil..." {...field} />
<Input placeholder="Ex: Gold, Platinum, Internacional..." {...field} />
</FormControl>
<FormMessage />
</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">
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>
Cancelar

View File

@ -60,6 +60,10 @@ export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoC
<CreditCard className="w-5 h-5 mr-2" />
{cartao.nome}
</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>
<CardContent>
<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 * as z from 'zod';
import { useToast } from '@/components/ui/use-toast';
import { CartaoCredito } from '@/types/cartaoTypes';
import { criarDespesa } from '@/services/cartaoCreditoService';
import { CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import {
Form,
FormControl,
@ -15,23 +19,31 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { CartaoCredito } from '@/types/cartaoTypes';
import {
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
SelectValue,
} 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({
cartaoId: z.string().min(1, { message: 'Cartão é obrigatório' }),
valor: z.string().min(1, { message: 'Valor é obrigatório' }),
data_despesa: z.string().min(1, { message: 'Data é obrigatória' }),
const despesaCartaoSchema = z.object({
cartao_id: z.string().min(1, { message: 'Selecione um cartão' }),
valor: z.number().positive({ message: 'O valor deve ser maior que zero' }),
data_despesa: z.date({
required_error: "Selecione uma data",
}),
descricao: z.string().min(1, { message: 'Descrição é obrigatória' }),
});
type DespesaFormValues = z.infer<typeof despesaSchema>;
type DespesaCartaoFormValues = z.infer<typeof despesaCartaoSchema>;
interface DespesaCartaoFormSelectProps {
cartoes: CartaoCredito[];
@ -42,44 +54,56 @@ interface DespesaCartaoFormSelectProps {
export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: DespesaCartaoFormSelectProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
const [selectedCartao, setSelectedCartao] = useState<CartaoCredito | null>(null);
const form = useForm<DespesaFormValues>({
resolver: zodResolver(despesaSchema),
const form = useForm<DespesaCartaoFormValues>({
resolver: zodResolver(despesaCartaoSchema),
defaultValues: {
cartaoId: '',
valor: '',
data_despesa: new Date().toISOString().split('T')[0],
valor: undefined,
data_despesa: new Date(),
descricao: '',
}
});
async function onSubmit(data: DespesaFormValues) {
const handleCartaoChange = (cartaoId: string) => {
const cartao = cartoes.find(c => c.id === cartaoId);
setSelectedCartao(cartao || null);
};
const formatCartaoLabel = (cartao: CartaoCredito) => {
let label = cartao.nome;
if (cartao.banco) label += ` - ${cartao.banco}`;
if (cartao.bandeira) label += ` (${cartao.bandeira})`;
return label;
};
async function onSubmit(data: DespesaCartaoFormValues) {
if (!selectedCartao || !selectedCartao.cartao_codigo) {
toast({
title: "Erro",
description: "Informações do cartão incompletas",
variant: "destructive"
});
return;
}
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 formattedDate = format(data.data_despesa, 'yyyy-MM-dd');
const resultado = await criarDespesa(
data.cartaoId,
valorNumerico,
data.data_despesa,
data.cartao_id,
selectedCartao.cartao_codigo,
data.valor,
formattedDate,
data.descricao
);
if (resultado) {
toast({
title: "Despesa adicionada",
description: "Despesa do cartão cadastrada com sucesso",
description: "Despesa do cartão registrada com sucesso",
});
onSuccess();
} 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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="cartaoId"
name="cartao_id"
render={({ field }) => (
<FormItem>
<FormLabel>Cartão de Crédito</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
onValueChange={(value) => {
field.onChange(value);
handleCartaoChange(value);
}}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
@ -125,7 +149,7 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<SelectContent>
{cartoes.map((cartao) => (
<SelectItem key={cartao.id} value={cartao.id}>
{cartao.nome}
{formatCartaoLabel(cartao)}
</SelectItem>
))}
</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
control={form.control}
name="valor"
@ -149,26 +167,56 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<FormLabel>Valor</FormLabel>
<FormControl>
<Input
placeholder="0,00"
{...field}
type="text"
inputMode="decimal"
type="number"
step="0.01"
placeholder="0.00"
{...field}
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="data_despesa"
render={({ field }) => (
<FormItem>
<FormLabel>Data</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormItem className="flex flex-col">
<FormLabel>Data da Despesa</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<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>
</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 />
</FormItem>
)}
@ -181,7 +229,7 @@ export function DespesaCartaoFormSelect({ cartoes, onSuccess, onCancel }: Despes
<FormItem>
<FormLabel>Descrição</FormLabel>
<FormControl>
<Input placeholder="Detalhes da despesa" {...field} />
<Input placeholder="Ex: Compras de supermercado, Restaurante..." {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

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

View File

@ -29,7 +29,7 @@ export async function getCartoes(): Promise<CartaoCredito[]> {
// 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);
const total = await getTotalDespesasCartao(cartao.cartao_codigo);
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
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
const userEmail = localStorage.getItem('userEmail');
// 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)
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 {
const { data, error } = await supabase
.from('cartoes_credito')
.insert([{
nome: nome,
bandeira: bandeira,
banco: banco,
cartao_codigo: cartao_codigo,
user_id: userId,
login: normalizedEmail
}])
@ -74,7 +116,7 @@ export async function criarCartao(nome: string): Promise<CartaoCredito | null> {
return data[0];
} catch (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
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 {
const { data, error } = await supabase
.from('despesas_cartao')
.select('*')
.eq('cartao_id', cartaoId)
.eq('cartao_codigo', cartaoCodigo)
.eq('login', normalizedEmail)
.order('data_despesa', { ascending: false });
if (error) {
@ -121,15 +175,31 @@ export async function getDespesasCartao(cartaoId: string): Promise<DespesaCartao
}
// 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 {
const { data, error } = await supabase
.from('despesas_cartao')
.insert([{
cartao_id: cartaoId,
cartao_id: cartao_id,
cartao_codigo: cartao_codigo,
valor: valor,
data_despesa: data_despesa,
descricao: descricao
descricao: descricao,
login: normalizedEmail,
user_id: userId
}])
.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
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 {
const { data, error } = await supabase
.from('despesas_cartao')
.select('valor')
.eq('cartao_id', cartaoId);
.eq('cartao_codigo', cartaoCodigo)
.eq('login', normalizedEmail);
if (error) {
console.error('Erro ao calcular total de despesas:', error);

View File

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