Files
traceability/src/components/budget/ExpenseBreakdownChart.tsx
2026-02-03 20:48:09 +01:00

229 lines
8.1 KiB
TypeScript

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 };