229 lines
8.1 KiB
TypeScript
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 };
|