Files
fintrack/hooks/use-transaction-mutations.ts

367 lines
11 KiB
TypeScript

"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";
import { invalidateAllTransactionQueries } from "@/lib/cache-utils";
interface UseTransactionMutationsProps {
transactionParams: TransactionsPaginatedParams;
transactionsData: { transactions: Transaction[]; total: number } | undefined;
}
export function useTransactionMutations({
transactionParams,
transactionsData,
}: 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}`);
}
// 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);
}
invalidateAllTransactionQueries(queryClient);
}
},
[transactionsData, transactionParams, queryClient],
);
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}`);
}
// 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);
}
invalidateAllTransactionQueries(queryClient);
}
},
[transactionsData, transactionParams, queryClient],
);
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));
// 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, categoryId } : t,
),
};
});
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}`);
}
// 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);
}
invalidateAllTransactionQueries(queryClient);
} finally {
setUpdatingTransactionIds((prev) => {
const next = new Set(prev);
next.delete(transactionId);
return next;
});
}
},
[transactionsData, transactionParams, queryClient],
);
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}`,
);
}
// 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);
}
invalidateAllTransactionQueries(queryClient);
}
},
[transactionsData, transactionParams, queryClient],
);
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 }),
}),
),
);
// 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);
}
invalidateAllTransactionQueries(queryClient);
}
},
[transactionsData, transactionParams, queryClient],
);
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;
});
// 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, categoryId } : t,
),
};
});
try {
await Promise.all(
transactionsToUpdate.map((t) =>
fetch("/api/banking/transactions", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...t, categoryId }),
}),
),
);
// 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);
}
invalidateAllTransactionQueries(queryClient);
} finally {
setUpdatingTransactionIds((prev) => {
const next = new Set(prev);
transactionIds.forEach((id) => next.delete(id));
return next;
});
}
},
[transactionsData, transactionParams, queryClient],
);
return {
toggleReconciled,
markReconciled,
setCategory,
deleteTransaction,
bulkReconcile,
bulkSetCategory,
updatingTransactionIds,
};
}