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