From 280eb14ee6ada623052ffcdb3b22df12678a3e4a Mon Sep 17 00:00:00 2001 From: alisaza Date: Mon, 7 Sep 2026 11:29:29 +0330 Subject: [PATCH] feat(users): expand admin users table with city, gender, wallet, and roles Show city, gender, wallet balance, and guest/host/admin userType with matching filters while keeping identity status for host verification. --- app/(dashboard)/users/page.tsx | 219 ++++++++++++++++++++++----------- constants/status.ts | 4 + texts/status.ts | 5 + types/definitions.ts | 3 + types/status.ts | 26 ++++ 5 files changed, 188 insertions(+), 69 deletions(-) diff --git a/app/(dashboard)/users/page.tsx b/app/(dashboard)/users/page.tsx index 36d216e..db30722 100644 --- a/app/(dashboard)/users/page.tsx +++ b/app/(dashboard)/users/page.tsx @@ -1,90 +1,168 @@ 'use client' +import { useEffect, useMemo, useState } from 'react' + import { coerceToString } from '@/helpers' import type { PaginationListColumnType } from '@/types' import PaginatedList from '@/components/PaginatedList' import StatusChip from '@/components/ui/StatusChip' import PageNavbar from '@/components/layouts/PageNavbar' import AdminTableViewButton from '@/components/ui/AdminTableViewButton' +import axiosInstance from '@/config/axios' import { APP_ROUTES } from '@/constants/routes' import { getIdentityStatus, getUserAccountStatus, - getUserRole, + getUserGender, + getUserType, IDENTITY_STATUS_FILTER_ITEMS, USER_ACCOUNT_STATUS_FILTER_ITEMS, - USER_ROLE_FILTER_ITEMS, + USER_GENDER_FILTER_ITEMS, + USER_TYPE_FILTER_ITEMS, } from '@/constants/status' -import { API_ROUTES } from '@/services/config' +import { formatToman } from '@/features/admin-users/adminUserDetailUi' import { formatIranianMobile, formatPersianDate } from '@/lib/formatters' +import { API_ROUTES } from '@/services/config' -const columns: PaginationListColumnType[] = [ - { - field: 'firstName', - label: 'نام', - filterable: true, - type: 'text', - sortable: false, - }, - { - field: 'lastName', - label: 'نام خانوادگی', - filterable: true, - type: 'text', - sortable: false, - }, - { - field: 'mobile', - label: 'موبایل', - filterable: true, - type: 'text', - sortable: true, - }, - { - field: 'role', - label: 'نقش', - filterable: true, - type: 'select', - sortable: true, - filterItems: USER_ROLE_FILTER_ITEMS, - }, - { - field: 'status', - label: 'وضعیت', - filterable: true, - type: 'select', - sortable: true, - filterItems: USER_ACCOUNT_STATUS_FILTER_ITEMS, - }, - { - field: 'identityStatus', - label: 'احراز هویت', - filterable: true, - type: 'select', - sortable: false, - filterItems: IDENTITY_STATUS_FILTER_ITEMS, - }, - { - field: 'lastLoginAt', - label: 'آخرین ورود', - filterable: true, - sortable: true, - type: 'dateFromTo', - }, - { - field: 'createdAt', - label: 'تاریخ ثبت', - filterable: true, - sortable: true, - type: 'dateFromTo', - }, - { - field: 'actions', - label: 'عملیات', - }, -] +interface NamedEntity { + id: number | string + name: string +} + +interface UserListRow { + id: string + cityName?: string | null + [key: string]: unknown +} + +interface FilterOption { + code: string + name: string +} + +const toSelectFilterItems = (items: NamedEntity[]): FilterOption[] => items.map((item) => ({ code: String(item.id), name: item.name })) + +const extractApiList = (raw: unknown): T[] => { + if (Array.isArray(raw)) return raw as T[] + + if (typeof raw === 'object' && raw !== null && 'data' in raw) { + const data = raw.data + + if (Array.isArray(data)) return data as T[] + } + + return [] +} const UsersPage = () => { + const [cityFilterItems, setCityFilterItems] = useState([]) + + useEffect(() => { + const loadCities = async () => { + try { + const citiesRes = await axiosInstance.get(API_ROUTES.GEOGRAPHY.ALL_CITIES) + + setCityFilterItems(toSelectFilterItems(extractApiList(citiesRes.data))) + } catch { + setCityFilterItems([]) + } + } + + void loadCities() + }, []) + + const columns: PaginationListColumnType[] = useMemo( + () => [ + { + field: 'firstName', + label: 'نام', + filterable: true, + type: 'text', + sortable: false, + }, + { + field: 'lastName', + label: 'نام خانوادگی', + filterable: true, + type: 'text', + sortable: false, + }, + { + field: 'mobile', + label: 'موبایل', + filterable: true, + type: 'text', + sortable: true, + }, + { + field: 'cityId', + label: 'شهر', + filterable: true, + type: 'select', + sortable: false, + filterItems: cityFilterItems, + }, + { + field: 'gender', + label: 'جنسیت', + filterable: true, + type: 'select', + sortable: false, + filterItems: USER_GENDER_FILTER_ITEMS, + }, + { + field: 'userType', + label: 'نقش', + filterable: true, + type: 'select', + sortable: false, + filterItems: USER_TYPE_FILTER_ITEMS, + }, + { + field: 'walletBalance', + label: 'موجودی کیف پول', + filterable: true, + type: 'inputFromTo', + sortable: false, + }, + { + field: 'status', + label: 'وضعیت حساب', + filterable: true, + type: 'select', + sortable: true, + filterItems: USER_ACCOUNT_STATUS_FILTER_ITEMS, + }, + { + field: 'identityStatus', + label: 'احراز هویت', + filterable: true, + type: 'select', + sortable: false, + filterItems: IDENTITY_STATUS_FILTER_ITEMS, + }, + { + field: 'createdAt', + label: 'تاریخ ثبت نام', + filterable: true, + sortable: true, + type: 'dateFromTo', + }, + { + field: 'lastLoginAt', + label: 'آخرین ورود', + filterable: true, + sortable: true, + type: 'dateFromTo', + }, + { + field: 'actions', + label: 'عملیات', + }, + ], + [cityFilterItems] + ) + return (
@@ -95,7 +173,10 @@ const UsersPage = () => { > {{ mobile: (_row, cellValue) => formatIranianMobile(cellValue), - role: (_row, cellValue) => , + cityId: (row) => (row as UserListRow).cityName || '—', + gender: (_row, cellValue) => , + userType: (_row, cellValue) => , + walletBalance: (_row, cellValue) => formatToman(cellValue), status: (_row, cellValue) => , identityStatus: (_row, cellValue) => , lastLoginAt: (_row, cellValue) => formatPersianDate(cellValue), diff --git a/constants/status.ts b/constants/status.ts index 3d7eacd..9f9ddcc 100644 --- a/constants/status.ts +++ b/constants/status.ts @@ -40,8 +40,10 @@ export { getBankAccountVerificationStatus, getSmsStatus, getUserAccountStatus, + getUserGender, getUserRole, getUserRoleLabel, + getUserType, getWithdrawalRequestStatus, IDENTITY_STATUS_FILTER_ITEMS, IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS, @@ -53,6 +55,8 @@ export { BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS, SMS_STATUS_FILTER_ITEMS, USER_ACCOUNT_STATUS_FILTER_ITEMS, + USER_GENDER_FILTER_ITEMS, USER_ROLE_FILTER_ITEMS, + USER_TYPE_FILTER_ITEMS, WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS, } from '@/types/status' diff --git a/texts/status.ts b/texts/status.ts index 115ece0..678aaca 100644 --- a/texts/status.ts +++ b/texts/status.ts @@ -31,6 +31,11 @@ export const status = { user, admin, }, + userType: { + guest: 'مهمان', + host: 'میزبان', + admin: 'ادمین', + }, userAccount: { active, pending, diff --git a/types/definitions.ts b/types/definitions.ts index b658592..2cfad6e 100644 --- a/types/definitions.ts +++ b/types/definitions.ts @@ -171,11 +171,14 @@ interface AdminUserListItem { lastName: string gender: UserGender cityId?: number | null + cityName?: string | null avatarUrl?: string | null bio?: string | null status: UserAccountStatus identityStatus: IdentityStatus role: UserRole + userType?: 'guest' | 'host' | 'admin' + walletBalance?: number mobileVerifiedAt?: string | null lastLoginAt?: string | null createdAt: string diff --git a/types/status.ts b/types/status.ts index 7e2a300..9a69629 100644 --- a/types/status.ts +++ b/types/status.ts @@ -34,12 +34,28 @@ function toStatusPresentation( return { label: meta.label, chipColor: meta.chipColor } } +/** User gender — label and table filter options. */ +const USER_GENDER: Record<'male' | 'female' | 'other', StatusMeta> = { + male: { label: texts.common.genderMale, chipColor: 'default', filterable: true }, + female: { label: texts.common.genderFemale, chipColor: 'default', filterable: true }, + other: { label: texts.profile.genderOther, chipColor: 'default', filterable: true }, +} + /** User role — label, chip color, and table filter options. */ const USER_ROLE: Record = { user: { label: texts.status.userRole.user, chipColor: 'default', filterable: true }, admin: { label: texts.status.userRole.admin, chipColor: 'warning', filterable: true }, } +/** Admin list derived type — guest / verified host / staff admin. */ +export type AdminUserType = 'guest' | 'host' | 'admin' + +const USER_TYPE: Record = { + guest: { label: texts.status.userType.guest, chipColor: 'default', filterable: true }, + host: { label: texts.status.userType.host, chipColor: 'success', filterable: true }, + admin: { label: texts.status.userType.admin, chipColor: 'warning', filterable: true }, +} + /** User account status — label, chip color, and table filter options. */ const USER_ACCOUNT_STATUS: Record = { active: { label: texts.status.userAccount.active, chipColor: 'success', filterable: true }, @@ -85,6 +101,8 @@ const REPORT_STATUS: Record = { } export const USER_ROLE_FILTER_ITEMS = toFilterItems(USER_ROLE) +export const USER_TYPE_FILTER_ITEMS = toFilterItems(USER_TYPE) +export const USER_GENDER_FILTER_ITEMS = toFilterItems(USER_GENDER) export const USER_ACCOUNT_STATUS_FILTER_ITEMS = toFilterItems(USER_ACCOUNT_STATUS) export const IDENTITY_STATUS_FILTER_ITEMS = toFilterItems(IDENTITY_STATUS) @@ -128,6 +146,14 @@ export function getUserRoleLabel(role?: UserRole | string): string { return getUserRole(role).label } +export function getUserType(userType?: AdminUserType | string | null): StatusPresentation { + return toStatusPresentation(USER_TYPE, userType ?? undefined) +} + +export function getUserGender(gender?: string | null): StatusPresentation { + return toStatusPresentation(USER_GENDER, gender ?? undefined) +} + export function getUserAccountStatus(status?: UserStatus | string): StatusPresentation { return toStatusPresentation(USER_ACCOUNT_STATUS, status) }