feat: all users table
This commit is contained in:
parent
f4c935babc
commit
af24047a20
5 changed files with 187 additions and 6 deletions
122
src/pages/Profile/AllUsersTable.tsx
Normal file
122
src/pages/Profile/AllUsersTable.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { App, Button, Table, Tooltip, Typography } from 'antd';
|
||||||
|
import type { TableColumnsType } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import EmptyValue from '@/components/ui/EmptyValue';
|
||||||
|
import { formatDate, formatDateTime } from '@/utils/formatDate';
|
||||||
|
import { usersApi } from '@/services/usersApi';
|
||||||
|
import type { AdminUserListItem } from '@/types/user';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
const GENDER_LABELS: Record<string, string> = {
|
||||||
|
m: 'مرد',
|
||||||
|
f: 'زن',
|
||||||
|
};
|
||||||
|
|
||||||
|
const truncateUuid = (value: string) => `${value.slice(0, 8)}…`;
|
||||||
|
|
||||||
|
const renderText = (value: string | null) => value ?? <EmptyValue />;
|
||||||
|
|
||||||
|
interface FetchResult {
|
||||||
|
page: number;
|
||||||
|
status: 'success' | 'error';
|
||||||
|
users: AdminUserListItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllUsersTableProps {
|
||||||
|
onSelectUuid: (uuid: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AllUsersTable = ({ onSelectUuid }: AllUsersTableProps) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [result, setResult] = useState<FetchResult | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
usersApi
|
||||||
|
.getAdminUsers({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE })
|
||||||
|
.then(({ results, count }) => {
|
||||||
|
setResult({ page, status: 'success', users: results, total: count });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setResult({ page, status: 'error', users: [], total: 0 });
|
||||||
|
message.error('دریافت لیست کاربران با خطا مواجه شد');
|
||||||
|
});
|
||||||
|
}, [page, message]);
|
||||||
|
|
||||||
|
const isCurrent = result?.page === page;
|
||||||
|
const isLoading = !isCurrent;
|
||||||
|
const users = isCurrent ? result.users : [];
|
||||||
|
const total = isCurrent ? result.total : 0;
|
||||||
|
|
||||||
|
const columns: TableColumnsType<AdminUserListItem> = [
|
||||||
|
{
|
||||||
|
title: 'شناسه یکتا',
|
||||||
|
dataIndex: 'uuid',
|
||||||
|
key: 'uuid',
|
||||||
|
render: (value: string) => (
|
||||||
|
<Tooltip title={value}>
|
||||||
|
<Text copyable={{ text: value }} style={{ direction: 'ltr', fontSize: 12 }}>
|
||||||
|
{truncateUuid(value)}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: 'نام کاربری', dataIndex: 'username', key: 'username', render: renderText },
|
||||||
|
{ title: 'ایمیل', dataIndex: 'email', key: 'email', render: renderText },
|
||||||
|
{
|
||||||
|
title: 'شماره تلفن',
|
||||||
|
dataIndex: 'phone_number',
|
||||||
|
key: 'phone_number',
|
||||||
|
render: (value: string | null) => (value ? <Text style={{ direction: 'ltr' }}>{value}</Text> : <EmptyValue />),
|
||||||
|
},
|
||||||
|
{ title: 'نام', dataIndex: 'first_name', key: 'first_name', render: renderText },
|
||||||
|
{ title: 'نام خانوادگی', dataIndex: 'last_name', key: 'last_name', render: renderText },
|
||||||
|
{ title: 'نام کامل', dataIndex: 'name', key: 'name', render: renderText },
|
||||||
|
{
|
||||||
|
title: 'جنسیت',
|
||||||
|
dataIndex: 'gender',
|
||||||
|
key: 'gender',
|
||||||
|
render: (value: string | null) => (value ? (GENDER_LABELS[value] ?? value) : <EmptyValue />),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تاریخ تولد',
|
||||||
|
dataIndex: 'birthdate',
|
||||||
|
key: 'birthdate',
|
||||||
|
render: (value: string | null) => (value ? formatDate(value) : <EmptyValue />),
|
||||||
|
},
|
||||||
|
{ title: 'تاریخ عضویت', dataIndex: 'date_joined', key: 'date_joined', render: formatDateTime },
|
||||||
|
{ title: 'آخرین بروزرسانی', dataIndex: 'last_update', key: 'last_update', render: formatDateTime },
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'actions',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button size="small" onClick={() => onSelectUuid(record.uuid)}>
|
||||||
|
مشاهده جزئیات
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowKey="uuid"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={users}
|
||||||
|
loading={isLoading}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
total,
|
||||||
|
showSizeChanger: false,
|
||||||
|
onChange: setPage,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AllUsersTable;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { App, Empty, Input, Spin } from 'antd';
|
import { App, Empty, Input, Spin, Typography } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { spacing } from '@/design/tokens/spacing';
|
import { spacing } from '@/design/tokens/spacing';
|
||||||
|
|
@ -8,6 +8,9 @@ import type { UserDetails } from '@/types/user';
|
||||||
import UserInfoCard from '@/pages/Profile/UserInfoCard';
|
import UserInfoCard from '@/pages/Profile/UserInfoCard';
|
||||||
import StatCardsRow from '@/pages/Profile/StatCards';
|
import StatCardsRow from '@/pages/Profile/StatCards';
|
||||||
import ProfileTabs from '@/pages/Profile/ProfileTabs';
|
import ProfileTabs from '@/pages/Profile/ProfileTabs';
|
||||||
|
import AllUsersTable from '@/pages/Profile/AllUsersTable';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
type SearchStatus = 'idle' | 'loading' | 'error' | 'success';
|
type SearchStatus = 'idle' | 'loading' | 'error' | 'success';
|
||||||
|
|
||||||
|
|
@ -69,6 +72,7 @@ const Profile = () => {
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: spacing.md }}>
|
<div style={{ marginBottom: spacing.md }}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
|
key={uuid}
|
||||||
placeholder="جستجو بر اساس شناسه (UUID) کاربر"
|
placeholder="جستجو بر اساس شناسه (UUID) کاربر"
|
||||||
defaultValue={uuid}
|
defaultValue={uuid}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
|
|
@ -79,10 +83,6 @@ const Profile = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status === 'idle' && (
|
|
||||||
<Empty description="برای مشاهده اطلاعات، شناسه (UUID) کاربر را جستجو کنید" style={{ padding: spacing.xl }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status === 'loading' && (
|
{status === 'loading' && (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', padding: spacing.xl }}>
|
<div style={{ display: 'flex', justifyContent: 'center', padding: spacing.xl }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
|
|
@ -101,6 +101,11 @@ const Profile = () => {
|
||||||
<ProfileTabs style={{ marginTop: spacing.lg }} />
|
<ProfileTabs style={{ marginTop: spacing.lg }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Title level={4} style={{ marginTop: spacing.xl, marginBottom: spacing.md }}>
|
||||||
|
لیست کاربران
|
||||||
|
</Title>
|
||||||
|
<AllUsersTable onSelectUuid={handleSearch} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ import axios from 'axios';
|
||||||
import { env } from '@/config/env';
|
import { env } from '@/config/env';
|
||||||
import { getAccessToken } from '@/utils/authToken';
|
import { getAccessToken } from '@/utils/authToken';
|
||||||
import { buildAuthUrl } from '@/utils/authUrl';
|
import { buildAuthUrl } from '@/utils/authUrl';
|
||||||
import type { UserDetails } from '@/types/user';
|
import type { AdminUsersResponse, UserDetails } from '@/types/user';
|
||||||
|
|
||||||
|
interface GetAdminUsersParams {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
getUserDetails: async (uuid: string): Promise<UserDetails> => {
|
getUserDetails: async (uuid: string): Promise<UserDetails> => {
|
||||||
|
|
@ -17,4 +22,18 @@ export const usersApi = {
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAdminUsers: async ({ limit, offset }: GetAdminUsersParams): Promise<AdminUsersResponse> => {
|
||||||
|
const response = await axios.get<AdminUsersResponse>(
|
||||||
|
buildAuthUrl('/accounts/users/api/admin/users/', env.apiBaseUrl),
|
||||||
|
{
|
||||||
|
params: { limit, offset },
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${getAccessToken()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,24 @@ export interface UserDetails {
|
||||||
postal_code: string;
|
postal_code: string;
|
||||||
address: string;
|
address: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminUserListItem {
|
||||||
|
uuid: string;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
phone_number: string | null;
|
||||||
|
first_name: string | null;
|
||||||
|
last_name: string | null;
|
||||||
|
name: string | null;
|
||||||
|
gender: string | null;
|
||||||
|
birthdate: string | null;
|
||||||
|
date_joined: string;
|
||||||
|
last_update: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUsersResponse {
|
||||||
|
count: number;
|
||||||
|
next: string | null;
|
||||||
|
previous: string | null;
|
||||||
|
results: AdminUserListItem[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,17 @@ export const formatDateTime = (value: string): string => {
|
||||||
})
|
})
|
||||||
.replace(',', '');
|
.replace(',', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatDate = (value: string): string => {
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue