diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 3bec9ce..eb7e199 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -28,6 +28,7 @@ import { updateFolder, deleteFolder, } from "@/lib/store-db"; +import { invalidateAllAccountQueries } from "@/lib/cache-utils"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { @@ -67,7 +68,7 @@ function FolderDropZone({
{children} @@ -85,13 +86,12 @@ export default function AccountsPage() { // refresh function is not used directly, invalidations are done inline const refreshSilent = async () => { - await queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - await queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); }; const [editingAccount, setEditingAccount] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const [selectedAccounts, setSelectedAccounts] = useState>( - new Set(), + new Set() ); const [formData, setFormData] = useState({ name: "", @@ -117,7 +117,7 @@ export default function AccountsPage() { activationConstraint: { distance: 8, }, - }), + }) ); if ( @@ -131,7 +131,7 @@ export default function AccountsPage() { // Convert accountsWithStats to regular accounts for compatibility const accounts = accountsWithStats.map( - ({ transactionCount: _transactionCount, ...account }) => account, + ({ transactionCount: _transactionCount, ...account }) => account ); const formatCurrency = (amount: number) => { @@ -166,8 +166,7 @@ export default function AccountsPage() { initialBalance: formData.initialBalance, }; await updateAccount(updatedAccount); - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); setIsDialogOpen(false); setEditingAccount(null); } catch (error) { @@ -181,8 +180,7 @@ export default function AccountsPage() { try { await deleteAccount(accountId); - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); } catch (error) { console.error("Error deleting account:", error); alert("Erreur lors de la suppression du compte"); @@ -193,7 +191,7 @@ export default function AccountsPage() { const count = selectedAccounts.size; if ( !confirm( - `Supprimer ${count} compte${count > 1 ? "s" : ""} et toutes leurs transactions ?`, + `Supprimer ${count} compte${count > 1 ? "s" : ""} et toutes leurs transactions ?` ) ) return; @@ -204,14 +202,13 @@ export default function AccountsPage() { `/api/banking/accounts?ids=${ids.join(",")}`, { method: "DELETE", - }, + } ); if (!response.ok) { throw new Error("Failed to delete accounts"); } setSelectedAccounts(new Set()); - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); } catch (error) { console.error("Error deleting accounts:", error); alert("Erreur lors de la suppression des comptes"); @@ -267,8 +264,7 @@ export default function AccountsPage() { icon: "folder", }); } - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); setIsFolderDialogOpen(false); } catch (error) { console.error("Error saving folder:", error); @@ -279,15 +275,14 @@ export default function AccountsPage() { const handleDeleteFolder = async (folderId: string) => { if ( !confirm( - "Supprimer ce dossier ? Les comptes seront déplacés à la racine.", + "Supprimer ce dossier ? Les comptes seront déplacés à la racine." ) ) return; try { await deleteFolder(folderId); - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); } catch (error) { console.error("Error deleting folder:", error); alert("Erreur lors de la suppression du dossier"); @@ -320,7 +315,7 @@ export default function AccountsPage() { // Déplacer vers le dossier du compte cible const targetAccountId = overId.replace("account-", ""); const targetAccount = accountsWithStats.find( - (a) => a.id === targetAccountId, + (a) => a.id === targetAccountId ); if (targetAccount) { targetFolderId = targetAccount.folderId; @@ -342,7 +337,7 @@ export default function AccountsPage() { (old: Array | undefined) => { if (!old) return old; return old.map((a) => (a.id === accountId ? updatedAccount : a)); - }, + } ); // Faire la requête en arrière-plan @@ -353,8 +348,7 @@ export default function AccountsPage() { } catch (error) { console.error("Error moving account:", error); // Rollback en cas d'erreur - refresh data - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + invalidateAllAccountQueries(queryClient); alert("Erreur lors du déplacement du compte"); } } @@ -368,7 +362,7 @@ export default function AccountsPage() { const totalBalance = accounts.reduce( (sum, a) => sum + getAccountBalance(a), - 0, + 0 ); // Grouper les comptes par folder @@ -381,7 +375,7 @@ export default function AccountsPage() { acc[folderId].push(account); return acc; }, - {} as Record, + {} as Record ); // Obtenir les folders racine (sans parent) et les trier par nom @@ -435,7 +429,7 @@ export default function AccountsPage() { className={cn( isMobile ? "text-xl" : "text-2xl", "font-bold", - totalBalance >= 0 ? "text-emerald-600" : "text-red-600", + totalBalance >= 0 ? "text-emerald-600" : "text-red-600" )} > {isMobile && ( @@ -492,17 +486,17 @@ export default function AccountsPage() { "text-xs sm:text-sm font-semibold tabular-nums shrink-0", accountsByFolder["no-folder"].reduce( (sum, a) => sum + getAccountBalance(a), - 0, + 0 ) >= 0 ? "text-emerald-600" - : "text-red-600", + : "text-red-600" )} > {formatCurrency( accountsByFolder["no-folder"].reduce( (sum, a) => sum + getAccountBalance(a), - 0, - ), + 0 + ) )}
@@ -511,7 +505,7 @@ export default function AccountsPage() {
{accountsByFolder["no-folder"].map((account) => { const folder = metadata.folders.find( - (f: FolderType) => f.id === account.folderId, + (f: FolderType) => f.id === account.folderId ); return ( @@ -539,7 +533,7 @@ export default function AccountsPage() { const folderAccounts = accountsByFolder[folder.id] || []; const folderBalance = folderAccounts.reduce( (sum, a) => sum + getAccountBalance(a), - 0, + 0 ); return ( @@ -568,7 +562,7 @@ export default function AccountsPage() { "text-xs sm:text-sm font-semibold tabular-nums shrink-0", folderBalance >= 0 ? "text-emerald-600" - : "text-red-600", + : "text-red-600" )} > {formatCurrency(folderBalance)} @@ -628,7 +622,7 @@ export default function AccountsPage() {
{folderAccounts.map((account) => { const accountFolder = metadata.folders.find( - (f: FolderType) => f.id === account.folderId, + (f: FolderType) => f.id === account.folderId ); return ( @@ -672,7 +666,7 @@ export default function AccountsPage() { {accounts.find( - (a) => a.id === activeId.replace("account-", ""), + (a) => a.id === activeId.replace("account-", "") )?.name || ""} diff --git a/app/api/banking/accounts/route.ts b/app/api/banking/accounts/route.ts index 81553e6..cb82f28 100644 --- a/app/api/banking/accounts/route.ts +++ b/app/api/banking/accounts/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { accountService } from "@/services/account.service"; import { bankingService } from "@/services/banking.service"; import { requireAuth } from "@/lib/auth-utils"; @@ -14,11 +15,7 @@ export async function GET(request: NextRequest) { if (withStats) { const accountsWithStats = await bankingService.getAccountsWithStats(); - return NextResponse.json(accountsWithStats, { - headers: { - "Cache-Control": "public, s-maxage=60, stale-while-revalidate=120", - }, - }); + return NextResponse.json(accountsWithStats); } return NextResponse.json({ error: "Invalid request" }, { status: 400 }); @@ -26,7 +23,7 @@ export async function GET(request: NextRequest) { console.error("Error fetching accounts:", error); return NextResponse.json( { error: "Failed to fetch accounts" }, - { status: 500 }, + { status: 500 } ); } } @@ -38,12 +35,23 @@ export async function POST(request: Request) { try { const data: Omit = await request.json(); const created = await accountService.create(data); - return NextResponse.json(created); + + // Revalider le cache serveur + revalidatePath("/accounts", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json(created, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error creating account:", error); return NextResponse.json( { error: "Failed to create account" }, - { status: 500 }, + { status: 500 } ); } } @@ -55,12 +63,23 @@ export async function PUT(request: Request) { try { const account: Account = await request.json(); const updated = await accountService.update(account.id, account); - return NextResponse.json(updated); + + // Revalider le cache serveur + revalidatePath("/accounts", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json(updated, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error updating account:", error); return NextResponse.json( { error: "Failed to update account" }, - { status: 500 }, + { status: 500 } ); } } @@ -80,27 +99,55 @@ export async function DELETE(request: Request) { if (accountIds.length === 0) { return NextResponse.json( { error: "At least one account ID is required" }, - { status: 400 }, + { status: 400 } ); } await accountService.deleteMany(accountIds); - return NextResponse.json({ success: true, count: accountIds.length }); + + // Revalider le cache serveur + revalidatePath("/accounts", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json( + { success: true, count: accountIds.length }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } if (!id) { return NextResponse.json( { error: "Account ID is required" }, - { status: 400 }, + { status: 400 } ); } await accountService.delete(id); - return NextResponse.json({ success: true }); + + // Revalider le cache serveur + revalidatePath("/accounts", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json( + { success: true }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } catch (error) { console.error("Error deleting account:", error); return NextResponse.json( { error: "Failed to delete account" }, - { status: 500 }, + { status: 500 } ); } } diff --git a/app/api/banking/categories/route.ts b/app/api/banking/categories/route.ts index c1ebd24..d292838 100644 --- a/app/api/banking/categories/route.ts +++ b/app/api/banking/categories/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { categoryService } from "@/services/category.service"; import { bankingService } from "@/services/banking.service"; import { requireAuth } from "@/lib/auth-utils"; @@ -14,11 +15,7 @@ export async function GET(request: NextRequest) { if (statsOnly) { const stats = await bankingService.getCategoryStats(); - return NextResponse.json(stats, { - headers: { - "Cache-Control": "public, s-maxage=60, stale-while-revalidate=120", - }, - }); + return NextResponse.json(stats); } return NextResponse.json({ error: "Invalid request" }, { status: 400 }); @@ -26,7 +23,7 @@ export async function GET(request: NextRequest) { console.error("Error fetching category stats:", error); return NextResponse.json( { error: "Failed to fetch category stats" }, - { status: 500 }, + { status: 500 } ); } } @@ -37,12 +34,23 @@ export async function POST(request: Request) { try { const data: Omit = await request.json(); const created = await categoryService.create(data); - return NextResponse.json(created); + + // Revalider le cache serveur + revalidatePath("/categories", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/rules", "page"); + + return NextResponse.json(created, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error creating category:", error); return NextResponse.json( { error: "Failed to create category" }, - { status: 500 }, + { status: 500 } ); } } @@ -54,12 +62,23 @@ export async function PUT(request: Request) { try { const category: Category = await request.json(); const updated = await categoryService.update(category.id, category); - return NextResponse.json(updated); + + // Revalider le cache serveur + revalidatePath("/categories", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/rules", "page"); + + return NextResponse.json(updated, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error updating category:", error); return NextResponse.json( { error: "Failed to update category" }, - { status: 500 }, + { status: 500 } ); } } @@ -75,17 +94,31 @@ export async function DELETE(request: Request) { if (!id) { return NextResponse.json( { error: "Category ID is required" }, - { status: 400 }, + { status: 400 } ); } await categoryService.delete(id); - return NextResponse.json({ success: true }); + + // Revalider le cache serveur + revalidatePath("/categories", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/rules", "page"); + + return NextResponse.json( + { success: true }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } catch (error) { console.error("Error deleting category:", error); return NextResponse.json( { error: "Failed to delete category" }, - { status: 500 }, + { status: 500 } ); } } diff --git a/app/api/banking/folders/route.ts b/app/api/banking/folders/route.ts index 8a7921d..3d1471e 100644 --- a/app/api/banking/folders/route.ts +++ b/app/api/banking/folders/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { folderService, FolderNotFoundError } from "@/services/folder.service"; import { requireAuth } from "@/lib/auth-utils"; import type { Folder } from "@/lib/types"; @@ -9,12 +10,20 @@ export async function POST(request: Request) { try { const data: Omit = await request.json(); const created = await folderService.create(data); - return NextResponse.json(created); + + // Revalider le cache des pages + revalidatePath("/accounts", "page"); + + return NextResponse.json(created, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error creating folder:", error); return NextResponse.json( { error: "Failed to create folder" }, - { status: 500 }, + { status: 500 } ); } } @@ -26,12 +35,20 @@ export async function PUT(request: Request) { try { const folder: Folder = await request.json(); const updated = await folderService.update(folder.id, folder); - return NextResponse.json(updated); + + // Revalider le cache des pages + revalidatePath("/accounts", "page"); + + return NextResponse.json(updated, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error updating folder:", error); return NextResponse.json( { error: "Failed to update folder" }, - { status: 500 }, + { status: 500 } ); } } @@ -47,12 +64,23 @@ export async function DELETE(request: Request) { if (!id) { return NextResponse.json( { error: "Folder ID is required" }, - { status: 400 }, + { status: 400 } ); } await folderService.delete(id); - return NextResponse.json({ success: true }); + + // Revalider le cache des pages + revalidatePath("/accounts", "page"); + + return NextResponse.json( + { success: true }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } catch (error) { if (error instanceof FolderNotFoundError) { return NextResponse.json({ error: "Folder not found" }, { status: 404 }); @@ -60,7 +88,7 @@ export async function DELETE(request: Request) { console.error("Error deleting folder:", error); return NextResponse.json( { error: "Failed to delete folder" }, - { status: 500 }, + { status: 500 } ); } } diff --git a/app/api/banking/route.ts b/app/api/banking/route.ts index d69bde7..158d43d 100644 --- a/app/api/banking/route.ts +++ b/app/api/banking/route.ts @@ -12,19 +12,11 @@ export async function GET(request: NextRequest) { if (metadataOnly) { const metadata = await bankingService.getMetadata(); - return NextResponse.json(metadata, { - headers: { - "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600", - }, - }); + return NextResponse.json(metadata); } const data = await bankingService.getAllData(); - return NextResponse.json(data, { - headers: { - "Cache-Control": "public, s-maxage=60, stale-while-revalidate=120", - }, - }); + return NextResponse.json(data); } catch (error) { console.error("Error fetching banking data:", error); return NextResponse.json( diff --git a/app/api/banking/transactions/clear-categories/route.ts b/app/api/banking/transactions/clear-categories/route.ts index 581c3ec..8499b59 100644 --- a/app/api/banking/transactions/clear-categories/route.ts +++ b/app/api/banking/transactions/clear-categories/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { prisma } from "@/lib/prisma"; import { requireAuth } from "@/lib/auth-utils"; @@ -15,10 +16,23 @@ export async function POST() { }, }); - return NextResponse.json({ - success: true, - count: result.count, - }); + // Revalider le cache des pages + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/categories", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json( + { + success: true, + count: result.count, + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } catch (error) { console.error("Error clearing categories:", error); return NextResponse.json( diff --git a/app/api/banking/transactions/deduplicate/route.ts b/app/api/banking/transactions/deduplicate/route.ts index d9b378d..f0bd865 100644 --- a/app/api/banking/transactions/deduplicate/route.ts +++ b/app/api/banking/transactions/deduplicate/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { transactionService } from "@/services/transaction.service"; import { requireAuth } from "@/lib/auth-utils"; @@ -8,7 +9,17 @@ export async function POST() { try { const result = await transactionService.deduplicate(); - return NextResponse.json(result); + + // Revalider le cache des pages + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json(result, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error deduplicating transactions:", error); return NextResponse.json( diff --git a/app/api/banking/transactions/route.ts b/app/api/banking/transactions/route.ts index f02ec4b..e0b33d2 100644 --- a/app/api/banking/transactions/route.ts +++ b/app/api/banking/transactions/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; import { transactionService } from "@/services/transaction.service"; import { bankingService } from "@/services/banking.service"; import { requireAuth } from "@/lib/auth-utils"; @@ -28,7 +29,7 @@ export async function GET(request: NextRequest) { searchParams.get("includeUncategorized") === "true"; const search = searchParams.get("search") || undefined; const isReconciledParam = searchParams.get("isReconciled"); - const isReconciled = + const isReconciled: boolean | "all" = isReconciledParam === "true" ? true : isReconciledParam === "false" @@ -40,7 +41,7 @@ export async function GET(request: NextRequest) { const sortOrder = (searchParams.get("sortOrder") as "asc" | "desc") || "desc"; - const result = await bankingService.getTransactionsPaginated({ + const params = { limit, offset, startDate, @@ -52,18 +53,18 @@ export async function GET(request: NextRequest) { isReconciled, sortField, sortOrder, - }); + }; - return NextResponse.json(result, { - headers: { - "Cache-Control": "public, s-maxage=60, stale-while-revalidate=120", - }, - }); + // Pas de cache serveur pour garantir des données toujours à jour + // Le cache client React Query gère déjà la mise en cache côté client + const result = await bankingService.getTransactionsPaginated(params); + + return NextResponse.json(result); } catch (error) { console.error("Error fetching transactions:", error); return NextResponse.json( { error: "Failed to fetch transactions" }, - { status: 500 }, + { status: 500 } ); } } @@ -74,12 +75,22 @@ export async function POST(request: Request) { try { const transactions: Transaction[] = await request.json(); const result = await transactionService.createMany(transactions); - return NextResponse.json(result); + + // Revalider le cache des pages + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json(result, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error creating transactions:", error); return NextResponse.json( { error: "Failed to create transactions" }, - { status: 500 }, + { status: 500 } ); } } @@ -92,14 +103,24 @@ export async function PUT(request: Request) { const transaction: Transaction = await request.json(); const updated = await transactionService.update( transaction.id, - transaction, + transaction ); - return NextResponse.json(updated); + + // Revalider le cache des pages + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json(updated, { + headers: { + "Cache-Control": "no-store", + }, + }); } catch (error) { console.error("Error updating transaction:", error); return NextResponse.json( { error: "Failed to update transaction" }, - { status: 500 }, + { status: 500 } ); } } @@ -115,12 +136,25 @@ export async function DELETE(request: Request) { if (!id) { return NextResponse.json( { error: "Transaction ID is required" }, - { status: 400 }, + { status: 400 } ); } await transactionService.delete(id); - return NextResponse.json({ success: true }); + + // Revalider le cache des pages + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json( + { success: true }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); } catch (error) { console.error("Error deleting transaction:", error); const errorMessage = diff --git a/app/categories/page.tsx b/app/categories/page.tsx index af56754..e991cec 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -28,6 +28,7 @@ import { deleteCategory, } from "@/lib/store-db"; import type { Category, Transaction } from "@/lib/types"; +import { invalidateAllCategoryQueries } from "@/lib/cache-utils"; interface RecategorizationResult { transaction: Transaction; @@ -41,7 +42,7 @@ export default function CategoriesPage() { const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingCategory, setEditingCategory] = useState(null); const [expandedParents, setExpandedParents] = useState>( - new Set(), + new Set() ); const [formData, setFormData] = useState({ name: "", @@ -52,7 +53,7 @@ export default function CategoriesPage() { }); const [searchQuery, setSearchQuery] = useState(""); const [recatResults, setRecatResults] = useState( - [], + [] ); const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false); const [isRecategorizing, setIsRecategorizing] = useState(false); @@ -68,7 +69,7 @@ export default function CategoriesPage() { }; const parents = metadata.categories.filter( - (c: Category) => c.parentId === null, + (c: Category) => c.parentId === null ); const children: Record = {}; const orphans: Category[] = []; @@ -77,7 +78,7 @@ export default function CategoriesPage() { .filter((c: Category) => c.parentId !== null) .forEach((child: Category) => { const parentExists = parents.some( - (p: Category) => p.id === child.parentId, + (p: Category) => p.id === child.parentId ); if (parentExists) { if (!children[child.parentId!]) { @@ -105,8 +106,7 @@ export default function CategoriesPage() { }, [parentCategories.length]); const refresh = useCallback(() => { - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); - queryClient.invalidateQueries({ queryKey: ["category-stats"] }); + invalidateAllCategoryQueries(queryClient); }, [queryClient]); const getCategoryStats = useCallback( @@ -136,7 +136,7 @@ export default function CategoriesPage() { return { total, count }; }, - [categoryStats, childrenByParent], + [categoryStats, childrenByParent] ); if (isLoadingMetadata || !metadata || isLoadingStats || !categoryStats) { @@ -248,7 +248,7 @@ export default function CategoriesPage() { try { // Fetch uncategorized transactions const uncategorizedResponse = await fetch( - "/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true", + "/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true" ); if (!uncategorizedResponse.ok) { throw new Error("Failed to fetch uncategorized transactions"); @@ -261,11 +261,11 @@ export default function CategoriesPage() { for (const transaction of uncategorized) { const categoryId = autoCategorize( transaction.description + " " + (transaction.memo || ""), - metadata.categories, + metadata.categories ); if (categoryId) { const category = metadata.categories.find( - (c: Category) => c.id === categoryId, + (c: Category) => c.id === categoryId ); if (category) { results.push({ transaction, category }); @@ -299,9 +299,9 @@ export default function CategoriesPage() { return children.some( (c) => c.name.toLowerCase().includes(query) || - c.keywords.some((k) => k.toLowerCase().includes(query)), + c.keywords.some((k) => k.toLowerCase().includes(query)) ); - }, + } ); return ( @@ -346,9 +346,9 @@ export default function CategoriesPage() { (c) => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || c.keywords.some((k) => - k.toLowerCase().includes(searchQuery.toLowerCase()), + k.toLowerCase().includes(searchQuery.toLowerCase()) ) || - parent.name.toLowerCase().includes(searchQuery.toLowerCase()), + parent.name.toLowerCase().includes(searchQuery.toLowerCase()) ) : allChildren; const stats = getCategoryStats(parent.id, true); @@ -435,7 +435,7 @@ export default function CategoriesPage() {

{new Date(result.transaction.date).toLocaleDateString( - "fr-FR", + "fr-FR" )} {" • "} {new Intl.NumberFormat("fr-FR", { diff --git a/app/rules/page.tsx b/app/rules/page.tsx index cf77a89..978c365 100644 --- a/app/rules/page.tsx +++ b/app/rules/page.tsx @@ -18,6 +18,10 @@ import { suggestKeyword, } from "@/components/rules/constants"; import type { Transaction } from "@/lib/types"; +import { + invalidateAllTransactionQueries, + invalidateAllCategoryQueries, +} from "@/lib/cache-utils"; interface TransactionGroup { key: string; @@ -42,21 +46,19 @@ export default function RulesPage() { offset: 0, includeUncategorized: true, }, - !!metadata, + !!metadata ); const refresh = useCallback(() => { - invalidateTransactions(); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); - queryClient.invalidateQueries({ queryKey: ["category-stats"] }); - queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); - }, [invalidateTransactions, queryClient]); + invalidateAllTransactionQueries(queryClient); + invalidateAllCategoryQueries(queryClient); + }, [queryClient]); const [searchQuery, setSearchQuery] = useState(""); const [sortBy, setSortBy] = useState<"count" | "amount" | "name">("count"); const [filterMinCount, setFilterMinCount] = useState(2); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [selectedGroup, setSelectedGroup] = useState( - null, + null ); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isAutoCategorizing, setIsAutoCategorizing] = useState(false); @@ -87,7 +89,7 @@ export default function RulesPage() { totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0), suggestedKeyword: suggestKeyword(descriptions), }; - }, + } ); // Filter by search query @@ -98,7 +100,7 @@ export default function RulesPage() { (g) => g.displayName.toLowerCase().includes(query) || g.key.includes(query) || - g.suggestedKeyword.toLowerCase().includes(query), + g.suggestedKeyword.toLowerCase().includes(query) ); } @@ -167,7 +169,7 @@ export default function RulesPage() { // 1. Add keyword to category const category = metadata.categories.find( - (c: { id: string }) => c.id === ruleData.categoryId, + (c: { id: string }) => c.id === ruleData.categoryId ); if (!category) { throw new Error("Category not found"); @@ -175,7 +177,7 @@ export default function RulesPage() { // Check if keyword already exists const keywordExists = category.keywords.some( - (k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase(), + (k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase() ); if (!keywordExists) { @@ -193,14 +195,16 @@ export default function RulesPage() { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, categoryId: ruleData.categoryId }), - }), - ), + }) + ) ); } - refresh(); + // Invalider toutes les queries liées + invalidateAllTransactionQueries(queryClient); + invalidateAllCategoryQueries(queryClient); }, - [metadata, refresh], + [metadata, queryClient] ); const handleAutoCategorize = useCallback(async () => { @@ -214,7 +218,7 @@ export default function RulesPage() { for (const transaction of uncategorized) { const categoryId = autoCategorize( transaction.description + " " + (transaction.memo || ""), - metadata.categories, + metadata.categories ); if (categoryId) { await fetch("/api/banking/transactions", { @@ -226,9 +230,11 @@ export default function RulesPage() { } } - refresh(); + // Invalider toutes les queries liées + invalidateAllTransactionQueries(queryClient); + invalidateAllCategoryQueries(queryClient); alert( - `${categorizedCount} transaction(s) catégorisée(s) automatiquement`, + `${categorizedCount} transaction(s) catégorisée(s) automatiquement` ); } catch (error) { console.error("Error auto-categorizing:", error); @@ -236,7 +242,7 @@ export default function RulesPage() { } finally { setIsAutoCategorizing(false); } - }, [metadata, transactionsData, refresh]); + }, [metadata, transactionsData, queryClient]); const handleCategorizeGroup = useCallback( async (group: TransactionGroup, categoryId: string | null) => { @@ -247,16 +253,18 @@ export default function RulesPage() { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...t, categoryId }), - }), - ), + }) + ) ); - refresh(); + // Invalider toutes les queries liées + invalidateAllTransactionQueries(queryClient); + invalidateAllCategoryQueries(queryClient); } catch (error) { console.error("Error categorizing group:", error); alert("Erreur lors de la catégorisation"); } }, - [refresh], + [queryClient] ); if ( diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index bba67a6..7f082eb 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -74,7 +74,6 @@ export default function TransactionsPage() { } = useTransactionMutations({ transactionParams, transactionsData, - invalidateTransactions, }); // Transaction rules diff --git a/hooks/use-transaction-mutations.ts b/hooks/use-transaction-mutations.ts index 970a8de..75cafe7 100644 --- a/hooks/use-transaction-mutations.ts +++ b/hooks/use-transaction-mutations.ts @@ -5,17 +5,16 @@ import { useQueryClient } from "@tanstack/react-query"; import type { Transaction } from "@/lib/types"; import { getTransactionsQueryKey } from "@/lib/hooks"; import type { TransactionsPaginatedParams } from "@/services/banking.service"; +import { invalidateAllTransactionQueries } from "@/lib/cache-utils"; interface UseTransactionMutationsProps { transactionParams: TransactionsPaginatedParams; transactionsData: { transactions: Transaction[]; total: number } | undefined; - invalidateTransactions: () => void; } export function useTransactionMutations({ transactionParams, transactionsData, - invalidateTransactions, }: UseTransactionMutationsProps) { const queryClient = useQueryClient(); const [updatingTransactionIds, setUpdatingTransactionIds] = useState< @@ -64,21 +63,19 @@ export function useTransactionMutations({ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } + + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to update transaction:", error); // Rollback on error if (previousData) { queryClient.setQueryData(queryKey, previousData); } - invalidateTransactions(); + invalidateAllTransactionQueries(queryClient); } }, - [ - transactionsData, - transactionParams, - queryClient, - invalidateTransactions, - ] + [transactionsData, transactionParams, queryClient] ); const markReconciled = useCallback( @@ -120,21 +117,19 @@ export function useTransactionMutations({ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } + + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to update transaction:", error); // Rollback on error if (previousData) { queryClient.setQueryData(queryKey, previousData); } - invalidateTransactions(); + invalidateAllTransactionQueries(queryClient); } }, - [ - transactionsData, - transactionParams, - queryClient, - invalidateTransactions, - ] + [transactionsData, transactionParams, queryClient] ); const setCategory = useCallback( @@ -148,6 +143,22 @@ export function useTransactionMutations({ setUpdatingTransactionIds((prev) => new Set(prev).add(transactionId)); + // Optimistic cache update + const queryKey = getTransactionsQueryKey(transactionParams); + const previousData = + queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return oldData; + + return { + ...oldData, + transactions: oldData.transactions.map((t) => + t.id === transactionId ? { ...t, categoryId } : t + ), + }; + }); + try { const response = await fetch("/api/banking/transactions", { method: "PUT", @@ -159,21 +170,15 @@ export function useTransactionMutations({ throw new Error(`HTTP error! status: ${response.status}`); } - // Optimistic cache update - const queryKey = getTransactionsQueryKey(transactionParams); - queryClient.setQueryData(queryKey, (oldData) => { - if (!oldData) return oldData; - - return { - ...oldData, - transactions: oldData.transactions.map((t) => - t.id === transactionId ? { ...t, categoryId } : t - ), - }; - }); + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to update transaction:", error); - invalidateTransactions(); + // Rollback on error + if (previousData) { + queryClient.setQueryData(queryKey, previousData); + } + invalidateAllTransactionQueries(queryClient); } finally { setUpdatingTransactionIds((prev) => { const next = new Set(prev); @@ -182,7 +187,7 @@ export function useTransactionMutations({ }); } }, - [transactionsData, transactionParams, queryClient, invalidateTransactions] + [transactionsData, transactionParams, queryClient] ); const deleteTransaction = useCallback( @@ -217,22 +222,23 @@ export function useTransactionMutations({ if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( - errorData.error || `Failed to delete transaction: ${response.status}` + errorData.error || + `Failed to delete transaction: ${response.status}` ); } - // Invalidate related queries - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to delete transaction:", error); // Rollback on error if (previousData) { queryClient.setQueryData(queryKey, previousData); } - invalidateTransactions(); + invalidateAllTransactionQueries(queryClient); } }, - [transactionsData, transactionParams, queryClient, invalidateTransactions] + [transactionsData, transactionParams, queryClient] ); const bulkReconcile = useCallback( @@ -272,21 +278,19 @@ export function useTransactionMutations({ }) ) ); + + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to update transactions:", error); // Rollback on error if (previousData) { queryClient.setQueryData(queryKey, previousData); } - invalidateTransactions(); + invalidateAllTransactionQueries(queryClient); } }, - [ - transactionsData, - transactionParams, - queryClient, - invalidateTransactions, - ] + [transactionsData, transactionParams, queryClient] ); const bulkSetCategory = useCallback( @@ -304,6 +308,21 @@ export function useTransactionMutations({ return next; }); + // Optimistic cache update + const queryKey = getTransactionsQueryKey(transactionParams); + const previousData = + queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return oldData; + return { + ...oldData, + transactions: oldData.transactions.map((t) => + transactionIds.includes(t.id) ? { ...t, categoryId } : t + ), + }; + }); + try { await Promise.all( transactionsToUpdate.map((t) => @@ -315,20 +334,15 @@ export function useTransactionMutations({ ) ); - // Optimistic cache update - const queryKey = getTransactionsQueryKey(transactionParams); - queryClient.setQueryData(queryKey, (oldData) => { - if (!oldData) return oldData; - return { - ...oldData, - transactions: oldData.transactions.map((t) => - transactionIds.includes(t.id) ? { ...t, categoryId } : t - ), - }; - }); + // TOUJOURS revalider après succès pour garantir la cohérence + invalidateAllTransactionQueries(queryClient); } catch (error) { console.error("Failed to update transactions:", error); - invalidateTransactions(); + // Rollback on error + if (previousData) { + queryClient.setQueryData(queryKey, previousData); + } + invalidateAllTransactionQueries(queryClient); } finally { setUpdatingTransactionIds((prev) => { const next = new Set(prev); @@ -337,7 +351,7 @@ export function useTransactionMutations({ }); } }, - [transactionsData, transactionParams, queryClient, invalidateTransactions] + [transactionsData, transactionParams, queryClient] ); return { @@ -350,4 +364,3 @@ export function useTransactionMutations({ updatingTransactionIds, }; } - diff --git a/hooks/use-transaction-rules.ts b/hooks/use-transaction-rules.ts index bec1adb..c2e7c7a 100644 --- a/hooks/use-transaction-rules.ts +++ b/hooks/use-transaction-rules.ts @@ -8,6 +8,10 @@ import { normalizeDescription, suggestKeyword, } from "@/components/rules/constants"; +import { + invalidateAllTransactionQueries, + invalidateAllCategoryQueries, +} from "@/lib/cache-utils"; interface UseTransactionRulesProps { transactionsData: { transactions: Transaction[] } | undefined; @@ -94,9 +98,9 @@ export function useTransactionRules({ ); } - // Invalidate queries - queryClient.invalidateQueries({ queryKey: ["transactions"] }); - queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + // Invalider toutes les queries liées + invalidateAllTransactionQueries(queryClient); + invalidateAllCategoryQueries(queryClient); setRuleDialogOpen(false); }, [metadata, queryClient] @@ -110,4 +114,3 @@ export function useTransactionRules({ handleSaveRule, }; } - diff --git a/lib/cache-utils.ts b/lib/cache-utils.ts new file mode 100644 index 0000000..43d7dde --- /dev/null +++ b/lib/cache-utils.ts @@ -0,0 +1,36 @@ +import { QueryClient } from "@tanstack/react-query"; + +/** + * Invalide toutes les queries liées aux transactions + * Utilisé après toute mutation de transaction (création, modification, suppression) + */ +export function invalidateAllTransactionQueries(queryClient: QueryClient) { + // Invalider toutes les queries de transactions (tous les paramètres) + queryClient.invalidateQueries({ queryKey: ["transactions"] }); + + // Invalider les queries liées qui peuvent être affectées + queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + queryClient.invalidateQueries({ queryKey: ["category-stats"] }); + queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); + queryClient.invalidateQueries({ queryKey: ["duplicate-ids"] }); +} + +/** + * Invalide toutes les queries liées aux catégories + * Utilisé après toute mutation de catégorie (création, modification, suppression) + */ +export function invalidateAllCategoryQueries(queryClient: QueryClient) { + queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + queryClient.invalidateQueries({ queryKey: ["category-stats"] }); + queryClient.invalidateQueries({ queryKey: ["transactions"] }); +} + +/** + * Invalide toutes les queries liées aux comptes + * Utilisé après toute mutation de compte ou dossier (création, modification, suppression) + */ +export function invalidateAllAccountQueries(queryClient: QueryClient) { + queryClient.invalidateQueries({ queryKey: ["banking-metadata"] }); + queryClient.invalidateQueries({ queryKey: ["accounts-with-stats"] }); + queryClient.invalidateQueries({ queryKey: ["transactions"] }); +}