Files
fintrack/app/statistics/page.tsx

418 lines
16 KiB
TypeScript

"use client"
import { useState, useMemo } from "react"
import { Sidebar } from "@/components/dashboard/sidebar"
import { useBankingData } from "@/lib/hooks"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RefreshCw, TrendingUp, TrendingDown, ArrowRight } from "lucide-react"
import { CategoryIcon } from "@/components/ui/category-icon"
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
LineChart,
Line,
Legend,
} from "recharts"
import { cn } from "@/lib/utils"
type Period = "3months" | "6months" | "12months" | "all"
export default function StatisticsPage() {
const { data, isLoading } = useBankingData()
const [period, setPeriod] = useState<Period>("6months")
const [selectedAccount, setSelectedAccount] = useState<string>("all")
const stats = useMemo(() => {
if (!data) return null
const now = new Date()
let startDate: Date
switch (period) {
case "3months":
startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1)
break
case "6months":
startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1)
break
case "12months":
startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1)
break
default:
startDate = new Date(0)
}
let transactions = data.transactions.filter((t) => new Date(t.date) >= startDate)
if (selectedAccount !== "all") {
transactions = transactions.filter((t) => t.accountId === selectedAccount)
}
// Monthly breakdown
const monthlyData = new Map<string, { income: number; expenses: number }>()
transactions.forEach((t) => {
const monthKey = t.date.substring(0, 7)
const current = monthlyData.get(monthKey) || { income: 0, expenses: 0 }
if (t.amount >= 0) {
current.income += t.amount
} else {
current.expenses += Math.abs(t.amount)
}
monthlyData.set(monthKey, current)
})
const monthlyChartData = Array.from(monthlyData.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([month, values]) => ({
month: new Date(month + "-01").toLocaleDateString("fr-FR", { month: "short", year: "2-digit" }),
revenus: Math.round(values.income),
depenses: Math.round(values.expenses),
solde: Math.round(values.income - values.expenses),
}))
// Category breakdown (expenses only)
const categoryTotals = new Map<string, number>()
transactions
.filter((t) => t.amount < 0)
.forEach((t) => {
const catId = t.categoryId || "uncategorized"
const current = categoryTotals.get(catId) || 0
categoryTotals.set(catId, current + Math.abs(t.amount))
})
const categoryChartData = Array.from(categoryTotals.entries())
.map(([categoryId, total]) => {
const category = data.categories.find((c) => c.id === categoryId)
return {
name: category?.name || "Non catégorisé",
value: Math.round(total),
color: category?.color || "#94a3b8",
}
})
.sort((a, b) => b.value - a.value)
.slice(0, 8)
// Top expenses
const topExpenses = transactions
.filter((t) => t.amount < 0)
.sort((a, b) => a.amount - b.amount)
.slice(0, 5)
// Summary
const totalIncome = transactions.filter((t) => t.amount >= 0).reduce((sum, t) => sum + t.amount, 0)
const totalExpenses = transactions.filter((t) => t.amount < 0).reduce((sum, t) => sum + Math.abs(t.amount), 0)
const avgMonthlyExpenses = monthlyData.size > 0 ? totalExpenses / monthlyData.size : 0
// Balance evolution
const sortedTransactions = [...transactions].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
let runningBalance = 0
const balanceByDate = new Map<string, number>()
sortedTransactions.forEach((t) => {
runningBalance += t.amount
balanceByDate.set(t.date, runningBalance)
})
const balanceChartData = Array.from(balanceByDate.entries()).map(([date, balance]) => ({
date: new Date(date).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" }),
solde: Math.round(balance),
}))
return {
monthlyChartData,
categoryChartData,
topExpenses,
totalIncome,
totalExpenses,
avgMonthlyExpenses,
balanceChartData,
transactionCount: transactions.length,
}
}, [data, period, selectedAccount])
if (isLoading || !data || !stats) {
return (
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 flex items-center justify-center">
<RefreshCw className="w-8 h-8 animate-spin text-muted-foreground" />
</main>
</div>
)
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(amount)
}
return (
<div className="flex h-screen bg-background">
<Sidebar />
<main className="flex-1 overflow-auto">
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">Statistiques</h1>
<p className="text-muted-foreground">Analysez vos dépenses et revenus</p>
</div>
<div className="flex gap-2">
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Compte" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les comptes</SelectItem>
{data.accounts.map((account) => (
<SelectItem key={account.id} value={account.id}>
{account.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={period} onValueChange={(v) => setPeriod(v as Period)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Période" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3months">3 mois</SelectItem>
<SelectItem value="6months">6 mois</SelectItem>
<SelectItem value="12months">12 mois</SelectItem>
<SelectItem value="all">Tout</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Total Revenus
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-emerald-600">{formatCurrency(stats.totalIncome)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-600" />
Total Dépenses
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{formatCurrency(stats.totalExpenses)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<ArrowRight className="w-4 h-4" />
Moyenne mensuelle
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatCurrency(stats.avgMonthlyExpenses)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Économies</CardTitle>
</CardHeader>
<CardContent>
<div
className={cn(
"text-2xl font-bold",
stats.totalIncome - stats.totalExpenses >= 0 ? "text-emerald-600" : "text-red-600",
)}
>
{formatCurrency(stats.totalIncome - stats.totalExpenses)}
</div>
</CardContent>
</Card>
</div>
{/* Charts */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Monthly Income vs Expenses */}
<Card>
<CardHeader>
<CardTitle>Revenus vs Dépenses par mois</CardTitle>
</CardHeader>
<CardContent>
{stats.monthlyChartData.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats.monthlyChartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="month" className="text-xs" />
<YAxis className="text-xs" tickFormatter={(v) => `${v}`} />
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Legend />
<Bar dataKey="revenus" fill="#22c55e" radius={[4, 4, 0, 0]} />
<Bar dataKey="depenses" fill="#ef4444" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
{/* Category Breakdown */}
<Card>
<CardHeader>
<CardTitle>Répartition par catégorie</CardTitle>
</CardHeader>
<CardContent>
{stats.categoryChartData.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={stats.categoryChartData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="value"
>
{stats.categoryChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Legend formatter={(value) => <span className="text-sm text-foreground">{value}</span>} />
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
{/* Balance Evolution */}
<Card>
<CardHeader>
<CardTitle>Évolution du solde</CardTitle>
</CardHeader>
<CardContent>
{stats.balanceChartData.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={stats.balanceChartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="date" className="text-xs" interval="preserveStartEnd" />
<YAxis className="text-xs" tickFormatter={(v) => `${v}`} />
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Line type="monotone" dataKey="solde" stroke="#6366f1" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
{/* Top Expenses */}
<Card>
<CardHeader>
<CardTitle>Top 5 dépenses</CardTitle>
</CardHeader>
<CardContent>
{stats.topExpenses.length > 0 ? (
<div className="space-y-4">
{stats.topExpenses.map((expense, index) => {
const category = data.categories.find((c) => c.id === expense.categoryId)
return (
<div key={expense.id} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-sm font-semibold">
{index + 1}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{expense.description}</p>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{new Date(expense.date).toLocaleDateString("fr-FR")}
</span>
{category && (
<span
className="text-xs px-1.5 py-0.5 rounded inline-flex items-center gap-1"
style={{ backgroundColor: `${category.color}20`, color: category.color }}
>
<CategoryIcon icon={category.icon} color={category.color} size={10} />
{category.name}
</span>
)}
</div>
</div>
<div className="text-red-600 font-semibold tabular-nums">
{formatCurrency(expense.amount)}
</div>
</div>
)
})}
</div>
) : (
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
Pas de dépenses pour cette période
</div>
)}
</CardContent>
</Card>
</div>
</div>
</main>
</div>
)
}