feat: integrate React Query for improved data fetching and state management across banking and transactions components
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user