new features
This commit is contained in:
228
src/components/budget/ExpenseBreakdownChart.tsx
Normal file
228
src/components/budget/ExpenseBreakdownChart.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useBudgetSummary, useMonthlyData } from '@/hooks/useBudget';
|
||||
import { useBudgetContext } from '@/contexts/BudgetContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
} from 'recharts';
|
||||
import {
|
||||
Server,
|
||||
Users,
|
||||
ShoppingCart,
|
||||
Building,
|
||||
Zap,
|
||||
Cpu,
|
||||
Package,
|
||||
Plane,
|
||||
Scale,
|
||||
Shield,
|
||||
Briefcase,
|
||||
HelpCircle,
|
||||
Megaphone,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
'Purchase': 'hsl(340 82% 52%)', // Vibrant pink
|
||||
'Salary': 'hsl(262 83% 58%)', // Rich purple
|
||||
'Cloud/Subscription': 'hsl(199 89% 48%)', // Bright cyan
|
||||
'Marketing': 'hsl(43 96% 56%)', // Golden yellow
|
||||
'Rent': 'hsl(280 68% 50%)', // Deep violet
|
||||
'Utilities': 'hsl(173 80% 40%)', // Teal
|
||||
'Hardware': 'hsl(22 92% 53%)', // Vivid orange
|
||||
'Software': 'hsl(142 76% 36%)', // Emerald green
|
||||
'Travel': 'hsl(217 91% 60%)', // Bright blue
|
||||
'Legal': 'hsl(350 89% 60%)', // Coral red
|
||||
'Insurance': 'hsl(192 91% 36%)', // Deep cyan
|
||||
'Office Supplies': 'hsl(83 78% 41%)', // Lime green
|
||||
'Consulting': 'hsl(291 64% 42%)', // Magenta
|
||||
'Other': 'hsl(220 14% 46%)', // Slate
|
||||
};
|
||||
|
||||
const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
||||
'Purchase': <ShoppingCart className="h-4 w-4" />,
|
||||
'Salary': <Users className="h-4 w-4" />,
|
||||
'Cloud/Subscription': <Server className="h-4 w-4" />,
|
||||
'Marketing': <Megaphone className="h-4 w-4" />,
|
||||
'Rent': <Building className="h-4 w-4" />,
|
||||
'Utilities': <Zap className="h-4 w-4" />,
|
||||
'Hardware': <Cpu className="h-4 w-4" />,
|
||||
'Software': <Package className="h-4 w-4" />,
|
||||
'Travel': <Plane className="h-4 w-4" />,
|
||||
'Legal': <Scale className="h-4 w-4" />,
|
||||
'Insurance': <Shield className="h-4 w-4" />,
|
||||
'Office Supplies': <FileText className="h-4 w-4" />,
|
||||
'Consulting': <Briefcase className="h-4 w-4" />,
|
||||
'Other': <HelpCircle className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
export function ExpenseBreakdownChart() {
|
||||
const { data: summary, isLoading: summaryLoading } = useBudgetSummary();
|
||||
const { data: monthlyData, isLoading: monthlyLoading } = useMonthlyData(6);
|
||||
const { formatCurrency } = useBudgetContext();
|
||||
|
||||
const pieData = summary?.expensesByCategory
|
||||
? Object.entries(summary.expensesByCategory)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
: [];
|
||||
|
||||
const barData = monthlyData?.map(d => ({
|
||||
...d,
|
||||
month: new Date(d.month + '-01').toLocaleDateString('en-US', { month: 'short' }),
|
||||
})) || [];
|
||||
|
||||
if (summaryLoading || monthlyLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center h-64">
|
||||
Loading charts...
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="breakdown" className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="breakdown">Expense Breakdown</TabsTrigger>
|
||||
<TabsTrigger value="trends">Monthly Trends</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="breakdown">
|
||||
{pieData.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
No expense data yet. Add transactions to see breakdown.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Pie Chart */}
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={CATEGORY_COLORS[entry.name] || CATEGORY_COLORS['Other']}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--popover))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
color: 'hsl(var(--popover-foreground))'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Category List */}
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{pieData.map((entry) => (
|
||||
<div
|
||||
key={entry.name}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: CATEGORY_COLORS[entry.name] || CATEGORY_COLORS['Other'] }}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{CATEGORY_ICONS[entry.name] || CATEGORY_ICONS['Other']}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{entry.name}</span>
|
||||
</div>
|
||||
<span className="text-sm font-mono">
|
||||
{formatCurrency(entry.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="trends">
|
||||
{barData.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
No monthly data yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={barData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
<YAxis
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--popover))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
color: 'hsl(var(--popover-foreground))'
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar
|
||||
dataKey="income"
|
||||
name="Income"
|
||||
fill="hsl(142 76% 36%)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="expenses"
|
||||
name="Expenses"
|
||||
fill="hsl(340 82% 52%)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export { CATEGORY_COLORS, CATEGORY_ICONS };
|
||||
Reference in New Issue
Block a user