From 2177ae7b4a215879c3d32fee6afefe91d3826520 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 8 Dec 2025 07:44:02 +0100 Subject: [PATCH] feat: implement optimistic updates for transaction handling and improve category selection in combobox components for enhanced user experience --- app/transactions/page.tsx | 33 ++++++++++++++++ components/ui/account-filter-combobox.tsx | 46 ++++++++-------------- components/ui/category-combobox.tsx | 39 +++++++----------- components/ui/category-filter-combobox.tsx | 46 ++++++++-------------- 4 files changed, 80 insertions(+), 84 deletions(-) diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 4a75131..ef45188 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -341,6 +341,20 @@ export default function TransactionsPage() { const updatedTransaction = { ...transaction, categoryId }; + // Optimistic update: update the cache immediately + queryClient.setQueryData( + ["transactions", transactionParams], + (oldData) => { + if (!oldData) return oldData; + return { + ...oldData, + transactions: oldData.transactions.map((t) => + t.id === transactionId ? updatedTransaction : t, + ), + }; + }, + ); + try { await fetch("/api/banking/transactions", { method: "PUT", @@ -350,6 +364,8 @@ export default function TransactionsPage() { invalidateTransactions(); } catch (error) { console.error("Failed to update transaction:", error); + // Revert optimistic update on error + invalidateTransactions(); } }; @@ -385,8 +401,23 @@ export default function TransactionsPage() { selectedTransactions.has(t.id), ); + const transactionIds = transactionsToUpdate.map((t) => t.id); setSelectedTransactions(new Set()); + // Optimistic update: update the cache immediately + queryClient.setQueryData( + ["transactions", transactionParams], + (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) => @@ -400,6 +431,8 @@ export default function TransactionsPage() { invalidateTransactions(); } catch (error) { console.error("Failed to update transactions:", error); + // Revert optimistic update on error + invalidateTransactions(); } }; diff --git a/components/ui/account-filter-combobox.tsx b/components/ui/account-filter-combobox.tsx index eac95a2..59f928c 100644 --- a/components/ui/account-filter-combobox.tsx +++ b/components/ui/account-filter-combobox.tsx @@ -171,7 +171,7 @@ export function AccountFilterCombobox({
{/* Folder row */} handleSelectFolder(folder.id)} style={{ paddingLeft: `${paddingLeft}px` }} className="font-medium" @@ -182,12 +182,9 @@ export function AccountFilterCombobox({ {isFolderPartiallySelected(folder.id) && (
)} - + {isFolderSelected(folder.id) && ( + + )}
@@ -198,7 +195,7 @@ export function AccountFilterCombobox({ return ( handleSelect(account.id)} style={{ paddingLeft: `${paddingLeft + 16}px` }} className="min-w-0" @@ -210,12 +207,9 @@ export function AccountFilterCombobox({ ({formatCurrency(total)}) )} - + {value.includes(account.id) && ( + + )} ); })} @@ -292,7 +286,7 @@ export function AccountFilterCombobox({ align="start" onOpenAutoFocus={(e) => e.preventDefault()} > - + Aucun compte trouvé. @@ -312,12 +306,9 @@ export function AccountFilterCombobox({ ) )} - + {isAll && ( + + )} @@ -333,7 +324,7 @@ export function AccountFilterCombobox({ return ( handleSelect(account.id)} className="min-w-0" > @@ -346,14 +337,9 @@ export function AccountFilterCombobox({ ({formatCurrency(total)}) )} - + {value.includes(account.id) && ( + + )} ); })} diff --git a/components/ui/category-combobox.tsx b/components/ui/category-combobox.tsx index f94b3f7..752c5e8 100644 --- a/components/ui/category-combobox.tsx +++ b/components/ui/category-combobox.tsx @@ -105,7 +105,7 @@ export function CategoryCombobox({ align={align} onOpenAutoFocus={(e) => e.preventDefault()} > - + Aucune catégorie trouvée. @@ -118,19 +118,16 @@ export function CategoryCombobox({ Aucune catégorie - + {value === null && ( + + )} {parentCategories.map((parent) => (
handleSelect(parent.id)} > {parent.name} - + {value === parent.id && ( + + )} {childrenByParent[parent.id]?.map((child) => ( handleSelect(child.id)} className="pl-8" > @@ -159,12 +153,9 @@ export function CategoryCombobox({ size={16} /> {child.name} - + {value === child.id && ( + + )} ))}
@@ -207,7 +198,7 @@ export function CategoryCombobox({ align={align} onOpenAutoFocus={(e) => e.preventDefault()} > - + Aucune catégorie trouvée. @@ -227,7 +218,7 @@ export function CategoryCombobox({ {parentCategories.map((parent) => (
handleSelect(parent.id)} > ( handleSelect(child.id)} className="pl-8" > diff --git a/components/ui/category-filter-combobox.tsx b/components/ui/category-filter-combobox.tsx index 4096459..38e1d65 100644 --- a/components/ui/category-filter-combobox.tsx +++ b/components/ui/category-filter-combobox.tsx @@ -193,7 +193,7 @@ export function CategoryFilterCombobox({ align="start" onOpenAutoFocus={(e) => e.preventDefault()} > - + Aucune catégorie trouvée. @@ -212,15 +212,12 @@ export function CategoryFilterCombobox({ ({filteredTransactions.length}) )} - + {isAll && ( + + )} handleSelect("uncategorized")} className="min-w-0" > @@ -231,19 +228,16 @@ export function CategoryFilterCombobox({ ({categoryCounts["uncategorized"]}) )} - + {isUncategorized && ( + + )} {parentCategories.map((parent) => (
handleSelect(parent.id)} className="min-w-0" > @@ -261,17 +255,14 @@ export function CategoryFilterCombobox({ ({categoryCounts[parent.id]}) )} - + {value.includes(parent.id) && ( + + )} {childrenByParent[parent.id]?.map((child) => ( handleSelect(child.id)} className="pl-8 min-w-0" > @@ -289,14 +280,9 @@ export function CategoryFilterCombobox({ ({categoryCounts[child.id]}) )} - + {value.includes(child.id) && ( + + )} ))}