admin/docs/admin-caching.md
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

3.9 KiB

Admin dashboard data caching (TanStack Query)

The admin dashboard (app/(dashboard)/**) is being migrated to TanStack Query v5, the same library already used by the consumer app (see docs/consumer-caching.md). Read that doc first for the shared basics (lib/queryClient.ts, QueryClientProvider in app/providers.tsx, unwrapService). This doc only covers where the admin side intentionally differs.

queries/admin/adminKeys.ts is the admin key factory

Parallel to queries/consumerKeys.ts, scoped under ['admin', ...] instead of ['consumer', ...]. One hook file per data domain lives in queries/admin/*.ts, same shape as queries/consumer/*.ts.

PaginatedList owns one useQuery internally -- most admin pages need no changes

17 of the 20 admin sections render data through components/PaginatedList.tsx, passing only url/itemsKey/columns/urlParams. That component now runs a single useQuery internally, keyed on adminKeys.list(url, resolvedParams) where resolvedParams is the exact {page, pageSize, sort, filters, ...} object sent to the server (from buildListParams). Callers don't need their own query hook for the list itself -- just keep passing url/columns as before. refresh() on the PaginatedListHandle ref now does queryClient.invalidateQueries({ queryKey: adminKeys.listByUrl(url) }) instead of a local re-render counter. The imperative method remains available for non-row workflows, while row actions use useAdminMutation directly as described below.

Why useQuery, not useInfiniteQuery: unlike every consumer list (consumer-caching.md's "always infinite query" rule), PaginatedList is a page-jump table -- page, pageSize, totalItemsCount, not an accumulating feed. useInfiniteQuery doesn't fit that shape. placeholderData: keepPreviousData is used instead, so changing page/sort/filter shows the previous page's rows (not a blank table) while the new page loads.

Why the fetch itself doesn't need a services/*/ServiceResult<T> wrapper: components/paginated-list/paginatedListApi.ts's fetchPaginatedList already calls axiosInstance directly and either resolves { items, pagination } or throws -- exactly what a bare queryFn wants. Only genuine cancellation (CanceledError/AbortError, from useQuery's own signal) is rethrown as-is so TanStack's built-in cancellation handling still applies; every other error is normalized through handleServiceError(error, { errorMode: 'silent' }) before being thrown, so query.error.message still gives the same localized text the old loadError state used to.

hooks/useAdminMutation.ts owns admin row mutations and list invalidation

The identity-verifications, reviews, settlements, user-reports, and withdrawal-requests sections use one mutation shape. The hook preserves the existing runAction(id, action, successMessage) call-site and exposes a per-row pendingId, while its successful mutation callback invalidates adminKeys.listByUrl(url). Pages therefore do not need to thread a PaginatedListHandle ref through action callbacks just to refresh the list.

runAction resolves to true on success and false after a handled failure. Modal actions use that result to close/reset the modal only after the server accepts the mutation. Axios/service errors are normalized in the hook and shown once through the shared toast copy.

Raw-axiosInstance mutations don't need a service-layer rewrite first

Most non-CRUD admin actions (approve/reject/hide/restore/etc.) call axiosInstance directly with no services/*.ts wrapper. Axios already throws on non-2xx, which is exactly what a bare mutationFn wants -- there's no need to introduce a ServiceResult<T> service function before writing the mutation hook. Reserve unwrapService(await X(..., { errorMode: 'parent' })) for the sections that already have a real service (tags, blog-articles, event-categories, admin user detail/edit, wallet credit, geography).