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:
parent
094af1cb8b
commit
280eb14ee6
@ -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),
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -31,6 +31,11 @@ export const status = {
|
||||
user,
|
||||
admin,
|
||||
},
|
||||
userType: {
|
||||
guest: 'مهمان',
|
||||
host: 'میزبان',
|
||||
admin: 'ادمین',
|
||||
},
|
||||
userAccount: {
|
||||
active,
|
||||
pending,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user