diff --git a/src/components/credito/CartaoCreditoForm.tsx b/src/components/credito/CartaoCreditoForm.tsx index 0b185e3..e1f412b 100644 --- a/src/components/credito/CartaoCreditoForm.tsx +++ b/src/components/credito/CartaoCreditoForm.tsx @@ -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; @@ -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 )} /> + +
+ ( + + Limite Total (Opcional) + + + + + + )} + /> + + ( + + Dia do Vencimento + + + + + + )} + /> + + ( + + Melhor Dia de Compra + + + + + + )} + /> +
+
+ + + +
+
+

Data de Vencimento

+

{faturaSelecionada.data_vencimento ? new Date(faturaSelecionada.data_vencimento).toLocaleDateString('pt-BR') : 'N/A'}

+
+ {faturaSelecionada.data_pagamento && ( +
+

Data de Pagamento

+

{new Date(faturaSelecionada.data_pagamento).toLocaleDateString('pt-BR')}

+
+ )} +
+
+ + )} + + {/* Despesas */} + + + Despesas da Fatura + + + + + + + ); +} diff --git a/src/components/credito/DespesasComParcelas.tsx b/src/components/credito/DespesasComParcelas.tsx new file mode 100644 index 0000000..5ef83e5 --- /dev/null +++ b/src/components/credito/DespesasComParcelas.tsx @@ -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>(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 ( + + {status} + + ); + }; + + // 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); + + return ( +
+ {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 ( + + +
+
+ +
+ {despesaPrincipal.descricao} +

+ {new Date(despesaPrincipal.data_despesa).toLocaleDateString('pt-BR')} +

+
+
+ +
+ {getStatusBadge(despesaPrincipal.status_conciliacao || 'pendente')} + +
+

+ {formatCurrency(despesaPrincipal.valor)} +

+ {temParcelas && ( +

+ {despesaPrincipal.parcela_atual}/{despesaPrincipal.total_parcelas} parcelas +

+ )} +
+
+
+ +
+
+ + + + + +
+ + {temParcelas && ( + + )} +
+
+ + {isExpanded && temParcelas && ( + + + + + Parcela + Valor + Fatura + Status + + + + {[despesaPrincipal, ...parcelas].map((parcela, index) => ( + + + {parcela.parcela_atual || index + 1}/{parcela.total_parcelas} + + {formatCurrency(parcela.valor)} + + {parcela.mes_fatura && parcela.ano_fatura + ? `${parcela.mes_fatura.toString().padStart(2, '0')}/${parcela.ano_fatura}` + : 'N/A' + } + + + {getStatusBadge(parcela.status_conciliacao || 'pendente')} + + + ))} + +
+
+ )} +
+ ); + })} + + {despesas.length === 0 && ( +
+ Nenhuma despesa encontrada para esta fatura. +
+ )} +
+ ); +} diff --git a/src/components/credito/FiltroFaturas.tsx b/src/components/credito/FiltroFaturas.tsx new file mode 100644 index 0000000..e99a99f --- /dev/null +++ b/src/components/credito/FiltroFaturas.tsx @@ -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(faturaAtual?.mes || new Date().getMonth() + 1); + const [anoSelecionado, setAnoSelecionado] = useState(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 ( +
+
+ + +
+ + + +
+ + +
+ +
+ {!faturaExiste && ( + + )} + +
+ {faturaExiste ? 'Fatura Existente' : 'Fatura Não Criada'} +
+
+
+ ); +} diff --git a/src/hooks/useFaturas.ts b/src/hooks/useFaturas.ts new file mode 100644 index 0000000..5b8ee87 --- /dev/null +++ b/src/hooks/useFaturas.ts @@ -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([]); + 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 + }; +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 8638603..b535cd7 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -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: { diff --git a/src/pages/CartoesCredito.tsx b/src/pages/CartoesCredito.tsx index 02bddeb..e13bb30 100644 --- a/src/pages/CartoesCredito.tsx +++ b/src/pages/CartoesCredito.tsx @@ -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 (
@@ -130,10 +124,11 @@ const CartoesCreditoPage = () => { onCartaoClick={handleCartaoClick} /> ) : cartaoSelecionado && ( - )}
diff --git a/src/types/cartaoTypes.ts b/src/types/cartaoTypes.ts index fafd16a..aa51e8e 100644 --- a/src/types/cartaoTypes.ts +++ b/src/types/cartaoTypes.ts @@ -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; } diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts new file mode 100644 index 0000000..b3c5d5c --- /dev/null +++ b/src/utils/formatters.ts @@ -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); +}; diff --git a/supabase/migrations/20250611003041-ef7d2ea4-296c-4f02-841f-2d716d8e911a.sql b/supabase/migrations/20250611003041-ef7d2ea4-296c-4f02-841f-2d716d8e911a.sql new file mode 100644 index 0000000..c8df2f2 --- /dev/null +++ b/supabase/migrations/20250611003041-ef7d2ea4-296c-4f02-841f-2d716d8e911a.sql @@ -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);