Run SQL to create credit card tables.
This commit is contained in:
parent
cb3f41b8ff
commit
47e003213d
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { useState, useEffect } 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';
|
||||||
@ -22,7 +23,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
// Opções para as listas suspensas
|
|
||||||
const bandeirasCartao = [
|
const bandeirasCartao = [
|
||||||
'Visa', 'Mastercard', 'Elo', 'American Express', 'Hipercard',
|
'Visa', 'Mastercard', 'Elo', 'American Express', 'Hipercard',
|
||||||
'Diners Club', 'Credicard', 'Sorocred', 'Cabal', 'Banescard',
|
'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' }),
|
nome: z.string().min(1, { message: 'Nome do cartão é obrigatório' }),
|
||||||
bandeira: z.string({ required_error: 'Selecione uma bandeira' }),
|
bandeira: z.string({ required_error: 'Selecione uma bandeira' }),
|
||||||
banco: z.string({ required_error: 'Selecione um banco' }),
|
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>;
|
type CartaoFormValues = z.infer<typeof cartaoSchema>;
|
||||||
@ -58,6 +61,9 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
|
|||||||
nome: '',
|
nome: '',
|
||||||
bandeira: '',
|
bandeira: '',
|
||||||
banco: '',
|
banco: '',
|
||||||
|
limite_total: '',
|
||||||
|
dia_vencimento: '10',
|
||||||
|
melhor_dia_compra: '5',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,7 +71,6 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Removed the fourth argument that was causing the error
|
|
||||||
const resultado = await criarCartao(
|
const resultado = await criarCartao(
|
||||||
data.nome,
|
data.nome,
|
||||||
data.bandeira,
|
data.bandeira,
|
||||||
@ -73,6 +78,23 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (resultado) {
|
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({
|
toast({
|
||||||
title: "Cartão adicionado",
|
title: "Cartão adicionado",
|
||||||
description: "Cartão de crédito cadastrado com sucesso",
|
description: "Cartão de crédito cadastrado com sucesso",
|
||||||
@ -163,6 +185,67 @@ export function CartaoCreditoForm({ onSuccess, onCancel }: CartaoCreditoFormProp
|
|||||||
</FormItem>
|
</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">
|
<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}>
|
||||||
|
|||||||
281
src/components/credito/CartaoDetalhesAvancado.tsx
Normal file
281
src/components/credito/CartaoDetalhesAvancado.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
src/components/credito/DespesasComParcelas.tsx
Normal file
188
src/components/credito/DespesasComParcelas.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
src/components/credito/FiltroFaturas.tsx
Normal file
126
src/components/credito/FiltroFaturas.tsx
Normal 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
118
src/hooks/useFaturas.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -101,23 +101,38 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
cartoes_credito: {
|
cartoes_credito: {
|
||||||
Row: {
|
Row: {
|
||||||
|
banco: string | null
|
||||||
|
bandeira: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
|
dia_vencimento: number | null
|
||||||
id: string
|
id: string
|
||||||
|
limite_total: number | null
|
||||||
login: string | null
|
login: string | null
|
||||||
|
melhor_dia_compra: number | null
|
||||||
nome: string
|
nome: string
|
||||||
user_id: string
|
user_id: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
banco?: string | null
|
||||||
|
bandeira?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
dia_vencimento?: number | null
|
||||||
id?: string
|
id?: string
|
||||||
|
limite_total?: number | null
|
||||||
login?: string | null
|
login?: string | null
|
||||||
|
melhor_dia_compra?: number | null
|
||||||
nome: string
|
nome: string
|
||||||
user_id: string
|
user_id: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
banco?: string | null
|
||||||
|
bandeira?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
dia_vencimento?: number | null
|
||||||
id?: string
|
id?: string
|
||||||
|
limite_total?: number | null
|
||||||
login?: string | null
|
login?: string | null
|
||||||
|
melhor_dia_compra?: number | null
|
||||||
nome?: string
|
nome?: string
|
||||||
user_id?: string
|
user_id?: string
|
||||||
}
|
}
|
||||||
@ -179,34 +194,58 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
despesas_cartao: {
|
despesas_cartao: {
|
||||||
Row: {
|
Row: {
|
||||||
|
ano_fatura: number | null
|
||||||
cartao_id: string | null
|
cartao_id: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
data_despesa: string
|
data_despesa: string
|
||||||
descricao: string
|
descricao: string
|
||||||
|
despesa_pai_id: string | null
|
||||||
id: string
|
id: string
|
||||||
login: string | null
|
login: string | null
|
||||||
|
mes_fatura: number | null
|
||||||
nome: string | null
|
nome: string | null
|
||||||
|
observacoes: string | null
|
||||||
|
parcela_atual: number | null
|
||||||
|
status_conciliacao: string | null
|
||||||
|
total_parcelas: number | null
|
||||||
valor: number
|
valor: number
|
||||||
|
valor_original: number | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
ano_fatura?: number | null
|
||||||
cartao_id?: string | null
|
cartao_id?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
data_despesa: string
|
data_despesa: string
|
||||||
descricao: string
|
descricao: string
|
||||||
|
despesa_pai_id?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
login?: string | null
|
login?: string | null
|
||||||
|
mes_fatura?: number | null
|
||||||
nome?: string | null
|
nome?: string | null
|
||||||
|
observacoes?: string | null
|
||||||
|
parcela_atual?: number | null
|
||||||
|
status_conciliacao?: string | null
|
||||||
|
total_parcelas?: number | null
|
||||||
valor: number
|
valor: number
|
||||||
|
valor_original?: number | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
ano_fatura?: number | null
|
||||||
cartao_id?: string | null
|
cartao_id?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
data_despesa?: string
|
data_despesa?: string
|
||||||
descricao?: string
|
descricao?: string
|
||||||
|
despesa_pai_id?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
login?: string | null
|
login?: string | null
|
||||||
|
mes_fatura?: number | null
|
||||||
nome?: string | null
|
nome?: string | null
|
||||||
|
observacoes?: string | null
|
||||||
|
parcela_atual?: number | null
|
||||||
|
status_conciliacao?: string | null
|
||||||
|
total_parcelas?: number | null
|
||||||
valor?: number
|
valor?: number
|
||||||
|
valor_original?: number | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
@ -216,6 +255,60 @@ export type Database = {
|
|||||||
referencedRelation: "cartoes_credito"
|
referencedRelation: "cartoes_credito"
|
||||||
referencedColumns: ["id"]
|
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: {
|
grupos_whatsapp: {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { CartaoCredito, DespesaCartao } from '@/types/cartaoTypes';
|
|||||||
import { getCartoes, getDespesasCartao, getTotalDespesasCartao } from '@/services/cartaoCreditoService';
|
import { getCartoes, getDespesasCartao, getTotalDespesasCartao } from '@/services/cartaoCreditoService';
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { CartaoActions } from '@/components/credito/CartaoActions';
|
import { CartaoActions } from '@/components/credito/CartaoActions';
|
||||||
import { CartaoDetalhes } from '@/components/credito/CartaoDetalhes';
|
import { CartaoDetalhesAvancado } from '@/components/credito/CartaoDetalhesAvancado';
|
||||||
import { CartaoListView } from '@/components/credito/CartaoListView';
|
import { CartaoListView } from '@/components/credito/CartaoListView';
|
||||||
|
|
||||||
const CartoesCreditoPage = () => {
|
const CartoesCreditoPage = () => {
|
||||||
@ -21,7 +21,6 @@ const CartoesCreditoPage = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await getCartoes();
|
const data = await getCartoes();
|
||||||
|
|
||||||
// Load the totals for each card
|
|
||||||
const cartoesWithTotals = await Promise.all(
|
const cartoesWithTotals = await Promise.all(
|
||||||
data.map(async (cartao) => {
|
data.map(async (cartao) => {
|
||||||
const total_despesas = await getTotalDespesasCartao(cartao.id);
|
const total_despesas = await getTotalDespesasCartao(cartao.id);
|
||||||
@ -43,18 +42,12 @@ const CartoesCreditoPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadCartoes();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadDespesasCartao = async (cartaoId: string) => {
|
const loadDespesasCartao = async (cartaoId: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// This function now uses login+nome matching internally instead of cartao_id
|
|
||||||
const despesasData = await getDespesasCartao(cartaoId);
|
const despesasData = await getDespesasCartao(cartaoId);
|
||||||
setDespesas(despesasData);
|
setDespesas(despesasData);
|
||||||
|
|
||||||
// Also refresh the total for the selected card
|
|
||||||
if (cartaoSelecionado) {
|
if (cartaoSelecionado) {
|
||||||
const totalDespesas = await getTotalDespesasCartao(cartaoId);
|
const totalDespesas = await getTotalDespesasCartao(cartaoId);
|
||||||
console.log(`Total atualizado para cartão ${cartaoSelecionado.nome}: ${totalDespesas}`);
|
console.log(`Total atualizado para cartão ${cartaoSelecionado.nome}: ${totalDespesas}`);
|
||||||
@ -84,10 +77,8 @@ const CartoesCreditoPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDespesaSuccess = () => {
|
const handleDespesaSuccess = () => {
|
||||||
// Recarregar detalhes do cartão após adicionar despesa
|
|
||||||
if (cartaoSelecionado) {
|
if (cartaoSelecionado) {
|
||||||
loadDespesasCartao(cartaoSelecionado.id);
|
loadDespesasCartao(cartaoSelecionado.id);
|
||||||
// Recarregar também a lista de cartões para atualizar os totais
|
|
||||||
loadCartoes();
|
loadCartoes();
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
@ -97,7 +88,6 @@ const CartoesCreditoPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCartaoClick = async (cartao: CartaoCredito) => {
|
const handleCartaoClick = async (cartao: CartaoCredito) => {
|
||||||
// Refresh the total first
|
|
||||||
const totalDespesas = await getTotalDespesasCartao(cartao.id);
|
const totalDespesas = await getTotalDespesasCartao(cartao.id);
|
||||||
const updatedCartao = { ...cartao, total_despesas: totalDespesas };
|
const updatedCartao = { ...cartao, total_despesas: totalDespesas };
|
||||||
|
|
||||||
@ -112,6 +102,10 @@ const CartoesCreditoPage = () => {
|
|||||||
setDespesas([]);
|
setDespesas([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCartoes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -130,10 +124,11 @@ const CartoesCreditoPage = () => {
|
|||||||
onCartaoClick={handleCartaoClick}
|
onCartaoClick={handleCartaoClick}
|
||||||
/>
|
/>
|
||||||
) : cartaoSelecionado && (
|
) : cartaoSelecionado && (
|
||||||
<CartaoDetalhes
|
<CartaoDetalhesAvancado
|
||||||
cartao={cartaoSelecionado}
|
cartao={cartaoSelecionado}
|
||||||
despesas={despesas}
|
despesas={despesas}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
onDespesaSuccess={handleDespesaSuccess}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,9 @@ export interface CartaoCredito {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
login?: string;
|
login?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
limite_total?: number;
|
||||||
|
dia_vencimento?: number;
|
||||||
|
melhor_dia_compra?: number;
|
||||||
total_despesas?: number;
|
total_despesas?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,4 +24,25 @@ export interface DespesaCartao {
|
|||||||
data_despesa: string;
|
data_despesa: string;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
created_at: 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
20
src/utils/formatters.ts
Normal 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);
|
||||||
|
};
|
||||||
@ -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);
|
||||||
Loading…
Reference in New Issue
Block a user