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,90 +1,168 @@
'use client' 'use client'
import { useEffect, useMemo, useState } from 'react'
import { coerceToString } from '@/helpers' import { coerceToString } from '@/helpers'
import type { PaginationListColumnType } from '@/types' import type { PaginationListColumnType } from '@/types'
import PaginatedList from '@/components/PaginatedList' import PaginatedList from '@/components/PaginatedList'
import StatusChip from '@/components/ui/StatusChip' import StatusChip from '@/components/ui/StatusChip'
import PageNavbar from '@/components/layouts/PageNavbar' import PageNavbar from '@/components/layouts/PageNavbar'
import AdminTableViewButton from '@/components/ui/AdminTableViewButton' import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
import axiosInstance from '@/config/axios'
import { APP_ROUTES } from '@/constants/routes' import { APP_ROUTES } from '@/constants/routes'
import { import {
getIdentityStatus, getIdentityStatus,
getUserAccountStatus, getUserAccountStatus,
getUserRole, getUserGender,
getUserType,
IDENTITY_STATUS_FILTER_ITEMS, IDENTITY_STATUS_FILTER_ITEMS,
USER_ACCOUNT_STATUS_FILTER_ITEMS, USER_ACCOUNT_STATUS_FILTER_ITEMS,
USER_ROLE_FILTER_ITEMS, USER_GENDER_FILTER_ITEMS,
USER_TYPE_FILTER_ITEMS,
} from '@/constants/status' } from '@/constants/status'
import { API_ROUTES } from '@/services/config' import { formatToman } from '@/features/admin-users/adminUserDetailUi'
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters' import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
import { API_ROUTES } from '@/services/config'
const columns: PaginationListColumnType[] = [ interface NamedEntity {
{ id: number | string
field: 'firstName', name: string
label: 'نام', }
filterable: true,
type: 'text', interface UserListRow {
sortable: false, id: string
}, cityName?: string | null
{ [key: string]: unknown
field: 'lastName', }
label: 'نام خانوادگی',
filterable: true, interface FilterOption {
type: 'text', code: string
sortable: false, name: string
}, }
{
field: 'mobile', const toSelectFilterItems = (items: NamedEntity[]): FilterOption[] => items.map((item) => ({ code: String(item.id), name: item.name }))
label: 'موبایل',
filterable: true, const extractApiList = <T,>(raw: unknown): T[] => {
type: 'text', if (Array.isArray(raw)) return raw as T[]
sortable: true,
}, if (typeof raw === 'object' && raw !== null && 'data' in raw) {
{ const data = raw.data
field: 'role',
label: 'نقش', if (Array.isArray(data)) return data as T[]
filterable: true, }
type: 'select',
sortable: true, return []
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: 'عملیات',
},
]
const UsersPage = () => { 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: 'نام',
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 ( return (
<section className="h-full w-full text-right"> <section className="h-full w-full text-right">
<PageNavbar pageTitle="کاربران" /> <PageNavbar pageTitle="کاربران" />
@ -95,7 +173,10 @@ const UsersPage = () => {
> >
{{ {{
mobile: (_row, cellValue) => formatIranianMobile(cellValue), 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))} />, status: (_row, cellValue) => <StatusChip {...getUserAccountStatus(coerceToString(cellValue))} />,
identityStatus: (_row, cellValue) => <StatusChip {...getIdentityStatus(coerceToString(cellValue))} />, identityStatus: (_row, cellValue) => <StatusChip {...getIdentityStatus(coerceToString(cellValue))} />,
lastLoginAt: (_row, cellValue) => formatPersianDate(cellValue), lastLoginAt: (_row, cellValue) => formatPersianDate(cellValue),

View File

@ -40,8 +40,10 @@ export {
getBankAccountVerificationStatus, getBankAccountVerificationStatus,
getSmsStatus, getSmsStatus,
getUserAccountStatus, getUserAccountStatus,
getUserGender,
getUserRole, getUserRole,
getUserRoleLabel, getUserRoleLabel,
getUserType,
getWithdrawalRequestStatus, getWithdrawalRequestStatus,
IDENTITY_STATUS_FILTER_ITEMS, IDENTITY_STATUS_FILTER_ITEMS,
IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS, IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS,
@ -53,6 +55,8 @@ export {
BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS, BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS,
SMS_STATUS_FILTER_ITEMS, SMS_STATUS_FILTER_ITEMS,
USER_ACCOUNT_STATUS_FILTER_ITEMS, USER_ACCOUNT_STATUS_FILTER_ITEMS,
USER_GENDER_FILTER_ITEMS,
USER_ROLE_FILTER_ITEMS, USER_ROLE_FILTER_ITEMS,
USER_TYPE_FILTER_ITEMS,
WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS, WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS,
} from '@/types/status' } from '@/types/status'

View File

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

View File

@ -171,11 +171,14 @@ interface AdminUserListItem {
lastName: string lastName: string
gender: UserGender gender: UserGender
cityId?: number | null cityId?: number | null
cityName?: string | null
avatarUrl?: string | null avatarUrl?: string | null
bio?: string | null bio?: string | null
status: UserAccountStatus status: UserAccountStatus
identityStatus: IdentityStatus identityStatus: IdentityStatus
role: UserRole role: UserRole
userType?: 'guest' | 'host' | 'admin'
walletBalance?: number
mobileVerifiedAt?: string | null mobileVerifiedAt?: string | null
lastLoginAt?: string | null lastLoginAt?: string | null
createdAt: string createdAt: string

View File

@ -34,12 +34,28 @@ function toStatusPresentation(
return { label: meta.label, chipColor: meta.chipColor } 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. */ /** User role — label, chip color, and table filter options. */
const USER_ROLE: Record<UserRole, StatusMeta> = { const USER_ROLE: Record<UserRole, StatusMeta> = {
user: { label: texts.status.userRole.user, chipColor: 'default', filterable: true }, user: { label: texts.status.userRole.user, chipColor: 'default', filterable: true },
admin: { label: texts.status.userRole.admin, chipColor: 'warning', 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. */ /** User account status — label, chip color, and table filter options. */
const USER_ACCOUNT_STATUS: Record<UserStatus, StatusMeta> = { const USER_ACCOUNT_STATUS: Record<UserStatus, StatusMeta> = {
active: { label: texts.status.userAccount.active, chipColor: 'success', filterable: true }, 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_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 USER_ACCOUNT_STATUS_FILTER_ITEMS = toFilterItems(USER_ACCOUNT_STATUS)
export const IDENTITY_STATUS_FILTER_ITEMS = toFilterItems(IDENTITY_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 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 { export function getUserAccountStatus(status?: UserStatus | string): StatusPresentation {
return toStatusPresentation(USER_ACCOUNT_STATUS, status) return toStatusPresentation(USER_ACCOUNT_STATUS, status)
} }