Compare commits
5 Commits
dbcf8e7abd
...
82e27524b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82e27524b5 | ||
|
|
8b62cd8385 | ||
|
|
a01345c1fb | ||
|
|
c358845033 | ||
|
|
b3e99a15d2 |
@@ -29,6 +29,7 @@ import {
|
|||||||
} from "@/lib/store-db";
|
} from "@/lib/store-db";
|
||||||
import type { Category, Transaction } from "@/lib/types";
|
import type { Category, Transaction } from "@/lib/types";
|
||||||
import { invalidateAllCategoryQueries } from "@/lib/cache-utils";
|
import { invalidateAllCategoryQueries } from "@/lib/cache-utils";
|
||||||
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
||||||
|
|
||||||
interface RecategorizationResult {
|
interface RecategorizationResult {
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
@@ -58,6 +59,12 @@ export default function CategoriesPage() {
|
|||||||
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
|
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
|
||||||
const [isRecategorizing, setIsRecategorizing] = 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
|
// Organiser les catégories par parent
|
||||||
const { parentCategories, childrenByParent, orphanCategories } =
|
const { parentCategories, childrenByParent, orphanCategories } =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
@@ -97,13 +104,17 @@ export default function CategoriesPage() {
|
|||||||
};
|
};
|
||||||
}, [metadata?.categories]);
|
}, [metadata?.categories]);
|
||||||
|
|
||||||
// Initialiser tous les parents comme ouverts
|
// Initialiser tous les parents selon la préférence sauvegardée
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parentCategories.length > 0 && expandedParents.size === 0) {
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [parentCategories.length]);
|
}, [parentCategories.length, expandAllByDefault]);
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
invalidateAllCategoryQueries(queryClient);
|
invalidateAllCategoryQueries(queryClient);
|
||||||
@@ -162,10 +173,12 @@ export default function CategoriesPage() {
|
|||||||
|
|
||||||
const expandAll = () => {
|
const expandAll = () => {
|
||||||
setExpandedParents(new Set(parentCategories.map((p: Category) => p.id)));
|
setExpandedParents(new Set(parentCategories.map((p: Category) => p.id)));
|
||||||
|
setExpandAllByDefault(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const collapseAll = () => {
|
const collapseAll = () => {
|
||||||
setExpandedParents(new Set());
|
setExpandedParents(new Set());
|
||||||
|
setExpandAllByDefault(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const allExpanded =
|
const allExpanded =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
|
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
|
||||||
import {
|
import {
|
||||||
StatsSummaryCards,
|
StatsSummaryCards,
|
||||||
@@ -46,6 +46,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
||||||
import type { Account, Category } from "@/lib/types";
|
import type { Account, Category } from "@/lib/types";
|
||||||
|
|
||||||
type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all";
|
type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all";
|
||||||
@@ -54,21 +55,59 @@ export default function StatisticsPage() {
|
|||||||
const { data, isLoading } = useBankingData();
|
const { data, isLoading } = useBankingData();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
const [period, setPeriod] = useState<Period>("6months");
|
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>(["all"]);
|
// Persister les filtres dans le localStorage
|
||||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([
|
const [period, setPeriod] = useLocalStorage<Period>(
|
||||||
"all",
|
"statistics-period",
|
||||||
]);
|
"6months"
|
||||||
|
);
|
||||||
|
const [selectedAccounts, setSelectedAccounts] = useLocalStorage<string[]>(
|
||||||
|
"statistics-selected-accounts",
|
||||||
|
["all"]
|
||||||
|
);
|
||||||
|
const [selectedCategories, setSelectedCategories] = useLocalStorage<string[]>(
|
||||||
|
"statistics-selected-categories",
|
||||||
|
["all"]
|
||||||
|
);
|
||||||
const [excludeInternalTransfers, setExcludeInternalTransfers] =
|
const [excludeInternalTransfers, setExcludeInternalTransfers] =
|
||||||
useState(true);
|
useLocalStorage("statistics-exclude-internal-transfers", true);
|
||||||
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
|
|
||||||
undefined,
|
// 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>(
|
const customEndDate = useMemo(
|
||||||
undefined,
|
() => (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);
|
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
|
// Get start date based on period
|
||||||
const startDate = useMemo(() => {
|
const startDate = useMemo(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { PageLayout, PageHeader } from "@/components/layout";
|
import { PageLayout, PageHeader } from "@/components/layout";
|
||||||
import {
|
import { RefreshCw, Receipt, Euro, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
RefreshCw,
|
|
||||||
Maximize2,
|
|
||||||
Minimize2,
|
|
||||||
Receipt,
|
|
||||||
Euro,
|
|
||||||
ChevronDown,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
TransactionFilters,
|
TransactionFilters,
|
||||||
TransactionBulkActions,
|
TransactionBulkActions,
|
||||||
@@ -30,20 +23,16 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { useTransactionsPage } from "@/hooks/use-transactions-page";
|
import { useTransactionsPage } from "@/hooks/use-transactions-page";
|
||||||
import { useTransactionMutations } from "@/hooks/use-transaction-mutations";
|
import { useTransactionMutations } from "@/hooks/use-transaction-mutations";
|
||||||
import { useTransactionRules } from "@/hooks/use-transaction-rules";
|
import { useTransactionRules } from "@/hooks/use-transaction-rules";
|
||||||
import { useTransactionsChartData } from "@/hooks/use-transactions-chart-data";
|
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() {
|
export default function TransactionsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
||||||
|
|
||||||
// Main page state and logic
|
// Main page state and logic
|
||||||
const {
|
const {
|
||||||
@@ -126,6 +115,28 @@ export default function TransactionsPage() {
|
|||||||
searchQuery,
|
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(() => {
|
const invalidateAll = useCallback(() => {
|
||||||
invalidateTransactions();
|
invalidateTransactions();
|
||||||
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
|
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
|
||||||
@@ -147,7 +158,11 @@ export default function TransactionsPage() {
|
|||||||
[handleBulkSetCategory, selectedTransactions, clearSelection]
|
[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 totalTransactions = transactionsData?.total || 0;
|
||||||
const hasMore = transactionsData?.hasMore || false;
|
const hasMore = transactionsData?.hasMore || false;
|
||||||
const uncategorizedCount = transactionsData?.uncategorizedCount || 0;
|
const uncategorizedCount = transactionsData?.uncategorizedCount || 0;
|
||||||
@@ -160,10 +175,11 @@ export default function TransactionsPage() {
|
|||||||
const totalAmount = chartTotalAmount ?? 0;
|
const totalAmount = chartTotalAmount ?? 0;
|
||||||
const displayTotalCount = chartTotalCount ?? totalTransactions;
|
const displayTotalCount = chartTotalCount ?? totalTransactions;
|
||||||
|
|
||||||
// For filter comboboxes, we'll use empty arrays for now
|
// Persist statistics collapsed state in localStorage
|
||||||
// They can be enhanced later with separate queries if needed
|
const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage(
|
||||||
const transactionsForAccountFilter: never[] = [];
|
"transactions-stats-expanded",
|
||||||
const transactionsForCategoryFilter: never[] = [];
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Early return for loading state - prevents sidebar flash
|
// Early return for loading state - prevents sidebar flash
|
||||||
if (isLoadingMetadata || !metadata) {
|
if (isLoadingMetadata || !metadata) {
|
||||||
@@ -223,15 +239,31 @@ export default function TransactionsPage() {
|
|||||||
|
|
||||||
{(!isLoadingChart || !isLoadingTransactions) && (
|
{(!isLoadingChart || !isLoadingTransactions) && (
|
||||||
<Card className="mb-6">
|
<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">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 py-3 px-6">
|
||||||
<CardTitle className="text-base font-semibold">
|
<CardTitle className="text-base font-semibold">
|
||||||
Statistiques
|
Statistiques
|
||||||
|
{!isStatsExpanded && (
|
||||||
|
<span className="ml-3 text-sm font-normal text-muted-foreground">
|
||||||
|
{displayTotalCount} opération
|
||||||
|
{displayTotalCount > 1 ? "s" : ""} •{" "}
|
||||||
|
{formatCurrency(totalAmount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-8">
|
<Button variant="ghost" size="sm" className="h-8">
|
||||||
<ChevronDown className="w-4 h-4 mr-1" />
|
{isStatsExpanded ? (
|
||||||
Réduire
|
<>
|
||||||
|
<ChevronUp className="w-4 h-4 mr-1" />
|
||||||
|
Réduire
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown className="w-4 h-4 mr-1" />
|
||||||
|
Afficher
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -310,19 +342,6 @@ export default function TransactionsPage() {
|
|||||||
</div>
|
</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
|
<TransactionTable
|
||||||
transactions={filteredTransactions}
|
transactions={filteredTransactions}
|
||||||
accounts={metadata.accounts}
|
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
|
<RuleCreateDialog
|
||||||
open={ruleDialogOpen}
|
open={ruleDialogOpen}
|
||||||
onOpenChange={setRuleDialogOpen}
|
onOpenChange={setRuleDialogOpen}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||||
@@ -37,7 +38,13 @@ export function CategoryCard({
|
|||||||
size={isMobile ? 10 : 12}
|
size={isMobile ? 10 : 12}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{!isMobile && (
|
||||||
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
|
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
|
||||||
{stats.count} opération{stats.count > 1 ? "s" : ""} •{" "}
|
{stats.count} opération{stats.count > 1 ? "s" : ""} •{" "}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -73,9 +74,13 @@ export function ParentCategoryRow({
|
|||||||
size={isMobile ? 10 : 14}
|
size={isMobile ? 10 : 14}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}
|
{parent.name}
|
||||||
</span>
|
</Link>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
|
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
|
||||||
{children.length} • {stats.count} opération
|
{children.length} • {stats.count} opération
|
||||||
|
|||||||
@@ -36,10 +36,43 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
|
|||||||
value: total,
|
value: total,
|
||||||
color: category?.color || "#94a3b8",
|
color: category?.color || "#94a3b8",
|
||||||
icon: category?.icon || "HelpCircle",
|
icon: category?.icon || "HelpCircle",
|
||||||
|
categoryId: categoryId === "uncategorized" ? null : categoryId,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.value - a.value)
|
.sort((a, b) => b.value - a.value);
|
||||||
.slice(0, 6);
|
|
||||||
|
// 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) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat("fr-FR", {
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
@@ -51,6 +84,8 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
|
|||||||
return (
|
return (
|
||||||
<CategoryPieChart
|
<CategoryPieChart
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
dataByParent={chartDataByParent}
|
||||||
|
categories={data.categories}
|
||||||
formatCurrency={formatCurrency}
|
formatCurrency={formatCurrency}
|
||||||
title="Dépenses par catégorie"
|
title="Dépenses par catégorie"
|
||||||
height={250}
|
height={250}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", label: "Tableau de bord", icon: LayoutDashboard },
|
{ href: "/", label: "Tableau de bord", icon: LayoutDashboard },
|
||||||
@@ -158,7 +158,7 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ open, onOpenChange }: SidebarProps) {
|
export function Sidebar({ open, onOpenChange }: SidebarProps) {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useLocalStorage("sidebar-collapsed", false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function TransactionTable({
|
|||||||
setFocusedIndex(index);
|
setFocusedIndex(index);
|
||||||
onMarkReconciled(transactionId);
|
onMarkReconciled(transactionId);
|
||||||
},
|
},
|
||||||
[onMarkReconciled],
|
[onMarkReconciled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
@@ -198,7 +198,7 @@ export function TransactionTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[focusedIndex, transactions, onMarkReconciled, virtualizer],
|
[focusedIndex, transactions, onMarkReconciled, virtualizer]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -215,7 +215,7 @@ export function TransactionTable({
|
|||||||
(accountId: string) => {
|
(accountId: string) => {
|
||||||
return accounts.find((a) => a.id === accountId);
|
return accounts.find((a) => a.id === accountId);
|
||||||
},
|
},
|
||||||
[accounts],
|
[accounts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCategory = useCallback(
|
const getCategory = useCallback(
|
||||||
@@ -223,7 +223,7 @@ export function TransactionTable({
|
|||||||
if (!categoryId) return null;
|
if (!categoryId) return null;
|
||||||
return categories.find((c) => c.id === categoryId);
|
return categories.find((c) => c.id === categoryId);
|
||||||
},
|
},
|
||||||
[categories],
|
[categories]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -281,7 +281,7 @@ export function TransactionTable({
|
|||||||
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
||||||
transaction.isReconciled && "bg-emerald-500/5",
|
transaction.isReconciled && "bg-emerald-500/5",
|
||||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||||
isDuplicate && "shadow-sm",
|
isDuplicate && "shadow-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<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",
|
"font-semibold tabular-nums text-sm md:text-base shrink-0",
|
||||||
transaction.amount >= 0
|
transaction.amount >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600",
|
: "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{transaction.amount >= 0 ? "+" : ""}
|
{transaction.amount >= 0 ? "+" : ""}
|
||||||
@@ -358,7 +358,7 @@ export function TransactionTable({
|
|||||||
showBadge
|
showBadge
|
||||||
align="start"
|
align="start"
|
||||||
disabled={updatingTransactionIds.has(
|
disabled={updatingTransactionIds.has(
|
||||||
transaction.id,
|
transaction.id
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +391,7 @@ export function TransactionTable({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (
|
if (
|
||||||
confirm(
|
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);
|
onDelete(transaction.id);
|
||||||
@@ -446,7 +446,7 @@ export function TransactionTable({
|
|||||||
<div className="p-3 text-sm font-medium text-muted-foreground">
|
<div className="p-3 text-sm font-medium text-muted-foreground">
|
||||||
Compte
|
Compte
|
||||||
</div>
|
</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
|
Catégorie
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 text-right">
|
<div className="p-3 text-right">
|
||||||
@@ -464,7 +464,7 @@ export function TransactionTable({
|
|||||||
<div className="p-3"></div>
|
<div className="p-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Body virtualisé */}
|
{/* Body virtualisé ou non selon le mode */}
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="overflow-auto"
|
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",
|
"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",
|
transaction.isReconciled && "bg-emerald-500/5",
|
||||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||||
isDuplicate && "shadow-sm",
|
isDuplicate && "shadow-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -552,7 +552,7 @@ export function TransactionTable({
|
|||||||
{account?.name || "-"}
|
{account?.name || "-"}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="p-3 relative"
|
className="p-3 relative flex items-center justify-center"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{updatingTransactionIds.has(transaction.id) && (
|
{updatingTransactionIds.has(transaction.id) && (
|
||||||
@@ -576,7 +576,7 @@ export function TransactionTable({
|
|||||||
"p-3 text-right font-semibold tabular-nums",
|
"p-3 text-right font-semibold tabular-nums",
|
||||||
transaction.amount >= 0
|
transaction.amount >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600",
|
: "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{transaction.amount >= 0 ? "+" : ""}
|
{transaction.amount >= 0 ? "+" : ""}
|
||||||
@@ -643,7 +643,7 @@ export function TransactionTable({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (
|
if (
|
||||||
confirm(
|
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);
|
onDelete(transaction.id);
|
||||||
|
|||||||
45
hooks/use-local-storage.ts
Normal file
45
hooks/use-local-storage.ts
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
109
hooks/use-transactions-for-account-filter.ts
Normal file
109
hooks/use-transactions-for-account-filter.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
105
hooks/use-transactions-for-category-filter.ts
Normal file
105
hooks/use-transactions-for-category-filter.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useTransactions,
|
useTransactions,
|
||||||
useDuplicateIds,
|
useDuplicateIds,
|
||||||
} from "@/lib/hooks";
|
} from "@/lib/hooks";
|
||||||
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
||||||
import type { TransactionsPaginatedParams } from "@/services/banking.service";
|
import type { TransactionsPaginatedParams } from "@/services/banking.service";
|
||||||
import type { Category } from "@/lib/types";
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export function useTransactionsPage() {
|
|||||||
"all",
|
"all",
|
||||||
]);
|
]);
|
||||||
const [showReconciled, setShowReconciled] = useState<string>("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>(
|
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user