Compare commits

...

5 Commits

12 changed files with 451 additions and 141 deletions

View File

@@ -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 =

View File

@@ -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<Period>("6months");
const [selectedAccounts, setSelectedAccounts] = useState<string[]>(["all"]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([
"all",
]);
// Persister les filtres dans le localStorage
const [period, setPeriod] = useLocalStorage<Period>(
"statistics-period",
"6months"
);
const [selectedAccounts, setSelectedAccounts] = useLocalStorage<string[]>(
"statistics-selected-accounts",
["all"]
);
const [selectedCategories, setSelectedCategories] = useLocalStorage<string[]>(
"statistics-selected-categories",
["all"]
);
const [excludeInternalTransfers, setExcludeInternalTransfers] =
useState(true);
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
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<Date | undefined>(
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();

View File

@@ -1,15 +1,8 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useMemo } from "react";
import { PageLayout, PageHeader } from "@/components/layout";
import {
RefreshCw,
Maximize2,
Minimize2,
Receipt,
Euro,
ChevronDown,
} from "lucide-react";
import { RefreshCw, Receipt, Euro, ChevronDown, ChevronUp } from "lucide-react";
import {
TransactionFilters,
TransactionBulkActions,
@@ -30,20 +23,16 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Upload } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
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 { useTransactionsForAccountFilter } from "@/hooks/use-transactions-for-account-filter";
import { useTransactionsForCategoryFilter } from "@/hooks/use-transactions-for-category-filter";
import { useLocalStorage } from "@/hooks/use-local-storage";
export default function TransactionsPage() {
const queryClient = useQueryClient();
const [isFullscreen, setIsFullscreen] = useState(false);
// Main page state and logic
const {
@@ -126,6 +115,28 @@ export default function TransactionsPage() {
searchQuery,
});
// Transactions for account filter (filtered by categories, period, search, reconciled - NOT by accounts)
const { transactions: transactionsForAccountFilter } =
useTransactionsForAccountFilter({
selectedCategories,
period,
customStartDate,
customEndDate,
showReconciled,
searchQuery,
});
// Transactions for category filter (filtered by accounts, period, search, reconciled - NOT by categories)
const { transactions: transactionsForCategoryFilter } =
useTransactionsForCategoryFilter({
selectedAccounts,
period,
customStartDate,
customEndDate,
showReconciled,
searchQuery,
});
const invalidateAll = useCallback(() => {
invalidateTransactions();
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
@@ -147,7 +158,11 @@ export default function TransactionsPage() {
[handleBulkSetCategory, selectedTransactions, clearSelection]
);
const filteredTransactions = transactionsData?.transactions || [];
// Stabilize transactions reference to prevent unnecessary re-renders
const filteredTransactions = useMemo(
() => transactionsData?.transactions || [],
[transactionsData?.transactions]
);
const totalTransactions = transactionsData?.total || 0;
const hasMore = transactionsData?.hasMore || false;
const uncategorizedCount = transactionsData?.uncategorizedCount || 0;
@@ -160,10 +175,11 @@ export default function TransactionsPage() {
const totalAmount = chartTotalAmount ?? 0;
const displayTotalCount = chartTotalCount ?? totalTransactions;
// For filter comboboxes, we'll use empty arrays for now
// They can be enhanced later with separate queries if needed
const transactionsForAccountFilter: never[] = [];
const transactionsForCategoryFilter: never[] = [];
// Persist statistics collapsed state in localStorage
const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage(
"transactions-stats-expanded",
true
);
// Early return for loading state - prevents sidebar flash
if (isLoadingMetadata || !metadata) {
@@ -223,15 +239,31 @@ export default function TransactionsPage() {
{(!isLoadingChart || !isLoadingTransactions) && (
<Card className="mb-6">
<Collapsible defaultOpen={true}>
<Collapsible open={isStatsExpanded} onOpenChange={setIsStatsExpanded}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 py-3 px-6">
<CardTitle className="text-base font-semibold">
Statistiques
{!isStatsExpanded && (
<span className="ml-3 text-sm font-normal text-muted-foreground">
{displayTotalCount} opération
{displayTotalCount > 1 ? "s" : ""} {" "}
{formatCurrency(totalAmount)}
</span>
)}
</CardTitle>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-8">
<ChevronDown className="w-4 h-4 mr-1" />
Réduire
{isStatsExpanded ? (
<>
<ChevronUp className="w-4 h-4 mr-1" />
Réduire
</>
) : (
<>
<ChevronDown className="w-4 h-4 mr-1" />
Afficher
</>
)}
</Button>
</CollapsibleTrigger>
</CardHeader>
@@ -310,19 +342,6 @@ export default function TransactionsPage() {
</div>
) : (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex-1" />
<Button
variant="outline"
size="sm"
onClick={() => setIsFullscreen(true)}
className="gap-2"
>
<Maximize2 className="w-4 h-4" />
Plein écran
</Button>
</div>
<TransactionTable
transactions={filteredTransactions}
accounts={metadata.accounts}
@@ -355,74 +374,6 @@ export default function TransactionsPage() {
</>
)}
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
<DialogContent
className="!max-w-none !max-h-none !w-full !h-full !p-0 flex flex-col !rounded-none !translate-x-0 !translate-y-0 !top-0 !left-0 !right-0 !bottom-0 !m-0"
showCloseButton={false}
style={{
width: "100vw",
height: "100vh",
maxWidth: "100vw",
maxHeight: "100vh",
}}
>
<DialogHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<div className="flex items-center justify-between">
<DialogTitle>Transactions</DialogTitle>
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(false)}
className="gap-2"
>
<Minimize2 className="w-4 h-4" />
Réduire
</Button>
</div>
</DialogHeader>
<div className="flex-1 overflow-auto px-6 pb-6 min-h-0">
<div className="mb-4">
<TransactionBulkActions
selectedCount={selectedTransactions.size}
categories={metadata.categories}
onReconcile={handleBulkReconcileWithClear}
onSetCategory={handleBulkSetCategoryWithClear}
/>
</div>
<TransactionTable
transactions={filteredTransactions}
accounts={metadata.accounts}
categories={metadata.categories}
selectedTransactions={selectedTransactions}
sortField={sortField}
sortOrder={sortOrder}
onSortChange={onSortChange}
onToggleSelectAll={onToggleSelectAll}
onToggleSelectTransaction={onToggleSelectTransaction}
onToggleReconciled={toggleReconciled}
onMarkReconciled={markReconciled}
onSetCategory={setCategory}
onCreateRule={handleCreateRule}
onDelete={deleteTransaction}
formatCurrency={formatCurrency}
formatDate={formatDate}
updatingTransactionIds={updatingTransactionIds}
duplicateIds={duplicateIds}
highlightDuplicates={showDuplicates}
/>
<div className="mt-4">
<TransactionPagination
page={page}
pageSize={pageSize}
total={totalTransactions}
hasMore={hasMore}
onPageChange={onPageChange}
/>
</div>
</div>
</DialogContent>
</Dialog>
<RuleCreateDialog
open={ruleDialogOpen}
onOpenChange={setRuleDialogOpen}

View File

@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CategoryIcon } from "@/components/ui/category-icon";
@@ -37,7 +38,13 @@ export function CategoryCard({
size={isMobile ? 10 : 12}
/>
</div>
<span className="text-xs md:text-sm truncate">{category.name}</span>
<Link
href={`/transactions?categoryIds=${category.id}`}
className="text-xs md:text-sm truncate hover:underline"
onClick={(e) => e.stopPropagation()}
>
{category.name}
</Link>
{!isMobile && (
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
{stats.count} opération{stats.count > 1 ? "s" : ""} {" "}

View File

@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -73,9 +74,13 @@ export function ParentCategoryRow({
size={isMobile ? 10 : 14}
/>
</div>
<span className="font-medium text-xs md:text-sm truncate">
<Link
href={`/transactions?categoryIds=${parent.id}`}
className="font-medium text-xs md:text-sm truncate hover:underline"
onClick={(e) => e.stopPropagation()}
>
{parent.name}
</span>
</Link>
{!isMobile && (
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
{children.length} {stats.count} opération

View File

@@ -36,10 +36,43 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
value: total,
color: category?.color || "#94a3b8",
icon: category?.icon || "HelpCircle",
categoryId: categoryId === "uncategorized" ? null : categoryId,
};
})
.sort((a, b) => b.value - a.value)
.slice(0, 6);
.sort((a, b) => b.value - a.value);
// Category breakdown grouped by parent
const categoryTotalsByParent = new Map<string, number>();
monthExpenses.forEach((t) => {
const category = data.categories.find((c) => c.id === t.categoryId);
// Use parent category ID if exists, otherwise use the category itself
let groupId: string;
if (!category) {
groupId = "uncategorized";
} else if (category.parentId) {
groupId = category.parentId;
} else {
// Category is a parent itself
groupId = category.id;
}
const current = categoryTotalsByParent.get(groupId) || 0;
categoryTotalsByParent.set(groupId, current + Math.abs(t.amount));
});
const chartDataByParent: CategoryChartData[] = Array.from(
categoryTotalsByParent.entries(),
)
.map(([groupId, total]) => {
const category = data.categories.find((c) => c.id === groupId);
return {
name: category?.name || "Non catégorisé",
value: total,
color: category?.color || "#94a3b8",
icon: category?.icon || "HelpCircle",
categoryId: groupId === "uncategorized" ? null : groupId,
};
})
.sort((a, b) => b.value - a.value);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("fr-FR", {
@@ -51,6 +84,8 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
return (
<CategoryPieChart
data={chartData}
dataByParent={chartDataByParent}
categories={data.categories}
formatCurrency={formatCurrency}
title="Dépenses par catégorie"
height={250}

View File

@@ -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) {

View File

@@ -169,7 +169,7 @@ export function TransactionTable({
setFocusedIndex(index);
onMarkReconciled(transactionId);
},
[onMarkReconciled],
[onMarkReconciled]
);
const handleKeyDown = useCallback(
@@ -198,7 +198,7 @@ export function TransactionTable({
}
}
},
[focusedIndex, transactions, onMarkReconciled, virtualizer],
[focusedIndex, transactions, onMarkReconciled, virtualizer]
);
useEffect(() => {
@@ -215,7 +215,7 @@ export function TransactionTable({
(accountId: string) => {
return accounts.find((a) => a.id === accountId);
},
[accounts],
[accounts]
);
const getCategory = useCallback(
@@ -223,7 +223,7 @@ export function TransactionTable({
if (!categoryId) return null;
return categories.find((c) => c.id === categoryId);
},
[categories],
[categories]
);
return (
@@ -281,7 +281,7 @@ export function TransactionTable({
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
transaction.isReconciled && "bg-emerald-500/5",
isFocused && "bg-primary/10 ring-1 ring-primary/30",
isDuplicate && "shadow-sm",
isDuplicate && "shadow-sm"
)}
>
<div className="flex items-start justify-between gap-2">
@@ -323,7 +323,7 @@ export function TransactionTable({
"font-semibold tabular-nums text-sm md:text-base shrink-0",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600",
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -358,7 +358,7 @@ export function TransactionTable({
showBadge
align="start"
disabled={updatingTransactionIds.has(
transaction.id,
transaction.id
)}
/>
</div>
@@ -391,7 +391,7 @@ export function TransactionTable({
e.stopPropagation();
if (
confirm(
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
)
) {
onDelete(transaction.id);
@@ -446,7 +446,7 @@ export function TransactionTable({
<div className="p-3 text-sm font-medium text-muted-foreground">
Compte
</div>
<div className="p-3 text-sm font-medium text-muted-foreground">
<div className="p-3 text-sm font-medium text-muted-foreground text-center">
Catégorie
</div>
<div className="p-3 text-right">
@@ -464,7 +464,7 @@ export function TransactionTable({
<div className="p-3"></div>
</div>
</div>
{/* Body virtualisé */}
{/* Body virtualisé ou non selon le mode */}
<div
ref={parentRef}
className="overflow-auto"
@@ -507,7 +507,7 @@ export function TransactionTable({
"grid grid-cols-[auto_120px_2fr_150px_180px_140px_auto_auto] gap-0 border-b border-border hover:bg-muted/50 cursor-pointer",
transaction.isReconciled && "bg-emerald-500/5",
isFocused && "bg-primary/10 ring-1 ring-primary/30",
isDuplicate && "shadow-sm",
isDuplicate && "shadow-sm"
)}
>
<div className="p-3" onClick={(e) => e.stopPropagation()}>
@@ -552,7 +552,7 @@ export function TransactionTable({
{account?.name || "-"}
</div>
<div
className="p-3 relative"
className="p-3 relative flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
{updatingTransactionIds.has(transaction.id) && (
@@ -576,7 +576,7 @@ export function TransactionTable({
"p-3 text-right font-semibold tabular-nums",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600",
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -643,7 +643,7 @@ export function TransactionTable({
e.stopPropagation();
if (
confirm(
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
)
) {
onDelete(transaction.id);

View File

@@ -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<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
// État pour stocker la valeur
const [storedValue, setStoredValue] = useState<T>(() => {
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];
}

View File

@@ -0,0 +1,109 @@
"use client";
import { useMemo } from "react";
import { useTransactions } from "@/lib/hooks";
import { useBankingMetadata } from "@/lib/hooks";
import type { TransactionsPaginatedParams } from "@/services/banking.service";
interface UseTransactionsForAccountFilterParams {
selectedCategories: string[];
period: "1month" | "3months" | "6months" | "12months" | "custom" | "all";
customStartDate?: Date;
customEndDate?: Date;
showReconciled: string;
searchQuery: string;
}
/**
* Hook to fetch transactions filtered by categories, period, search, reconciled
* but NOT by accounts. Used for displaying account totals in account filter.
*/
export function useTransactionsForAccountFilter({
selectedCategories,
period,
customStartDate,
customEndDate,
showReconciled,
searchQuery,
}: UseTransactionsForAccountFilterParams) {
const { data: metadata } = useBankingMetadata();
// Calculate start date based on period
const startDate = useMemo(() => {
const now = new Date();
switch (period) {
case "1month":
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
case "3months":
return new Date(now.getFullYear(), now.getMonth() - 3, 1);
case "6months":
return new Date(now.getFullYear(), now.getMonth() - 6, 1);
case "12months":
return new Date(now.getFullYear(), now.getMonth() - 12, 1);
case "custom":
return customStartDate || new Date(0);
default:
return new Date(0);
}
}, [period, customStartDate]);
// Calculate end date (only for custom period)
const endDate = useMemo(() => {
if (period === "custom" && customEndDate) {
return customEndDate;
}
return undefined;
}, [period, customEndDate]);
// Build params for fetching all transactions (no pagination, no account filter)
const filterParams = useMemo<TransactionsPaginatedParams>(() => {
const params: TransactionsPaginatedParams = {
limit: 10000, // Large limit to get all transactions
offset: 0,
sortField: "date",
sortOrder: "asc",
};
if (startDate && period !== "all") {
params.startDate = startDate.toISOString().split("T")[0];
}
if (endDate) {
params.endDate = endDate.toISOString().split("T")[0];
}
// NOTE: We intentionally don't filter by accounts here
if (!selectedCategories.includes("all")) {
if (selectedCategories.includes("uncategorized")) {
params.includeUncategorized = true;
} else {
params.categoryIds = selectedCategories;
}
}
if (searchQuery) {
params.search = searchQuery;
}
if (showReconciled !== "all") {
params.isReconciled = showReconciled === "reconciled";
}
return params;
}, [
startDate,
endDate,
selectedCategories,
searchQuery,
showReconciled,
period,
]);
// Fetch all filtered transactions (without account filter)
const { data: transactionsData, isLoading } = useTransactions(
filterParams,
!!metadata
);
return {
transactions: transactionsData?.transactions || [],
isLoading,
};
}

View File

@@ -0,0 +1,105 @@
"use client";
import { useMemo } from "react";
import { useTransactions } from "@/lib/hooks";
import { useBankingMetadata } from "@/lib/hooks";
import type { TransactionsPaginatedParams } from "@/services/banking.service";
interface UseTransactionsForCategoryFilterParams {
selectedAccounts: string[];
period: "1month" | "3months" | "6months" | "12months" | "custom" | "all";
customStartDate?: Date;
customEndDate?: Date;
showReconciled: string;
searchQuery: string;
}
/**
* Hook to fetch transactions filtered by accounts, period, search, reconciled
* but NOT by categories. Used for displaying transaction counts in category filter.
*/
export function useTransactionsForCategoryFilter({
selectedAccounts,
period,
customStartDate,
customEndDate,
showReconciled,
searchQuery,
}: UseTransactionsForCategoryFilterParams) {
const { data: metadata } = useBankingMetadata();
// Calculate start date based on period
const startDate = useMemo(() => {
const now = new Date();
switch (period) {
case "1month":
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
case "3months":
return new Date(now.getFullYear(), now.getMonth() - 3, 1);
case "6months":
return new Date(now.getFullYear(), now.getMonth() - 6, 1);
case "12months":
return new Date(now.getFullYear(), now.getMonth() - 12, 1);
case "custom":
return customStartDate || new Date(0);
default:
return new Date(0);
}
}, [period, customStartDate]);
// Calculate end date (only for custom period)
const endDate = useMemo(() => {
if (period === "custom" && customEndDate) {
return customEndDate;
}
return undefined;
}, [period, customEndDate]);
// Build params for fetching all transactions (no pagination, no category filter)
const filterParams = useMemo<TransactionsPaginatedParams>(() => {
const params: TransactionsPaginatedParams = {
limit: 10000, // Large limit to get all transactions
offset: 0,
sortField: "date",
sortOrder: "asc",
};
if (startDate && period !== "all") {
params.startDate = startDate.toISOString().split("T")[0];
}
if (endDate) {
params.endDate = endDate.toISOString().split("T")[0];
}
if (!selectedAccounts.includes("all")) {
params.accountIds = selectedAccounts;
}
// NOTE: We intentionally don't filter by categories here
if (searchQuery) {
params.search = searchQuery;
}
if (showReconciled !== "all") {
params.isReconciled = showReconciled === "reconciled";
}
return params;
}, [
startDate,
endDate,
selectedAccounts,
searchQuery,
showReconciled,
period,
]);
// Fetch all filtered transactions (without category filter)
const { data: transactionsData, isLoading } = useTransactions(
filterParams,
!!metadata
);
return {
transactions: transactionsData?.transactions || [],
isLoading,
};
}

View File

@@ -7,6 +7,7 @@ import {
useTransactions,
useDuplicateIds,
} from "@/lib/hooks";
import { useLocalStorage } from "@/hooks/use-local-storage";
import type { TransactionsPaginatedParams } from "@/services/banking.service";
import type { Category } from "@/lib/types";
@@ -30,7 +31,7 @@ export function useTransactionsPage() {
"all",
]);
const [showReconciled, setShowReconciled] = useState<string>("all");
const [period, setPeriod] = useState<Period>("3months");
const [period, setPeriod] = useLocalStorage<Period>("transactions-period", "3months");
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
undefined,
);