diff --git a/src/api/client.ts b/src/api/client.ts index 3c08779..2208ed3 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -25,10 +25,15 @@ async function request(path: string, init?: RequestInit, baseUrl: string = ge const token = getToken() let response: Response try { + // Always bypass browser/CDN HTTP caches for live FunZone/accounting data + // (Arvan in front of api/acc can otherwise serve stale withdrawal lists). response = await fetch(`${baseUrl}${path}`, { ...init, + cache: 'no-store', headers: { 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(init?.headers ?? {}), }, diff --git a/src/pages/sales/usePendingCustomerWithdrawals.ts b/src/pages/sales/usePendingCustomerWithdrawals.ts index cf8f91f..ed493f0 100644 --- a/src/pages/sales/usePendingCustomerWithdrawals.ts +++ b/src/pages/sales/usePendingCustomerWithdrawals.ts @@ -1,34 +1,65 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { apiGet, apiPatch } from '../../api/client' import { ENDPOINTS } from '../../api/config' import type { ApiWithdrawal, WalletTxnStatus } from '../../api/types' +const REFRESH_INTERVAL_MS = 30_000 + +function sortWithdrawalsNewestFirst(list: ApiWithdrawal[]): ApiWithdrawal[] { + return [...list].sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ) +} + /** All owner/customer withdrawals from the FunZone admin withdrawals API. */ export function usePendingCustomerWithdrawals(enabled = true) { const [withdrawals, setWithdrawals] = useState([]) const [loading, setLoading] = useState(false) const [updatingId, setUpdatingId] = useState(null) const [error, setError] = useState(null) + const inFlightRef = useRef(false) + const enabledRef = useRef(enabled) + enabledRef.current = enabled - const load = useCallback(async () => { - setLoading(true) + const load = useCallback(async (opts?: { soft?: boolean }) => { + if (!enabledRef.current || inFlightRef.current) return + + const soft = Boolean(opts?.soft) + inFlightRef.current = true + if (!soft) setLoading(true) setError(null) try { const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS) - const list = [...(res.withdrawals ?? [])].sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ) - setWithdrawals(list) + setWithdrawals(sortWithdrawalsNewestFirst(res.withdrawals ?? [])) } catch (err) { setError(err instanceof Error ? err.message : 'خطا در بارگذاری برداشت‌ها') - setWithdrawals([]) + // Keep last known list on soft refresh / transient CDN errors so the UI + // does not flash empty while production caches catch up. + if (!soft) setWithdrawals([]) } finally { + inFlightRef.current = false setLoading(false) } }, []) useEffect(() => { - if (enabled) void load() + if (!enabled) return + + void load() + + const softReload = () => { + if (document.visibilityState === 'visible') void load({ soft: true }) + } + + document.addEventListener('visibilitychange', softReload) + window.addEventListener('focus', softReload) + const intervalId = window.setInterval(softReload, REFRESH_INTERVAL_MS) + + return () => { + document.removeEventListener('visibilitychange', softReload) + window.removeEventListener('focus', softReload) + window.clearInterval(intervalId) + } }, [enabled, load]) const toggleStatus = useCallback(async (withdrawal: ApiWithdrawal) => { @@ -55,7 +86,9 @@ export function usePendingCustomerWithdrawals(enabled = true) { } }, []) - return { withdrawals, loading, updatingId, error, reload: load, toggleStatus } + const reload = useCallback(() => void load(), [load]) + + return { withdrawals, loading, updatingId, error, reload, toggleStatus } } export const withdrawalNoteTag = (withdrawalId: string): string => `withdrawal:${withdrawalId}` diff --git a/src/parties/useFunZoneParties.ts b/src/parties/useFunZoneParties.ts index c636173..61e853a 100644 --- a/src/parties/useFunZoneParties.ts +++ b/src/parties/useFunZoneParties.ts @@ -115,12 +115,34 @@ export function usePartyWithdrawals(kind: PartyKind) { import('../api/types').ApiWithdrawal[] >([]) useEffect(() => { - apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS) - .then((res) => { - const userType = kind === 'supplier' ? 'owner' : 'customer' - setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType)) - }) - .catch(() => setWithdrawals([])) + let cancelled = false + const userType = kind === 'supplier' ? 'owner' : 'customer' + + const load = () => { + apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS) + .then((res) => { + if (cancelled) return + setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType)) + }) + .catch(() => { + // Keep last known list; avoid wiping party stats on a transient/CDN miss. + }) + } + + load() + const onVisible = () => { + if (document.visibilityState === 'visible') load() + } + document.addEventListener('visibilitychange', onVisible) + window.addEventListener('focus', onVisible) + const intervalId = window.setInterval(onVisible, 30_000) + + return () => { + cancelled = true + document.removeEventListener('visibilitychange', onVisible) + window.removeEventListener('focus', onVisible) + window.clearInterval(intervalId) + } }, [kind]) return withdrawals }