- Implemented year-long view for "Receitas vs Despesas" chart. - Improved dashboard card design and functionality, including navigation to filtered transactions on click. - Verified the accuracy of the "Despesa por Categoria" chart.
285 lines
9.2 KiB
TypeScript
285 lines
9.2 KiB
TypeScript
import { supabase } from "@/integrations/supabase/client";
|
|
import { CategorySummary, MonthlyData, ResumoFinanceiro } from "@/types/financialTypes";
|
|
import { getUserEmail, getUserGroups } from "./baseService";
|
|
|
|
/**
|
|
* Get transaction summary (total income, expenses, balance)
|
|
* @returns Promise with transaction summary
|
|
*/
|
|
export async function getTransactionSummary(): Promise<{ totalReceitas: number; totalDespesas: number; saldo: number }> {
|
|
console.log("📊 [getTransactionSummary] Calculando resumo de transações...");
|
|
|
|
const normalizedEmail = getUserEmail();
|
|
|
|
if (!normalizedEmail) {
|
|
console.error("❌ [getTransactionSummary] Email não encontrado");
|
|
return { totalReceitas: 0, totalDespesas: 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('❌ [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('❌ [getResumoFinanceiro] Erro:', error);
|
|
return { totalReceitas: 0, totalDespesas: 0, totalCartoes: 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);
|
|
|
|
// Get credit card expenses
|
|
const { data: cartaoData, error: cartaoError } = await supabase
|
|
.from('despesas_cartao')
|
|
.select('valor')
|
|
.eq('login', normalizedEmail);
|
|
|
|
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
|
|
};
|
|
} catch (error) {
|
|
console.error('💥 [getResumoFinanceiro] Erro geral:', error);
|
|
return { totalReceitas: 0, totalDespesas: 0, totalCartoes: 0, saldo: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(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 {
|
|
const groupIds = await getUserGroups(normalizedEmail);
|
|
|
|
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);
|
|
} else {
|
|
query = query.eq('login', normalizedEmail);
|
|
}
|
|
|
|
// Apply type filter if not 'all'
|
|
if (tipo !== 'all') {
|
|
query = query.eq('tipo', tipo);
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
|
|
if (error) {
|
|
console.error('❌ [getCategorySummary] Erro:', error);
|
|
return [];
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
console.warn(`⚠️ [getCategorySummary] Nenhuma transação encontrada para tipo: ${tipo}`);
|
|
return [];
|
|
}
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Enhanced color palette with more distinct colors - avoiding white/light colors
|
|
const colors = [
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
|
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
|
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2',
|
|
'#A3E4D7', '#FAD7A0', '#D5A6BD', '#AED6F1', '#A9DFBF',
|
|
'#FF8A80', '#80CBC4', '#81C784', '#FFB74D', '#F48FB1',
|
|
'#CE93D8', '#90CAF9', '#A5D6A7', '#FFCC02', '#FFAB40'
|
|
];
|
|
|
|
// Convert to CategorySummary array with colors
|
|
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('💥 [getCategorySummary] Erro geral:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get monthly data for charts - full year
|
|
* @returns Promise with array of monthly data
|
|
*/
|
|
export async function getMonthlyData(): Promise<MonthlyData[]> {
|
|
console.log("📈 [getMonthlyData] Obtendo dados mensais do ano completo...");
|
|
|
|
const normalizedEmail = getUserEmail();
|
|
|
|
if (!normalizedEmail) {
|
|
console.error("❌ [getMonthlyData] Email não encontrado");
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const groupIds = await getUserGroups(normalizedEmail);
|
|
|
|
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);
|
|
} else {
|
|
query = query.eq('login', normalizedEmail);
|
|
}
|
|
|
|
// Get full current year
|
|
const currentYear = new Date().getFullYear();
|
|
const startDate = new Date(currentYear, 0, 1); // January 1st
|
|
const endDate = new Date(currentYear, 11, 31); // December 31st
|
|
|
|
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('❌ [getMonthlyData] Erro:', error);
|
|
return [];
|
|
}
|
|
|
|
// Group by month
|
|
const monthlyMap: { [key: string]: { receitas: number; despesas: number } } = {};
|
|
|
|
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 };
|
|
}
|
|
|
|
const valor = Math.abs(Number(transaction.valor || 0));
|
|
|
|
if (transaction.tipo?.toLowerCase() === 'receita') {
|
|
monthlyMap[monthKey].receitas += valor;
|
|
} else {
|
|
monthlyMap[monthKey].despesas += valor;
|
|
}
|
|
});
|
|
|
|
// 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(`📈 [getMonthlyData] ${monthlyArray.length} meses processados`);
|
|
return monthlyArray;
|
|
} catch (error) {
|
|
console.error('💥 [getMonthlyData] Erro geral:', error);
|
|
return [];
|
|
}
|
|
}
|