Fix: Improve dashboard charts and card styles
- Replace the "Receitas vs Despesas" section with a chart. - Ensure all expense categories are displayed in the "Gastos por Categoria" chart, and fix the color issue. - Apply circular card styles with colors to the dashboard cards.
This commit is contained in:
parent
5c3280f64e
commit
45234a1a61
@ -11,6 +11,20 @@ interface CategoryChartProps {
|
||||
const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = false }) => {
|
||||
const validCategories = categories.filter(cat => cat.valor > 0);
|
||||
|
||||
// Enhanced color palette with more distinct colors
|
||||
const enhancedColors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2',
|
||||
'#A3E4D7', '#FAD7A0', '#D5A6BD', '#AED6F1', '#A9DFBF'
|
||||
];
|
||||
|
||||
// Assign colors to categories, ensuring no white or very light colors
|
||||
const categoriesWithColors = validCategories.map((category, index) => ({
|
||||
...category,
|
||||
color: enhancedColors[index % enhancedColors.length]
|
||||
}));
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
@ -21,15 +35,15 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
|
||||
const renderCustomLegend = (props: any) => {
|
||||
const { payload } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2 mt-2 text-xs">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2 mt-4 text-sm">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={`item-${index}`} className="flex items-center">
|
||||
<div
|
||||
className="w-3 h-3 mr-2 rounded-full shadow"
|
||||
className="w-4 h-4 mr-3 rounded-full shadow-sm border border-gray-200"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="truncate font-semibold text-zinc-700 dark:text-white">
|
||||
{entry.value} ({(entry.payload.percentage * 100).toFixed(0)}%)
|
||||
<span className="truncate font-medium text-zinc-700 dark:text-white">
|
||||
{entry.value} ({(entry.payload.percentage * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@ -40,10 +54,12 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
|
||||
const renderCustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 p-3 rounded-xl shadow-lg transition-all duration-300">
|
||||
<p className="font-semibold text-sm text-zinc-800 dark:text-white mb-1">{payload[0].name}</p>
|
||||
<p className="font-bold text-base">{formatCurrency(payload[0].value)}</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">{`${(payload[0].payload.percentage * 100).toFixed(1)}%`}</p>
|
||||
<div className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 p-4 rounded-xl shadow-lg transition-all duration-300">
|
||||
<p className="font-semibold text-base text-zinc-800 dark:text-white mb-2">{payload[0].name}</p>
|
||||
<p className="font-bold text-lg text-blue-600">{formatCurrency(payload[0].value)}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{`${(payload[0].payload.percentage * 100).toFixed(1)}% do total`}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -53,43 +69,52 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
|
||||
return (
|
||||
<div className="h-full animate-fade-in">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-[260px]">
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
||||
</div>
|
||||
) : validCategories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
|
||||
) : categoriesWithColors.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-[300px] text-muted-foreground">
|
||||
<span>Sem dados disponíveis</span>
|
||||
<span className="text-xs mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[300px] relative">
|
||||
<div className="h-[400px] relative">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={validCategories}
|
||||
data={categoriesWithColors}
|
||||
cx="50%"
|
||||
cy="45%"
|
||||
outerRadius={90}
|
||||
innerRadius={40}
|
||||
cy="40%"
|
||||
outerRadius={100}
|
||||
innerRadius={50}
|
||||
fill="#8884d8"
|
||||
dataKey="valor"
|
||||
nameKey="categoria"
|
||||
paddingAngle={2}
|
||||
paddingAngle={1}
|
||||
strokeWidth={2}
|
||||
stroke="#fff"
|
||||
isAnimationActive={true}
|
||||
animationDuration={1300}
|
||||
animationDuration={1500}
|
||||
>
|
||||
{validCategories.map((entry, index) => (
|
||||
{categoriesWithColors.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.color}
|
||||
style={{ filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.08))' }}
|
||||
style={{
|
||||
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.1))',
|
||||
stroke: '#ffffff',
|
||||
strokeWidth: 2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={renderCustomTooltip} />
|
||||
<Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" />
|
||||
<Legend
|
||||
content={renderCustomLegend}
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
wrapperStyle={{ paddingTop: '20px' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@ -3,16 +3,18 @@ import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target, TrendingUpIcon, TrendingDownIcon } from "lucide-react";
|
||||
import { getResumoFinanceiro, getCategorySummary } from "@/services/transacao";
|
||||
import { ResumoFinanceiro, CategorySummary } from "@/types/financialTypes";
|
||||
import { getResumoFinanceiro, getCategorySummary, getMonthlyData } from "@/services/transacao";
|
||||
import { ResumoFinanceiro, CategorySummary, MonthlyData } from "@/types/financialTypes";
|
||||
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
||||
import { useTransactions } from "@/hooks/useTransactions";
|
||||
import CategoryChart from "@/components/dashboard/CategoryChart";
|
||||
import MonthlyChart from "@/components/dashboard/MonthlyChart";
|
||||
import { SimpleCard } from "@/components/ui/simple-card";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
||||
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -50,6 +52,10 @@ const Dashboard = () => {
|
||||
// Load categories for chart
|
||||
const categoriesData = await getCategorySummary('despesa');
|
||||
setCategories(categoriesData);
|
||||
|
||||
// Load monthly data for chart
|
||||
const monthlyDataResult = await getMonthlyData();
|
||||
setMonthlyData(monthlyDataResult);
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar resumo:", error);
|
||||
toast({
|
||||
@ -96,96 +102,79 @@ const Dashboard = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<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">Total Receitas</CardTitle>
|
||||
<ArrowUpIcon className="h-4 w-4 text-green-600" />
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Entradas do mês</p>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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">Total Despesas</CardTitle>
|
||||
<ArrowDownIcon className="h-4 w-4 text-red-600" />
|
||||
<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" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{resumo ? formatCurrency(resumo.totalDespesas) : 'R$ 0,00'}
|
||||
{resumo ? formatCurrency(resumo.totalDespesas + (resumo.totalCartoes || 0)) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Saídas do mês</p>
|
||||
<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>
|
||||
<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">Cartões de Crédito</CardTitle>
|
||||
<CreditCardIcon className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{resumo ? formatCurrency(resumo.totalCartoes || 0) : 'R$ 0,00'}
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground">Gastos nos cartões</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Saldo</CardTitle>
|
||||
<Target className="h-4 w-4 text-purple-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${
|
||||
saldo >= 0 ? 'text-green-600' : 'text-red-600'
|
||||
saldo >= 0 ? 'text-blue-600' : 'text-red-600'
|
||||
}`}>
|
||||
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Resultado do mês</p>
|
||||
</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" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
-22.2%
|
||||
</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 */}
|
||||
{/* 1. Receitas vs Despesas Chart */}
|
||||
<SimpleCard title="Receitas vs Despesas" className="border-green-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<TrendingUpIcon className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Receitas</p>
|
||||
<p className="text-xl font-bold text-green-600">
|
||||
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-red-100 rounded-lg">
|
||||
<TrendingDownIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Despesas</p>
|
||||
<p className="text-xl font-bold text-red-600">
|
||||
{resumo ? formatCurrency(resumo.totalDespesas + (resumo.totalCartoes || 0)) : 'R$ 0,00'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Resultado:</span>
|
||||
<span className={`text-lg font-bold ${saldo >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<MonthlyChart data={monthlyData} isLoading={isLoading} />
|
||||
</SimpleCard>
|
||||
|
||||
{/* 2. Gastos por Categoria */}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
|
||||
// Re-export all transaction service functions
|
||||
export { getTransacoes } from './transacaoFetchService';
|
||||
export {
|
||||
getTransactionSummary,
|
||||
getCategorySummary,
|
||||
getMonthlyData,
|
||||
getResumoFinanceiro
|
||||
getResumoFinanceiro,
|
||||
getCategorySummary,
|
||||
getMonthlyData
|
||||
} from './summaryService';
|
||||
export {
|
||||
deleteTransacao,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user