Fix: Implement card deletion and display card details

Added card deletion functionality. Improved card details display to show the month and the total expenses for each card. Fixed dashboard card layout issues on smaller screens. Separated expenses and credit card expenses in the dashboard summary.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-24 18:56:23 +00:00
parent c2c12ae5b3
commit 13c4fb0760
7 changed files with 285 additions and 89 deletions

View File

@ -2,17 +2,33 @@
import { useState } from 'react';
import { CartaoCredito } from '@/types/cartaoTypes';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useToast } from '@/components/ui/use-toast';
import { CreditCard } from 'lucide-react';
import { CreditCard, Trash2 } from 'lucide-react';
import { excluirCartao } from '@/services/cartaoCreditoService';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface CartaoCreditoListProps {
cartoes: CartaoCredito[];
isLoading: boolean;
onCartaoClick: (cartao: CartaoCredito) => void;
onCartaoDeleted?: () => void;
}
export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoCreditoListProps) {
export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick, onCartaoDeleted }: CartaoCreditoListProps) {
const { toast } = useToast();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [cartaoToDelete, setCartaoToDelete] = useState<CartaoCredito | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const formatCurrency = (value: number = 0) => {
return new Intl.NumberFormat('pt-BR', {
@ -21,6 +37,53 @@ export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoC
}).format(value);
};
const getCurrentMonthYear = () => {
const now = new Date();
const month = now.toLocaleDateString('pt-BR', { month: 'long' });
const year = now.getFullYear();
return `${month.charAt(0).toUpperCase() + month.slice(1)} ${year}`;
};
const handleDeleteClick = (e: React.MouseEvent, cartao: CartaoCredito) => {
e.stopPropagation();
setCartaoToDelete(cartao);
setDeleteDialogOpen(true);
};
const handleConfirmDelete = async () => {
if (!cartaoToDelete) return;
setIsDeleting(true);
try {
const success = await excluirCartao(cartaoToDelete.id);
if (success) {
toast({
title: "Cartão excluído",
description: "O cartão e todas as suas despesas foram removidos com sucesso.",
});
onCartaoDeleted?.();
} else {
toast({
title: "Erro ao excluir cartão",
description: "Não foi possível excluir o cartão. Tente novamente.",
variant: "destructive"
});
}
} catch (error) {
console.error('Erro ao excluir cartão:', error);
toast({
title: "Erro ao excluir cartão",
description: "Ocorreu um erro inesperado. Tente novamente.",
variant: "destructive"
});
} finally {
setIsDeleting(false);
setDeleteDialogOpen(false);
setCartaoToDelete(null);
}
};
if (isLoading) {
return (
<div className="flex justify-center p-8">
@ -48,36 +111,71 @@ export function CartaoCreditoList({ cartoes, isLoading, onCartaoClick }: CartaoC
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{cartoes.map((cartao) => {
console.log(`Cartão na lista: ${cartao.nome}, total: ${cartao.total_despesas}`);
return (
<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>
<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">
{formatCurrency(cartao.total_despesas)}
</p>
<p className="text-xs text-muted-foreground">
Total em despesas
</p>
</CardContent>
</Card>
);
})}
</div>
<>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{cartoes.map((cartao) => {
console.log(`Cartão na lista: ${cartao.nome}, total: ${cartao.total_despesas}`);
return (
<Card
key={cartao.id}
className="hover:shadow-md transition-shadow cursor-pointer relative group"
onClick={() => onCartaoClick(cartao)}
>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center text-lg">
<CreditCard className="w-5 h-5 mr-2" />
{cartao.nome}
</CardTitle>
<Button
variant="ghost"
size="sm"
className="opacity-0 group-hover:opacity-100 transition-opacity text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={(e) => handleDeleteClick(e, cartao)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<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">
{formatCurrency(cartao.total_despesas)}
</p>
<p className="text-xs text-muted-foreground">
Fatura de {getCurrentMonthYear()}
</p>
</CardContent>
</Card>
);
})}
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>
Tem certeza que deseja excluir o cartão "{cartaoToDelete?.nome}"?
Esta ação removerá permanentemente o cartão e todas as suas despesas associadas.
Esta ação não pode ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancelar</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={isDeleting}
className="bg-red-600 hover:bg-red-700"
>
{isDeleting ? 'Excluindo...' : 'Excluir'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View File

@ -1,19 +1,21 @@
import { CartaoCredito } from "@/types/cartaoTypes";
import { CartaoCreditoList } from "./CartaoCreditoList";
import { CartaoCreditoList } from './CartaoCreditoList';
import { CartaoCredito } from '@/types/cartaoTypes';
interface CartaoListViewProps {
cartoes: CartaoCredito[];
isLoading: boolean;
onCartaoClick: (cartao: CartaoCredito) => void;
onCartaoDeleted?: () => void;
}
export function CartaoListView({ cartoes, isLoading, onCartaoClick }: CartaoListViewProps) {
export function CartaoListView({ cartoes, isLoading, onCartaoClick, onCartaoDeleted }: CartaoListViewProps) {
return (
<CartaoCreditoList
cartoes={cartoes}
cartoes={cartoes}
isLoading={isLoading}
onCartaoClick={onCartaoClick}
onCartaoClick={onCartaoClick}
onCartaoDeleted={onCartaoDeleted}
/>
);
}

View File

@ -14,28 +14,27 @@ const DashboardSummaryCards: React.FC<DashboardSummaryCardsProps> = ({ resumo, f
const { navigateToTransactions } = useNavigateWithFilter();
const saldo = resumo ? resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0) : 0;
const totalDespesasGeral = resumo ? resumo.totalDespesas + (resumo.totalCartoes || 0) : 0;
return (
<div className="grid gap-6 md:grid-cols-4">
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{/* Card Receitas - Clicável */}
<Card
className="relative overflow-hidden border-2 border-green-200 bg-gradient-to-br from-green-50 to-emerald-100 shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer group transform hover:scale-105"
onClick={() => navigateToTransactions('receita')}
>
<div className="absolute inset-0 bg-gradient-to-r from-green-400/10 to-emerald-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-green-500 rounded-full shadow-lg">
<ArrowUpIcon className="h-6 w-6 text-white" />
<CardContent className="p-4 sm:p-6 relative">
<div className="flex items-center justify-between mb-2 sm:mb-4">
<div className="p-2 sm:p-3 bg-green-500 rounded-full shadow-lg">
<ArrowUpIcon className="h-4 w-4 sm:h-6 sm:w-6 text-white" />
</div>
<div className="text-xs font-bold px-3 py-1 bg-green-500 text-white rounded-full shadow-sm">
<div className="text-xs font-bold px-2 py-1 bg-green-500 text-white rounded-full shadow-sm">
+5%
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-green-700">Receitas</p>
<p className="text-3xl font-bold text-green-600">
<div className="space-y-1 sm:space-y-2">
<p className="text-xs sm:text-sm font-semibold text-green-700">Receitas</p>
<p className="text-lg sm:text-2xl lg:text-3xl font-bold text-green-600 break-words">
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
</p>
<p className="text-xs text-green-600 opacity-80">Clique para ver detalhes</p>
@ -49,22 +48,27 @@ const DashboardSummaryCards: React.FC<DashboardSummaryCardsProps> = ({ resumo, f
onClick={() => navigateToTransactions('despesa')}
>
<div className="absolute inset-0 bg-gradient-to-r from-red-400/10 to-rose-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-red-500 rounded-full shadow-lg">
<ArrowDownIcon className="h-6 w-6 text-white" />
<CardContent className="p-4 sm:p-6 relative">
<div className="flex items-center justify-between mb-2 sm:mb-4">
<div className="p-2 sm:p-3 bg-red-500 rounded-full shadow-lg">
<ArrowDownIcon className="h-4 w-4 sm:h-6 sm:w-6 text-white" />
</div>
<div className="text-xs font-bold px-3 py-1 bg-red-500 text-white rounded-full shadow-sm">
<div className="text-xs font-bold px-2 py-1 bg-red-500 text-white rounded-full shadow-sm">
-2%
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-red-700">Despesas</p>
<p className="text-3xl font-bold text-red-600">
{resumo ? formatCurrency(totalDespesasGeral) : 'R$ 0,00'}
<div className="space-y-1 sm:space-y-2">
<p className="text-xs sm:text-sm font-semibold text-red-700">Despesas</p>
<p className="text-lg sm:text-2xl lg:text-3xl font-bold text-red-600 break-words">
{resumo ? formatCurrency(resumo.totalDespesas) : 'R$ 0,00'}
</p>
{resumo && resumo.totalCartoes > 0 && (
<p className="text-xs text-red-500">Cartões: {formatCurrency(resumo.totalCartoes)}</p>
<div className="pt-1 border-t border-red-200">
<p className="text-xs text-red-500">
<span className="font-medium">Cartões: </span>
{formatCurrency(resumo.totalCartoes)}
</p>
</div>
)}
<p className="text-xs text-red-600 opacity-80">Clique para ver detalhes</p>
</div>
@ -74,15 +78,15 @@ const DashboardSummaryCards: React.FC<DashboardSummaryCardsProps> = ({ resumo, f
{/* Card Saldo */}
<Card className="relative overflow-hidden border-2 border-blue-200 bg-gradient-to-br from-blue-50 to-sky-100 shadow-lg hover:shadow-xl transition-all duration-300 group transform hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-r from-blue-400/10 to-sky-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-blue-500 rounded-full shadow-lg">
<CreditCardIcon className="h-6 w-6 text-white" />
<CardContent className="p-4 sm:p-6 relative">
<div className="flex items-center justify-between mb-2 sm:mb-4">
<div className="p-2 sm:p-3 bg-blue-500 rounded-full shadow-lg">
<CreditCardIcon className="h-4 w-4 sm:h-6 sm:w-6 text-white" />
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-blue-700">Saldo</p>
<p className={`text-3xl font-bold ${
<div className="space-y-1 sm:space-y-2">
<p className="text-xs sm:text-sm font-semibold text-blue-700">Saldo</p>
<p className={`text-lg sm:text-2xl lg:text-3xl font-bold break-words ${
saldo >= 0 ? 'text-blue-600' : 'text-red-600'
}`}>
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
@ -94,18 +98,18 @@ const DashboardSummaryCards: React.FC<DashboardSummaryCardsProps> = ({ resumo, f
{/* Card Economia */}
<Card className="relative overflow-hidden border-2 border-purple-200 bg-gradient-to-br from-purple-50 to-violet-100 shadow-lg hover:shadow-xl transition-all duration-300 group transform hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-r from-purple-400/10 to-violet-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-purple-500 rounded-full shadow-lg">
<Target className="h-6 w-6 text-white" />
<CardContent className="p-4 sm:p-6 relative">
<div className="flex items-center justify-between mb-2 sm:mb-4">
<div className="p-2 sm:p-3 bg-purple-500 rounded-full shadow-lg">
<Target className="h-4 w-4 sm:h-6 sm:w-6 text-white" />
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-purple-700">Economia</p>
<p className="text-3xl font-bold text-purple-600">
<div className="space-y-1 sm:space-y-2">
<p className="text-xs sm:text-sm font-semibold text-purple-700">Economia</p>
<p className="text-lg sm:text-2xl lg:text-3xl font-bold text-purple-600 break-words">
-22.2%
</p>
<p className="text-xs text-purple-500">{resumo ? formatCurrency(Math.abs(saldo)) : 'R$ 0,00'}</p>
<p className="text-xs text-purple-500 break-words">{resumo ? formatCurrency(Math.abs(saldo)) : 'R$ 0,00'}</p>
</div>
</CardContent>
</Card>

View File

@ -20,7 +20,7 @@ export const TransactionSummaryCards = ({
const totalGeral = totalDespesas + totalCartoes;
return (
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<SimpleCard className="border-green-200">
<div className="pb-2 bg-green-50 rounded-t-lg -m-6 mb-4 p-6">
<div className="text-green-700 flex items-center font-bold">
@ -29,7 +29,7 @@ export const TransactionSummaryCards = ({
</div>
</div>
<div className="pt-0">
<p className="text-2xl font-bold text-green-600">
<p className="text-xl sm:text-2xl font-bold text-green-600 break-words">
{formatCurrency(totalReceitas)}
</p>
</div>
@ -42,10 +42,21 @@ export const TransactionSummaryCards = ({
Gastos do mês
</div>
</div>
<div className="pt-0">
<p className="text-2xl font-bold text-red-600">
{formatCurrency(totalDespesas)}
</p>
<div className="pt-0 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 font-medium">Despesas:</span>
<span className="text-lg sm:text-xl font-bold text-red-600 break-words">
{formatCurrency(totalDespesas)}
</span>
</div>
{totalCartoes > 0 && (
<div className="flex items-center justify-between pt-1 border-t border-red-100">
<span className="text-sm text-gray-600 font-medium">Cartões:</span>
<span className="text-lg sm:text-xl font-bold text-red-500 break-words">
{formatCurrency(totalCartoes)}
</span>
</div>
)}
</div>
</SimpleCard>
@ -53,17 +64,16 @@ export const TransactionSummaryCards = ({
<div className="pb-2 bg-blue-50 rounded-t-lg -m-6 mb-4 p-6">
<div className="text-blue-700 flex items-center font-bold">
<CreditCard className="h-5 w-5 mr-2 text-blue-600" />
Gastos em cartões
Total geral
</div>
</div>
<div className="pt-0 space-y-1">
<p className="text-2xl font-bold text-blue-600">
{formatCurrency(totalCartoes)}
<div className="pt-0">
<p className="text-xl sm:text-2xl font-bold text-blue-600 break-words">
{formatCurrency(totalGeral)}
</p>
<p className="text-xs text-blue-500 mt-1">
Despesas + Cartões
</p>
<div className="flex items-center justify-between pt-2 text-sm border-t">
<span className="text-gray-600 font-medium">Total geral:</span>
<span className="font-bold text-red-600">{formatCurrency(totalGeral)}</span>
</div>
</div>
</SimpleCard>
</div>

View File

@ -86,6 +86,15 @@ const CartoesCreditoPage = () => {
});
};
const handleCartaoDeleted = () => {
loadCartoes();
if (detalheCartaoAberto) {
setDetalheCartaoAberto(false);
setCartaoSelecionado(null);
setDespesas([]);
}
};
const handleCartaoClick = async (cartao: CartaoCredito) => {
const totalDespesas = await getTotalDespesasCartao(cartao.id);
const updatedCartao = { ...cartao, total_despesas: totalDespesas };
@ -120,6 +129,7 @@ const CartoesCreditoPage = () => {
cartoes={cartoes}
isLoading={isLoading}
onCartaoClick={handleCartaoClick}
onCartaoDeleted={handleCartaoDeleted}
/>
) : cartaoSelecionado && (
<CartaoDetalhesAvancado

View File

@ -1,4 +1,3 @@
import { supabase } from "@/integrations/supabase/client";
import { CartaoCredito } from "@/types/cartaoTypes";
import { gerarCartaoCodigo } from "./cartaoCodigoUtils";
@ -187,3 +186,75 @@ export async function criarCartao(
return null;
}
}
/**
* Deletes a credit card and all its associated expenses
* @param cartaoId Card ID to delete
* @returns Success status
*/
export async function excluirCartao(cartaoId: string): Promise<boolean> {
const userEmail = localStorage.getItem('userEmail');
if (!userEmail) {
console.error('Email do usuário não encontrado no localStorage');
return false;
}
const normalizedEmail = userEmail.trim().toLowerCase();
try {
// First, get the card name to delete associated expenses
const { data: cardData, error: cardError } = await supabase
.from('cartoes_credito')
.select('nome')
.eq('id', cartaoId)
.eq('login', normalizedEmail)
.single();
if (cardError) {
console.error('Erro ao obter dados do cartão:', cardError);
return false;
}
// Delete all expenses associated with this card
const { error: expensesError } = await supabase
.from('despesas_cartao')
.delete()
.eq('nome', cardData.nome)
.eq('login', normalizedEmail);
if (expensesError) {
console.error('Erro ao excluir despesas do cartão:', expensesError);
return false;
}
// Delete all invoices associated with this card
const { error: invoicesError } = await supabase
.from('faturas_cartao')
.delete()
.eq('cartao_id', cartaoId)
.eq('login', normalizedEmail);
if (invoicesError) {
console.error('Erro ao excluir faturas do cartão:', invoicesError);
// Continue with card deletion even if invoices fail
}
// Finally, delete the card itself
const { error: cardDeleteError } = await supabase
.from('cartoes_credito')
.delete()
.eq('id', cartaoId)
.eq('login', normalizedEmail);
if (cardDeleteError) {
console.error('Erro ao excluir cartão:', cardDeleteError);
return false;
}
return true;
} catch (error) {
console.error('Erro ao excluir cartão:', error);
return false;
}
}

View File

@ -7,7 +7,8 @@
export {
getCartoes,
criarCartao,
getCartao
getCartao,
excluirCartao
} from './cartao/cartoesService';
// Re-export all functions from despesas services