fix(dashboard): ролевые виды и ссылка на аудит вместо дубля

Moderator/support видят свою статистику; admin_activity заменён ссылкой на /audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:54:51 +03:00
parent bc8ec9aeea
commit a1bd11afee
2 changed files with 83 additions and 120 deletions
+78 -120
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Card, Col, Row, Statistic, Spin, Alert, Table, Tag } from 'antd';
import { Card, Col, Row, Statistic, Spin, Alert, Button } from 'antd';
import {
UserOutlined,
CalendarOutlined,
@@ -10,138 +10,99 @@ import {
ClockCircleOutlined,
DollarOutlined,
} from '@ant-design/icons';
import { Link, useNavigate } from 'react-router-dom';
import { useDashboardStats } from '../../hooks/useDashboard';
import type { ColumnsType } from 'antd/es/table';
import { AdminActivity } from '../../types/api';
import {
AreaChart,
Area,
XAxis,
Tooltip,
} from 'recharts';
import { useAuthStore } from '../../store/authStore';
import DailyLineChartCard from '../../components/DailyLineChartCard';
import StatisticSparklineCard from '../../components/StatisticSparklineCard';
const DashboardPage: React.FC = () => {
const { data, isLoading, error } = useDashboardStats();
const role = useAuthStore((s) => s.user?.role);
const navigate = useNavigate();
if (isLoading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
if (error) return <Alert type="error" message="Ошибка загрузки статистики" />;
if (!data) return null;
const roleColors: Record<string, string> = {
superadmin: 'red',
admin: 'blue',
moderator: 'purple',
support: 'cyan',
};
if (role === 'moderator') {
return (
<div>
<h2>Модерация</h2>
<Row gutter={[16, 16]}>
<Col xs={24} sm={8}>
<Card><Statistic title="Рассмотрено" value={data.reports_reviewed ?? 0} prefix={<WarningOutlined />} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="Отклонено" value={data.reports_dismissed ?? 0} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="Ср. время (ч)" value={data.avg_report_resolution_h ?? 0} precision={1} prefix={<ClockCircleOutlined />} /></Card>
</Col>
</Row>
<Button type="primary" style={{ marginTop: 16 }} onClick={() => navigate('/reports')}>
Перейти к жалобам
</Button>
</div>
);
}
const adminColumns: ColumnsType<AdminActivity> = [
{ title: 'Email', dataIndex: 'email', key: 'email' },
{ title: 'Ник', dataIndex: 'nickname', key: 'nickname' },
{
title: 'Роль',
dataIndex: 'role',
key: 'role',
render: (role: string) => (
<Tag color={roleColors[role] || 'default'}>{role}</Tag>
),
},
{ title: 'Действий', dataIndex: 'actions', key: 'actions' },
{ title: 'Последний вход', dataIndex: 'last_login', key: 'last_login' },
];
if (role === 'support') {
return (
<div>
<h2>Поддержка</h2>
<Row gutter={[16, 16]}>
<Col xs={24} sm={8}>
<Card><Statistic title="Назначено открытых" value={data.tickets_assigned_open ?? 0} prefix={<BugOutlined />} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="Всего назначено" value={data.tickets_assigned_total ?? 0} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="Ср. решение (ч)" value={data.avg_ticket_resolution_h ?? 0} precision={1} prefix={<ClockCircleOutlined />} /></Card>
</Col>
</Row>
<Button type="primary" style={{ marginTop: 16 }} onClick={() => navigate('/tickets')}>
Перейти к тикетам
</Button>
</div>
);
}
const eventsChartData = data.events_by_day?.map((item) => ({
const registrationsSparkline = data.registrations_by_day?.map((item) => ({
date: item.date,
events: item.count,
})) || [];
value: item.count,
})) ?? [];
const registrationsChartData = data.registrations_by_day?.map((item) => ({
const eventsSparkline = data.events_by_day?.map((item) => ({
date: item.date,
registrations: item.count,
})) || [];
value: item.count,
})) ?? [];
return (
<div>
<h2>Общая статистика</h2>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Statistic
title="Пользователи"
value={data.users_total}
prefix={<UserOutlined />}
style={{ flex: '0 0 auto', marginRight: 16 }}
/>
{registrationsChartData.length > 0 && (
<AreaChart
width={150}
height={50}
data={registrationsChartData}
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorRegistrations" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#52c41a" stopOpacity={0.4} />
<stop offset="95%" stopColor="#52c41a" stopOpacity={0.05} />
</linearGradient>
</defs>
<XAxis dataKey="date" hide />
<Tooltip labelFormatter={(label) => `Дата: ${label}`} />
<Area
type="monotone"
dataKey="registrations"
name="Регистрации"
stroke="#52c41a"
fill="url(#colorRegistrations)"
strokeWidth={2}
dot={false}
activeDot={{ r: 3, strokeWidth: 0 }}
/>
</AreaChart>
)}
</div>
</Card>
<StatisticSparklineCard
title="Пользователи"
value={data.users_total}
prefix={<UserOutlined />}
data={registrationsSparkline}
color="#52c41a"
gradientId="colorRegistrations"
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Statistic
title="События"
value={data.events_total}
prefix={<TeamOutlined />}
style={{ flex: '0 0 auto', marginRight: 16 }}
/>
{eventsChartData.length > 0 && (
<AreaChart
width={150}
height={50}
data={eventsChartData}
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorEvents" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#1890ff" stopOpacity={0.4} />
<stop offset="95%" stopColor="#1890ff" stopOpacity={0.05} />
</linearGradient>
</defs>
<XAxis dataKey="date" hide />
<Tooltip labelFormatter={(label) => `Дата: ${label}`} />
<Area
type="monotone"
dataKey="events"
name="События"
stroke="#1890ff"
fill="url(#colorEvents)"
strokeWidth={2}
dot={false}
activeDot={{ r: 3, strokeWidth: 0 }}
/>
</AreaChart>
)}
</div>
</Card>
<StatisticSparklineCard
title="События"
value={data.events_total}
prefix={<TeamOutlined />}
data={eventsSparkline}
color="#1890ff"
gradientId="colorEvents"
/>
</Col>
<Col xs={24} sm={12} lg={6}>
@@ -240,17 +201,14 @@ const DashboardPage: React.FC = () => {
<DailyLineChartCard title="Неподтверждённые по дням" data={data.pending_users_by_day} color="#fa8c16" />
</Row>
{data.admin_activity && data.admin_activity.length > 0 && (
<>
<h2 style={{ marginTop: 32 }}>Активность администраторов</h2>
<Table
columns={adminColumns}
dataSource={data.admin_activity}
rowKey="admin_id"
pagination={false}
size="small"
/>
</>
{role === 'superadmin' && (
<Alert
style={{ marginTop: 32 }}
type="info"
showIcon
message="Активность администраторов"
description={<>Подробный журнал действий в разделе <Link to="/audit">Аудит</Link>.</>}
/>
)}
</div>
);
+5
View File
@@ -350,6 +350,11 @@ export interface DashboardStats {
tickets_by_day?: DayCount[];
subscriptions_by_day?: DayCount[];
pending_users_by_day?: DayCount[];
avg_report_resolution_h?: number;
events_moderated?: number;
// support
tickets_assigned_open?: number;
tickets_assigned_total?: number;
}
export interface AdminActivity {