150 lines
7.1 KiB
TypeScript
150 lines
7.1 KiB
TypeScript
import React, { useEffect } from 'react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import { Card, Descriptions, Spin, Button, Tag, Space, Form, Select, Input } from 'antd';
|
||
import { Link } from 'react-router-dom';
|
||
import { useTicket, useUpdateTicket } from '../../hooks/useTickets';
|
||
import { useUser } from '../../hooks/useUsers';
|
||
import { useAdmin, useAdmins } from '../../hooks/useAdmins';
|
||
import dayjs from 'dayjs';
|
||
|
||
const TicketDetailPage: React.FC = () => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
const { data: ticket, isLoading } = useTicket(id || '');
|
||
const updateTicket = useUpdateTicket();
|
||
const [form] = Form.useForm();
|
||
|
||
const { data: admins, isLoading: loadingAdmins } = useAdmins({ limit: 1000, offset: 0 });
|
||
|
||
const reporterId = ticket?.reporter_id;
|
||
const { data: reporter, isLoading: loadingReporter } = useUser(reporterId || '');
|
||
|
||
const assignedId = ticket?.assigned_to;
|
||
const { data: assignedAdmin, isLoading: loadingAssigned } = useAdmin(assignedId || '');
|
||
|
||
useEffect(() => {
|
||
if (ticket) {
|
||
form.setFieldsValue({
|
||
status: ticket.status,
|
||
assigned_to: !isBadValue(ticket.assigned_to) ? ticket.assigned_to : undefined,
|
||
resolution_note: !isBadValue(ticket.resolution_note) ? ticket.resolution_note : '',
|
||
});
|
||
}
|
||
}, [ticket, form]);
|
||
|
||
const handleSave = () => {
|
||
form.validateFields().then((values) => {
|
||
const payload = Object.fromEntries(
|
||
Object.entries(values).filter(([_, v]) => v !== '' && v !== undefined && v !== null)
|
||
);
|
||
updateTicket.mutate(
|
||
{ id: id!, data: payload },
|
||
{ onSuccess: () => navigate('/tickets') }
|
||
);
|
||
});
|
||
};
|
||
|
||
const isBadValue = (val: any) =>
|
||
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
if (isBadValue(dateStr)) return '-';
|
||
const d = dayjs(dateStr);
|
||
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
|
||
};
|
||
|
||
const getUserLink = () => {
|
||
if (loadingReporter) return <Spin size="small" />;
|
||
if (!reporter) return reporterId || '-';
|
||
const name = reporter.nickname && !isBadValue(reporter.nickname)
|
||
? reporter.nickname
|
||
: reporter.email;
|
||
return <Link to={`/users/${reporter.id}`}>{name || reporter.id}</Link>;
|
||
};
|
||
|
||
if (isLoading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
|
||
if (!ticket) return <p>Тикет не найден</p>;
|
||
|
||
return (
|
||
<Card title={`Тикет ${ticket.id}`}>
|
||
<Descriptions bordered column={1} size="small" style={{ marginBottom: 24 }}>
|
||
<Descriptions.Item label="ID">{ticket.id}</Descriptions.Item>
|
||
<Descriptions.Item label="Отправитель">{getUserLink()}</Descriptions.Item>
|
||
<Descriptions.Item label="Хеш ошибки">{ticket.error_hash}</Descriptions.Item>
|
||
<Descriptions.Item label="Сообщение">{ticket.error_message}</Descriptions.Item>
|
||
<Descriptions.Item label="Стектрейс">
|
||
<pre style={{ maxHeight: 200, overflow: 'auto' }}>
|
||
{isBadValue(ticket.stacktrace) ? '-' : ticket.stacktrace}
|
||
</pre>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Контекст">
|
||
{isBadValue(ticket.context) ? '-' : ticket.context}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Количество">{ticket.count}</Descriptions.Item>
|
||
<Descriptions.Item label="Первый раз">{formatDate(ticket.first_seen)}</Descriptions.Item>
|
||
<Descriptions.Item label="Последний раз">{formatDate(ticket.last_seen)}</Descriptions.Item>
|
||
<Descriptions.Item label="Статус">
|
||
<Tag color={
|
||
ticket.status === 'open' ? 'red' :
|
||
ticket.status === 'in_progress' ? 'blue' :
|
||
ticket.status === 'resolved' ? 'green' : 'default'
|
||
}>
|
||
{ticket.status}
|
||
</Tag>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Назначен">
|
||
{loadingAssigned ? <Spin size="small" /> : (
|
||
assignedAdmin ? (
|
||
<Link to={`/admins/${assignedAdmin.id}`}>
|
||
{!isBadValue(assignedAdmin.nickname) ? assignedAdmin.nickname : assignedAdmin.email || assignedAdmin.id}
|
||
</Link>
|
||
) : (isBadValue(ticket.assigned_to) ? '-' : ticket.assigned_to)
|
||
)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Решение">
|
||
{isBadValue(ticket.resolution_note) ? '-' : ticket.resolution_note}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
|
||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||
<Form.Item label="Статус" name="status">
|
||
<Select>
|
||
<Select.Option value="open">open</Select.Option>
|
||
<Select.Option value="in_progress">in_progress</Select.Option>
|
||
<Select.Option value="resolved">resolved</Select.Option>
|
||
<Select.Option value="closed">closed</Select.Option>
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item label="Назначить" name="assigned_to">
|
||
<Select
|
||
placeholder="Выберите администратора"
|
||
loading={loadingAdmins}
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
>
|
||
{(admins?.data || []).map((admin) => {
|
||
const label = `${admin.email}${admin.nickname && admin.nickname !== '-' ? ` – ${admin.nickname}` : ''}`;
|
||
return (
|
||
<Select.Option key={admin.id} value={admin.id} label={label}>
|
||
{label}
|
||
</Select.Option>
|
||
);
|
||
})}
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item label="Комментарий решения" name="resolution_note">
|
||
<Input.TextArea rows={3} />
|
||
</Form.Item>
|
||
<Space>
|
||
<Button type="primary" htmlType="submit" loading={updateTicket.isPending}>
|
||
Сохранить
|
||
</Button>
|
||
<Button onClick={() => navigate('/tickets')}>Назад к списку</Button>
|
||
</Space>
|
||
</Form>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default TicketDetailPage; |