feat: integrate React Query for improved data fetching and state management across banking and transactions components

This commit is contained in:
Julien Froidefond
2025-12-06 09:36:06 +01:00
parent e26eb0f039
commit b1a8f9cd60
16 changed files with 3488 additions and 4713 deletions

View File

@@ -7,14 +7,14 @@ import {
RuleCreateDialog,
RulesSearchBar,
} from "@/components/rules";
import { useBankingData } from "@/lib/hooks";
import { useBankingMetadata, useTransactions } from "@/lib/hooks";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Sparkles, RefreshCw } from "lucide-react";
import {
updateCategory,
autoCategorize,
updateTransaction,
} from "@/lib/store-db";
import {
normalizeDescription,
@@ -31,7 +31,27 @@ interface TransactionGroup {
}
export default function RulesPage() {
const { data, isLoading, refresh } = useBankingData();
const queryClient = useQueryClient();
const { data: metadata, isLoading: isLoadingMetadata } = useBankingMetadata();
// Fetch uncategorized transactions only
const {
data: transactionsData,
isLoading: isLoadingTransactions,
invalidate: invalidateTransactions,
} = useTransactions(
{
limit: 10000, // Large limit to get all uncategorized
offset: 0,
includeUncategorized: true,
},
!!metadata,
);
const refresh = useCallback(() => {
invalidateTransactions();
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
}, [invalidateTransactions, queryClient]);
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<"count" | "amount" | "name">("count");
const [filterMinCount, setFilterMinCount] = useState(2);
@@ -44,9 +64,9 @@ export default function RulesPage() {
// Group uncategorized transactions by normalized description
const transactionGroups = useMemo(() => {
if (!data?.transactions) return [];
if (!transactionsData?.transactions) return [];
const uncategorized = data.transactions.filter((t) => !t.categoryId);
const uncategorized = transactionsData.transactions;
const groups: Record<string, Transaction[]> = {};
uncategorized.forEach((transaction) => {
@@ -101,12 +121,9 @@ export default function RulesPage() {
});
return filtered;
}, [data?.transactions, searchQuery, sortBy, filterMinCount]);
}, [transactionsData?.transactions, searchQuery, sortBy, filterMinCount]);
const uncategorizedCount = useMemo(() => {
if (!data?.transactions) return 0;
return data.transactions.filter((t) => !t.categoryId).length;
}, [data?.transactions]);
const uncategorizedCount = transactionsData?.total || 0;
const formatCurrency = useCallback((amount: number) => {
return new Intl.NumberFormat("fr-FR", {
@@ -147,11 +164,11 @@ export default function RulesPage() {
applyToExisting: boolean;
transactionIds: string[];
}) => {
if (!data) return;
if (!metadata) return;
// 1. Add keyword to category
const category = data.categories.find(
(c) => c.id === ruleData.categoryId,
const category = metadata.categories.find(
(c: { id: string }) => c.id === ruleData.categoryId,
);
if (!category) {
throw new Error("Category not found");
@@ -159,7 +176,7 @@ export default function RulesPage() {
// Check if keyword already exists
const keywordExists = category.keywords.some(
(k) => k.toLowerCase() === ruleData.keyword.toLowerCase(),
(k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase(),
);
if (!keywordExists) {
@@ -171,37 +188,41 @@ export default function RulesPage() {
// 2. Apply to existing transactions if requested
if (ruleData.applyToExisting) {
const transactions = data.transactions.filter((t) =>
ruleData.transactionIds.includes(t.id),
);
await Promise.all(
transactions.map((t) =>
updateTransaction({ ...t, categoryId: ruleData.categoryId }),
ruleData.transactionIds.map((id) =>
fetch("/api/banking/transactions", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, categoryId: ruleData.categoryId }),
}),
),
);
}
refresh();
},
[data, refresh],
[metadata, refresh],
);
const handleAutoCategorize = useCallback(async () => {
if (!data) return;
if (!metadata || !transactionsData) return;
setIsAutoCategorizing(true);
try {
const uncategorized = data.transactions.filter((t) => !t.categoryId);
const uncategorized = transactionsData.transactions;
let categorizedCount = 0;
for (const transaction of uncategorized) {
const categoryId = autoCategorize(
transaction.description + " " + (transaction.memo || ""),
data.categories,
metadata.categories,
);
if (categoryId) {
await updateTransaction({ ...transaction, categoryId });
await fetch("/api/banking/transactions", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...transaction, categoryId }),
});
categorizedCount++;
}
}
@@ -216,16 +237,18 @@ export default function RulesPage() {
} finally {
setIsAutoCategorizing(false);
}
}, [data, refresh]);
}, [metadata, transactionsData, refresh]);
const handleCategorizeGroup = useCallback(
async (group: TransactionGroup, categoryId: string | null) => {
if (!data) return;
try {
await Promise.all(
group.transactions.map((t) =>
updateTransaction({ ...t, categoryId }),
fetch("/api/banking/transactions", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...t, categoryId }),
}),
),
);
refresh();
@@ -234,10 +257,10 @@ export default function RulesPage() {
alert("Erreur lors de la catégorisation");
}
},
[data, refresh],
[refresh],
);
if (isLoading || !data) {
if (isLoadingMetadata || !metadata || isLoadingTransactions || !transactionsData) {
return <LoadingState />;
}
@@ -312,7 +335,7 @@ export default function RulesPage() {
onCategorize={(categoryId) =>
handleCategorizeGroup(group, categoryId)
}
categories={data.categories}
categories={metadata.categories}
formatCurrency={formatCurrency}
formatDate={formatDate}
/>
@@ -324,7 +347,7 @@ export default function RulesPage() {
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
group={selectedGroup}
categories={data.categories}
categories={metadata.categories}
onSave={handleSaveRule}
/>
</PageLayout>