Files
EventHubFrontAdmin/src/pages/users/UserListPage.tsx
T
2026-05-23 22:31:14 +03:00

354 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef } from 'react';
import { Table, Button, Tag, Space, Modal, Form, Select, Spin, Input, Tooltip } from 'antd';
import { InfoCircleOutlined, EditOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useUsers, useUpdateUser, useDeleteUser, useUser } from '../../hooks/useUsers';
import { User, UserListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
const UserListPage: React.FC = () => {
const navigate = useNavigate();
const [params, setParams] = useState<UserListParams>({ limit: 20, offset: 0, sort: 'id', order: 'asc' });
const { data, isLoading } = useUsers(params);
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
// Редактирование
const [editModal, setEditModal] = useState<{ open: boolean; userId: string | null }>({
open: false,
userId: null,
});
const { data: editingUser, isLoading: loadingUser } = useUser(editModal.userId || '');
const [editForm] = Form.useForm();
const [editHasErrors, setEditHasErrors] = useState(true);
const originalUserRef = useRef<User | null>(null);
// Изменение статуса
const [statusModal, setStatusModal] = useState<{
open: boolean;
userId: string | null;
newStatus: string;
currentReason: string;
}>({
open: false,
userId: null,
newStatus: 'active',
currentReason: '',
});
const [statusForm] = Form.useForm();
// ==================== Редактирование ====================
const handleEdit = (id: string) => {
setEditModal({ open: true, userId: id });
};
const handleSaveEdit = () => {
editForm.validateFields().then((values) => {
if (!editModal.userId || !originalUserRef.current) return;
const original = originalUserRef.current;
const cleanedValues: Record<string, any> = {};
const allKeys = new Set([...Object.keys(values), ...Object.keys(original)]);
allKeys.forEach((key) => {
const newVal = values[key];
if (newVal === '' || newVal === undefined || newVal === null) {
const origVal = (original as any)[key];
if (origVal && origVal !== '-' && origVal !== '' && origVal !== undefined && origVal !== null) {
cleanedValues[key] = null;
}
return;
}
if (newVal === '-') return;
cleanedValues[key] = newVal;
});
const payload = Object.fromEntries(
Object.entries(cleanedValues).filter(([key, v]) => {
const origVal = (original as any)[key];
return JSON.stringify(v) !== JSON.stringify(origVal === '-' ? undefined : origVal);
})
);
updateUser.mutate(
{ id: editModal.userId, data: payload },
{ onSuccess: () => setEditModal({ open: false, userId: null }) }
);
});
};
useEffect(() => {
if (editingUser && editModal.open) {
originalUserRef.current = editingUser;
const clean = (val: any) => (val === '-' || val === 'undefined' ? undefined : val);
setTimeout(() => {
editForm.setFieldsValue({
email: clean(editingUser.email),
nickname: clean(editingUser.nickname),
role: clean(editingUser.role),
status: clean(editingUser.status),
reason: clean(editingUser.reason),
});
validateEditForm();
}, 0);
}
}, [editingUser, editModal.open, editForm]);
const validateEditForm = () => {
const role = editForm.getFieldValue('role');
const status = editForm.getFieldValue('status');
const hasErrors = !role || !status;
setEditHasErrors(hasErrors);
};
// ==================== Изменение статуса ====================
const handleStatusChange = (user: User) => {
if (user.status === 'deleted') return;
const newStatus = user.status === 'active' ? 'frozen' : 'active';
const currentReason = user.reason && user.reason !== '-' ? user.reason : '';
setStatusModal({
open: true,
userId: user.id,
newStatus,
currentReason,
});
};
useEffect(() => {
if (statusModal.open) {
setTimeout(() => {
statusForm.setFieldsValue({ reason: statusModal.currentReason });
}, 0);
}
}, [statusModal.open, statusModal.currentReason, statusForm]);
const handleStatusSave = () => {
statusForm.validateFields().then((values) => {
if (!statusModal.userId) return;
const payload: any = { status: statusModal.newStatus };
if (values.reason && values.reason.trim() !== '') {
payload.reason = values.reason;
}
updateUser.mutate(
{ id: statusModal.userId, data: payload },
{ onSuccess: () => setStatusModal({ open: false, userId: null, newStatus: 'active', currentReason: '' }) }
);
});
};
// ==================== Удаление ====================
const handleDelete = (id: string) => {
Modal.confirm({
title: 'Удалить пользователя?',
content: 'Это действие нельзя отменить. При необходимости предварительно укажите причину через редактирование.',
okText: 'Удалить',
okType: 'danger',
cancelText: 'Отмена',
onOk: () => deleteUser.mutate(id),
});
};
// ==================== Таблица ====================
const handleTableChange = (
pagination: any,
_filters: any,
sorter: SorterResult<User> | SorterResult<User>[]
) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter;
setParams(prev => ({
...prev,
sort: s.field as string,
order: s.order === 'ascend' ? 'asc' : s.order === 'descend' ? 'desc' : undefined,
offset: ((pagination.current - 1) * pagination.pageSize) || 0,
limit: pagination.pageSize || prev.limit,
}));
};
const columns: ColumnsType<User> = [
{
title: <InfoCircleOutlined />,
key: 'detail',
width: 48,
align: 'center',
render: (_, record) => (
<Tooltip title="Открыть страницу пользователя">
<Button
icon={<InfoCircleOutlined />}
size="small"
type="link"
onClick={() => navigate(`/users/${record.id}`)}
/>
</Tooltip>
),
},
{ title: 'Email', dataIndex: 'email', key: 'email', sorter: true, ellipsis: true },
{ title: 'Ник', dataIndex: 'nickname', key: 'nickname', sorter: true, ellipsis: true },
{
title: 'Роль',
dataIndex: 'role',
key: 'role',
width: 100,
sorter: true,
render: (role: string) => {
const color = role === 'user' ? 'blue' : role === 'bot' ? 'purple' : 'default';
return <Tag color={color}>{role}</Tag>;
},
},
{
title: 'Статус',
dataIndex: 'status',
key: 'status',
width: 100,
sorter: true,
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : status === 'frozen' ? 'orange' : 'red'}>
{status}
</Tag>
),
},
{ title: 'Последний вход', dataIndex: 'last_login', key: 'last_login', sorter: true },
{
title: 'Действия',
key: 'actions',
width: 120,
render: (_, record) => {
const isDeleted = record.status === 'deleted';
return (
<Space>
<Tooltip title="Редактировать">
<Button
icon={<EditOutlined />}
size="small"
onClick={() => handleEdit(record.id)}
/>
</Tooltip>
<Tooltip title={record.status === 'active' ? 'Заморозить' : 'Разморозить'}>
<Button
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
size="small"
onClick={() => handleStatusChange(record)}
disabled={isDeleted}
/>
</Tooltip>
<Tooltip title="Удалить">
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => handleDelete(record.id)}
disabled={isDeleted}
/>
</Tooltip>
</Space>
);
},
},
];
return (
<div>
<h2>Пользователи</h2>
<Table
columns={columns}
dataSource={data?.data}
rowKey="id"
loading={isLoading}
onChange={handleTableChange}
pagination={{
total: data?.total,
current: (params.offset || 0) / (params.limit || 20) + 1,
pageSize: params.limit || 20,
showSizeChanger: false,
}}
/>
{/* Модальное окно редактирования */}
<Modal
title="Редактировать пользователя"
open={editModal.open}
onCancel={() => setEditModal({ open: false, userId: null })}
onOk={handleSaveEdit}
confirmLoading={updateUser.isPending}
destroyOnHidden
okButtonProps={{ disabled: editHasErrors }}
>
{loadingUser ? (
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
) : (
<Form form={editForm} layout="vertical" preserve={false} onValuesChange={validateEditForm}>
<Form.Item label="Email" name="email">
<Input />
</Form.Item>
<Form.Item label="Ник" name="nickname">
<Input />
</Form.Item>
<Form.Item
label="Роль"
name="role"
rules={[{ required: true, message: 'Выберите роль' }]}
>
<Select placeholder="Выберите роль">
<Select.Option value="user">user</Select.Option>
<Select.Option value="bot">bot</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Статус"
name="status"
rules={[{ required: true, message: 'Выберите статус' }]}
>
<Select placeholder="Выберите статус">
<Select.Option value="active">active</Select.Option>
<Select.Option value="frozen">frozen</Select.Option>
<Select.Option value="deleted">deleted</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Причина"
name="reason"
rules={[
({ getFieldValue }) => ({
validator(_, value) {
const status = getFieldValue('status');
if (status && status !== 'active' && (!value || value.trim() === '')) {
return Promise.reject(new Error('Укажите причину изменения статуса'));
}
return Promise.resolve();
},
}),
]}
>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
)}
</Modal>
{/* Модальное окно изменения статуса */}
<Modal
title={`Изменить статус на «${statusModal.newStatus}»`}
open={statusModal.open}
onCancel={() => setStatusModal({ open: false, userId: null, newStatus: 'active', currentReason: '' })}
onOk={handleStatusSave}
confirmLoading={updateUser.isPending}
destroyOnHidden
>
<Form form={statusForm} layout="vertical" preserve={false}>
<Form.Item
label="Причина"
name="reason"
rules={[
{
required: statusModal.newStatus !== 'active',
message: 'Укажите причину',
},
]}
>
<Input.TextArea rows={3} placeholder="Введите причину изменения статуса" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default UserListPage;