refactor: improve code formatting and consistency in StatisticsPage and TopExpensesList components; standardize quotation marks and enhance readability across various sections
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
This commit is contained in:
@@ -59,15 +59,15 @@ export default function StatisticsPage() {
|
|||||||
// Persister les filtres dans le localStorage
|
// Persister les filtres dans le localStorage
|
||||||
const [period, setPeriod] = useLocalStorage<Period>(
|
const [period, setPeriod] = useLocalStorage<Period>(
|
||||||
"statistics-period",
|
"statistics-period",
|
||||||
"6months",
|
"6months"
|
||||||
);
|
);
|
||||||
const [selectedAccounts, setSelectedAccounts] = useLocalStorage<string[]>(
|
const [selectedAccounts, setSelectedAccounts] = useLocalStorage<string[]>(
|
||||||
"statistics-selected-accounts",
|
"statistics-selected-accounts",
|
||||||
["all"],
|
["all"]
|
||||||
);
|
);
|
||||||
const [selectedCategories, setSelectedCategories] = useLocalStorage<string[]>(
|
const [selectedCategories, setSelectedCategories] = useLocalStorage<string[]>(
|
||||||
"statistics-selected-categories",
|
"statistics-selected-categories",
|
||||||
["all"],
|
["all"]
|
||||||
);
|
);
|
||||||
const [excludeInternalTransfers, setExcludeInternalTransfers] =
|
const [excludeInternalTransfers, setExcludeInternalTransfers] =
|
||||||
useLocalStorage("statistics-exclude-internal-transfers", true);
|
useLocalStorage("statistics-exclude-internal-transfers", true);
|
||||||
@@ -83,11 +83,11 @@ export default function StatisticsPage() {
|
|||||||
// Convertir les ISO strings en Date
|
// Convertir les ISO strings en Date
|
||||||
const customStartDate = useMemo(
|
const customStartDate = useMemo(
|
||||||
() => (customStartDateISO ? new Date(customStartDateISO) : undefined),
|
() => (customStartDateISO ? new Date(customStartDateISO) : undefined),
|
||||||
[customStartDateISO],
|
[customStartDateISO]
|
||||||
);
|
);
|
||||||
const customEndDate = useMemo(
|
const customEndDate = useMemo(
|
||||||
() => (customEndDateISO ? new Date(customEndDateISO) : undefined),
|
() => (customEndDateISO ? new Date(customEndDateISO) : undefined),
|
||||||
[customEndDateISO],
|
[customEndDateISO]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fonctions pour mettre à jour les dates avec persistance
|
// Fonctions pour mettre à jour les dates avec persistance
|
||||||
@@ -145,7 +145,7 @@ export default function StatisticsPage() {
|
|||||||
const internalTransferCategory = useMemo(() => {
|
const internalTransferCategory = useMemo(() => {
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
return data.categories.find(
|
return data.categories.find(
|
||||||
(c) => c.name.toLowerCase() === "virement interne",
|
(c) => c.name.toLowerCase() === "virement interne"
|
||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ export default function StatisticsPage() {
|
|||||||
// Filter by accounts
|
// Filter by accounts
|
||||||
if (!selectedAccounts.includes("all")) {
|
if (!selectedAccounts.includes("all")) {
|
||||||
transactions = transactions.filter((t) =>
|
transactions = transactions.filter((t) =>
|
||||||
selectedAccounts.includes(t.accountId),
|
selectedAccounts.includes(t.accountId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ export default function StatisticsPage() {
|
|||||||
transactions = transactions.filter((t) => !t.categoryId);
|
transactions = transactions.filter((t) => !t.categoryId);
|
||||||
} else {
|
} else {
|
||||||
transactions = transactions.filter(
|
transactions = transactions.filter(
|
||||||
(t) => t.categoryId && selectedCategories.includes(t.categoryId),
|
(t) => t.categoryId && selectedCategories.includes(t.categoryId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +279,7 @@ export default function StatisticsPage() {
|
|||||||
// Exclude "Virement interne" category if checkbox is checked
|
// Exclude "Virement interne" category if checkbox is checked
|
||||||
if (excludeInternalTransfers && internalTransferCategory) {
|
if (excludeInternalTransfers && internalTransferCategory) {
|
||||||
transactions = transactions.filter(
|
transactions = transactions.filter(
|
||||||
(t) => t.categoryId !== internalTransferCategory.id,
|
(t) => t.categoryId !== internalTransferCategory.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ export default function StatisticsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const categoryChartDataByParent = Array.from(
|
const categoryChartDataByParent = Array.from(
|
||||||
categoryTotalsByParent.entries(),
|
categoryTotalsByParent.entries()
|
||||||
)
|
)
|
||||||
.map(([groupId, total]) => {
|
.map(([groupId, total]) => {
|
||||||
const category = data.categories.find((c) => c.id === groupId);
|
const category = data.categories.find((c) => c.id === groupId);
|
||||||
@@ -368,7 +368,7 @@ export default function StatisticsPage() {
|
|||||||
|
|
||||||
// Top expenses by top parent categories - deduplicate by ID
|
// Top expenses by top parent categories - deduplicate by ID
|
||||||
const uniqueTransactions = Array.from(
|
const uniqueTransactions = Array.from(
|
||||||
new Map(transactions.map((t) => [t.id, t])).values(),
|
new Map(transactions.map((t) => [t.id, t])).values()
|
||||||
);
|
);
|
||||||
const expenses = uniqueTransactions.filter((t) => t.amount < 0);
|
const expenses = uniqueTransactions.filter((t) => t.amount < 0);
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ export default function StatisticsPage() {
|
|||||||
|
|
||||||
// Balance evolution - Aggregated (using filtered transactions)
|
// Balance evolution - Aggregated (using filtered transactions)
|
||||||
const sortedFilteredTransactions = [...transactions].sort(
|
const sortedFilteredTransactions = [...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()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate starting balance: initialBalance + transactions before startDate
|
// Calculate starting balance: initialBalance + transactions before startDate
|
||||||
@@ -437,7 +437,7 @@ export default function StatisticsPage() {
|
|||||||
// Start with initial balances
|
// Start with initial balances
|
||||||
runningBalance = accountsToUse.reduce(
|
runningBalance = accountsToUse.reduce(
|
||||||
(sum, acc) => sum + (acc.initialBalance || 0),
|
(sum, acc) => sum + (acc.initialBalance || 0),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add all transactions before the start date for these accounts
|
// Add all transactions before the start date for these accounts
|
||||||
@@ -474,7 +474,7 @@ export default function StatisticsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const aggregatedBalanceData = Array.from(
|
const aggregatedBalanceData = Array.from(
|
||||||
aggregatedBalanceByDate.entries(),
|
aggregatedBalanceByDate.entries()
|
||||||
).map(([date, balance]) => ({
|
).map(([date, balance]) => ({
|
||||||
date: new Date(date).toLocaleDateString("fr-FR", {
|
date: new Date(date).toLocaleDateString("fr-FR", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
@@ -697,6 +697,7 @@ export default function StatisticsPage() {
|
|||||||
aggregatedBalanceData,
|
aggregatedBalanceData,
|
||||||
perAccountBalanceData,
|
perAccountBalanceData,
|
||||||
transactionCount: transactions.length,
|
transactionCount: transactions.length,
|
||||||
|
transactions, // Toutes les transactions filtrées pour le graphique
|
||||||
savingsTrendData,
|
savingsTrendData,
|
||||||
categoryTrendData,
|
categoryTrendData,
|
||||||
categoryTrendDataByParent,
|
categoryTrendDataByParent,
|
||||||
@@ -931,7 +932,7 @@ export default function StatisticsPage() {
|
|||||||
onRemoveAccount={(id) => {
|
onRemoveAccount={(id) => {
|
||||||
const newAccounts = selectedAccounts.filter((a) => a !== id);
|
const newAccounts = selectedAccounts.filter((a) => a !== id);
|
||||||
setSelectedAccounts(
|
setSelectedAccounts(
|
||||||
newAccounts.length > 0 ? newAccounts : ["all"],
|
newAccounts.length > 0 ? newAccounts : ["all"]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClearAccounts={() => setSelectedAccounts(["all"])}
|
onClearAccounts={() => setSelectedAccounts(["all"])}
|
||||||
@@ -939,7 +940,7 @@ export default function StatisticsPage() {
|
|||||||
onRemoveCategory={(id) => {
|
onRemoveCategory={(id) => {
|
||||||
const newCategories = selectedCategories.filter((c) => c !== id);
|
const newCategories = selectedCategories.filter((c) => c !== id);
|
||||||
setSelectedCategories(
|
setSelectedCategories(
|
||||||
newCategories.length > 0 ? newCategories : ["all"],
|
newCategories.length > 0 ? newCategories : ["all"]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClearCategories={() => setSelectedCategories(["all"])}
|
onClearCategories={() => setSelectedCategories(["all"])}
|
||||||
@@ -1129,17 +1130,17 @@ export default function StatisticsPage() {
|
|||||||
onRemoveAccount={(id) => {
|
onRemoveAccount={(id) => {
|
||||||
const newAccounts = selectedAccounts.filter((a) => a !== id);
|
const newAccounts = selectedAccounts.filter((a) => a !== id);
|
||||||
setSelectedAccounts(
|
setSelectedAccounts(
|
||||||
newAccounts.length > 0 ? newAccounts : ["all"],
|
newAccounts.length > 0 ? newAccounts : ["all"]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClearAccounts={() => setSelectedAccounts(["all"])}
|
onClearAccounts={() => setSelectedAccounts(["all"])}
|
||||||
selectedCategories={selectedCategories}
|
selectedCategories={selectedCategories}
|
||||||
onRemoveCategory={(id) => {
|
onRemoveCategory={(id) => {
|
||||||
const newCategories = selectedCategories.filter(
|
const newCategories = selectedCategories.filter(
|
||||||
(c) => c !== id,
|
(c) => c !== id
|
||||||
);
|
);
|
||||||
setSelectedCategories(
|
setSelectedCategories(
|
||||||
newCategories.length > 0 ? newCategories : ["all"],
|
newCategories.length > 0 ? newCategories : ["all"]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClearCategories={() => setSelectedCategories(["all"])}
|
onClearCategories={() => setSelectedCategories(["all"])}
|
||||||
@@ -1244,6 +1245,7 @@ export default function StatisticsPage() {
|
|||||||
expensesByCategory={stats.topExpensesByCategory}
|
expensesByCategory={stats.topExpensesByCategory}
|
||||||
categories={data.categories}
|
categories={data.categories}
|
||||||
formatCurrency={formatCurrency}
|
formatCurrency={formatCurrency}
|
||||||
|
allTransactions={stats.transactions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1289,7 +1291,7 @@ function ActiveFilters({
|
|||||||
|
|
||||||
const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id));
|
const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id));
|
||||||
const selectedCats = categories.filter((c) =>
|
const selectedCats = categories.filter((c) =>
|
||||||
selectedCategories.includes(c.id),
|
selectedCategories.includes(c.id)
|
||||||
);
|
);
|
||||||
const isUncategorized = selectedCategories.includes("uncategorized");
|
const isUncategorized = selectedCategories.includes("uncategorized");
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from "recharts";
|
||||||
|
import { List, ListOrdered } from "lucide-react";
|
||||||
import type { Transaction, Category } from "@/lib/types";
|
import type { Transaction, Category } from "@/lib/types";
|
||||||
|
|
||||||
interface TopExpensesListProps {
|
interface TopExpensesListProps {
|
||||||
@@ -15,14 +27,17 @@ interface TopExpensesListProps {
|
|||||||
}>;
|
}>;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
formatCurrency: (amount: number) => string;
|
formatCurrency: (amount: number) => string;
|
||||||
|
allTransactions?: Transaction[]; // Toutes les transactions filtrées pour calculer toutes les dépenses
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopExpensesList({
|
export function TopExpensesList({
|
||||||
expensesByCategory,
|
expensesByCategory,
|
||||||
categories,
|
categories,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
|
allTransactions = [],
|
||||||
}: TopExpensesListProps) {
|
}: TopExpensesListProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [showAllExpenses, setShowAllExpenses] = useState(false);
|
||||||
|
|
||||||
// Filtrer les catégories qui ont des dépenses
|
// Filtrer les catégories qui ont des dépenses
|
||||||
const categoriesWithExpenses = expensesByCategory.filter(
|
const categoriesWithExpenses = expensesByCategory.filter(
|
||||||
@@ -37,6 +52,216 @@ export function TopExpensesList({
|
|||||||
? categoriesWithExpenses[0].categoryId || "uncategorized"
|
? categoriesWithExpenses[0].categoryId || "uncategorized"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(() => defaultTabValue);
|
||||||
|
|
||||||
|
// Mettre à jour activeTab quand defaultTabValue change ou si activeTab est invalide
|
||||||
|
useEffect(() => {
|
||||||
|
if (!defaultTabValue) return;
|
||||||
|
|
||||||
|
// Vérifier si activeTab correspond à une catégorie valide
|
||||||
|
const isValidTab = categoriesWithExpenses.some(
|
||||||
|
(group) => (group.categoryId || "uncategorized") === activeTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si activeTab est vide ou invalide, utiliser defaultTabValue
|
||||||
|
if (!activeTab || !isValidTab) {
|
||||||
|
setActiveTab(defaultTabValue);
|
||||||
|
}
|
||||||
|
}, [defaultTabValue, categoriesWithExpenses, activeTab]);
|
||||||
|
|
||||||
|
// Calculer les données du graphique pour la catégorie active
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
if (!hasExpenses) return [];
|
||||||
|
|
||||||
|
// Utiliser activeTab ou defaultTabValue comme fallback
|
||||||
|
const currentTab = activeTab || defaultTabValue;
|
||||||
|
if (!currentTab) return [];
|
||||||
|
|
||||||
|
const activeCategoryGroup = categoriesWithExpenses.find(
|
||||||
|
(group) => (group.categoryId || "uncategorized") === currentTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activeCategoryGroup) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si showAllExpenses est activé et qu'on a toutes les transactions, utiliser toutes les dépenses de la catégorie
|
||||||
|
let expenses: Transaction[];
|
||||||
|
if (showAllExpenses && allTransactions.length > 0) {
|
||||||
|
// Filtrer toutes les transactions pour obtenir toutes les dépenses de cette catégorie parente
|
||||||
|
const categoryId = activeCategoryGroup.categoryId;
|
||||||
|
expenses = allTransactions
|
||||||
|
.filter((t) => t.amount < 0) // Seulement les dépenses
|
||||||
|
.filter((t) => {
|
||||||
|
if (categoryId === null) {
|
||||||
|
return !t.categoryId;
|
||||||
|
}
|
||||||
|
// Vérifier si la transaction appartient à cette catégorie parente ou ses sous-catégories
|
||||||
|
const category = categories.find((c) => c.id === t.categoryId);
|
||||||
|
if (!category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const transactionGroupId = category.parentId || category.id;
|
||||||
|
return transactionGroupId === categoryId;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Utiliser seulement les top 10
|
||||||
|
expenses = activeCategoryGroup.expenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenses.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grouper les dépenses par période
|
||||||
|
// Si moins de 30 jours, groupe par jour
|
||||||
|
// Si moins de 6 mois, groupe par semaine
|
||||||
|
// Sinon groupe par mois
|
||||||
|
const sortedExpenses = [...expenses].sort(
|
||||||
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sortedExpenses.length === 0) return [];
|
||||||
|
|
||||||
|
const firstDate = new Date(sortedExpenses[0].date);
|
||||||
|
const lastDate = new Date(
|
||||||
|
sortedExpenses[sortedExpenses.length - 1].date,
|
||||||
|
);
|
||||||
|
const daysDiff =
|
||||||
|
(lastDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
let groupBy: "day" | "week" | "month";
|
||||||
|
if (daysDiff <= 30) {
|
||||||
|
groupBy = "day";
|
||||||
|
} else if (daysDiff <= 180) {
|
||||||
|
groupBy = "week";
|
||||||
|
} else {
|
||||||
|
groupBy = "month";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction helper pour obtenir la clé de période d'une date
|
||||||
|
const getPeriodKey = (date: Date): { key: string; dateKey: string } => {
|
||||||
|
if (groupBy === "day") {
|
||||||
|
return {
|
||||||
|
key: date.toLocaleDateString("fr-FR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
}),
|
||||||
|
dateKey: date.toISOString().substring(0, 10), // YYYY-MM-DD
|
||||||
|
};
|
||||||
|
} else if (groupBy === "week") {
|
||||||
|
// Semaine commençant le lundi
|
||||||
|
const weekStart = new Date(date);
|
||||||
|
const day = weekStart.getDay();
|
||||||
|
const diff = weekStart.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
|
weekStart.setDate(diff);
|
||||||
|
return {
|
||||||
|
key: `Sem. ${weekStart.toLocaleDateString("fr-FR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
})}`,
|
||||||
|
dateKey: weekStart.toISOString().substring(0, 10), // YYYY-MM-DD
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
key: date.toLocaleDateString("fr-FR", {
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
|
dateKey: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`, // YYYY-MM
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grouper les dépenses par période
|
||||||
|
const groupedData = new Map<
|
||||||
|
string,
|
||||||
|
{ total: number; dateKey: string }
|
||||||
|
>();
|
||||||
|
|
||||||
|
sortedExpenses.forEach((expense) => {
|
||||||
|
const date = new Date(expense.date);
|
||||||
|
const { key, dateKey } = getPeriodKey(date);
|
||||||
|
|
||||||
|
const current = groupedData.get(key);
|
||||||
|
if (current) {
|
||||||
|
current.total += Math.abs(expense.amount);
|
||||||
|
} else {
|
||||||
|
groupedData.set(key, {
|
||||||
|
total: Math.abs(expense.amount),
|
||||||
|
dateKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Générer toutes les périodes entre la première et la dernière date
|
||||||
|
const allPeriodsMap = new Map<
|
||||||
|
string,
|
||||||
|
{ period: string; montant: number; dateKey: string }
|
||||||
|
>();
|
||||||
|
const currentDate = new Date(firstDate);
|
||||||
|
const endDate = new Date(lastDate);
|
||||||
|
|
||||||
|
// Normaliser les dates selon le type de groupement
|
||||||
|
if (groupBy === "day") {
|
||||||
|
currentDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
} else if (groupBy === "week") {
|
||||||
|
// Début de la semaine de la première date
|
||||||
|
const day = currentDate.getDay();
|
||||||
|
const diff = currentDate.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
|
currentDate.setDate(diff);
|
||||||
|
currentDate.setHours(0, 0, 0, 0);
|
||||||
|
// Début de la semaine de la dernière date (pour la boucle)
|
||||||
|
const lastDay = endDate.getDay();
|
||||||
|
const lastDiff = endDate.getDate() - lastDay + (lastDay === 0 ? -6 : 1);
|
||||||
|
endDate.setDate(lastDiff);
|
||||||
|
endDate.setHours(0, 0, 0, 0);
|
||||||
|
} else {
|
||||||
|
// Mois : premier jour du mois
|
||||||
|
currentDate.setDate(1);
|
||||||
|
currentDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate.setDate(1);
|
||||||
|
endDate.setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (currentDate <= endDate) {
|
||||||
|
const { key, dateKey } = getPeriodKey(currentDate);
|
||||||
|
const existingData = groupedData.get(key);
|
||||||
|
|
||||||
|
// Utiliser dateKey comme clé unique pour éviter les doublons
|
||||||
|
if (!allPeriodsMap.has(dateKey)) {
|
||||||
|
allPeriodsMap.set(dateKey, {
|
||||||
|
period: key,
|
||||||
|
montant: existingData ? Math.round(existingData.total) : 0,
|
||||||
|
dateKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passer à la période suivante
|
||||||
|
if (groupBy === "day") {
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
} else if (groupBy === "week") {
|
||||||
|
currentDate.setDate(currentDate.getDate() + 7);
|
||||||
|
} else {
|
||||||
|
// Mois suivant
|
||||||
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(allPeriodsMap.values()) .sort((a, b) =>
|
||||||
|
a.dateKey.localeCompare(b.dateKey),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
hasExpenses,
|
||||||
|
categoriesWithExpenses,
|
||||||
|
activeTab,
|
||||||
|
defaultTabValue,
|
||||||
|
showAllExpenses,
|
||||||
|
allTransactions,
|
||||||
|
categories,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="card-hover">
|
<Card className="card-hover">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -46,7 +271,12 @@ export function TopExpensesList({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{hasExpenses ? (
|
{hasExpenses ? (
|
||||||
<Tabs defaultValue={defaultTabValue} className="w-full">
|
<Tabs
|
||||||
|
defaultValue={defaultTabValue}
|
||||||
|
value={activeTab || defaultTabValue}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<TabsList className="w-full flex-wrap h-auto p-1 mb-4">
|
<TabsList className="w-full flex-wrap h-auto p-1 mb-4">
|
||||||
{categoriesWithExpenses.map(({ categoryId, expenses }) => {
|
{categoriesWithExpenses.map(({ categoryId, expenses }) => {
|
||||||
const category = categoryId
|
const category = categoryId
|
||||||
@@ -149,6 +379,89 @@ export function TopExpensesList({
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* Graphique d'évolution temporelle */}
|
||||||
|
{chartData.length > 0 && (() => {
|
||||||
|
const currentTab = activeTab || defaultTabValue;
|
||||||
|
const activeCategoryGroup = categoriesWithExpenses.find(
|
||||||
|
(group) => (group.categoryId || "uncategorized") === currentTab,
|
||||||
|
);
|
||||||
|
const activeCategory = activeCategoryGroup?.categoryId
|
||||||
|
? categories.find((c) => c.id === activeCategoryGroup.categoryId)
|
||||||
|
: null;
|
||||||
|
const barColor = activeCategory?.color || "var(--destructive)";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 pt-6 border-t border-border">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-semibold">
|
||||||
|
Évolution des dépenses dans le temps
|
||||||
|
</h3>
|
||||||
|
{allTransactions.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowAllExpenses(!showAllExpenses)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{showAllExpenses ? (
|
||||||
|
<>
|
||||||
|
<ListOrdered className="w-3 h-3 mr-1.5" />
|
||||||
|
Top 10 seulement
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<List className="w-3 h-3 mr-1.5" />
|
||||||
|
Voir toutes les dépenses
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-[250px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={chartData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
className="stroke-muted"
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="period"
|
||||||
|
className="text-xs"
|
||||||
|
interval="preserveStartEnd"
|
||||||
|
angle={-45}
|
||||||
|
textAnchor="end"
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
className="text-xs"
|
||||||
|
width={80}
|
||||||
|
tickFormatter={(v) => {
|
||||||
|
if (Math.abs(v) >= 1000) {
|
||||||
|
return `${(v / 1000).toFixed(1)}k€`;
|
||||||
|
}
|
||||||
|
return `${Math.round(v)}€`;
|
||||||
|
}}
|
||||||
|
tick={{ fill: "var(--muted-foreground)" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) => formatCurrency(value)}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "var(--card)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="montant"
|
||||||
|
fill={barColor}
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[200px] flex items-center justify-center text-muted-foreground text-xs md:text-sm">
|
<div className="h-[200px] flex items-center justify-center text-muted-foreground text-xs md:text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user