Enhance dashboard cards and charts

- 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.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-24 18:38:44 +00:00
parent 45234a1a61
commit 4e8972cb55
5 changed files with 260 additions and 171 deletions

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
BarChart,
Bar,
@ -48,6 +47,31 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
}).format(value);
};
// Criar dados para o ano completo (12 meses)
const generateFullYearData = () => {
const currentYear = new Date().getFullYear();
const months = [
'Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'
];
const fullYearData = months.map((month, index) => {
const monthKey = `${currentYear}-${String(index + 1).padStart(2, '0')}`;
const existingData = data.find(d => d.month === monthKey);
return {
month: month,
monthKey: monthKey,
receitas: existingData ? existingData.receitas : 0,
despesas: existingData ? existingData.despesas : 0
};
});
return fullYearData;
};
const fullYearData = generateFullYearData();
const customTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
@ -64,7 +88,6 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
return null;
};
// Custom label component that only shows values above a threshold
const CustomLabel = ({ x, y, width, value, dataKey }: any) => {
if (!value || value < 100) return null;
const isReceita = dataKey === 'receitas';
@ -84,105 +107,94 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
};
return (
<Card className="dashboard-card h-full shadow-xl rounded-2xl animate-fade-in bg-gradient-to-br from-slate-50 via-white to-blue-50 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900 border-0">
<CardHeader className="pb-2 bg-gradient-to-r from-blue-50/50 to-blue-100/0 dark:from-zinc-900 dark:to-zinc-900 rounded-t-2xl">
<CardTitle className="text-lg font-bold tracking-tight text-zinc-700 dark:text-white flex items-center">
<span className="mr-2">📈</span> Receitas vs Despesas
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center h-[260px]">
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
</div>
) : data.length === 0 ? (
<div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
<span>Sem dados disponíveis</span>
</div>
) : (
<div className="h-[320px] relative">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
margin={{ top: 30, right: 30, left: 20, bottom: 15 }}
barGap={8}
<div className="h-full animate-fade-in">
{isLoading ? (
<div className="flex items-center justify-center h-[320px]">
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
</div>
) : (
<div className="h-[320px] relative">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={fullYearData}
margin={{ top: 30, right: 30, left: 20, bottom: 15 }}
barGap={8}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.20} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 13, fontWeight: 500, fill: "#334155" }}
dy={10}
/>
<YAxis
tickFormatter={formatCompactCurrency}
axisLine={false}
tickLine={false}
tick={{ fontSize: 13, fill: "#64748b" }}
width={80}
/>
<Tooltip
cursor={{ fill: 'rgba(16, 185, 129, 0.07)' }}
content={customTooltip}
/>
<Legend
verticalAlign="top"
height={36}
iconType="circle"
iconSize={12}
wrapperStyle={{ fontWeight: 600, fontSize: 13, color: "#52525b" }}
/>
<Bar
name="Receitas"
dataKey="receitas"
fill="#10B981"
radius={[8, 8, 0, 0]}
barSize={36}
animationDuration={1200}
label={<CustomLabel dataKey="receitas" />}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.20} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 13, fontWeight: 500, fill: "#334155" }}
dy={10}
/>
<YAxis
tickFormatter={formatCompactCurrency}
axisLine={false}
tickLine={false}
tick={{ fontSize: 13, fill: "#64748b" }}
width={80}
/>
<Tooltip
cursor={{ fill: 'rgba(16, 185, 129, 0.07)' }}
content={customTooltip}
/>
<Legend
verticalAlign="top"
height={36}
iconType="circle"
iconSize={12}
wrapperStyle={{ fontWeight: 600, fontSize: 13, color: "#52525b" }}
/>
<Bar
name="Receitas"
dataKey="receitas"
fill="#10B981"
radius={[8, 8, 0, 0]}
barSize={36}
animationDuration={1200}
label={<CustomLabel dataKey="receitas" />}
>
{data.map((entry, index) => (
<Cell
key={`cell-receitas-${index}`}
fill="url(#receitaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(16,185,129,0.10))' }}
/>
))}
</Bar>
<Bar
name="Despesas"
dataKey="despesas"
fill="#EF4444"
radius={[8, 8, 0, 0]}
barSize={36}
animationDuration={1200}
label={<CustomLabel dataKey="despesas" />}
>
{data.map((entry, index) => (
<Cell
key={`cell-despesas-${index}`}
fill="url(#despesaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(239,68,68,0.08))' }}
/>
))}
</Bar>
<defs>
<linearGradient id="receitaBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#17ead9" />
<stop offset="100%" stopColor="#10B981" />
</linearGradient>
<linearGradient id="despesaBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbc2eb" />
<stop offset="100%" stopColor="#EF4444" />
</linearGradient>
</defs>
</BarChart>
</ResponsiveContainer>
</div>
)}
</CardContent>
</Card>
{fullYearData.map((entry, index) => (
<Cell
key={`cell-receitas-${index}`}
fill="url(#receitaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(16,185,129,0.10))' }}
/>
))}
</Bar>
<Bar
name="Despesas"
dataKey="despesas"
fill="#EF4444"
radius={[8, 8, 0, 0]}
barSize={36}
animationDuration={1200}
label={<CustomLabel dataKey="despesas" />}
>
{fullYearData.map((entry, index) => (
<Cell
key={`cell-despesas-${index}`}
fill="url(#despesaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(239,68,68,0.08))' }}
/>
))}
</Bar>
<defs>
<linearGradient id="receitaBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#17ead9" />
<stop offset="100%" stopColor="#10B981" />
</linearGradient>
<linearGradient id="despesaBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbc2eb" />
<stop offset="100%" stopColor="#EF4444" />
</linearGradient>
</defs>
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,21 @@
import { useNavigate } from 'react-router-dom';
export const useNavigateWithFilter = () => {
const navigate = useNavigate();
const navigateToTransactions = (tipo?: 'receita' | 'despesa') => {
if (tipo) {
navigate('/transacoes', {
state: {
filter: tipo,
filterValue: tipo === 'receita' ? 'receita' : 'despesa'
}
});
} else {
navigate('/transacoes');
}
};
return { navigateToTransactions };
};

View File

@ -1,4 +1,3 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
@ -10,6 +9,7 @@ import { useTransactions } from "@/hooks/useTransactions";
import CategoryChart from "@/components/dashboard/CategoryChart";
import MonthlyChart from "@/components/dashboard/MonthlyChart";
import { SimpleCard } from "@/components/ui/simple-card";
import { useNavigateWithFilter } from "@/hooks/useNavigateWithFilter";
const Dashboard = () => {
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
@ -17,6 +17,7 @@ const Dashboard = () => {
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast();
const { navigateToTransactions } = useNavigateWithFilter();
const getCurrentMonth = () => {
const now = new Date();
@ -94,6 +95,7 @@ const Dashboard = () => {
}
const saldo = resumo ? resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0) : 0;
const totalDespesasGeral = resumo ? resumo.totalDespesas + (resumo.totalCartoes || 0) : 0;
return (
<div className="space-y-6" data-tour="dashboard-content">
@ -101,89 +103,113 @@ const Dashboard = () => {
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
</div>
<div className="grid gap-4 md:grid-cols-4">
<Card className="border-2 border-green-200 bg-green-50/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700">Receitas</CardTitle>
<div className="p-2 bg-green-100 rounded-full">
<ArrowUpIcon className="h-4 w-4 text-green-600" />
<div className="grid gap-6 md:grid-cols-4">
{/* Card Receitas - Clicável */}
<Card
className="relative overflow-hidden border-0 bg-gradient-to-br from-green-50 to-emerald-100 shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer group transform hover:scale-105"
onClick={() => navigateToTransactions('receita')}
>
<div className="absolute inset-0 bg-gradient-to-r from-green-400/10 to-emerald-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-green-500 rounded-full shadow-lg">
<ArrowUpIcon className="h-6 w-6 text-white" />
</div>
<div className="text-xs font-bold px-3 py-1 bg-green-500 text-white rounded-full shadow-sm">
+5%
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
</div>
<div className="flex items-center text-xs text-green-600 mt-1">
<span className="bg-green-100 px-2 py-1 rounded-full">+5%</span>
<div className="space-y-2">
<p className="text-sm font-semibold text-green-700">Receitas</p>
<p className="text-3xl font-bold text-green-600">
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
</p>
<p className="text-xs text-green-600 opacity-80">Clique para ver detalhes</p>
</div>
</CardContent>
</Card>
<Card className="border-2 border-red-200 bg-red-50/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-red-700">Despesas</CardTitle>
<div className="p-2 bg-red-100 rounded-full">
<ArrowDownIcon className="h-4 w-4 text-red-600" />
{/* Card Despesas - Clicável */}
<Card
className="relative overflow-hidden border-0 bg-gradient-to-br from-red-50 to-rose-100 shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer group transform hover:scale-105"
onClick={() => navigateToTransactions('despesa')}
>
<div className="absolute inset-0 bg-gradient-to-r from-red-400/10 to-rose-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-red-500 rounded-full shadow-lg">
<ArrowDownIcon className="h-6 w-6 text-white" />
</div>
<div className="text-xs font-bold px-3 py-1 bg-red-500 text-white rounded-full shadow-sm">
-2%
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{resumo ? formatCurrency(resumo.totalDespesas + (resumo.totalCartoes || 0)) : 'R$ 0,00'}
</div>
<div className="flex items-center text-xs text-red-600 mt-1">
<span className="bg-red-100 px-2 py-1 rounded-full">-2%</span>
</div>
{resumo && resumo.totalCartoes > 0 && (
<p className="text-xs text-red-500 mt-1">Cartões: {formatCurrency(resumo.totalCartoes)}</p>
)}
</CardContent>
</Card>
<Card className="border-2 border-blue-200 bg-blue-50/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700">Saldo</CardTitle>
<div className="p-2 bg-blue-100 rounded-full">
<CreditCardIcon className="h-4 w-4 text-blue-600" />
</div>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold ${
saldo >= 0 ? 'text-blue-600' : 'text-red-600'
}`}>
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
<div className="space-y-2">
<p className="text-sm font-semibold text-red-700">Despesas</p>
<p className="text-3xl font-bold text-red-600">
{resumo ? formatCurrency(totalDespesasGeral) : 'R$ 0,00'}
</p>
{resumo && resumo.totalCartoes > 0 && (
<p className="text-xs text-red-500">Cartões: {formatCurrency(resumo.totalCartoes)}</p>
)}
<p className="text-xs text-red-600 opacity-80">Clique para ver detalhes</p>
</div>
</CardContent>
</Card>
<Card className="border-2 border-purple-200 bg-purple-50/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-purple-700">Economia</CardTitle>
<div className="p-2 bg-purple-100 rounded-full">
<Target className="h-4 w-4 text-purple-600" />
{/* Card Saldo */}
<Card className="relative overflow-hidden border-0 bg-gradient-to-br from-blue-50 to-sky-100 shadow-lg hover:shadow-xl transition-all duration-300 group transform hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-r from-blue-400/10 to-sky-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-blue-500 rounded-full shadow-lg">
<CreditCardIcon className="h-6 w-6 text-white" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
-22.2%
<div className="space-y-2">
<p className="text-sm font-semibold text-blue-700">Saldo</p>
<p className={`text-3xl font-bold ${
saldo >= 0 ? 'text-blue-600' : 'text-red-600'
}`}>
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
</p>
</div>
</CardContent>
</Card>
{/* Card Economia */}
<Card className="relative overflow-hidden border-0 bg-gradient-to-br from-purple-50 to-violet-100 shadow-lg hover:shadow-xl transition-all duration-300 group transform hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-r from-purple-400/10 to-violet-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<CardContent className="p-6 relative">
<div className="flex items-center justify-between mb-4">
<div className="p-3 bg-purple-500 rounded-full shadow-lg">
<Target className="h-6 w-6 text-white" />
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold text-purple-700">Economia</p>
<p className="text-3xl font-bold text-purple-600">
-22.2%
</p>
<p className="text-xs text-purple-500">{resumo ? formatCurrency(Math.abs(saldo)) : 'R$ 0,00'}</p>
</div>
<p className="text-xs text-purple-500 mt-1">{resumo ? formatCurrency(Math.abs(saldo)) : 'R$ 0,00'}</p>
</CardContent>
</Card>
</div>
<div className="space-y-6">
{/* 1. Receitas vs Despesas Chart */}
<SimpleCard title="Receitas vs Despesas" className="border-green-200">
<SimpleCard title="📈 Receitas vs Despesas" className="border-green-200">
<MonthlyChart data={monthlyData} isLoading={isLoading} />
</SimpleCard>
{/* 2. Gastos por Categoria */}
<SimpleCard title="Despesas por Categoria" className="border-orange-200">
<SimpleCard title="📊 Despesas por Categoria" className="border-orange-200">
<CategoryChart categories={categories} isLoading={isLoading} />
</SimpleCard>
{/* 3. Últimas Transações */}
<SimpleCard title="Últimas Transações" className="border-blue-200">
<SimpleCard title="📋 Últimas Transações" className="border-blue-200">
<TransactionsTable
transactions={transactions.slice(0, 5)}
isLoading={transactionsLoading}

View File

@ -1,4 +1,6 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTransactions } from '@/hooks/useTransactions';
import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards';
import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs';
@ -7,6 +9,7 @@ import TransactionsTable from '@/components/dashboard/TransactionsTable';
import { SimpleCard } from "@/components/ui/simple-card";
const Transacoes = () => {
const location = useLocation();
const {
transactions,
isLoading,
@ -30,6 +33,25 @@ const Transacoes = () => {
loadTransactions
} = useTransactions();
// Aplicar filtro inicial se vier do dashboard
useEffect(() => {
if (location.state?.filter) {
// Aqui você pode implementar lógica adicional para filtrar automaticamente
// Por exemplo, definir um filtro inicial na tabela
console.log('Filtro aplicado:', location.state.filter);
}
}, [location.state]);
// Filtrar transações baseado no filtro vindo do dashboard
const getFilteredTransactions = () => {
if (location.state?.filter) {
return transactions.filter(t => t.tipo === location.state.filter);
}
return transactions;
};
const filteredTransactions = getFilteredTransactions();
return (
<div className="space-y-6">
<TransactionHeader
@ -44,9 +66,14 @@ const Transacoes = () => {
formatCurrency={formatCurrency}
/>
<SimpleCard title="Todas as Transações" className="border-gray-200">
<SimpleCard
title={`${location.state?.filter ?
(location.state.filter === 'receita' ? 'Receitas' : 'Despesas')
: 'Todas as Transações'}`}
className="border-gray-200"
>
<TransactionsTable
transactions={transactions}
transactions={filteredTransactions}
isLoading={isLoading}
showPagination={true}
onEdit={handleEditTransaction}

View File

@ -1,4 +1,3 @@
import { supabase } from "@/integrations/supabase/client";
import { CategorySummary, MonthlyData, ResumoFinanceiro } from "@/types/financialTypes";
import { getUserEmail, getUserGroups } from "./baseService";
@ -177,13 +176,17 @@ export async function getCategorySummary(tipo: string = 'despesa'): Promise<Cate
total += valor;
});
// Convert to CategorySummary array with colors
// Enhanced color palette with more distinct colors - avoiding white/light colors
const colors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
'#9966FF', '#FF9F40', '#FF6384', '#C9CBCF',
'#4BC0C0', '#FF6384', '#36A2EB', '#FFCE56'
'#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,
@ -202,11 +205,11 @@ export async function getCategorySummary(tipo: string = 'despesa'): Promise<Cate
}
/**
* Get monthly data for charts
* 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...");
console.log("📈 [getMonthlyData] Obtendo dados mensais do ano completo...");
const normalizedEmail = getUserEmail();
@ -227,10 +230,10 @@ 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);
// 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])