refactor: standardize quotation marks across all files and improve code consistency
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
"use client"
|
||||
"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 { 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,
|
||||
@@ -21,111 +27,130 @@ import {
|
||||
LineChart,
|
||||
Line,
|
||||
Legend,
|
||||
} from "recharts"
|
||||
import { cn } from "@/lib/utils"
|
||||
} from "recharts";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Period = "3months" | "6months" | "12months" | "all"
|
||||
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 { data, isLoading } = useBankingData();
|
||||
const [period, setPeriod] = useState<Period>("6months");
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>("all");
|
||||
|
||||
const stats = useMemo(() => {
|
||||
if (!data) return null
|
||||
if (!data) return null;
|
||||
|
||||
const now = new Date()
|
||||
let startDate: Date
|
||||
const now = new Date();
|
||||
let startDate: Date;
|
||||
|
||||
switch (period) {
|
||||
case "3months":
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1)
|
||||
break
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1);
|
||||
break;
|
||||
case "6months":
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1)
|
||||
break
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1);
|
||||
break;
|
||||
case "12months":
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1)
|
||||
break
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1);
|
||||
break;
|
||||
default:
|
||||
startDate = new Date(0)
|
||||
startDate = new Date(0);
|
||||
}
|
||||
|
||||
let transactions = data.transactions.filter((t) => new Date(t.date) >= startDate)
|
||||
let transactions = data.transactions.filter(
|
||||
(t) => new Date(t.date) >= startDate,
|
||||
);
|
||||
|
||||
if (selectedAccount !== "all") {
|
||||
transactions = transactions.filter((t) => t.accountId === selectedAccount)
|
||||
transactions = transactions.filter(
|
||||
(t) => t.accountId === selectedAccount,
|
||||
);
|
||||
}
|
||||
|
||||
// Monthly breakdown
|
||||
const monthlyData = new Map<string, { income: number; expenses: number }>()
|
||||
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 }
|
||||
const monthKey = t.date.substring(0, 7);
|
||||
const current = monthlyData.get(monthKey) || { income: 0, expenses: 0 };
|
||||
if (t.amount >= 0) {
|
||||
current.income += t.amount
|
||||
current.income += t.amount;
|
||||
} else {
|
||||
current.expenses += Math.abs(t.amount)
|
||||
current.expenses += Math.abs(t.amount);
|
||||
}
|
||||
monthlyData.set(monthKey, current)
|
||||
})
|
||||
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" }),
|
||||
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>()
|
||||
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 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)
|
||||
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)
|
||||
.slice(0, 8);
|
||||
|
||||
// Top expenses
|
||||
const topExpenses = transactions
|
||||
.filter((t) => t.amount < 0)
|
||||
.sort((a, b) => a.amount - b.amount)
|
||||
.slice(0, 5)
|
||||
.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
|
||||
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())
|
||||
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>()
|
||||
let runningBalance = 0;
|
||||
const balanceByDate = new Map<string, number>();
|
||||
sortedTransactions.forEach((t) => {
|
||||
runningBalance += t.amount
|
||||
balanceByDate.set(t.date, runningBalance)
|
||||
})
|
||||
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),
|
||||
}))
|
||||
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,
|
||||
@@ -136,8 +161,8 @@ export default function StatisticsPage() {
|
||||
avgMonthlyExpenses,
|
||||
balanceChartData,
|
||||
transactionCount: transactions.length,
|
||||
}
|
||||
}, [data, period, selectedAccount])
|
||||
};
|
||||
}, [data, period, selectedAccount]);
|
||||
|
||||
if (isLoading || !data || !stats) {
|
||||
return (
|
||||
@@ -147,15 +172,15 @@ export default function StatisticsPage() {
|
||||
<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)
|
||||
}
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
@@ -164,11 +189,18 @@ export default function StatisticsPage() {
|
||||
<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>
|
||||
<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}>
|
||||
<Select
|
||||
value={selectedAccount}
|
||||
onValueChange={setSelectedAccount}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Compte" />
|
||||
</SelectTrigger>
|
||||
@@ -181,7 +213,10 @@ export default function StatisticsPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={period} onValueChange={(v) => setPeriod(v as Period)}>
|
||||
<Select
|
||||
value={period}
|
||||
onValueChange={(v) => setPeriod(v as Period)}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Période" />
|
||||
</SelectTrigger>
|
||||
@@ -205,7 +240,9 @@ export default function StatisticsPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-emerald-600">{formatCurrency(stats.totalIncome)}</div>
|
||||
<div className="text-2xl font-bold text-emerald-600">
|
||||
{formatCurrency(stats.totalIncome)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -217,7 +254,9 @@ export default function StatisticsPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{formatCurrency(stats.totalExpenses)}</div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{formatCurrency(stats.totalExpenses)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -229,19 +268,25 @@ export default function StatisticsPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatCurrency(stats.avgMonthlyExpenses)}</div>
|
||||
<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>
|
||||
<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",
|
||||
stats.totalIncome - stats.totalExpenses >= 0
|
||||
? "text-emerald-600"
|
||||
: "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(stats.totalIncome - stats.totalExpenses)}
|
||||
@@ -262,9 +307,15 @@ export default function StatisticsPage() {
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={stats.monthlyChartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
className="stroke-muted"
|
||||
/>
|
||||
<XAxis dataKey="month" className="text-xs" />
|
||||
<YAxis className="text-xs" tickFormatter={(v) => `${v}€`} />
|
||||
<YAxis
|
||||
className="text-xs"
|
||||
tickFormatter={(v) => `${v}€`}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
@@ -274,8 +325,16 @@ export default function StatisticsPage() {
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="revenus" fill="#22c55e" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="depenses" fill="#ef4444" radius={[4, 4, 0, 0]} />
|
||||
<Bar
|
||||
dataKey="revenus"
|
||||
fill="#22c55e"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="depenses"
|
||||
fill="#ef4444"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -318,7 +377,13 @@ export default function StatisticsPage() {
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Legend formatter={(value) => <span className="text-sm text-foreground">{value}</span>} />
|
||||
<Legend
|
||||
formatter={(value) => (
|
||||
<span className="text-sm text-foreground">
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -340,9 +405,19 @@ export default function StatisticsPage() {
|
||||
<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}€`} />
|
||||
<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={{
|
||||
@@ -351,7 +426,13 @@ export default function StatisticsPage() {
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Line type="monotone" dataKey="solde" stroke="#6366f1" strokeWidth={2} dot={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="solde"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -372,24 +453,40 @@ export default function StatisticsPage() {
|
||||
{stats.topExpenses.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{stats.topExpenses.map((expense, index) => {
|
||||
const category = data.categories.find((c) => c.id === expense.categoryId)
|
||||
const category = data.categories.find(
|
||||
(c) => c.id === expense.categoryId,
|
||||
);
|
||||
return (
|
||||
<div key={expense.id} className="flex items-center gap-3">
|
||||
<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>
|
||||
<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")}
|
||||
{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 }}
|
||||
style={{
|
||||
backgroundColor: `${category.color}20`,
|
||||
color: category.color,
|
||||
}}
|
||||
>
|
||||
<CategoryIcon icon={category.icon} color={category.color} size={10} />
|
||||
<CategoryIcon
|
||||
icon={category.icon}
|
||||
color={category.color}
|
||||
size={10}
|
||||
/>
|
||||
{category.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -399,7 +496,7 @@ export default function StatisticsPage() {
|
||||
{formatCurrency(expense.amount)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
@@ -413,5 +510,5 @@ export default function StatisticsPage() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user