feat: refactor dashboard and account pages to utilize new layout components, enhancing structure and loading states

This commit is contained in:
Julien Froidefond
2025-11-27 12:44:44 +01:00
parent e469656e0d
commit 88937579e2
40 changed files with 2781 additions and 2226 deletions

View File

@@ -1,9 +1,15 @@
"use client";
import { useState, useMemo } from "react";
import { Sidebar } from "@/components/dashboard/sidebar";
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
import {
StatsSummaryCards,
MonthlyChart,
CategoryPieChart,
BalanceLineChart,
TopExpensesList,
} from "@/components/statistics";
import { useBankingData } from "@/lib/hooks";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
@@ -11,24 +17,6 @@ import {
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";
@@ -58,12 +46,12 @@ export default function StatisticsPage() {
}
let transactions = data.transactions.filter(
(t) => new Date(t.date) >= startDate,
(t) => new Date(t.date) >= startDate
);
if (selectedAccount !== "all") {
transactions = transactions.filter(
(t) => t.accountId === selectedAccount,
(t) => t.accountId === selectedAccount
);
}
@@ -132,7 +120,7 @@ export default function StatisticsPage() {
// Balance evolution
const sortedTransactions = [...transactions].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
let runningBalance = 0;
@@ -149,7 +137,7 @@ export default function StatisticsPage() {
month: "short",
}),
solde: Math.round(balance),
}),
})
);
return {
@@ -164,17 +152,6 @@ export default function StatisticsPage() {
};
}, [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",
@@ -182,333 +159,74 @@ export default function StatisticsPage() {
}).format(amount);
};
if (isLoading || !data || !stats) {
return <LoadingState />;
}
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>
<PageLayout>
<PageHeader
title="Statistiques"
description="Analysez vos dépenses et revenus"
actions={
<>
<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>
</>
}
/>
{/* 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>
<StatsSummaryCards
totalIncome={stats.totalIncome}
totalExpenses={stats.totalExpenses}
avgMonthlyExpenses={stats.avgMonthlyExpenses}
formatCurrency={formatCurrency}
/>
<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>
<div className="grid gap-6 lg:grid-cols-2">
<MonthlyChart
data={stats.monthlyChartData}
formatCurrency={formatCurrency}
/>
<CategoryPieChart
data={stats.categoryChartData}
formatCurrency={formatCurrency}
/>
<BalanceLineChart
data={stats.balanceChartData}
formatCurrency={formatCurrency}
/>
<TopExpensesList
expenses={stats.topExpenses}
categories={data.categories}
formatCurrency={formatCurrency}
/>
</div>
</PageLayout>
);
}