feat: integrate monthly chart with collapsible feature in transactions page; update transaction period to default to last 3 months
This commit is contained in:
328
hooks/use-transactions-balance-chart.ts
Normal file
328
hooks/use-transactions-balance-chart.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useTransactions } from "@/lib/hooks";
|
||||
import { useBankingMetadata } from "@/lib/hooks";
|
||||
import type { TransactionsPaginatedParams } from "@/services/banking.service";
|
||||
import type { Account } from "@/lib/types";
|
||||
|
||||
interface BalanceChartDataPoint {
|
||||
date: string;
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
interface UseTransactionsBalanceChartParams {
|
||||
selectedAccounts: string[];
|
||||
selectedCategories: string[];
|
||||
period: "1month" | "3months" | "6months" | "12months" | "custom" | "all";
|
||||
customStartDate?: Date;
|
||||
customEndDate?: Date;
|
||||
showReconciled: string;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export function useTransactionsBalanceChart({
|
||||
selectedAccounts,
|
||||
selectedCategories,
|
||||
period,
|
||||
customStartDate,
|
||||
customEndDate,
|
||||
showReconciled,
|
||||
searchQuery,
|
||||
}: UseTransactionsBalanceChartParams) {
|
||||
const { data: metadata } = useBankingMetadata();
|
||||
|
||||
// Calculate start date based on period
|
||||
const startDate = useMemo(() => {
|
||||
const now = new Date();
|
||||
switch (period) {
|
||||
case "1month":
|
||||
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
case "3months":
|
||||
return new Date(now.getFullYear(), now.getMonth() - 3, 1);
|
||||
case "6months":
|
||||
return new Date(now.getFullYear(), now.getMonth() - 6, 1);
|
||||
case "12months":
|
||||
return new Date(now.getFullYear(), now.getMonth() - 12, 1);
|
||||
case "custom":
|
||||
return customStartDate || new Date(0);
|
||||
default:
|
||||
return new Date(0);
|
||||
}
|
||||
}, [period, customStartDate]);
|
||||
|
||||
// Calculate end date (only for custom period)
|
||||
const endDate = useMemo(() => {
|
||||
if (period === "custom" && customEndDate) {
|
||||
return customEndDate;
|
||||
}
|
||||
return undefined;
|
||||
}, [period, customEndDate]);
|
||||
|
||||
// Build params for fetching all transactions (no pagination)
|
||||
const chartParams = useMemo<TransactionsPaginatedParams>(() => {
|
||||
const params: TransactionsPaginatedParams = {
|
||||
limit: 10000, // Large limit to get all transactions
|
||||
offset: 0,
|
||||
sortField: "date",
|
||||
sortOrder: "asc", // Ascending for balance calculation
|
||||
};
|
||||
|
||||
if (startDate && period !== "all") {
|
||||
params.startDate = startDate.toISOString().split("T")[0];
|
||||
}
|
||||
if (endDate) {
|
||||
params.endDate = endDate.toISOString().split("T")[0];
|
||||
}
|
||||
if (!selectedAccounts.includes("all")) {
|
||||
params.accountIds = selectedAccounts;
|
||||
}
|
||||
if (!selectedCategories.includes("all")) {
|
||||
if (selectedCategories.includes("uncategorized")) {
|
||||
params.includeUncategorized = true;
|
||||
} else {
|
||||
params.categoryIds = selectedCategories;
|
||||
}
|
||||
}
|
||||
if (searchQuery) {
|
||||
params.search = searchQuery;
|
||||
}
|
||||
if (showReconciled !== "all") {
|
||||
params.isReconciled = showReconciled === "reconciled";
|
||||
}
|
||||
|
||||
return params;
|
||||
}, [
|
||||
startDate,
|
||||
endDate,
|
||||
selectedAccounts,
|
||||
selectedCategories,
|
||||
searchQuery,
|
||||
showReconciled,
|
||||
period,
|
||||
]);
|
||||
|
||||
// Build params for fetching transactions before startDate (for initial balance)
|
||||
const beforeStartDateParams = useMemo<TransactionsPaginatedParams>(() => {
|
||||
if (period === "all" || !startDate) {
|
||||
return { limit: 0, offset: 0 }; // Don't fetch if no start date
|
||||
}
|
||||
|
||||
const params: TransactionsPaginatedParams = {
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
sortField: "date",
|
||||
sortOrder: "asc",
|
||||
endDate: new Date(startDate.getTime() - 1).toISOString().split("T")[0], // Day before startDate
|
||||
};
|
||||
|
||||
if (!selectedAccounts.includes("all")) {
|
||||
params.accountIds = selectedAccounts;
|
||||
}
|
||||
if (!selectedCategories.includes("all")) {
|
||||
if (selectedCategories.includes("uncategorized")) {
|
||||
params.includeUncategorized = true;
|
||||
} else {
|
||||
params.categoryIds = selectedCategories;
|
||||
}
|
||||
}
|
||||
if (searchQuery) {
|
||||
params.search = searchQuery;
|
||||
}
|
||||
if (showReconciled !== "all") {
|
||||
params.isReconciled = showReconciled === "reconciled";
|
||||
}
|
||||
|
||||
return params;
|
||||
}, [
|
||||
startDate,
|
||||
selectedAccounts,
|
||||
selectedCategories,
|
||||
searchQuery,
|
||||
showReconciled,
|
||||
period,
|
||||
]);
|
||||
|
||||
// Fetch transactions before startDate for initial balance calculation
|
||||
const { data: beforeStartDateData } = useTransactions(
|
||||
beforeStartDateParams,
|
||||
!!metadata && period !== "all" && !!startDate
|
||||
);
|
||||
|
||||
// Fetch all filtered transactions for chart
|
||||
const { data: transactionsData, isLoading } = useTransactions(
|
||||
chartParams,
|
||||
!!metadata
|
||||
);
|
||||
|
||||
// Calculate balance chart data
|
||||
const chartData = useMemo(() => {
|
||||
if (!transactionsData || !metadata) {
|
||||
return {
|
||||
aggregatedData: [],
|
||||
perAccountData: [],
|
||||
};
|
||||
}
|
||||
|
||||
const transactions = transactionsData.transactions;
|
||||
const accounts = metadata.accounts;
|
||||
|
||||
// Sort transactions by date
|
||||
const sortedTransactions = [...transactions].sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
|
||||
// Calculate starting balance: initialBalance + transactions before startDate
|
||||
let runningBalance = 0;
|
||||
const accountsToUse = selectedAccounts.includes("all")
|
||||
? accounts
|
||||
: accounts.filter((acc: Account) => selectedAccounts.includes(acc.id));
|
||||
|
||||
// Start with initial balances
|
||||
runningBalance = accountsToUse.reduce(
|
||||
(sum: number, acc: Account) => sum + (acc.initialBalance || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Add transactions before startDate if we have them
|
||||
if (beforeStartDateData?.transactions) {
|
||||
const beforeStartTransactions = beforeStartDateData.transactions.filter(
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate;
|
||||
}
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
runningBalance += t.amount;
|
||||
});
|
||||
}
|
||||
|
||||
const aggregatedBalanceByDate = new Map<string, number>();
|
||||
|
||||
// Calculate balance evolution
|
||||
sortedTransactions.forEach((t) => {
|
||||
runningBalance += t.amount;
|
||||
aggregatedBalanceByDate.set(t.date, runningBalance);
|
||||
});
|
||||
|
||||
const aggregatedBalanceData: BalanceChartDataPoint[] = Array.from(
|
||||
aggregatedBalanceByDate.entries()
|
||||
).map(([date, balance]) => ({
|
||||
date: new Date(date).toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}),
|
||||
solde: Math.round(balance),
|
||||
}));
|
||||
|
||||
// Per account balance calculation
|
||||
const accountBalances = new Map<string, Map<string, number>>();
|
||||
accounts.forEach((account: Account) => {
|
||||
accountBalances.set(account.id, new Map());
|
||||
});
|
||||
|
||||
// Calculate running balance per account
|
||||
const accountRunningBalances = new Map<string, number>();
|
||||
accounts.forEach((account: Account) => {
|
||||
accountRunningBalances.set(account.id, account.initialBalance || 0);
|
||||
});
|
||||
|
||||
// Add transactions before startDate per account
|
||||
if (beforeStartDateData?.transactions) {
|
||||
const beforeStartTransactions = beforeStartDateData.transactions.filter(
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate;
|
||||
}
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
const currentBalance = accountRunningBalances.get(t.accountId) || 0;
|
||||
accountRunningBalances.set(t.accountId, currentBalance + t.amount);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter transactions by account if needed
|
||||
const transactionsForAccounts = selectedAccounts.includes("all")
|
||||
? sortedTransactions
|
||||
: sortedTransactions.filter((t) =>
|
||||
selectedAccounts.includes(t.accountId)
|
||||
);
|
||||
|
||||
transactionsForAccounts.forEach((t) => {
|
||||
const currentBalance = accountRunningBalances.get(t.accountId) || 0;
|
||||
const newBalance = currentBalance + t.amount;
|
||||
accountRunningBalances.set(t.accountId, newBalance);
|
||||
|
||||
const accountDates = accountBalances.get(t.accountId);
|
||||
if (accountDates) {
|
||||
accountDates.set(t.date, newBalance);
|
||||
}
|
||||
});
|
||||
|
||||
// Merge all dates and create data points
|
||||
const allDates = new Set<string>();
|
||||
accountBalances.forEach((dates) => {
|
||||
dates.forEach((_, date) => allDates.add(date));
|
||||
});
|
||||
|
||||
const sortedDates = Array.from(allDates).sort();
|
||||
const lastBalances = new Map<string, number>();
|
||||
accounts.forEach((account: Account) => {
|
||||
// Start with initial balance + transactions before startDate
|
||||
let accountStartingBalance = account.initialBalance || 0;
|
||||
if (beforeStartDateData?.transactions) {
|
||||
const beforeStartTransactions = beforeStartDateData.transactions.filter(
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate && t.accountId === account.id;
|
||||
}
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
accountStartingBalance += t.amount;
|
||||
});
|
||||
}
|
||||
lastBalances.set(account.id, accountStartingBalance);
|
||||
});
|
||||
|
||||
const perAccountBalanceData: BalanceChartDataPoint[] = sortedDates.map(
|
||||
(date) => {
|
||||
const point: BalanceChartDataPoint = {
|
||||
date: new Date(date).toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}),
|
||||
};
|
||||
|
||||
accounts.forEach((account: Account) => {
|
||||
const accountDates = accountBalances.get(account.id);
|
||||
if (accountDates?.has(date)) {
|
||||
lastBalances.set(account.id, accountDates.get(date)!);
|
||||
}
|
||||
point[account.id] = Math.round(lastBalances.get(account.id) || 0);
|
||||
});
|
||||
|
||||
return point;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
aggregatedData: aggregatedBalanceData,
|
||||
perAccountData: perAccountBalanceData,
|
||||
};
|
||||
}, [
|
||||
transactionsData,
|
||||
beforeStartDateData,
|
||||
metadata,
|
||||
selectedAccounts,
|
||||
startDate,
|
||||
]);
|
||||
|
||||
return {
|
||||
aggregatedData: chartData.aggregatedData,
|
||||
perAccountData: chartData.perAccountData,
|
||||
accounts: metadata?.accounts || [],
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user