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 CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = false }) => {
|
||||||
const validCategories = categories.filter(cat => cat.valor > 0);
|
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) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('pt-BR', {
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@ -21,15 +35,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-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) => (
|
{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-full shadow"
|
className="w-4 h-4 mr-3 rounded-full shadow-sm border border-gray-200"
|
||||||
style={{ backgroundColor: entry.color }}
|
style={{ backgroundColor: entry.color }}
|
||||||
/>
|
/>
|
||||||
<span className="truncate font-semibold text-zinc-700 dark:text-white">
|
<span className="truncate font-medium text-zinc-700 dark:text-white">
|
||||||
{entry.value} ({(entry.payload.percentage * 100).toFixed(0)}%)
|
{entry.value} ({(entry.payload.percentage * 100).toFixed(1)}%)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -40,10 +54,12 @@ 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-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 p-3 rounded-xl shadow-lg transition-all duration-300">
|
<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-sm text-zinc-800 dark:text-white mb-1">{payload[0].name}</p>
|
<p className="font-semibold text-base text-zinc-800 dark:text-white mb-2">{payload[0].name}</p>
|
||||||
<p className="font-bold text-base">{formatCurrency(payload[0].value)}</p>
|
<p className="font-bold text-lg text-blue-600">{formatCurrency(payload[0].value)}</p>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">{`${(payload[0].payload.percentage * 100).toFixed(1)}%`}</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -53,43 +69,52 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
|
|||||||
return (
|
return (
|
||||||
<div className="h-full animate-fade-in">
|
<div className="h-full animate-fade-in">
|
||||||
{isLoading ? (
|
{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 className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : validCategories.length === 0 ? (
|
) : categoriesWithColors.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
|
<div className="flex flex-col items-center justify-center h-[300px] text-muted-foreground">
|
||||||
<span>Sem dados disponíveis</span>
|
<span>Sem dados disponíveis</span>
|
||||||
<span className="text-xs mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</span>
|
<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-[400px] relative">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={validCategories}
|
data={categoriesWithColors}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="45%"
|
cy="40%"
|
||||||
outerRadius={90}
|
outerRadius={100}
|
||||||
innerRadius={40}
|
innerRadius={50}
|
||||||
fill="#8884d8"
|
fill="#8884d8"
|
||||||
dataKey="valor"
|
dataKey="valor"
|
||||||
nameKey="categoria"
|
nameKey="categoria"
|
||||||
paddingAngle={2}
|
paddingAngle={1}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
stroke="#fff"
|
stroke="#fff"
|
||||||
isAnimationActive={true}
|
isAnimationActive={true}
|
||||||
animationDuration={1300}
|
animationDuration={1500}
|
||||||
>
|
>
|
||||||
{validCategories.map((entry, index) => (
|
{categoriesWithColors.map((entry, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
key={`cell-${index}`}
|
key={`cell-${index}`}
|
||||||
fill={entry.color}
|
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>
|
</Pie>
|
||||||
<Tooltip content={renderCustomTooltip} />
|
<Tooltip content={renderCustomTooltip} />
|
||||||
<Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" />
|
<Legend
|
||||||
|
content={renderCustomLegend}
|
||||||
|
layout="horizontal"
|
||||||
|
verticalAlign="bottom"
|
||||||
|
wrapperStyle={{ paddingTop: '20px' }}
|
||||||
|
/>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,16 +3,18 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target, TrendingUpIcon, TrendingDownIcon } from "lucide-react";
|
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target, TrendingUpIcon, TrendingDownIcon } from "lucide-react";
|
||||||
import { getResumoFinanceiro, getCategorySummary } from "@/services/transacao";
|
import { getResumoFinanceiro, getCategorySummary, getMonthlyData } from "@/services/transacao";
|
||||||
import { ResumoFinanceiro, CategorySummary } from "@/types/financialTypes";
|
import { ResumoFinanceiro, CategorySummary, MonthlyData } from "@/types/financialTypes";
|
||||||
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
||||||
import { useTransactions } from "@/hooks/useTransactions";
|
import { useTransactions } from "@/hooks/useTransactions";
|
||||||
import CategoryChart from "@/components/dashboard/CategoryChart";
|
import CategoryChart from "@/components/dashboard/CategoryChart";
|
||||||
|
import MonthlyChart from "@/components/dashboard/MonthlyChart";
|
||||||
import { SimpleCard } from "@/components/ui/simple-card";
|
import { SimpleCard } from "@/components/ui/simple-card";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
||||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
||||||
|
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -50,6 +52,10 @@ const Dashboard = () => {
|
|||||||
// Load categories for chart
|
// Load categories for chart
|
||||||
const categoriesData = await getCategorySummary('despesa');
|
const categoriesData = await getCategorySummary('despesa');
|
||||||
setCategories(categoriesData);
|
setCategories(categoriesData);
|
||||||
|
|
||||||
|
// Load monthly data for chart
|
||||||
|
const monthlyDataResult = await getMonthlyData();
|
||||||
|
setMonthlyData(monthlyDataResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar resumo:", error);
|
console.error("Erro ao carregar resumo:", error);
|
||||||
toast({
|
toast({
|
||||||
@ -96,96 +102,79 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
<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">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Total Receitas</CardTitle>
|
<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" />
|
<ArrowUpIcon className="h-4 w-4 text-green-600" />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">
|
<div className="text-2xl font-bold text-green-600">
|
||||||
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Total Despesas</CardTitle>
|
<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" />
|
<ArrowDownIcon className="h-4 w-4 text-red-600" />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-red-600">
|
<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>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
<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>
|
<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" />
|
<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'}
|
|
||||||
</div>
|
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className={`text-2xl font-bold ${
|
<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'}
|
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 1. Receitas vs Despesas */}
|
{/* 1. Receitas vs Despesas Chart */}
|
||||||
<SimpleCard title="Receitas vs Despesas" className="border-green-200">
|
<SimpleCard title="Receitas vs Despesas" className="border-green-200">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<MonthlyChart data={monthlyData} isLoading={isLoading} />
|
||||||
<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>
|
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
||||||
{/* 2. Gastos por Categoria */}
|
{/* 2. Gastos por Categoria */}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
// Re-export all transaction service functions
|
// Re-export all transaction service functions
|
||||||
export { getTransacoes } from './transacaoFetchService';
|
export { getTransacoes } from './transacaoFetchService';
|
||||||
export {
|
export {
|
||||||
getTransactionSummary,
|
getTransactionSummary,
|
||||||
|
getResumoFinanceiro,
|
||||||
getCategorySummary,
|
getCategorySummary,
|
||||||
getMonthlyData,
|
getMonthlyData
|
||||||
getResumoFinanceiro
|
|
||||||
} from './summaryService';
|
} from './summaryService';
|
||||||
export {
|
export {
|
||||||
deleteTransacao,
|
deleteTransacao,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user