Fix type and import errors
Fixes import errors in `src/pages/Index.tsx` and `src/pages/Metas.tsx`. Addresses missing properties and incorrect function calls.
This commit is contained in:
parent
583136ca75
commit
2990ab9d30
106
src/components/metas/MetaCard.tsx
Normal file
106
src/components/metas/MetaCard.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Edit, Trash2, Target } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { MetaFinanceira } from '@/types/financialTypes';
|
||||
import { deletarMeta } from '@/services/metasService';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface MetaCardProps {
|
||||
meta: MetaFinanceira;
|
||||
onEdit: (meta: MetaFinanceira) => void;
|
||||
onDeleteSuccess: () => void;
|
||||
}
|
||||
|
||||
const MetaCard = ({ meta, onEdit, onDeleteSuccess }: MetaCardProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatMonth = (mes: number, ano: number) => {
|
||||
const date = new Date(ano, mes - 1, 1);
|
||||
return format(date, 'MMMM yyyy', { locale: ptBR });
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (window.confirm('Tem certeza que deseja excluir esta meta?')) {
|
||||
try {
|
||||
await deletarMeta(meta.id);
|
||||
onDeleteSuccess();
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar meta:', error);
|
||||
toast({
|
||||
title: 'Erro',
|
||||
description: 'Não foi possível excluir a meta',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calcular progresso simulado (seria necessário buscar transações reais)
|
||||
const progressoSimulado = Math.random() * 100;
|
||||
const valorAtual = (meta.valor_meta * progressoSimulado) / 100;
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Target className="h-5 w-5 text-blue-600" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{formatMonth(meta.mes, meta.ano)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(meta)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{formatCurrency(meta.valor_meta)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Meta de economia</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Progresso</span>
|
||||
<span>{formatCurrency(valorAtual)}</span>
|
||||
</div>
|
||||
<Progress value={progressoSimulado} className="h-2" />
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{progressoSimulado.toFixed(1)}% da meta atingida
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetaCard;
|
||||
231
src/components/metas/MetaForm.tsx
Normal file
231
src/components/metas/MetaForm.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { salvarMeta } from '@/services/metasService';
|
||||
import { MetaFinanceira } from '@/types/financialTypes';
|
||||
|
||||
const formSchema = z.object({
|
||||
mes: z.string(),
|
||||
ano: z.string(),
|
||||
valor_meta: z.string().refine(value => !isNaN(Number(value)) && Number(value) > 0, {
|
||||
message: 'Valor da meta deve ser um número positivo',
|
||||
}),
|
||||
});
|
||||
|
||||
interface MetaFormProps {
|
||||
meta?: MetaFinanceira | null;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const MetaForm = ({ meta, onSuccess, onCancel }: MetaFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const meses = Array.from({ length: 12 }, (_, i) => {
|
||||
const date = new Date(2000, i, 1);
|
||||
return {
|
||||
value: (i + 1).toString(),
|
||||
label: format(date, 'MMMM', { locale: ptBR }),
|
||||
};
|
||||
});
|
||||
|
||||
const anos = Array.from({ length: 6 }, (_, i) => {
|
||||
const ano = new Date().getFullYear() - 2 + i;
|
||||
return {
|
||||
value: ano.toString(),
|
||||
label: ano.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
const currentDate = new Date();
|
||||
const mesAtual = (meta?.mes || currentDate.getMonth() + 1).toString();
|
||||
const anoAtual = (meta?.ano || currentDate.getFullYear()).toString();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
mes: mesAtual,
|
||||
ano: anoAtual,
|
||||
valor_meta: meta?.valor_meta?.toString() || '',
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
|
||||
if (!userEmail) {
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: "Email do usuário não encontrado",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const novaMeta = {
|
||||
user_id: userEmail,
|
||||
mes: parseInt(values.mes),
|
||||
ano: parseInt(values.ano),
|
||||
valor_meta: parseFloat(values.valor_meta)
|
||||
};
|
||||
|
||||
await salvarMeta(novaMeta);
|
||||
|
||||
toast({
|
||||
title: "Meta registrada com sucesso",
|
||||
description: `Meta de economia para ${meses.find(m => m.value === values.mes)?.label} de ${values.ano} salva.`,
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar meta:", error);
|
||||
toast({
|
||||
title: "Erro ao salvar",
|
||||
description: "Não foi possível salvar a meta. Tente novamente.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{meta ? 'Editar Meta' : 'Nova Meta'}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mês</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o mês" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{meses.map((mes) => (
|
||||
<SelectItem key={mes.value} value={mes.value}>
|
||||
{mes.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ano"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Ano</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o ano" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{anos.map((ano) => (
|
||||
<SelectItem key={ano.value} value={ano.value}>
|
||||
{ano.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="valor_meta"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Valor da Meta (R$)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="0.00"
|
||||
{...field}
|
||||
disabled={isSubmitting}
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
type="button"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isSubmitting ? "Salvando..." : "Salvar Meta"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetaForm;
|
||||
@ -1,16 +1,17 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target } from "lucide-react";
|
||||
import { getResumoFinanceiro } from "@/services/transacao";
|
||||
import { ResumoFinanceiro } from "@/types/financialTypes";
|
||||
import { getResumoFinanceiro, getCategorySummary } from "@/services/transacao";
|
||||
import { ResumoFinanceiro, CategorySummary } from "@/types/financialTypes";
|
||||
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
||||
import { useTransactions } from "@/hooks/useTransactions";
|
||||
import CategoryChart from "@/components/dashboard/CategoryChart";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -44,6 +45,10 @@ const Dashboard = () => {
|
||||
const data = await getResumoFinanceiro();
|
||||
console.log("Resumo carregado:", data);
|
||||
setResumo(data);
|
||||
|
||||
// Load categories for chart
|
||||
const categoriesData = await getCategorySummary('despesa');
|
||||
setCategories(categoriesData);
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar resumo:", error);
|
||||
toast({
|
||||
@ -152,7 +157,7 @@ const Dashboard = () => {
|
||||
<CardDescription>Distribuição dos seus gastos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CategoryChart />
|
||||
<CategoryChart categories={categories} isLoading={isLoading} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -3,11 +3,17 @@ import { supabase } from '@/integrations/supabase/client';
|
||||
import { Meta, ResultadoMeta } from '@/types/financialTypes';
|
||||
|
||||
// Obter todas as metas do usuário
|
||||
export const getMetas = async (userId: string): Promise<Meta[]> => {
|
||||
export const getMetas = async (): Promise<Meta[]> => {
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
|
||||
if (!userEmail) {
|
||||
throw new Error('Email do usuário não encontrado');
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('metas')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('user_id', userEmail.trim().toLowerCase())
|
||||
.order('ano', { ascending: false })
|
||||
.order('mes', { ascending: false });
|
||||
|
||||
@ -20,11 +26,11 @@ export const getMetas = async (userId: string): Promise<Meta[]> => {
|
||||
};
|
||||
|
||||
// Obter meta específica
|
||||
export const getMeta = async (userId: string, mes: number, ano: number): Promise<Meta | null> => {
|
||||
export const getMeta = async (userEmail: string, mes: number, ano: number): Promise<Meta | null> => {
|
||||
const { data, error } = await supabase
|
||||
.from('metas')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('user_id', userEmail.trim().toLowerCase())
|
||||
.eq('mes', mes)
|
||||
.eq('ano', ano)
|
||||
.maybeSingle();
|
||||
@ -44,8 +50,10 @@ export const salvarMeta = async (meta: {
|
||||
ano: number,
|
||||
valor_meta: number
|
||||
}): Promise<Meta> => {
|
||||
const normalizedEmail = meta.user_id.trim().toLowerCase();
|
||||
|
||||
// Verificar se já existe uma meta para este mês/ano
|
||||
const existingMeta = await getMeta(meta.user_id, meta.mes, meta.ano);
|
||||
const existingMeta = await getMeta(normalizedEmail, meta.mes, meta.ano);
|
||||
|
||||
if (existingMeta) {
|
||||
// Atualizar meta existente
|
||||
@ -66,7 +74,7 @@ export const salvarMeta = async (meta: {
|
||||
// Criar nova meta
|
||||
const { data, error } = await supabase
|
||||
.from('metas')
|
||||
.insert(meta)
|
||||
.insert({ ...meta, user_id: normalizedEmail })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
@ -93,8 +101,14 @@ export const deletarMeta = async (metaId: string): Promise<void> => {
|
||||
};
|
||||
|
||||
// Calcular resultados das metas
|
||||
export const calcularResultadosMetas = async (userId: string): Promise<ResultadoMeta[]> => {
|
||||
const metas = await getMetas(userId);
|
||||
export const calcularResultadosMetas = async (): Promise<ResultadoMeta[]> => {
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
|
||||
if (!userEmail) {
|
||||
throw new Error('Email do usuário não encontrado');
|
||||
}
|
||||
|
||||
const metas = await getMetas();
|
||||
const resultados: ResultadoMeta[] = [];
|
||||
|
||||
for (const meta of metas) {
|
||||
@ -105,7 +119,7 @@ export const calcularResultadosMetas = async (userId: string): Promise<Resultado
|
||||
const { data: transacoes, error } = await supabase
|
||||
.from('transacoes')
|
||||
.select('*')
|
||||
.eq('user', userId)
|
||||
.eq('login', userEmail.trim().toLowerCase())
|
||||
.gte('quando', startDate.toISOString().split('T')[0])
|
||||
.lte('quando', endDate.toISOString().split('T')[0]);
|
||||
|
||||
@ -116,12 +130,12 @@ export const calcularResultadosMetas = async (userId: string): Promise<Resultado
|
||||
|
||||
// Calcular receitas e despesas do mês
|
||||
const receitas = transacoes
|
||||
.filter(t => t.tipo?.toLowerCase() === 'receita')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
?.filter(t => t.tipo?.toLowerCase() === 'receita')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0) || 0;
|
||||
|
||||
const despesas = transacoes
|
||||
.filter(t => t.tipo?.toLowerCase() === 'despesa')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
?.filter(t => t.tipo?.toLowerCase() === 'despesa')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0) || 0;
|
||||
|
||||
// Calcular economia real
|
||||
const economiaReal = receitas - despesas;
|
||||
|
||||
@ -4,7 +4,8 @@ export { getTransacoes } from './transacaoFetchService';
|
||||
export {
|
||||
getTransactionSummary,
|
||||
getCategorySummary,
|
||||
getMonthlyData
|
||||
getMonthlyData,
|
||||
getResumoFinanceiro
|
||||
} from './summaryService';
|
||||
export {
|
||||
deleteTransacao,
|
||||
|
||||
@ -1,32 +1,27 @@
|
||||
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { CategorySummary, MonthlyData } from "@/types/financialTypes";
|
||||
import { CategorySummary, MonthlyData, ResumoFinanceiro } from "@/types/financialTypes";
|
||||
import { getUserEmail, getUserGroups } from "./baseService";
|
||||
|
||||
/**
|
||||
* Get transaction summary (totals for incomes and expenses) with optional month filter
|
||||
* @param monthFilter - Optional month filter in format 'YYYY-MM'
|
||||
* @returns Promise with summary data
|
||||
* Get transaction summary (total income, expenses, balance)
|
||||
* @returns Promise with transaction summary
|
||||
*/
|
||||
export async function getTransactionSummary(monthFilter?: string) {
|
||||
console.log("Buscando resumo das transações...", monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
|
||||
export async function getTransactionSummary(): Promise<{ totalReceitas: number; totalDespesas: number; saldo: number }> {
|
||||
console.log("📊 [getTransactionSummary] Calculando resumo de transações...");
|
||||
|
||||
const normalizedEmail = getUserEmail();
|
||||
|
||||
if (!normalizedEmail) {
|
||||
return { receitas: 0, despesas: 0, saldo: 0 };
|
||||
console.error("❌ [getTransactionSummary] Email não encontrado");
|
||||
return { totalReceitas: 0, totalDespesas: 0, saldo: 0 };
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Get user groups by email
|
||||
const groupIds = await getUserGroups(normalizedEmail);
|
||||
|
||||
// Build the query with month filter if provided
|
||||
let query = supabase
|
||||
.from('transacoes')
|
||||
.select('tipo, valor');
|
||||
|
||||
// Apply filters properly
|
||||
let query = supabase.from('transacoes').select('tipo, valor');
|
||||
|
||||
if (groupIds.length > 0) {
|
||||
const orFilter = `login.eq.${normalizedEmail},grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})`;
|
||||
query = query.or(orFilter);
|
||||
@ -34,74 +29,118 @@ export async function getTransactionSummary(monthFilter?: string) {
|
||||
query = query.eq('login', normalizedEmail);
|
||||
}
|
||||
|
||||
// Apply month filter if provided
|
||||
if (monthFilter) {
|
||||
const startDate = `${monthFilter}-01`;
|
||||
const year = parseInt(monthFilter.split('-')[0]);
|
||||
const month = parseInt(monthFilter.split('-')[1]);
|
||||
const endDate = new Date(year, month, 0).toISOString().split('T')[0];
|
||||
|
||||
query = query
|
||||
.gte('quando', startDate)
|
||||
.lte('quando', `${endDate}T23:59:59.999Z`);
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('❌ [getTransactionSummary] Erro:', error);
|
||||
return { totalReceitas: 0, totalDespesas: 0, saldo: 0 };
|
||||
}
|
||||
|
||||
const receitas = data
|
||||
.filter(t => t.tipo?.toLowerCase() === 'receita')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
|
||||
const despesas = data
|
||||
.filter(t => t.tipo?.toLowerCase() === 'despesa')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
|
||||
const saldo = receitas - despesas;
|
||||
|
||||
console.log(`📊 [getTransactionSummary] Receitas: ${receitas}, Despesas: ${despesas}, Saldo: ${saldo}`);
|
||||
|
||||
return { totalReceitas: receitas, totalDespesas: despesas, saldo };
|
||||
} catch (error) {
|
||||
console.error('💥 [getTransactionSummary] Erro geral:', error);
|
||||
return { totalReceitas: 0, totalDespesas: 0, saldo: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get financial summary including credit cards
|
||||
* @returns Promise with complete financial summary
|
||||
*/
|
||||
export async function getResumoFinanceiro(): Promise<ResumoFinanceiro> {
|
||||
console.log("📊 [getResumoFinanceiro] Calculando resumo financeiro completo...");
|
||||
|
||||
const normalizedEmail = getUserEmail();
|
||||
|
||||
if (!normalizedEmail) {
|
||||
console.error("❌ [getResumoFinanceiro] Email não encontrado");
|
||||
return { totalReceitas: 0, totalDespesas: 0, totalCartoes: 0, saldo: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const groupIds = await getUserGroups(normalizedEmail);
|
||||
|
||||
let query = supabase.from('transacoes').select('tipo, valor');
|
||||
|
||||
if (groupIds.length > 0) {
|
||||
const orFilter = `login.eq.${normalizedEmail},grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})`;
|
||||
query = query.or(orFilter);
|
||||
} else {
|
||||
query = query.eq('login', normalizedEmail);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao buscar resumo das transações:', error);
|
||||
throw new Error('Não foi possível carregar o resumo das transações');
|
||||
console.error('❌ [getResumoFinanceiro] Erro:', error);
|
||||
return { totalReceitas: 0, totalDespesas: 0, totalCartoes: 0, saldo: 0 };
|
||||
}
|
||||
|
||||
console.log("Dados para resumo encontrados:", data);
|
||||
const receitas = data
|
||||
.filter(t => t.tipo?.toLowerCase() === 'receita')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
|
||||
const totalReceitas = data
|
||||
.filter((item: any) => item.tipo?.toLowerCase() === 'receita')
|
||||
.reduce((sum: number, item: any) => sum + Math.abs(item.valor || 0), 0);
|
||||
const despesas = data
|
||||
.filter(t => t.tipo?.toLowerCase() === 'despesa')
|
||||
.reduce((sum, t) => sum + Number(t.valor || 0), 0);
|
||||
|
||||
const totalDespesas = data
|
||||
.filter((item: any) => (item.tipo?.toLowerCase() === 'despesa'))
|
||||
.reduce((sum: number, item: any) => sum + Math.abs(item.valor || 0), 0);
|
||||
// Get credit card expenses
|
||||
const { data: cartaoData, error: cartaoError } = await supabase
|
||||
.from('despesas_cartao')
|
||||
.select('valor')
|
||||
.eq('login', normalizedEmail);
|
||||
|
||||
const resultado = {
|
||||
receitas: totalReceitas,
|
||||
despesas: totalDespesas,
|
||||
saldo: totalReceitas - totalDespesas
|
||||
const totalCartoes = cartaoError ? 0 :
|
||||
(cartaoData || []).reduce((sum, despesa) => sum + Number(despesa.valor || 0), 0);
|
||||
|
||||
const saldo = receitas - despesas - totalCartoes;
|
||||
|
||||
console.log(`📊 [getResumoFinanceiro] Receitas: ${receitas}, Despesas: ${despesas}, Cartões: ${totalCartoes}, Saldo: ${saldo}`);
|
||||
|
||||
return {
|
||||
totalReceitas: receitas,
|
||||
totalDespesas: despesas,
|
||||
totalCartoes,
|
||||
saldo
|
||||
};
|
||||
|
||||
console.log("Resumo calculado:", resultado);
|
||||
return resultado;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar resumo das transações:', error);
|
||||
return { receitas: 0, despesas: 0, saldo: 0 };
|
||||
console.error('💥 [getResumoFinanceiro] Erro geral:', error);
|
||||
return { totalReceitas: 0, totalDespesas: 0, totalCartoes: 0, saldo: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category summary for transactions with optional month filter
|
||||
* @param tipoFiltro Filter by transaction type ('receita', 'despesa', or 'all')
|
||||
* @param monthFilter - Optional month filter in format 'YYYY-MM'
|
||||
* @returns Promise with category summary data
|
||||
* Get category summary for expenses or income
|
||||
* @param tipo - Transaction type filter ('despesa', 'receita', 'all')
|
||||
* @returns Promise with array of category summaries
|
||||
*/
|
||||
export async function getCategorySummary(tipoFiltro: string = 'despesa', monthFilter?: string): Promise<CategorySummary[]> {
|
||||
console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`, monthFilter ? `Filtro do mês: ${monthFilter}` : "Sem filtro de mês");
|
||||
export async function getCategorySummary(tipo: string = 'despesa'): Promise<CategorySummary[]> {
|
||||
console.log(`📋 [getCategorySummary] Obtendo resumo por categoria para tipo: ${tipo}`);
|
||||
|
||||
const normalizedEmail = getUserEmail();
|
||||
|
||||
if (!normalizedEmail) {
|
||||
console.error("❌ [getCategorySummary] Email não encontrado");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Get user groups by email
|
||||
const groupIds = await getUserGroups(normalizedEmail);
|
||||
|
||||
// Build the query with month filter if provided
|
||||
let query = supabase
|
||||
.from('transacoes')
|
||||
.select('categoria, valor, tipo');
|
||||
|
||||
// Apply filters properly
|
||||
let query = supabase.from('transacoes').select('categoria, valor, tipo');
|
||||
|
||||
if (groupIds.length > 0) {
|
||||
const orFilter = `login.eq.${normalizedEmail},grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})`;
|
||||
query = query.or(orFilter);
|
||||
@ -109,94 +148,78 @@ export async function getCategorySummary(tipoFiltro: string = 'despesa', monthFi
|
||||
query = query.eq('login', normalizedEmail);
|
||||
}
|
||||
|
||||
// Apply month filter if provided
|
||||
if (monthFilter) {
|
||||
const startDate = `${monthFilter}-01`;
|
||||
const year = parseInt(monthFilter.split('-')[0]);
|
||||
const month = parseInt(monthFilter.split('-')[1]);
|
||||
const endDate = new Date(year, month, 0).toISOString().split('T')[0];
|
||||
|
||||
query = query
|
||||
.gte('quando', startDate)
|
||||
.lte('quando', `${endDate}T23:59:59.999Z`);
|
||||
// Apply type filter if not 'all'
|
||||
if (tipo !== 'all') {
|
||||
query = query.eq('tipo', tipo);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao buscar resumo de categorias:', error);
|
||||
throw new Error('Não foi possível carregar o resumo por categoria');
|
||||
console.error('❌ [getCategorySummary] Erro:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log("Dados de categorias encontrados:", data);
|
||||
|
||||
// Filter according to the requested type (incomes, expenses, or both)
|
||||
let filteredData = data;
|
||||
if (tipoFiltro.toLowerCase() === 'despesa') {
|
||||
filteredData = data.filter((item: any) => item.tipo?.toLowerCase() === 'despesa');
|
||||
} else if (tipoFiltro.toLowerCase() === 'receita') {
|
||||
filteredData = data.filter((item: any) => item.tipo?.toLowerCase() === 'receita');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
console.warn(`⚠️ [getCategorySummary] Nenhuma transação encontrada para tipo: ${tipo}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`${filteredData.length} itens filtrados para tipo: ${tipoFiltro}`);
|
||||
|
||||
// Group by category
|
||||
const categorias: Record<string, number> = {};
|
||||
filteredData.forEach((item: any) => {
|
||||
if (item.valor) {
|
||||
const categoriaKey = item.categoria || 'Outros';
|
||||
if (!categorias[categoriaKey]) {
|
||||
categorias[categoriaKey] = 0;
|
||||
}
|
||||
categorias[categoriaKey] += Math.abs(item.valor);
|
||||
}
|
||||
|
||||
// Group by category and sum values
|
||||
const categoryMap: { [key: string]: number } = {};
|
||||
let total = 0;
|
||||
|
||||
data.forEach((transaction) => {
|
||||
const categoria = transaction.categoria || 'Outros';
|
||||
const valor = Math.abs(Number(transaction.valor || 0));
|
||||
|
||||
categoryMap[categoria] = (categoryMap[categoria] || 0) + valor;
|
||||
total += valor;
|
||||
});
|
||||
|
||||
// Calculate the total for percentages
|
||||
const total = Object.values(categorias).reduce((sum, valor) => sum + valor, 0);
|
||||
|
||||
// Colors for categories (reuse colors in mockData)
|
||||
const cores = ["#F59E0B", "#60A5FA", "#8B5CF6", "#EF4444", "#10B981", "#6366F1", "#EC4899", "#14B8A6"];
|
||||
|
||||
// Map to the expected format
|
||||
const resultado = Object.entries(categorias).map(([categoria, valor], index) => ({
|
||||
categoria,
|
||||
valor,
|
||||
percentage: total > 0 ? valor / total : 0,
|
||||
color: cores[index % cores.length]
|
||||
}));
|
||||
// Convert to CategorySummary array with colors
|
||||
const colors = [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
|
||||
'#9966FF', '#FF9F40', '#FF6384', '#C9CBCF',
|
||||
'#4BC0C0', '#FF6384', '#36A2EB', '#FFCE56'
|
||||
];
|
||||
|
||||
console.log("Resumo de categorias calculado:", resultado);
|
||||
return resultado;
|
||||
const categoryArray = Object.entries(categoryMap)
|
||||
.map(([categoria, valor], index) => ({
|
||||
categoria,
|
||||
valor,
|
||||
percentage: total > 0 ? valor / total : 0,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
.sort((a, b) => b.valor - a.valor);
|
||||
|
||||
console.log(`📋 [getCategorySummary] ${categoryArray.length} categorias processadas`);
|
||||
return categoryArray;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar resumo de categorias:', error);
|
||||
console.error('💥 [getCategorySummary] Erro geral:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monthly data for transactions
|
||||
* @returns Promise with monthly data
|
||||
* Get monthly data for charts
|
||||
* @returns Promise with array of monthly data
|
||||
*/
|
||||
export async function getMonthlyData(): Promise<MonthlyData[]> {
|
||||
console.log("Buscando dados mensais...");
|
||||
console.log("📈 [getMonthlyData] Obtendo dados mensais...");
|
||||
|
||||
const normalizedEmail = getUserEmail();
|
||||
|
||||
if (!normalizedEmail) {
|
||||
console.error("❌ [getMonthlyData] Email não encontrado");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Get user groups by email
|
||||
const groupIds = await getUserGroups(normalizedEmail);
|
||||
|
||||
// Build the query properly
|
||||
let query = supabase
|
||||
.from('transacoes')
|
||||
.select('quando, valor, tipo');
|
||||
|
||||
// Apply filters properly
|
||||
let query = supabase.from('transacoes').select('quando, tipo, valor');
|
||||
|
||||
if (groupIds.length > 0) {
|
||||
const orFilter = `login.eq.${normalizedEmail},grupo_id.in.(${groupIds.map(id => `"${id}"`).join(',')})`;
|
||||
query = query.or(orFilter);
|
||||
@ -204,49 +227,55 @@ export async function getMonthlyData(): Promise<MonthlyData[]> {
|
||||
query = query.eq('login', normalizedEmail);
|
||||
}
|
||||
|
||||
// Get last 12 months
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setMonth(startDate.getMonth() - 11);
|
||||
|
||||
query = query
|
||||
.gte('quando', startDate.toISOString().split('T')[0])
|
||||
.lte('quando', endDate.toISOString().split('T')[0]);
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Erro ao buscar dados mensais:', error);
|
||||
throw new Error('Não foi possível carregar os dados mensais');
|
||||
console.error('❌ [getMonthlyData] Erro:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log("Dados mensais encontrados:", data);
|
||||
// Group by month
|
||||
const monthlyMap: { [key: string]: { receitas: number; despesas: number } } = {};
|
||||
|
||||
const meses: Record<string, { receitas: number, despesas: number }> = {};
|
||||
const nomesMeses = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
|
||||
// Initialize months
|
||||
nomesMeses.forEach(mes => {
|
||||
meses[mes] = { receitas: 0, despesas: 0 };
|
||||
});
|
||||
data?.forEach((transaction) => {
|
||||
const date = new Date(transaction.quando);
|
||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
if (!monthlyMap[monthKey]) {
|
||||
monthlyMap[monthKey] = { receitas: 0, despesas: 0 };
|
||||
}
|
||||
|
||||
// Group by month, normalizing the type
|
||||
data.forEach((item: any) => {
|
||||
if (item.quando && item.valor) {
|
||||
const data = new Date(item.quando);
|
||||
const mesIndex = data.getMonth();
|
||||
const nomeMes = nomesMeses[mesIndex];
|
||||
|
||||
if (item.tipo?.toLowerCase() === 'receita') {
|
||||
meses[nomeMes].receitas += Math.abs(item.valor);
|
||||
} else {
|
||||
meses[nomeMes].despesas += Math.abs(item.valor);
|
||||
}
|
||||
const valor = Math.abs(Number(transaction.valor || 0));
|
||||
|
||||
if (transaction.tipo?.toLowerCase() === 'receita') {
|
||||
monthlyMap[monthKey].receitas += valor;
|
||||
} else {
|
||||
monthlyMap[monthKey].despesas += valor;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to the expected format
|
||||
const resultado = Object.entries(meses).map(([month, values]) => ({
|
||||
month,
|
||||
receitas: values.receitas,
|
||||
despesas: values.despesas
|
||||
}));
|
||||
// Convert to array and sort
|
||||
const monthlyArray = Object.entries(monthlyMap)
|
||||
.map(([month, data]) => ({
|
||||
month,
|
||||
receitas: data.receitas,
|
||||
despesas: data.despesas
|
||||
}))
|
||||
.sort((a, b) => a.month.localeCompare(b.month));
|
||||
|
||||
console.log("Dados mensais calculados:", resultado);
|
||||
return resultado;
|
||||
console.log(`📈 [getMonthlyData] ${monthlyArray.length} meses processados`);
|
||||
return monthlyArray;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar dados mensais:', error);
|
||||
console.error('💥 [getMonthlyData] Erro geral:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,5 +7,6 @@ export {
|
||||
getMonthlyData,
|
||||
deleteTransacao,
|
||||
updateTransacao,
|
||||
formatCurrency
|
||||
formatCurrency,
|
||||
getResumoFinanceiro
|
||||
} from './transacao';
|
||||
|
||||
@ -49,6 +49,22 @@ export interface Meta {
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface MetaFinanceira {
|
||||
id: string;
|
||||
user_id: string;
|
||||
mes: number;
|
||||
ano: number;
|
||||
valor_meta: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface ResumoFinanceiro {
|
||||
totalReceitas: number;
|
||||
totalDespesas: number;
|
||||
totalCartoes?: number;
|
||||
saldo?: number;
|
||||
}
|
||||
|
||||
export interface ResultadoMeta {
|
||||
mes: number;
|
||||
ano: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user