Refactor: Improve dashboard chart layout and design

Stacked charts and improved styling for a modern look.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-15 22:00:39 +00:00
parent b0bbce2637
commit f4f614e86b
3 changed files with 77 additions and 74 deletions

View File

@ -10,19 +10,15 @@ interface CategoryChartProps {
} }
const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = false }) => { const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = false }) => {
// Filter out any categories with zero value to prevent rendering issues
const validCategories = categories.filter(cat => cat.valor > 0); const validCategories = categories.filter(cat => cat.valor > 0);
// Calcular o valor total para exibir no centro do gráfico
const totalValue = validCategories.reduce((sum, cat) => sum + cat.valor, 0); const totalValue = validCategories.reduce((sum, cat) => sum + cat.valor, 0);
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index, name }: any) => { const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index }: any) => {
if (percent < 0.05) return null; // Não mostrar texto para fatias muito pequenas if (percent < 0.05) return null;
const radius = innerRadius + (outerRadius - innerRadius) * 0.7; const radius = innerRadius + (outerRadius - innerRadius) * 0.7;
const x = cx + radius * Math.cos(-midAngle * Math.PI / 180); const x = cx + radius * Math.cos(-midAngle * Math.PI / 180);
const y = cy + radius * Math.sin(-midAngle * Math.PI / 180); const y = cy + radius * Math.sin(-midAngle * Math.PI / 180);
return ( return (
<text <text
x={x} x={x}
@ -30,7 +26,7 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
fill="#fff" fill="#fff"
textAnchor={x > cx ? 'start' : 'end'} textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central" dominantBaseline="central"
className="text-xs font-bold" className="text-xs font-bold drop-shadow-lg"
> >
{`${(percent * 100).toFixed(0)}%`} {`${(percent * 100).toFixed(0)}%`}
</text> </text>
@ -46,16 +42,15 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
const renderCustomLegend = (props: any) => { const renderCustomLegend = (props: any) => {
const { payload } = props; const { payload } = props;
return ( return (
<div className="grid grid-cols-2 gap-x-4 gap-y-2 mt-3 text-xs"> <div className="grid grid-cols-2 gap-x-4 gap-y-2 mt-2 text-xs">
{payload.map((entry: any, index: number) => ( {payload.map((entry: any, index: number) => (
<div key={`item-${index}`} className="flex items-center"> <div key={`item-${index}`} className="flex items-center">
<div <div
className="w-3 h-3 mr-2 rounded-sm" className="w-3 h-3 mr-2 rounded-full shadow"
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
/> />
<span className="truncate font-medium">{entry.value}</span> <span className="truncate font-semibold text-zinc-700 dark:text-white">{entry.value}</span>
</div> </div>
))} ))}
</div> </div>
@ -65,58 +60,59 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
const renderCustomTooltip = ({ active, payload }: any) => { const renderCustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="bg-background border border-border p-2 rounded-lg shadow-lg"> <div className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 p-2 rounded-xl shadow-lg transition-all duration-300">
<p className="font-medium">{payload[0].name}</p> <p className="font-semibold text-sm text-zinc-800 dark:text-white">{payload[0].name}</p>
<p className="font-bold">{formatCurrency(payload[0].value)}</p> <p className="font-bold">{formatCurrency(payload[0].value)}</p>
<p className="text-sm">{`${(payload[0].payload.percentage * 100).toFixed(1)}%`}</p> <p className="text-xs">{`${(payload[0].payload.percentage * 100).toFixed(1)}%`}</p>
</div> </div>
); );
} }
return null; return null;
}; };
return ( return (
<Card className="dashboard-card h-full shadow-md"> <Card className="dashboard-card h-full shadow-xl rounded-2xl animate-fade-in bg-gradient-to-br from-orange-50 via-white to-indigo-50 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900 border-0">
<CardHeader className="pb-2 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900"> <CardHeader className="pb-2 bg-gradient-to-r from-indigo-50/60 to-orange-50/0 dark:from-zinc-900 dark:to-zinc-900 rounded-t-2xl">
<CardTitle className="text-lg flex items-center"> <CardTitle className="text-lg font-bold tracking-tight text-zinc-700 dark:text-white flex items-center">
Gastos por Categoria <span className="mr-2">🧾</span> Gastos por Categoria
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-[250px]"> <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 className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
</div> </div>
) : validCategories.length === 0 ? ( ) : validCategories.length === 0 ? (
<div className="flex flex-col items-center justify-center h-[250px] text-muted-foreground"> <div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
<p>Sem dados disponíveis</p> <span>Sem dados disponíveis</span>
<p className="text-sm mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</p> <span className="text-xs mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</span>
</div> </div>
) : ( ) : (
<div className="h-[300px] relative"> <div className="h-[320px] relative">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={validCategories} data={validCategories}
cx="50%" cx="50%"
cy="45%" cy="46%"
labelLine={false} labelLine={false}
label={renderCustomizedLabel} label={renderCustomizedLabel}
outerRadius={100} outerRadius={105}
innerRadius={60} innerRadius={65}
fill="#8884d8" fill="#8884d8"
dataKey="valor" dataKey="valor"
nameKey="categoria" nameKey="categoria"
paddingAngle={1} paddingAngle={2}
strokeWidth={1} strokeWidth={2}
stroke="#fff" stroke="#fff"
isAnimationActive={true}
animationDuration={1300}
> >
{validCategories.map((entry, index) => ( {validCategories.map((entry, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={entry.color} fill={entry.color}
style={{ filter: 'drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.1))' }} style={{ filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.08))' }}
/> />
))} ))}
</Pie> </Pie>
@ -124,13 +120,12 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
<Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" /> <Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
{/* Valor total no centro */}
{/* Valor total no centro do gráfico */}
<div <div
className="absolute top-[45%] left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none" className="absolute top-[46%] left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none"
style={{ width: '120px' }} style={{ width: '120px' }}
> >
<div className="font-bold text-lg">{formatCurrency(totalValue)}</div> <div className="font-bold text-lg text-zinc-800 dark:text-white">{formatCurrency(totalValue)}</div>
</div> </div>
</div> </div>
)} )}

View File

@ -47,19 +47,15 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
maximumFractionDigits: 0, maximumFractionDigits: 0,
}).format(value); }).format(value);
}; };
const customTooltip = ({ active, payload, label }: any) => { const customTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="bg-white p-3 border border-gray-200 rounded-md shadow-lg"> <div className="bg-white dark:bg-zinc-900 p-3 border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg font-sans min-w-[180px] transition-all duration-300">
<p className="font-bold mb-1">{`Mês: ${label}`}</p> <p className="font-bold mb-1 text-[15px] text-zinc-800 dark:text-zinc-100">{label}</p>
<p className="text-green-600"> <p className="text-green-600">{`Receitas: ${formatCurrency(payload[0].value)}`}</p>
{`Receitas: ${formatCurrency(payload[0].value)}`} <p className="text-red-600">{`Despesas: ${formatCurrency(payload[1].value)}`}</p>
</p> <p className="text-gray-700 dark:text-gray-300 font-medium pt-1 border-t border-gray-200 dark:border-zinc-700 mt-1">
<p className="text-red-600">
{`Despesas: ${formatCurrency(payload[1].value)}`}
</p>
<p className="text-gray-700 font-medium pt-1 border-t border-gray-200 mt-1">
{`Saldo: ${formatCurrency(payload[0].value - payload[1].value)}`} {`Saldo: ${formatCurrency(payload[0].value - payload[1].value)}`}
</p> </p>
</div> </div>
@ -70,12 +66,9 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
// Custom label component that only shows values above a threshold // Custom label component that only shows values above a threshold
const CustomLabel = ({ x, y, width, value, dataKey }: any) => { const CustomLabel = ({ x, y, width, value, dataKey }: any) => {
// Only show label if value is significant (above 100)
if (!value || value < 100) return null; if (!value || value < 100) return null;
const isReceita = dataKey === 'receitas'; const isReceita = dataKey === 'receitas';
const yPosition = isReceita ? y - 5 : y - 5; const yPosition = isReceita ? y - 5 : y - 5;
return ( return (
<text <text
x={x + width / 2} x={x + width / 2}
@ -83,7 +76,7 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
fill={isReceita ? '#10B981' : '#EF4444'} fill={isReceita ? '#10B981' : '#EF4444'}
textAnchor="middle" textAnchor="middle"
dominantBaseline="middle" dominantBaseline="middle"
className="text-xs font-semibold" className="text-xs font-semibold drop-shadow-lg"
> >
{formatCompactCurrency(value)} {formatCompactCurrency(value)}
</text> </text>
@ -91,66 +84,69 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
}; };
return ( return (
<Card className="dashboard-card h-full shadow-md"> <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-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900"> <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">Receitas vs Despesas</CardTitle> <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> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-[250px]"> <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 className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
</div> </div>
) : data.length === 0 ? ( ) : data.length === 0 ? (
<div className="flex items-center justify-center h-[250px] text-muted-foreground"> <div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
Sem dados disponíveis <span>Sem dados disponíveis</span>
</div> </div>
) : ( ) : (
<div className="h-[300px]"> <div className="h-[320px] relative">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={data} data={data}
margin={{ top: 30, right: 30, left: 20, bottom: 15 }} margin={{ top: 30, right: 30, left: 20, bottom: 15 }}
barGap={8} barGap={8}
> >
<CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.4} /> <CartesianGrid strokeDasharray="3 3" vertical={false} opacity={0.20} />
<XAxis <XAxis
dataKey="month" dataKey="month"
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fontSize: 12, fontWeight: 500 }} tick={{ fontSize: 13, fontWeight: 500, fill: "#334155" }}
dy={10} dy={10}
/> />
<YAxis <YAxis
tickFormatter={formatCompactCurrency} tickFormatter={formatCompactCurrency}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fontSize: 12 }} tick={{ fontSize: 13, fill: "#64748b" }}
width={80} width={80}
/> />
<Tooltip <Tooltip
cursor={{ fill: 'rgba(0, 0, 0, 0.05)' }} cursor={{ fill: 'rgba(16, 185, 129, 0.07)' }}
content={customTooltip} content={customTooltip}
/> />
<Legend <Legend
verticalAlign="top" verticalAlign="top"
height={36} height={36}
iconType="circle" iconType="circle"
iconSize={10} iconSize={12}
wrapperStyle={{ fontWeight: 600, fontSize: 13, color: "#52525b" }}
/> />
<Bar <Bar
name="Receitas" name="Receitas"
dataKey="receitas" dataKey="receitas"
fill="#10B981" fill="#10B981"
radius={[4, 4, 0, 0]} radius={[8, 8, 0, 0]}
barSize={35} barSize={36}
animationDuration={1000} animationDuration={1200}
label={<CustomLabel dataKey="receitas" />} label={<CustomLabel dataKey="receitas" />}
> >
{data.map((entry, index) => ( {data.map((entry, index) => (
<Cell <Cell
key={`cell-receitas-${index}`} key={`cell-receitas-${index}`}
fill="#10B981" fill="url(#receitaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.1))' }} style={{ filter: 'drop-shadow(0px 2px 2px rgba(16,185,129,0.10))' }}
/> />
))} ))}
</Bar> </Bar>
@ -158,19 +154,29 @@ const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false })
name="Despesas" name="Despesas"
dataKey="despesas" dataKey="despesas"
fill="#EF4444" fill="#EF4444"
radius={[4, 4, 0, 0]} radius={[8, 8, 0, 0]}
barSize={35} barSize={36}
animationDuration={1000} animationDuration={1200}
label={<CustomLabel dataKey="despesas" />} label={<CustomLabel dataKey="despesas" />}
> >
{data.map((entry, index) => ( {data.map((entry, index) => (
<Cell <Cell
key={`cell-despesas-${index}`} key={`cell-despesas-${index}`}
fill="#EF4444" fill="url(#despesaBar)"
style={{ filter: 'drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.1))' }} style={{ filter: 'drop-shadow(0px 2px 2px rgba(239,68,68,0.08))' }}
/> />
))} ))}
</Bar> </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> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -126,6 +126,7 @@ const Dashboard = () => {
/> />
</div> </div>
{/* Resumo em cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<SummaryCard <SummaryCard
title="Receitas" title="Receitas"
@ -161,9 +162,10 @@ const Dashboard = () => {
/> />
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> {/* ---- GRÁFICOS EM COLUNA ---- */}
<CategoryChart categories={categories} isLoading={isLoading} /> <div className="flex flex-col gap-6 w-full">
<MonthlyChart data={monthlyData} isLoading={isLoading} /> <MonthlyChart data={monthlyData} isLoading={isLoading} />
<CategoryChart categories={categories} isLoading={isLoading} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">