Разработка админ-панели 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
+244
View File
@@ -0,0 +1,244 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Space, Select, DatePicker, Spin } from 'antd';
import { Link } from 'react-router-dom';
import { useAudit } from '../../hooks/useAudit';
import { useAdmin, useAdmins } from '../../hooks/useAdmins';
import { useUser } from '../../hooks/useUsers';
import { useEvent } from '../../hooks/useEvents';
import { useReview } from '../../hooks/useReviews';
import { AuditRecord, AuditListParams } from '../../types/api';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
const { RangePicker } = DatePicker;
const AuditPage: React.FC = () => {
const [params, setParams] = useState<AuditListParams>({ limit: 20, offset: 0, sort: 'timestamp', order: 'desc' });
const { data, isLoading } = useAudit(params);
const { data: admins, isLoading: loadingAdmins } = useAdmins({ limit: 1000, offset: 0 });
const [uniqueActions, setUniqueActions] = useState<string[]>([]);
useEffect(() => {
if (data?.data) {
const actions = new Set(data.data.map(record => record.action).filter(Boolean));
setUniqueActions(prev => {
const merged = new Set([...prev, ...actions]);
return Array.from(merged).sort();
});
}
}, [data]);
const AdminCell: React.FC<{ adminId: string }> = ({ adminId }) => {
const { data: admin, isLoading: loading } = useAdmin(adminId);
if (loading) return <Spin size="small" />;
if (!admin) return <span>{adminId}</span>;
const name = admin.nickname && admin.nickname !== '-' ? admin.nickname : admin.email;
return <Link to={`/admins/${admin.id}`}>{name || admin.id}</Link>;
};
const isBadValue = (val: any) =>
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
const EntityNameCell: React.FC<{ entityType: string; entityId: string }> = ({ entityType, entityId }) => {
const { data: user, isLoading: loadingUser } = useUser(entityType === 'user' ? entityId : '');
const { data: event, isLoading: loadingEvent } = useEvent(entityType === 'event' ? entityId : '');
const { data: review, isLoading: loadingReview } = useReview(entityType === 'review' ? entityId : '');
if (entityType === 'user') {
if (loadingUser) return <Spin size="small" />;
if (!user) return <span>{entityId}</span>;
const name = !isBadValue(user.nickname) ? user.nickname : user.email;
return <Link to={`/users/${user.id}`}>{!isBadValue(name) ? name : user.id}</Link>;
}
if (entityType === 'event') {
if (loadingEvent) return <Spin size="small" />;
if (!event) return <span>{entityId}</span>;
const name = !isBadValue(event.title) ? event.title : event.id;
return <Link to={`/events/${event.id}`}>{name}</Link>;
}
if (entityType === 'review') {
if (loadingReview) return <Spin size="small" />;
// Для отзыва показываем ссылку на страницу отзыва
return <Link to={`/reviews/${entityId}`}>{entityId}</Link>;
}
// Для остальных типов (calendar, report, ticket, subscription, admin) пока просто ID
return <span>{entityId}</span>;
};
const handleTableChange = (
pagination: any,
filters: any,
sorter: SorterResult<AuditRecord> | SorterResult<AuditRecord>[]
) => {
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 handleDateChange = (dates: any) => {
if (dates) {
setParams({
...params,
date_from: dates[0]?.toISOString(),
date_to: dates[1]?.toISOString(),
offset: 0,
});
} else {
setParams({ ...params, date_from: undefined, date_to: undefined, offset: 0 });
}
};
const roleColors: Record<string, string> = {
superadmin: 'red',
admin: 'blue',
moderator: 'purple',
support: 'cyan',
};
const actionColors: Record<string, string> = {
create: 'green',
update: 'blue',
delete: 'red',
freeze: 'orange',
unfreeze: 'cyan',
block: 'orange',
unblock: 'cyan',
hide: 'orange',
unhide: 'cyan',
login: 'geekblue',
logout: 'default',
};
const entityTypeColors: Record<string, string> = {
user: 'blue',
event: 'green',
calendar: 'orange',
review: 'purple',
report: 'red',
ticket: 'default',
subscription: 'cyan',
admin: 'magenta',
};
const columns: ColumnsType<AuditRecord> = [
{
title: 'Админ',
key: 'admin',
render: (_, record) => <AdminCell adminId={record.admin_id} />,
},
{
title: 'Роль',
dataIndex: 'role',
key: 'role',
width: 120,
sorter: true,
render: (role: string) => (
<Tag color={roleColors[role] || 'default'}>{role}</Tag>
),
},
{
title: 'Действие',
dataIndex: 'action',
key: 'action',
width: 120,
sorter: true,
render: (action: string) => (
<Tag color={actionColors[action] || 'default'}>{action}</Tag>
),
},
{
title: 'Тип',
dataIndex: 'entity_type',
key: 'entity_type',
width: 120,
sorter: true,
render: (type: string) => (
<Tag color={entityTypeColors[type] || 'default'}>{type}</Tag>
),
},
{
title: 'Наименование',
key: 'entity_name',
render: (_, record) => (
<EntityNameCell entityType={record.entity_type} entityId={record.entity_id} />
),
},
{
title: 'Дата',
dataIndex: 'timestamp',
key: 'timestamp',
sorter: true,
},
{ title: 'IP', dataIndex: 'ip', key: 'ip', width: 130 },
{
title: 'Причина',
dataIndex: 'reason',
key: 'reason',
ellipsis: true,
},
];
return (
<div>
<h2>Аудит</h2>
<Space style={{ marginBottom: 16 }}>
<Select
placeholder="Администратор"
loading={loadingAdmins}
allowClear
onChange={(val) => setParams(prev => ({ ...prev, admin_id: val || undefined, offset: 0 }))}
style={{ width: 250 }}
showSearch
optionFilterProp="label"
>
{(admins?.data || []).map(admin => {
const label = admin.email || admin.id;
return (
<Select.Option key={admin.id} value={admin.id} label={label}>
{label}
</Select.Option>
);
})}
</Select>
<Select
placeholder="Действие"
allowClear
onChange={(val) => setParams(prev => ({ ...prev, action: val || undefined, offset: 0 }))}
style={{ width: 200 }}
>
{uniqueActions.map(action => (
<Select.Option key={action} value={action}>{action}</Select.Option>
))}
</Select>
<RangePicker
onChange={handleDateChange}
allowClear
/>
</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,
}}
/>
</div>
);
};
export default AuditPage;