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
+69 -111
View File
@@ -1,5 +1,5 @@
import React from 'react'; 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 { import {
UserOutlined, UserOutlined,
CalendarOutlined, CalendarOutlined,
@@ -10,138 +10,99 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
DollarOutlined, DollarOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Link, useNavigate } from 'react-router-dom';
import { useDashboardStats } from '../../hooks/useDashboard'; import { useDashboardStats } from '../../hooks/useDashboard';
import type { ColumnsType } from 'antd/es/table'; import { useAuthStore } from '../../store/authStore';
import { AdminActivity } from '../../types/api';
import {
AreaChart,
Area,
XAxis,
Tooltip,
} from 'recharts';
import DailyLineChartCard from '../../components/DailyLineChartCard'; import DailyLineChartCard from '../../components/DailyLineChartCard';
import StatisticSparklineCard from '../../components/StatisticSparklineCard';
const DashboardPage: React.FC = () => { const DashboardPage: React.FC = () => {
const { data, isLoading, error } = useDashboardStats(); 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 (isLoading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
if (error) return <Alert type="error" message="Ошибка загрузки статистики" />; if (error) return <Alert type="error" message="Ошибка загрузки статистики" />;
if (!data) return null; if (!data) return null;
const roleColors: Record<string, string> = { if (role === 'moderator') {
superadmin: 'red', return (
admin: 'blue', <div>
moderator: 'purple', <h2>Модерация</h2>
support: 'cyan', <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> = [ if (role === 'support') {
{ title: 'Email', dataIndex: 'email', key: 'email' }, return (
{ title: 'Ник', dataIndex: 'nickname', key: 'nickname' }, <div>
{ <h2>Поддержка</h2>
title: 'Роль', <Row gutter={[16, 16]}>
dataIndex: 'role', <Col xs={24} sm={8}>
key: 'role', <Card><Statistic title="Назначено открытых" value={data.tickets_assigned_open ?? 0} prefix={<BugOutlined />} /></Card>
render: (role: string) => ( </Col>
<Tag color={roleColors[role] || 'default'}>{role}</Tag> <Col xs={24} sm={8}>
), <Card><Statistic title="Всего назначено" value={data.tickets_assigned_total ?? 0} /></Card>
}, </Col>
{ title: 'Действий', dataIndex: 'actions', key: 'actions' }, <Col xs={24} sm={8}>
{ title: 'Последний вход', dataIndex: 'last_login', key: 'last_login' }, <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, 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, date: item.date,
registrations: item.count, value: item.count,
})) || []; })) ?? [];
return ( return (
<div> <div>
<h2>Общая статистика</h2> <h2>Общая статистика</h2>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Card> <StatisticSparklineCard
<div style={{ display: 'flex', alignItems: 'center' }}>
<Statistic
title="Пользователи" title="Пользователи"
value={data.users_total} value={data.users_total}
prefix={<UserOutlined />} prefix={<UserOutlined />}
style={{ flex: '0 0 auto', marginRight: 16 }} data={registrationsSparkline}
color="#52c41a"
gradientId="colorRegistrations"
/> />
{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>
</Col> </Col>
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>
<Card> <StatisticSparklineCard
<div style={{ display: 'flex', alignItems: 'center' }}>
<Statistic
title="События" title="События"
value={data.events_total} value={data.events_total}
prefix={<TeamOutlined />} prefix={<TeamOutlined />}
style={{ flex: '0 0 auto', marginRight: 16 }} data={eventsSparkline}
color="#1890ff"
gradientId="colorEvents"
/> />
{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>
</Col> </Col>
<Col xs={24} sm={12} lg={6}> <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" /> <DailyLineChartCard title="Неподтверждённые по дням" data={data.pending_users_by_day} color="#fa8c16" />
</Row> </Row>
{data.admin_activity && data.admin_activity.length > 0 && ( {role === 'superadmin' && (
<> <Alert
<h2 style={{ marginTop: 32 }}>Активность администраторов</h2> style={{ marginTop: 32 }}
<Table type="info"
columns={adminColumns} showIcon
dataSource={data.admin_activity} message="Активность администраторов"
rowKey="admin_id" description={<>Подробный журнал действий в разделе <Link to="/audit">Аудит</Link>.</>}
pagination={false}
size="small"
/> />
</>
)} )}
</div> </div>
); );
+5
View File
@@ -350,6 +350,11 @@ export interface DashboardStats {
tickets_by_day?: DayCount[]; tickets_by_day?: DayCount[];
subscriptions_by_day?: DayCount[]; subscriptions_by_day?: DayCount[];
pending_users_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 { export interface AdminActivity {