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.
This commit is contained in:
alisaza 2026-09-07 11:29:29 +03:30
parent 094af1cb8b
commit 280eb14ee6
5 changed files with 188 additions and 69 deletions

View File

@ -1,24 +1,78 @@
'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[] = [
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 = <T,>(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<FilterOption[]>([])
useEffect(() => {
const loadCities = async () => {
try {
const citiesRes = await axiosInstance.get(API_ROUTES.GEOGRAPHY.ALL_CITIES)
setCityFilterItems(toSelectFilterItems(extractApiList<NamedEntity>(citiesRes.data)))
} catch {
setCityFilterItems([])
}
}
void loadCities()
}, [])
const columns: PaginationListColumnType[] = useMemo(
() => [
{
field: 'firstName',
label: 'نام',
@ -41,16 +95,39 @@ const columns: PaginationListColumnType[] = [
sortable: true,
},
{
field: 'role',
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: true,
filterItems: USER_ROLE_FILTER_ITEMS,
sortable: false,
filterItems: USER_TYPE_FILTER_ITEMS,
},
{
field: 'walletBalance',
label: 'موجودی کیف پول',
filterable: true,
type: 'inputFromTo',
sortable: false,
},
{
field: 'status',
label: 'وضعیت',
label: 'وضعیت حساب',
filterable: true,
type: 'select',
sortable: true,
@ -65,15 +142,15 @@ const columns: PaginationListColumnType[] = [
filterItems: IDENTITY_STATUS_FILTER_ITEMS,
},
{
field: 'lastLoginAt',
label: 'آخرین ورود',
field: 'createdAt',
label: 'تاریخ ثبت نام',
filterable: true,
sortable: true,
type: 'dateFromTo',
},
{
field: 'createdAt',
label: 'تاریخ ثبت',
field: 'lastLoginAt',
label: 'آخرین ورود',
filterable: true,
sortable: true,
type: 'dateFromTo',
@ -82,9 +159,10 @@ const columns: PaginationListColumnType[] = [
field: 'actions',
label: 'عملیات',
},
]
],
[cityFilterItems]
)
const UsersPage = () => {
return (
<section className="h-full w-full text-right">
<PageNavbar pageTitle="کاربران" />
@ -95,7 +173,10 @@ const UsersPage = () => {
>
{{
mobile: (_row, cellValue) => formatIranianMobile(cellValue),
role: (_row, cellValue) => <StatusChip {...getUserRole(coerceToString(cellValue))} />,
cityId: (row) => (row as UserListRow).cityName || '—',
gender: (_row, cellValue) => <StatusChip {...getUserGender(coerceToString(cellValue))} />,
userType: (_row, cellValue) => <StatusChip {...getUserType(coerceToString(cellValue))} />,
walletBalance: (_row, cellValue) => formatToman(cellValue),
status: (_row, cellValue) => <StatusChip {...getUserAccountStatus(coerceToString(cellValue))} />,
identityStatus: (_row, cellValue) => <StatusChip {...getIdentityStatus(coerceToString(cellValue))} />,
lastLoginAt: (_row, cellValue) => formatPersianDate(cellValue),

View File

@ -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'

View File

@ -31,6 +31,11 @@ export const status = {
user,
admin,
},
userType: {
guest: 'مهمان',
host: 'میزبان',
admin: 'ادمین',
},
userAccount: {
active,
pending,

View File

@ -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

View File

@ -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<UserRole, StatusMeta> = {
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<AdminUserType, StatusMeta> = {
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<UserStatus, StatusMeta> = {
active: { label: texts.status.userAccount.active, chipColor: 'success', filterable: true },
@ -85,6 +101,8 @@ const REPORT_STATUS: Record<ReportStatus, StatusMeta> = {
}
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)
}