import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useGetFundTransferHistoryQuery } from '../../payments/services/paymentsApi'; import type { BeneficiaryMode, FundTransferHistoryItem, FundTransferStatus, } from '../../../types/cbs'; export const HISTORY_PAGE_SIZE = 10; export interface HistoryFilters { status: FundTransferStatus | 'ALL'; mode: BeneficiaryMode | 'ALL'; /** Inclusive, `yyyy-MM-dd`, sent only as a pair. */ fromDate?: string; toDate?: string; } /** * The transfer history, accumulated page by page — the same shape as * `useStatement`, for the same reason: RTK Query caches one entry per page, and * the list needs every page walked so far. * * Any change of filter starts again from page 0, as the guide requires; page 2 * of the old filter has no meaning under the new one. The next page is asked * for only while the server says `last` is false. */ export const useFundTransferHistory = (filters: HistoryFilters) => { const queryKey = `${filters.status}|${filters.mode}|${filters.fromDate ?? ''}|${filters.toDate ?? ''}`; const [page, setPage] = useState(0); const [pages, setPages] = useState>({}); const loadedKey = useRef(queryKey); const { data, isLoading, isFetching, error, refetch } = useGetFundTransferHistoryQuery( { ...filters, page, size: HISTORY_PAGE_SIZE }, { refetchOnMountOrArgChange: true }, ); // Reset before the new filter's first response lands, so the list is never // briefly the old filter's rows under the new filter's chips. if (loadedKey.current !== queryKey) { loadedKey.current = queryKey; if (page !== 0) { setPage(0); } if (Object.keys(pages).length > 0) { setPages({}); } } useEffect(() => { if (!data) { return; } setPages((current) => current[data.number] === data.content ? current : { ...current, [data.number]: data.content }, ); }, [data]); /** * Flattened in page order with repeats dropped. The server caches a page for * about 30 seconds, so a row can shift across a page boundary mid-browse; * `transferId` is the stable key that stops it appearing twice. */ const transfers = useMemo(() => { const seen = new Set(); const ordered: FundTransferHistoryItem[] = []; Object.keys(pages) .map(Number) .sort((left, right) => left - right) .forEach((index) => { (pages[index] ?? []).forEach((item) => { if (!seen.has(item.transferId)) { seen.add(item.transferId); ordered.push(item); } }); }); return ordered; }, [pages]); const hasMore = data ? !data.last : false; const loadMore = useCallback(() => { if (!hasMore || isFetching) { return; } setPage((current) => current + 1); }, [hasMore, isFetching]); /** * Pull-to-refresh: start again from page 0. * * The fresh page is written straight from the refetch's own result, not left * to the `[data]` effect above. The server caches a page for ~30 seconds, so a * refresh usually returns exactly what it returned before — and RTK Query's * structural sharing then hands back the SAME `data` object, the effect never * fires, and a list cleared before the request stayed empty. The current rows * also stay on screen until the fresh page lands, rather than blanking. */ const reload = useCallback(() => { if (page !== 0) { // A different cache entry, so `data` genuinely changes and the effect // refills the list. setPages({}); setPage(0); return; } refetch() .unwrap() .then((fresh) => setPages({ [fresh.number]: fresh.content })) .catch(() => { // Surfaced through `error`; the rows already on screen are kept. }); }, [page, refetch]); return { transfers, totalElements: data?.totalElements ?? 0, hasMore, isLoading: isLoading || (isFetching && transfers.length === 0), isLoadingMore: isFetching && page > 0, isRefreshing: isFetching && page === 0 && transfers.length > 0, error, loadMore, reload, }; };