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

71 lines
3.9 KiB
Markdown

# 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).