Разработка админ-панели EventHubFrontAdmin v1.0 #1

This commit is contained in:
2026-05-23 20:43:21 +03:00
parent 36c95b71a4
commit f5961c5529
68 changed files with 10374 additions and 3 deletions
@@ -0,0 +1,263 @@
import React, { useState, useEffect } from 'react';
import { Table, Button, Tag, Space, Modal, Form, Select, DatePicker, Tooltip, Spin } from 'antd';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { useSubscriptions, useUpdateSubscription, useDeleteSubscription, useSubscription } from '../../hooks/useSubscriptions';
import { useUser } from '../../hooks/useUsers';
import { Subscription, SubscriptionListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import dayjs from 'dayjs';
const SubscriptionListPage: React.FC = () => {
const [params, setParams] = useState<SubscriptionListParams>({ limit: 20, offset: 0 });
const { data, isLoading } = useSubscriptions(params);
const updateSubscription = useUpdateSubscription();
const deleteSubscription = useDeleteSubscription();
const [editModal, setEditModal] = useState<{ open: boolean; subscriptionId: string | null }>({
open: false,
subscriptionId: null,
});
const { data: editingSubscription, isLoading: loadingSubscription } = useSubscription(editModal.subscriptionId || '');
const [form] = Form.useForm();
// Заполняем форму с задержкой, когда данные загружены
useEffect(() => {
if (editingSubscription && editModal.open) {
setTimeout(() => {
form.setFieldsValue({
plan: editingSubscription.plan,
status: editingSubscription.status,
trial_used: editingSubscription.trial_used,
expires_at: editingSubscription.expires_at ? dayjs(editingSubscription.expires_at) : null,
});
}, 0);
}
}, [editingSubscription, editModal.open, form]);
const handleEdit = (sub: Subscription) => {
// Сбрасываем форму перед открытием
form.resetFields();
setEditModal({ open: true, subscriptionId: sub.id });
};
const handleSave = () => {
form.validateFields().then((values) => {
if (!editModal.subscriptionId) return;
const payload = Object.fromEntries(
Object.entries(values)
.filter(([_, v]) => v !== '' && v !== undefined && v !== null)
.map(([key, val]) => {
if (dayjs.isDayjs(val)) return [key, val.toISOString()];
return [key, val];
})
);
updateSubscription.mutate(
{ id: editModal.subscriptionId, data: payload },
{ onSuccess: () => setEditModal({ open: false, subscriptionId: null }) }
);
});
};
const handleDelete = (id: string) => {
Modal.confirm({
title: 'Удалить подписку?',
content: 'Это действие нельзя отменить.',
okText: 'Удалить',
okType: 'danger',
cancelText: 'Отмена',
onOk: () => deleteSubscription.mutate(id),
});
};
const handleTableChange = (
pagination: any,
filters: any,
sorter: SorterResult<Subscription> | SorterResult<Subscription>[]
) => {
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 isBadValue = (val: any) => val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
// Компонент для резолвинга пользователя
const UserCell: React.FC<{ userId: string }> = ({ userId }) => {
const { data: user, isLoading: loading } = useUser(userId);
if (loading) return <Spin size="small" />;
if (!user) return <span>{userId}</span>;
const name = !isBadValue(user.nickname) ? user.nickname : user.email;
return <Link to={`/users/${user.id}`}>{!isBadValue(name) ? name : user.id}</Link>;
};
const formatDate = (dateStr: string) => {
if (isBadValue(dateStr)) return '-';
const d = dayjs(dateStr);
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
};
const planColors: Record<string, string> = {
trial: 'cyan',
monthly: 'blue',
quarterly: 'green',
biannual: 'purple',
annual: 'orange',
};
const columns: ColumnsType<Subscription> = [
{
title: 'Пользователь',
key: 'user',
ellipsis: true,
render: (_, record) => <UserCell userId={record.user_id} />,
},
{
title: 'План',
dataIndex: 'plan',
key: 'plan',
width: 100,
sorter: true,
render: (plan: string) => (
<Tag color={planColors[plan] || 'default'}>{plan}</Tag>
),
},
{
title: 'Статус',
dataIndex: 'status',
key: 'status',
width: 100,
sorter: true,
render: (status: string) => {
const color =
status === 'active' ? 'green' :
status === 'expired' ? 'orange' : 'red';
return <Tag color={color}>{status}</Tag>;
},
},
{
title: 'Пробный',
dataIndex: 'trial_used',
key: 'trial_used',
width: 100,
render: (v: boolean) => (v ? 'Да' : 'Нет'),
},
{ title: 'Начало', dataIndex: 'started_at', key: 'started_at', render: (val) => formatDate(val) },
{ title: 'Окончание', dataIndex: 'expires_at', key: 'expires_at', render: (val) => formatDate(val) },
{
title: 'Действия',
key: 'actions',
width: 100,
render: (_, record) => (
<Space>
<Tooltip title="Редактировать">
<Button
icon={<EditOutlined />}
size="small"
onClick={() => handleEdit(record)}
/>
</Tooltip>
<Tooltip title="Удалить">
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => handleDelete(record.id)}
/>
</Tooltip>
</Space>
),
},
];
return (
<div>
<h2>Подписки</h2>
<Space style={{ marginBottom: 16 }}>
<Select
allowClear
placeholder="План"
onChange={(val) => setParams({ ...params, plan: val, offset: 0 })}
style={{ width: 150 }}
>
<Select.Option value="monthly">monthly</Select.Option>
<Select.Option value="quarterly">quarterly</Select.Option>
<Select.Option value="biannual">biannual</Select.Option>
<Select.Option value="annual">annual</Select.Option>
<Select.Option value="trial">trial</Select.Option>
</Select>
<Select
allowClear
placeholder="Статус"
onChange={(val) => setParams({ ...params, status: val, offset: 0 })}
style={{ width: 150 }}
>
<Select.Option value="active">active</Select.Option>
<Select.Option value="expired">expired</Select.Option>
<Select.Option value="cancelled">cancelled</Select.Option>
</Select>
</Space>
<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, subscriptionId: null })}
onOk={handleSave}
confirmLoading={updateSubscription.isPending}
destroyOnHidden
>
{loadingSubscription ? (
<Spin />
) : (
<Form form={form} layout="vertical" preserve={false}>
<Form.Item label="План" name="plan" rules={[{ required: true }]}>
<Select>
<Select.Option value="monthly">monthly</Select.Option>
<Select.Option value="quarterly">quarterly</Select.Option>
<Select.Option value="biannual">biannual</Select.Option>
<Select.Option value="annual">annual</Select.Option>
<Select.Option value="trial">trial</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Статус" name="status" rules={[{ required: true }]}>
<Select>
<Select.Option value="active">active</Select.Option>
<Select.Option value="expired">expired</Select.Option>
<Select.Option value="cancelled">cancelled</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Пробный период использован" name="trial_used">
<Select>
<Select.Option value={true}>Да</Select.Option>
<Select.Option value={false}>Нет</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Дата окончания" name="expires_at">
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} />
</Form.Item>
</Form>
)}
</Modal>
</div>
);
};
export default SubscriptionListPage;