fix(dashboard): ролевые виды и ссылка на аудит вместо дубля
Moderator/support видят свою статистику; admin_activity заменён ссылкой на /audit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user