refactor: streamline transaction page logic by consolidating state management and enhancing pagination, improving overall performance and maintainability
This commit is contained in:
353
hooks/use-transaction-mutations.ts
Normal file
353
hooks/use-transaction-mutations.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { Transaction } from "@/lib/types";
|
||||
import { getTransactionsQueryKey } from "@/lib/hooks";
|
||||
import type { TransactionsPaginatedParams } from "@/services/banking.service";
|
||||
|
||||
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<
|
||||
Set<string>
|
||||
>(new Set());
|
||||
|
||||
const toggleReconciled = useCallback(
|
||||
async (transactionId: string) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
const transaction = transactionsData.transactions.find(
|
||||
(t) => t.id === transactionId
|
||||
);
|
||||
if (!transaction) return;
|
||||
|
||||
const newReconciledState = !transaction.isReconciled;
|
||||
const updatedTransaction = {
|
||||
...transaction,
|
||||
isReconciled: newReconciledState,
|
||||
};
|
||||
|
||||
// Optimistic cache update
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
const previousData =
|
||||
queryClient.getQueryData<typeof transactionsData>(queryKey);
|
||||
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
t.id === transactionId
|
||||
? { ...t, isReconciled: newReconciledState }
|
||||
: t
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedTransaction),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error);
|
||||
// Rollback on error
|
||||
if (previousData) {
|
||||
queryClient.setQueryData(queryKey, previousData);
|
||||
}
|
||||
invalidateTransactions();
|
||||
}
|
||||
},
|
||||
[
|
||||
transactionsData,
|
||||
transactionParams,
|
||||
queryClient,
|
||||
invalidateTransactions,
|
||||
]
|
||||
);
|
||||
|
||||
const markReconciled = useCallback(
|
||||
async (transactionId: string) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
const transaction = transactionsData.transactions.find(
|
||||
(t) => t.id === transactionId
|
||||
);
|
||||
if (!transaction || transaction.isReconciled) return;
|
||||
|
||||
const updatedTransaction = {
|
||||
...transaction,
|
||||
isReconciled: true,
|
||||
};
|
||||
|
||||
// Optimistic cache update
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
const previousData =
|
||||
queryClient.getQueryData<typeof transactionsData>(queryKey);
|
||||
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
t.id === transactionId ? { ...t, isReconciled: true } : t
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedTransaction),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error);
|
||||
// Rollback on error
|
||||
if (previousData) {
|
||||
queryClient.setQueryData(queryKey, previousData);
|
||||
}
|
||||
invalidateTransactions();
|
||||
}
|
||||
},
|
||||
[
|
||||
transactionsData,
|
||||
transactionParams,
|
||||
queryClient,
|
||||
invalidateTransactions,
|
||||
]
|
||||
);
|
||||
|
||||
const setCategory = useCallback(
|
||||
async (transactionId: string, categoryId: string | null) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
const transaction = transactionsData.transactions.find(
|
||||
(t) => t.id === transactionId
|
||||
);
|
||||
if (!transaction) return;
|
||||
|
||||
setUpdatingTransactionIds((prev) => new Set(prev).add(transactionId));
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...transaction, categoryId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Optimistic cache update
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
t.id === transactionId ? { ...t, categoryId } : t
|
||||
),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error);
|
||||
invalidateTransactions();
|
||||
} finally {
|
||||
setUpdatingTransactionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(transactionId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[transactionsData, transactionParams, queryClient, invalidateTransactions]
|
||||
);
|
||||
|
||||
const deleteTransaction = useCallback(
|
||||
async (transactionId: string) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
// Save current data for rollback
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
const previousData =
|
||||
queryClient.getQueryData<typeof transactionsData>(queryKey);
|
||||
|
||||
// Optimistic cache update
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.filter(
|
||||
(t) => t.id !== transactionId
|
||||
),
|
||||
total: oldData.total - 1,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/banking/transactions?id=${transactionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.error || `Failed to delete transaction: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete transaction:", error);
|
||||
// Rollback on error
|
||||
if (previousData) {
|
||||
queryClient.setQueryData(queryKey, previousData);
|
||||
}
|
||||
invalidateTransactions();
|
||||
}
|
||||
},
|
||||
[transactionsData, transactionParams, queryClient, invalidateTransactions]
|
||||
);
|
||||
|
||||
const bulkReconcile = useCallback(
|
||||
async (reconciled: boolean, selectedTransactionIds: Set<string>) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
||||
selectedTransactionIds.has(t.id)
|
||||
);
|
||||
|
||||
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
||||
|
||||
// Optimistic cache update
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
const previousData =
|
||||
queryClient.getQueryData<typeof transactionsData>(queryKey);
|
||||
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
transactionIds.includes(t.id)
|
||||
? { ...t, isReconciled: reconciled }
|
||||
: t
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
transactionsToUpdate.map((t) =>
|
||||
fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...t, isReconciled: reconciled }),
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update transactions:", error);
|
||||
// Rollback on error
|
||||
if (previousData) {
|
||||
queryClient.setQueryData(queryKey, previousData);
|
||||
}
|
||||
invalidateTransactions();
|
||||
}
|
||||
},
|
||||
[
|
||||
transactionsData,
|
||||
transactionParams,
|
||||
queryClient,
|
||||
invalidateTransactions,
|
||||
]
|
||||
);
|
||||
|
||||
const bulkSetCategory = useCallback(
|
||||
async (categoryId: string | null, selectedTransactionIds: Set<string>) => {
|
||||
if (!transactionsData) return;
|
||||
|
||||
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
||||
selectedTransactionIds.has(t.id)
|
||||
);
|
||||
|
||||
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
||||
setUpdatingTransactionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
transactionIds.forEach((id) => next.add(id));
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
transactionsToUpdate.map((t) =>
|
||||
fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...t, categoryId }),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Optimistic cache update
|
||||
const queryKey = getTransactionsQueryKey(transactionParams);
|
||||
queryClient.setQueryData<typeof transactionsData>(queryKey, (oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
transactionIds.includes(t.id) ? { ...t, categoryId } : t
|
||||
),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update transactions:", error);
|
||||
invalidateTransactions();
|
||||
} finally {
|
||||
setUpdatingTransactionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
transactionIds.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[transactionsData, transactionParams, queryClient, invalidateTransactions]
|
||||
);
|
||||
|
||||
return {
|
||||
toggleReconciled,
|
||||
markReconciled,
|
||||
setCategory,
|
||||
deleteTransaction,
|
||||
bulkReconcile,
|
||||
bulkSetCategory,
|
||||
updatingTransactionIds,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user