Run SQL to create credit card tables.

This commit is contained in:
gpt-engineer-app[bot] 2025-06-11 00:33:12 +00:00
parent cb3f41b8ff
commit 47e003213d
10 changed files with 981 additions and 14 deletions

View File

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@ -22,7 +23,6 @@ import {
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',
@ -39,6 +39,9 @@ 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' }),
limite_total: z.string().optional(),
dia_vencimento: z.string().optional(),
melhor_dia_compra: z.string().optional(),
});
type CartaoFormValues = z.infer<typeof cartaoSchema>;
@ -58,6 +61,9 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
nome: '',
bandeira: '',
banco: '',
limite_total: '',
dia_vencimento: '10',
melhor_dia_compra: '5',
}
});
@ -65,7 +71,6 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
setIsSubmitting(true);
try {
// Removed the fourth argument that was causing the error
const resultado = await criarCartao(
data.nome,
data.bandeira,
@ -73,6 +78,23 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
);
if (resultado) {
// Atualizar campos adicionais se fornecidos
if (data.limite_total || data.dia_vencimento || data.melhor_dia_compra) {
const { supabase } = await import('@/integrations/supabase/client');
const userEmail = localStorage.getItem('userEmail');
const updateData: any = {};
if (data.limite_total) updateData.limite_total = parseFloat(data.limite_total);
if (data.dia_vencimento) updateData.dia_vencimento = parseInt(data.dia_vencimento);
if (data.melhor_dia_compra) updateData.melhor_dia_compra = parseInt(data.melhor_dia_compra);
await supabase
.from('cartoes_credito')
.update(updateData)
.eq('id', resultado.id)
.eq('login', userEmail?.trim().toLowerCase());
}
toast({
title: "Cartão adicionado",
description: "Cartão de crédito cadastrado com sucesso",
@ -163,6 +185,67 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="limite_total"
render={({ field }) => (
<FormItem>
<FormLabel>Limite Total (Opcional)</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="0,00"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dia_vencimento"
render={({ field }) => (
<FormItem>
<FormLabel>Dia do Vencimento</FormLabel>
<FormControl>
<Input
type="number"
min="1"
max="31"
placeholder="10"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="melhor_dia_compra"
render={({ field }) => (
<FormItem>
<FormLabel>Melhor Dia de Compra</FormLabel>
<FormControl>
<Input
type="number"
min="1"
max="31"
placeholder="5"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-2 pt-2">
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>

View File

@ -0,0 +1,281 @@
import { useState, useEffect } from 'react';
import { CartaoCredito, DespesaCartao, FaturaCartao } from "@/types/cartaoTypes";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CreditCard, Calendar, DollarSign, TrendingUp, FileText } from "lucide-react";
import { FiltroFaturas } from "./FiltroFaturas";
import { DespesasComParcelas } from "./DespesasComParcelas";
import { useFaturas } from "@/hooks/useFaturas";
import { formatCurrency } from "@/utils/formatters";
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
interface CartaoDetalhesAvancadoProps {
cartao: CartaoCredito;
despesas: DespesaCartao[];
isLoading: boolean;
onDespesaSuccess: () => void;
}
export function CartaoDetalhesAvancado({
cartao,
despesas: todasDespesas,
isLoading,
onDespesaSuccess
}: CartaoDetalhesAvancadoProps) {
const [faturaAtual, setFaturaAtual] = useState<{ mes: number; ano: number } | null>(null);
const [despesasFiltradas, setDespesasFiltradas] = useState<DespesaCartao[]>([]);
const [faturaSelecionada, setFaturaSelecionada] = useState<FaturaCartao | null>(null);
const { toast } = useToast();
const {
faturas,
isLoading: loadingFaturas,
calcularFaturaAtual,
criarFatura,
atualizarStatusPagamento
} = useFaturas({ cartaoId: cartao.id });
useEffect(() => {
// Calcular fatura atual baseada no dia de vencimento
const fatura = calcularFaturaAtual(cartao.dia_vencimento, cartao.melhor_dia_compra);
setFaturaAtual(fatura);
}, [cartao, calcularFaturaAtual]);
useEffect(() => {
// Filtrar despesas pela fatura selecionada
if (faturaAtual) {
const despesasDaFatura = todasDespesas.filter(despesa =>
despesa.mes_fatura === faturaAtual.mes && despesa.ano_fatura === faturaAtual.ano
);
setDespesasFiltradas(despesasDaFatura);
// Buscar dados da fatura
const fatura = faturas.find(f => f.mes === faturaAtual.mes && f.ano === faturaAtual.ano);
setFaturaSelecionada(fatura || null);
}
}, [faturaAtual, todasDespesas, faturas]);
const handleFaturaChange = (mes: number, ano: number) => {
setFaturaAtual({ mes, ano });
};
const handleCriarFatura = async (mes: number, ano: number) => {
const novaFatura = await criarFatura(mes, ano, cartao.dia_vencimento);
if (novaFatura) {
toast({
title: "Fatura criada",
description: `Fatura de ${mes}/${ano} criada com sucesso`,
});
}
};
const handleEditDespesa = (despesa: DespesaCartao) => {
// TODO: Implementar edição de despesa
console.log('Editar despesa:', despesa);
};
const handleDeleteDespesa = async (id: string) => {
try {
const { error } = await supabase
.from('despesas_cartao')
.delete()
.eq('id', id);
if (error) throw error;
onDespesaSuccess();
toast({
title: "Despesa removida",
description: "Despesa removida com sucesso",
});
} catch (error) {
console.error('Erro ao remover despesa:', error);
toast({
title: "Erro ao remover despesa",
description: "Não foi possível remover a despesa",
variant: "destructive"
});
}
};
const handleToggleConciliacao = async (id: string, novoStatus: 'pendente' | 'conciliado' | 'divergente') => {
try {
const { error } = await supabase
.from('despesas_cartao')
.update({ status_conciliacao: novoStatus })
.eq('id', id);
if (error) throw error;
onDespesaSuccess();
toast({
title: "Status atualizado",
description: `Despesa marcada como ${novoStatus}`,
});
} catch (error) {
console.error('Erro ao atualizar status:', error);
toast({
title: "Erro ao atualizar status",
description: "Não foi possível atualizar o status da despesa",
variant: "destructive"
});
}
};
const valorTotalFatura = despesasFiltradas.reduce((total, despesa) => total + despesa.valor, 0);
const totalConciliado = despesasFiltradas.filter(d => d.status_conciliacao === 'conciliado').length;
const totalPendente = despesasFiltradas.filter(d => d.status_conciliacao === 'pendente').length;
return (
<div className="space-y-6">
{/* Header do Cartão */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<CreditCard className="h-8 w-8 text-blue-600" />
<div>
<CardTitle className="text-2xl">{cartao.nome}</CardTitle>
<p className="text-muted-foreground">
{cartao.banco} {cartao.bandeira}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">Limite Total</p>
<p className="text-xl font-semibold">
{cartao.limite_total ? formatCurrency(cartao.limite_total) : 'N/A'}
</p>
</div>
</div>
</CardHeader>
</Card>
{/* Filtro de Faturas */}
{faturaAtual && (
<FiltroFaturas
faturas={faturas}
faturaAtual={faturaAtual}
onFaturaChange={handleFaturaChange}
onCriarFatura={handleCriarFatura}
/>
)}
{/* Resumo da Fatura */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<DollarSign className="h-5 w-5 text-green-600" />
<div>
<p className="text-sm text-muted-foreground">Valor Total</p>
<p className="text-lg font-semibold">{formatCurrency(valorTotalFatura)}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<FileText className="h-5 w-5 text-blue-600" />
<div>
<p className="text-sm text-muted-foreground">Total Despesas</p>
<p className="text-lg font-semibold">{despesasFiltradas.length}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<TrendingUp className="h-5 w-5 text-green-600" />
<div>
<p className="text-sm text-muted-foreground">Conciliadas</p>
<p className="text-lg font-semibold">{totalConciliado}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<Calendar className="h-5 w-5 text-yellow-600" />
<div>
<p className="text-sm text-muted-foreground">Pendentes</p>
<p className="text-lg font-semibold">{totalPendente}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Status da Fatura */}
{faturaSelecionada && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Status da Fatura</CardTitle>
<div className="flex items-center space-x-2">
<Badge variant={
faturaSelecionada.status_pagamento === 'pago' ? 'default' :
faturaSelecionada.status_pagamento === 'vencido' ? 'destructive' : 'secondary'
}>
{faturaSelecionada.status_pagamento}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => atualizarStatusPagamento(
faturaSelecionada.id,
faturaSelecionada.status_pagamento === 'pago' ? 'pendente' : 'pago',
faturaSelecionada.status_pagamento === 'pago' ? undefined : new Date().toISOString().split('T')[0]
)}
>
{faturaSelecionada.status_pagamento === 'pago' ? 'Marcar como Pendente' : 'Marcar como Pago'}
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Data de Vencimento</p>
<p>{faturaSelecionada.data_vencimento ? new Date(faturaSelecionada.data_vencimento).toLocaleDateString('pt-BR') : 'N/A'}</p>
</div>
{faturaSelecionada.data_pagamento && (
<div>
<p className="text-muted-foreground">Data de Pagamento</p>
<p>{new Date(faturaSelecionada.data_pagamento).toLocaleDateString('pt-BR')}</p>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Despesas */}
<Card>
<CardHeader>
<CardTitle>Despesas da Fatura</CardTitle>
</CardHeader>
<CardContent>
<DespesasComParcelas
despesas={despesasFiltradas}
onEditDespesa={handleEditDespesa}
onDeleteDespesa={handleDeleteDespesa}
onToggleConciliacao={handleToggleConciliacao}
/>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,188 @@
import { useState } from 'react';
import { DespesaCartao } from '@/types/cartaoTypes';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Edit, Trash2, Check, X, CreditCard } from 'lucide-react';
import { formatCurrency } from '@/utils/formatters';
interface DespesasComParcelasProps {
despesas: DespesaCartao[];
onEditDespesa: (despesa: DespesaCartao) => void;
onDeleteDespesa: (id: string) => void;
onToggleConciliacao: (id: string, status: 'pendente' | 'conciliado' | 'divergente') => void;
}
export function DespesasComParcelas({
despesas,
onEditDespesa,
onDeleteDespesa,
onToggleConciliacao
}: DespesasComParcelasProps) {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const toggleExpanded = (id: string) => {
const newExpanded = new Set(expandedItems);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedItems(newExpanded);
};
const getStatusBadge = (status: string) => {
const variants = {
pendente: 'bg-yellow-100 text-yellow-800',
conciliado: 'bg-green-100 text-green-800',
divergente: 'bg-red-100 text-red-800'
};
return (
<Badge className={variants[status as keyof typeof variants] || variants.pendente}>
{status}
</Badge>
);
};
// Agrupar despesas por despesa pai (para parcelas)
const despesasAgrupadas = despesas.reduce((acc, despesa) => {
const chave = despesa.despesa_pai_id || despesa.id;
if (!acc[chave]) {
acc[chave] = [];
}
acc[chave].push(despesa);
return acc;
}, {} as Record<string, DespesaCartao[]>);
return (
<div className="space-y-4">
{Object.entries(despesasAgrupadas).map(([chaveGrupo, grupoDesp]) => {
const despesaPrincipal = grupoDesp.find(d => !d.despesa_pai_id) || grupoDesp[0];
const parcelas = grupoDesp.filter(d => d.despesa_pai_id);
const temParcelas = parcelas.length > 0 || (despesaPrincipal.total_parcelas && despesaPrincipal.total_parcelas > 1);
const isExpanded = expandedItems.has(chaveGrupo);
return (
<Card key={chaveGrupo} className="w-full">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<CreditCard className="h-5 w-5 text-blue-600" />
<div>
<CardTitle className="text-lg">{despesaPrincipal.descricao}</CardTitle>
<p className="text-sm text-muted-foreground">
{new Date(despesaPrincipal.data_despesa).toLocaleDateString('pt-BR')}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
{getStatusBadge(despesaPrincipal.status_conciliacao || 'pendente')}
<div className="text-right">
<p className="text-lg font-semibold">
{formatCurrency(despesaPrincipal.valor)}
</p>
{temParcelas && (
<p className="text-sm text-muted-foreground">
{despesaPrincipal.parcela_atual}/{despesaPrincipal.total_parcelas} parcelas
</p>
)}
</div>
</div>
</div>
<div className="flex items-center justify-between mt-3">
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onEditDespesa(despesaPrincipal)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDeleteDespesa(despesaPrincipal.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onToggleConciliacao(
despesaPrincipal.id,
despesaPrincipal.status_conciliacao === 'conciliado' ? 'pendente' : 'conciliado'
)}
>
{despesaPrincipal.status_conciliacao === 'conciliado' ? (
<X className="h-4 w-4" />
) : (
<Check className="h-4 w-4" />
)}
</Button>
</div>
{temParcelas && (
<Button
variant="ghost"
size="sm"
onClick={() => toggleExpanded(chaveGrupo)}
>
{isExpanded ? 'Ocultar' : 'Ver'} Parcelas
</Button>
)}
</div>
</CardHeader>
{isExpanded && temParcelas && (
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Parcela</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Fatura</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[despesaPrincipal, ...parcelas].map((parcela, index) => (
<TableRow key={parcela.id}>
<TableCell>
{parcela.parcela_atual || index + 1}/{parcela.total_parcelas}
</TableCell>
<TableCell>{formatCurrency(parcela.valor)}</TableCell>
<TableCell>
{parcela.mes_fatura && parcela.ano_fatura
? `${parcela.mes_fatura.toString().padStart(2, '0')}/${parcela.ano_fatura}`
: 'N/A'
}
</TableCell>
<TableCell>
{getStatusBadge(parcela.status_conciliacao || 'pendente')}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
)}
</Card>
);
})}
{despesas.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nenhuma despesa encontrada para esta fatura.
</div>
)}
</div>
);
}

View File

@ -0,0 +1,126 @@
import { useState, useEffect } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react';
import { FaturaCartao } from '@/types/cartaoTypes';
interface FiltroFaturasProps {
faturas: FaturaCartao[];
faturaAtual: { mes: number; ano: number } | null;
onFaturaChange: (mes: number, ano: number) => void;
onCriarFatura: (mes: number, ano: number) => void;
}
const meses = [
'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'
];
export function FiltroFaturas({ faturas, faturaAtual, onFaturaChange, onCriarFatura }: FiltroFaturasProps) {
const [mesSelecionado, setMesSelecionado] = useState<number>(faturaAtual?.mes || new Date().getMonth() + 1);
const [anoSelecionado, setAnoSelecionado] = useState<number>(faturaAtual?.ano || new Date().getFullYear());
const faturaExiste = faturas.some(f => f.mes === mesSelecionado && f.ano === anoSelecionado);
const navegarMes = (direcao: 'anterior' | 'proximo') => {
if (direcao === 'anterior') {
if (mesSelecionado === 1) {
setMesSelecionado(12);
setAnoSelecionado(anoSelecionado - 1);
} else {
setMesSelecionado(mesSelecionado - 1);
}
} else {
if (mesSelecionado === 12) {
setMesSelecionado(1);
setAnoSelecionado(anoSelecionado + 1);
} else {
setMesSelecionado(mesSelecionado + 1);
}
}
};
useEffect(() => {
onFaturaChange(mesSelecionado, anoSelecionado);
}, [mesSelecionado, anoSelecionado, onFaturaChange]);
const gerarAnos = () => {
const anoAtual = new Date().getFullYear();
const anos = [];
for (let ano = anoAtual - 2; ano <= anoAtual + 2; ano++) {
anos.push(ano);
}
return anos;
};
return (
<div className="flex items-center justify-between bg-card p-4 rounded-lg border">
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="icon"
onClick={() => navegarMes('anterior')}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center space-x-2">
<Select value={mesSelecionado.toString()} onValueChange={(value) => setMesSelecionado(parseInt(value))}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{meses.map((mes, index) => (
<SelectItem key={index + 1} value={(index + 1).toString()}>
{mes}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={anoSelecionado.toString()} onValueChange={(value) => setAnoSelecionado(parseInt(value))}>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
{gerarAnos().map((ano) => (
<SelectItem key={ano} value={ano.toString()}>
{ano}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
variant="outline"
size="icon"
onClick={() => navegarMes('proximo')}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
{!faturaExiste && (
<Button
variant="outline"
size="sm"
onClick={() => onCriarFatura(mesSelecionado, anoSelecionado)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Criar Fatura
</Button>
)}
<div className={`px-2 py-1 rounded text-sm ${
faturaExiste ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'
}`}>
{faturaExiste ? 'Fatura Existente' : 'Fatura Não Criada'}
</div>
</div>
</div>
);
}

118
src/hooks/useFaturas.ts Normal file
View File

@ -0,0 +1,118 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { FaturaCartao } from '@/types/cartaoTypes';
interface UseFaturasProps {
cartaoId: string;
}
export function useFaturas({ cartaoId }: UseFaturasProps) {
const [faturas, setFaturas] = useState<FaturaCartao[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [faturaAtual, setFaturaAtual] = useState<{ mes: number; ano: number } | null>(null);
const calcularFaturaAtual = (diaVencimento: number = 10, melhorDiaCompra: number = 5) => {
const hoje = new Date();
const diaAtual = hoje.getDate();
const mesAtual = hoje.getMonth() + 1;
const anoAtual = hoje.getFullYear();
// Se estamos antes do dia de vencimento, a fatura atual é do mês atual
// Se estamos depois do dia de vencimento, a fatura atual é do próximo mês
if (diaAtual <= diaVencimento) {
return { mes: mesAtual, ano: anoAtual };
} else {
const proximoMes = mesAtual === 12 ? 1 : mesAtual + 1;
const proximoAno = mesAtual === 12 ? anoAtual + 1 : anoAtual;
return { mes: proximoMes, ano: proximoAno };
}
};
const loadFaturas = async () => {
setIsLoading(true);
try {
const userEmail = localStorage.getItem('userEmail');
if (!userEmail) return;
const { data, error } = await supabase
.from('faturas_cartao')
.select('*')
.eq('cartao_id', cartaoId)
.eq('login', userEmail.trim().toLowerCase())
.order('ano', { ascending: false })
.order('mes', { ascending: false });
if (error) throw error;
setFaturas(data || []);
} catch (error) {
console.error('Erro ao carregar faturas:', error);
} finally {
setIsLoading(false);
}
};
const criarFatura = async (mes: number, ano: number, diaVencimento: number = 10) => {
try {
const userEmail = localStorage.getItem('userEmail');
if (!userEmail) return null;
const dataVencimento = new Date(ano, mes - 1, diaVencimento);
const { data, error } = await supabase
.from('faturas_cartao')
.insert({
cartao_id: cartaoId,
mes,
ano,
data_vencimento: dataVencimento.toISOString().split('T')[0],
login: userEmail.trim().toLowerCase(),
valor_total: 0
})
.select()
.single();
if (error) throw error;
await loadFaturas();
return data;
} catch (error) {
console.error('Erro ao criar fatura:', error);
return null;
}
};
const atualizarStatusPagamento = async (faturaId: string, status: 'pendente' | 'pago' | 'vencido', dataPagamento?: string) => {
try {
const { error } = await supabase
.from('faturas_cartao')
.update({
status_pagamento: status,
data_pagamento: dataPagamento || null
})
.eq('id', faturaId);
if (error) throw error;
await loadFaturas();
} catch (error) {
console.error('Erro ao atualizar status da fatura:', error);
}
};
useEffect(() => {
if (cartaoId) {
loadFaturas();
}
}, [cartaoId]);
return {
faturas,
isLoading,
faturaAtual,
calcularFaturaAtual,
setFaturaAtual,
loadFaturas,
criarFatura,
atualizarStatusPagamento
};
}

View File

@ -101,23 +101,38 @@ export type Database = {
}
cartoes_credito: {
Row: {
banco: string | null
bandeira: string | null
created_at: string
dia_vencimento: number | null
id: string
limite_total: number | null
login: string | null
melhor_dia_compra: number | null
nome: string
user_id: string
}
Insert: {
banco?: string | null
bandeira?: string | null
created_at?: string
dia_vencimento?: number | null
id?: string
limite_total?: number | null
login?: string | null
melhor_dia_compra?: number | null
nome: string
user_id: string
}
Update: {
banco?: string | null
bandeira?: string | null
created_at?: string
dia_vencimento?: number | null
id?: string
limite_total?: number | null
login?: string | null
melhor_dia_compra?: number | null
nome?: string
user_id?: string
}
@ -179,34 +194,58 @@ export type Database = {
}
despesas_cartao: {
Row: {
ano_fatura: number | null
cartao_id: string | null
created_at: string
data_despesa: string
descricao: string
despesa_pai_id: string | null
id: string
login: string | null
mes_fatura: number | null
nome: string | null
observacoes: string | null
parcela_atual: number | null
status_conciliacao: string | null
total_parcelas: number | null
valor: number
valor_original: number | null
}
Insert: {
ano_fatura?: number | null
cartao_id?: string | null
created_at?: string
data_despesa: string
descricao: string
despesa_pai_id?: string | null
id?: string
login?: string | null
mes_fatura?: number | null
nome?: string | null
observacoes?: string | null
parcela_atual?: number | null
status_conciliacao?: string | null
total_parcelas?: number | null
valor: number
valor_original?: number | null
}
Update: {
ano_fatura?: number | null
cartao_id?: string | null
created_at?: string
data_despesa?: string
descricao?: string
despesa_pai_id?: string | null
id?: string
login?: string | null
mes_fatura?: number | null
nome?: string | null
observacoes?: string | null
parcela_atual?: number | null
status_conciliacao?: string | null
total_parcelas?: number | null
valor?: number
valor_original?: number | null
}
Relationships: [
{
@ -216,6 +255,60 @@ export type Database = {
referencedRelation: "cartoes_credito"
referencedColumns: ["id"]
},
{
foreignKeyName: "despesas_cartao_despesa_pai_id_fkey"
columns: ["despesa_pai_id"]
isOneToOne: false
referencedRelation: "despesas_cartao"
referencedColumns: ["id"]
},
]
}
faturas_cartao: {
Row: {
ano: number
cartao_id: string | null
created_at: string | null
data_pagamento: string | null
data_vencimento: string | null
id: string
login: string | null
mes: number
status_pagamento: string | null
valor_total: number | null
}
Insert: {
ano: number
cartao_id?: string | null
created_at?: string | null
data_pagamento?: string | null
data_vencimento?: string | null
id?: string
login?: string | null
mes: number
status_pagamento?: string | null
valor_total?: number | null
}
Update: {
ano?: number
cartao_id?: string | null
created_at?: string | null
data_pagamento?: string | null
data_vencimento?: string | null
id?: string
login?: string | null
mes?: number
status_pagamento?: string | null
valor_total?: number | null
}
Relationships: [
{
foreignKeyName: "faturas_cartao_cartao_id_fkey"
columns: ["cartao_id"]
isOneToOne: false
referencedRelation: "cartoes_credito"
referencedColumns: ["id"]
},
]
}
grupos_whatsapp: {

View File

@ -5,7 +5,7 @@ import { CartaoCredito, DespesaCartao } from '@/types/cartaoTypes';
import { getCartoes, getDespesasCartao, getTotalDespesasCartao } from '@/services/cartaoCreditoService';
import { useToast } from "@/components/ui/use-toast";
import { CartaoActions } from '@/components/credito/CartaoActions';
import { CartaoDetalhes } from '@/components/credito/CartaoDetalhes';
import { CartaoDetalhesAvancado } from '@/components/credito/CartaoDetalhesAvancado';
import { CartaoListView } from '@/components/credito/CartaoListView';
const CartoesCreditoPage = () => {
@ -21,7 +21,6 @@ const CartoesCreditoPage = () => {
setIsLoading(true);
const data = await getCartoes();
// Load the totals for each card
const cartoesWithTotals = await Promise.all(
data.map(async (cartao) => {
const total_despesas = await getTotalDespesasCartao(cartao.id);
@ -43,18 +42,12 @@ const CartoesCreditoPage = () => {
}
};
useEffect(() => {
loadCartoes();
}, []);
const loadDespesasCartao = async (cartaoId: string) => {
try {
setIsLoading(true);
// This function now uses login+nome matching internally instead of cartao_id
const despesasData = await getDespesasCartao(cartaoId);
setDespesas(despesasData);
// Also refresh the total for the selected card
if (cartaoSelecionado) {
const totalDespesas = await getTotalDespesasCartao(cartaoId);
console.log(`Total atualizado para cartão ${cartaoSelecionado.nome}: ${totalDespesas}`);
@ -84,10 +77,8 @@ const CartoesCreditoPage = () => {
};
const handleDespesaSuccess = () => {
// 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({
@ -97,7 +88,6 @@ const CartoesCreditoPage = () => {
};
const handleCartaoClick = async (cartao: CartaoCredito) => {
// Refresh the total first
const totalDespesas = await getTotalDespesasCartao(cartao.id);
const updatedCartao = { ...cartao, total_despesas: totalDespesas };
@ -112,6 +102,10 @@ const CartoesCreditoPage = () => {
setDespesas([]);
};
useEffect(() => {
loadCartoes();
}, []);
return (
<Layout>
<div className="space-y-6">
@ -130,10 +124,11 @@ const CartoesCreditoPage = () => {
onCartaoClick={handleCartaoClick}
/>
) : cartaoSelecionado && (
<CartaoDetalhes
<CartaoDetalhesAvancado
cartao={cartaoSelecionado}
despesas={despesas}
isLoading={isLoading}
onDespesaSuccess={handleDespesaSuccess}
/>
)}
</div>

View File

@ -8,6 +8,9 @@ export interface CartaoCredito {
user_id: string;
login?: string;
created_at: string;
limite_total?: number;
dia_vencimento?: number;
melhor_dia_compra?: number;
total_despesas?: number;
}
@ -21,4 +24,25 @@ export interface DespesaCartao {
data_despesa: string;
descricao: string;
created_at: string;
parcela_atual?: number;
total_parcelas?: number;
valor_original?: number;
despesa_pai_id?: string;
status_conciliacao?: 'pendente' | 'conciliado' | 'divergente';
mes_fatura?: number;
ano_fatura?: number;
observacoes?: string;
}
export interface FaturaCartao {
id: string;
cartao_id: string;
mes: number;
ano: number;
valor_total: number;
data_vencimento?: string;
status_pagamento: 'pendente' | 'pago' | 'vencido';
data_pagamento?: string;
login?: string;
created_at: string;
}

20
src/utils/formatters.ts Normal file
View File

@ -0,0 +1,20 @@
export const formatCurrency = (value: number): string => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
};
export const formatDate = (date: string | Date): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return dateObj.toLocaleDateString('pt-BR');
};
export const formatPercent = (value: number): string => {
return new Intl.NumberFormat('pt-BR', {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
}).format(value / 100);
};

View File

@ -0,0 +1,39 @@
-- Adicionar campos à tabela de cartões de crédito
ALTER TABLE cartoes_credito
ADD COLUMN IF NOT EXISTS limite_total DECIMAL(10,2),
ADD COLUMN IF NOT EXISTS dia_vencimento INTEGER DEFAULT 10,
ADD COLUMN IF NOT EXISTS melhor_dia_compra INTEGER DEFAULT 5,
ADD COLUMN IF NOT EXISTS bandeira TEXT,
ADD COLUMN IF NOT EXISTS banco TEXT;
-- Criar tabela para faturas dos cartões
CREATE TABLE IF NOT EXISTS faturas_cartao (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
cartao_id UUID REFERENCES cartoes_credito(id) ON DELETE CASCADE,
mes INTEGER NOT NULL,
ano INTEGER NOT NULL,
valor_total DECIMAL(10,2) DEFAULT 0,
data_vencimento DATE,
status_pagamento TEXT DEFAULT 'pendente' CHECK (status_pagamento IN ('pendente', 'pago', 'vencido')),
data_pagamento DATE,
login TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
UNIQUE(cartao_id, mes, ano)
);
-- Atualizar tabela de despesas para incluir parcelas
ALTER TABLE despesas_cartao
ADD COLUMN IF NOT EXISTS parcela_atual INTEGER DEFAULT 1,
ADD COLUMN IF NOT EXISTS total_parcelas INTEGER DEFAULT 1,
ADD COLUMN IF NOT EXISTS valor_original DECIMAL(10,2),
ADD COLUMN IF NOT EXISTS despesa_pai_id UUID REFERENCES despesas_cartao(id),
ADD COLUMN IF NOT EXISTS status_conciliacao TEXT DEFAULT 'pendente' CHECK (status_conciliacao IN ('pendente', 'conciliado', 'divergente')),
ADD COLUMN IF NOT EXISTS mes_fatura INTEGER,
ADD COLUMN IF NOT EXISTS ano_fatura INTEGER,
ADD COLUMN IF NOT EXISTS observacoes TEXT;
-- Criar índices para melhor performance
CREATE INDEX IF NOT EXISTS idx_faturas_cartao_mes_ano ON faturas_cartao(cartao_id, mes, ano);
CREATE INDEX IF NOT EXISTS idx_despesas_cartao_fatura ON despesas_cartao(cartao_id, mes_fatura, ano_fatura);
CREATE INDEX IF NOT EXISTS idx_despesas_cartao_pai ON despesas_cartao(despesa_pai_id);