diff --git a/app/categories/page.tsx b/app/categories/page.tsx index 6e9d336..7127f0f 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -29,6 +29,7 @@ import { } from "@/lib/store-db"; import type { Category, Transaction } from "@/lib/types"; import { invalidateAllCategoryQueries } from "@/lib/cache-utils"; +import { useLocalStorage } from "@/hooks/use-local-storage"; interface RecategorizationResult { transaction: Transaction; @@ -58,6 +59,12 @@ export default function CategoriesPage() { const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false); const [isRecategorizing, setIsRecategorizing] = useState(false); + // Persister l'état "tout déplier" dans le localStorage + const [expandAllByDefault, setExpandAllByDefault] = useLocalStorage( + "categories-expand-all-by-default", + true + ); + // Organiser les catégories par parent const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => { @@ -97,13 +104,17 @@ export default function CategoriesPage() { }; }, [metadata?.categories]); - // Initialiser tous les parents comme ouverts + // Initialiser tous les parents selon la préférence sauvegardée useEffect(() => { if (parentCategories.length > 0 && expandedParents.size === 0) { - setExpandedParents(new Set(parentCategories.map((p: Category) => p.id))); + if (expandAllByDefault) { + setExpandedParents(new Set(parentCategories.map((p: Category) => p.id))); + } else { + setExpandedParents(new Set()); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [parentCategories.length]); + }, [parentCategories.length, expandAllByDefault]); const refresh = useCallback(() => { invalidateAllCategoryQueries(queryClient); @@ -162,10 +173,12 @@ export default function CategoriesPage() { const expandAll = () => { setExpandedParents(new Set(parentCategories.map((p: Category) => p.id))); + setExpandAllByDefault(true); }; const collapseAll = () => { setExpandedParents(new Set()); + setExpandAllByDefault(false); }; const allExpanded = diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 128026b..5df3931 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; import { StatsSummaryCards, @@ -46,6 +46,7 @@ import { Button } from "@/components/ui/button"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import { useIsMobile } from "@/hooks/use-mobile"; +import { useLocalStorage } from "@/hooks/use-local-storage"; import type { Account, Category } from "@/lib/types"; type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all"; @@ -54,21 +55,59 @@ export default function StatisticsPage() { const { data, isLoading } = useBankingData(); const isMobile = useIsMobile(); const [sheetOpen, setSheetOpen] = useState(false); - const [period, setPeriod] = useState("6months"); - const [selectedAccounts, setSelectedAccounts] = useState(["all"]); - const [selectedCategories, setSelectedCategories] = useState([ - "all", - ]); + + // Persister les filtres dans le localStorage + const [period, setPeriod] = useLocalStorage( + "statistics-period", + "6months" + ); + const [selectedAccounts, setSelectedAccounts] = useLocalStorage( + "statistics-selected-accounts", + ["all"] + ); + const [selectedCategories, setSelectedCategories] = useLocalStorage( + "statistics-selected-categories", + ["all"] + ); const [excludeInternalTransfers, setExcludeInternalTransfers] = - useState(true); - const [customStartDate, setCustomStartDate] = useState( - undefined, + useLocalStorage("statistics-exclude-internal-transfers", true); + + // Pour les dates, on stocke les ISO strings et on les convertit + const [customStartDateISO, setCustomStartDateISO] = useLocalStorage< + string | null + >("statistics-custom-start-date", null); + const [customEndDateISO, setCustomEndDateISO] = useLocalStorage< + string | null + >("statistics-custom-end-date", null); + + // Convertir les ISO strings en Date + const customStartDate = useMemo( + () => (customStartDateISO ? new Date(customStartDateISO) : undefined), + [customStartDateISO] ); - const [customEndDate, setCustomEndDate] = useState( - undefined, + const customEndDate = useMemo( + () => (customEndDateISO ? new Date(customEndDateISO) : undefined), + [customEndDateISO] ); + + // Fonctions pour mettre à jour les dates avec persistance + const setCustomStartDate = (date: Date | undefined) => { + setCustomStartDateISO(date ? date.toISOString() : null); + }; + const setCustomEndDate = (date: Date | undefined) => { + setCustomEndDateISO(date ? date.toISOString() : null); + }; + const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false); + // Nettoyer les dates personnalisées quand on change de période (sauf si on passe à "custom") + useEffect(() => { + if (period !== "custom" && (customStartDateISO || customEndDateISO)) { + setCustomStartDateISO(null); + setCustomEndDateISO(null); + } + }, [period, customStartDateISO, customEndDateISO, setCustomStartDateISO, setCustomEndDateISO]); + // Get start date based on period const startDate = useMemo(() => { const now = new Date(); diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index b2ee4d9..cdc5eff 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import { PageLayout, PageHeader } from "@/components/layout"; -import { RefreshCw, Receipt, Euro, ChevronDown } from "lucide-react"; +import { RefreshCw, Receipt, Euro, ChevronDown, ChevronUp } from "lucide-react"; import { TransactionFilters, TransactionBulkActions, @@ -27,6 +27,7 @@ import { useTransactionsPage } from "@/hooks/use-transactions-page"; import { useTransactionMutations } from "@/hooks/use-transaction-mutations"; import { useTransactionRules } from "@/hooks/use-transaction-rules"; import { useTransactionsChartData } from "@/hooks/use-transactions-chart-data"; +import { useLocalStorage } from "@/hooks/use-local-storage"; export default function TransactionsPage() { const queryClient = useQueryClient(); @@ -150,6 +151,12 @@ export default function TransactionsPage() { const totalAmount = chartTotalAmount ?? 0; const displayTotalCount = chartTotalCount ?? totalTransactions; + // Persist statistics collapsed state in localStorage + const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage( + "transactions-stats-expanded", + true + ); + // For filter comboboxes, we'll use empty arrays for now // They can be enhanced later with separate queries if needed const transactionsForAccountFilter: never[] = []; @@ -213,15 +220,24 @@ export default function TransactionsPage() { {(!isLoadingChart || !isLoadingTransactions) && ( - + Statistiques diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx index 2be50ef..0f125ad 100644 --- a/components/dashboard/sidebar.tsx +++ b/components/dashboard/sidebar.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { signOut } from "next-auth/react"; @@ -21,6 +20,7 @@ import { import { toast } from "sonner"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { useIsMobile } from "@/hooks/use-mobile"; +import { useLocalStorage } from "@/hooks/use-local-storage"; const navItems = [ { href: "/", label: "Tableau de bord", icon: LayoutDashboard }, @@ -158,7 +158,7 @@ interface SidebarProps { } export function Sidebar({ open, onOpenChange }: SidebarProps) { - const [collapsed, setCollapsed] = useState(false); + const [collapsed, setCollapsed] = useLocalStorage("sidebar-collapsed", false); const isMobile = useIsMobile(); if (isMobile) { diff --git a/components/transactions/transaction-table.tsx b/components/transactions/transaction-table.tsx index 02fc445..5c09eb1 100644 --- a/components/transactions/transaction-table.tsx +++ b/components/transactions/transaction-table.tsx @@ -446,7 +446,7 @@ export function TransactionTable({
Compte
-
+
Catégorie
@@ -552,7 +552,7 @@ export function TransactionTable({ {account?.name || "-"}
e.stopPropagation()} > {updatingTransactionIds.has(transaction.id) && ( diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts new file mode 100644 index 0000000..e442dfe --- /dev/null +++ b/hooks/use-local-storage.ts @@ -0,0 +1,45 @@ +"use client"; + +import { useState } from "react"; + +/** + * Hook pour gérer la persistance d'une valeur dans le localStorage + */ +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void] { + // État pour stocker la valeur + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === "undefined") { + return initialValue; + } + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + // Fonction pour mettre à jour la valeur + const setValue = (value: T | ((val: T) => T)) => { + try { + // Permet d'utiliser une fonction comme setState + const valueToStore = + value instanceof Function ? value(storedValue) : value; + + setStoredValue(valueToStore); + + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.error(`Error setting localStorage key "${key}":`, error); + } + }; + + return [storedValue, setValue]; +} +