15.12.25 - Этап 3. Детализация карточек и тикеты
All checks were successful
Mirror frontend to backend repo / mirror (push) Successful in 6s
All checks were successful
Mirror frontend to backend repo / mirror (push) Successful in 6s
This commit is contained in:
13
src/App.tsx
13
src/App.tsx
@@ -11,6 +11,11 @@ import SearchPage from '@/pages/SearchPage';
|
||||
import TasksPage from '@/pages/TasksPage';
|
||||
import CompanyPage from '@/pages/companies/CompanyPage';
|
||||
|
||||
// Импорт детальных страниц
|
||||
import ServerDetails from '@/pages/equipment/ServerDetails';
|
||||
import FiscalDetails from '@/pages/equipment/FiscalDetails';
|
||||
import WorkstationDetails from '@/pages/equipment/WorkstationDetails';
|
||||
|
||||
import { useUiStore } from '@/store/uiStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { getThemeConfig } from '@/theme/themeConfig';
|
||||
@@ -37,9 +42,7 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const App: React.FC = () => {
|
||||
const themeMode = useUiStore((state) => state.themeMode);
|
||||
|
||||
// Обновляем логику фона: синхронизируем с themeConfig
|
||||
useEffect(() => {
|
||||
// #000000 для темной, #f0f2f5 для светлой
|
||||
const colorBgLayout = themeMode === 'dark' ? '#000000' : '#f0f2f5';
|
||||
document.body.style.backgroundColor = colorBgLayout;
|
||||
}, [themeMode]);
|
||||
@@ -69,13 +72,13 @@ const App: React.FC = () => {
|
||||
|
||||
{/* Роуты для оборудования */}
|
||||
<Route path="servers" element={<div>Список серверов</div>} />
|
||||
<Route path="servers/:id" element={<div>Детали сервера (в разработке)</div>} />
|
||||
<Route path="servers/:id" element={<ServerDetails />} />
|
||||
|
||||
<Route path="workstations" element={<div>Список РС</div>} />
|
||||
<Route path="workstations/:id" element={<div>Детали РС (в разработке)</div>} />
|
||||
<Route path="workstations/:id" element={<WorkstationDetails />} />
|
||||
|
||||
<Route path="fiscals" element={<div>Список ФР</div>} />
|
||||
<Route path="fiscals/:id" element={<div>Детали ФР (в разработке)</div>} />
|
||||
<Route path="fiscals/:id" element={<FiscalDetails />} />
|
||||
|
||||
<Route path="admin" element={<div>Админка</div>} />
|
||||
</Route>
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
import apiClient from './axios';
|
||||
import { ApiResponse } from '@/types/api';
|
||||
import { ApiResponse, ServerEntity, WorkstationEntity, FiscalEntity, UpdateServerDTO, UpdateWorkstationDTO, UpdateFiscalDTO } from '@/types/api';
|
||||
|
||||
export const equipmentApi = {
|
||||
// Принудительный опрос сервера
|
||||
// --- Servers ---
|
||||
getServer: async (uuid: string) => {
|
||||
const response = await apiClient.get<ApiResponse<ServerEntity>>(`/servers/${uuid}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateServer: async (uuid: string, data: UpdateServerDTO) => {
|
||||
const response = await apiClient.put<ApiResponse<ServerEntity>>(`/servers/${uuid}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
pollServer: async (uuid: string) => {
|
||||
// Предполагаем, что бэкенд поддерживает этот эндпоинт
|
||||
const response = await apiClient.post<ApiResponse<void>>(`/servers/${uuid}/poll`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- Workstations ---
|
||||
getWorkstation: async (uuid: string) => {
|
||||
const response = await apiClient.get<ApiResponse<WorkstationEntity>>(`/workstations/${uuid}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateWorkstation: async (uuid: string, data: UpdateWorkstationDTO) => {
|
||||
const response = await apiClient.put<ApiResponse<WorkstationEntity>>(`/workstations/${uuid}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- Fiscals ---
|
||||
getFiscal: async (uuid: string) => {
|
||||
const response = await apiClient.get<ApiResponse<FiscalEntity>>(`/fiscals/${uuid}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateFiscal: async (uuid: string, data: UpdateFiscalDTO) => {
|
||||
const response = await apiClient.put<ApiResponse<FiscalEntity>>(`/fiscals/${uuid}`, data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
16
src/api/tickets.ts
Normal file
16
src/api/tickets.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import apiClient from './axios';
|
||||
import { ApiResponse, TicketDTO, TicketListParams } from '@/types/api';
|
||||
|
||||
export const ticketsApi = {
|
||||
getTickets: async (params: TicketListParams = {}) => {
|
||||
const response = await apiClient.get<ApiResponse<TicketDTO[]>>('/tickets', {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTicket: async (id: number | string) => {
|
||||
const response = await apiClient.get<ApiResponse<TicketDTO>>(`/tickets/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
86
src/components/tickets/TicketTable.tsx
Normal file
86
src/components/tickets/TicketTable.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { Table, Tag, Typography } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ticketsApi } from '@/api/tickets';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface Props {
|
||||
companyId?: string;
|
||||
limit?: number;
|
||||
showPagination?: boolean;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const TicketTable: React.FC<Props> = ({ companyId, limit = 10, showPagination = true }) => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['tickets', companyId],
|
||||
queryFn: () => ticketsApi.getTickets({ company_id: companyId, limit }),
|
||||
});
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
switch (status) {
|
||||
case 'registered': return <Tag color="blue">Новая</Tag>;
|
||||
case 'inprogress': return <Tag color="processing">В работе</Tag>;
|
||||
case 'wait': return <Tag color="orange">Ожидание</Tag>;
|
||||
case 'closed': return <Tag color="green">Закрыта</Tag>;
|
||||
default: return <Tag>{status}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Номер',
|
||||
dataIndex: 'number',
|
||||
key: 'number',
|
||||
width: 100,
|
||||
render: (val: number) => <Text strong>#{val}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Тема',
|
||||
dataIndex: 'subject',
|
||||
key: 'subject',
|
||||
render: (text: string) => <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Статус',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: 'Дата',
|
||||
dataIndex: 'updated_at',
|
||||
key: 'updated_at',
|
||||
width: 150,
|
||||
render: (date: string) => dayjs(date).format('DD.MM.YYYY HH:mm'),
|
||||
},
|
||||
{
|
||||
title: 'Исполнитель',
|
||||
dataIndex: 'assignee',
|
||||
key: 'assignee',
|
||||
render: (assignee?: { fullName: string }) => assignee?.fullName || '-',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={data?.data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
pagination={showPagination ? { pageSize: limit } : false}
|
||||
size="small"
|
||||
onRow={() => ({
|
||||
onClick: () => {
|
||||
// Заглушка перехода, пока нет страницы тикета
|
||||
console.log('Go to ticket details');
|
||||
},
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketTable;
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Typography, Tabs, Tag, Descriptions, Spin, Empty, Row, Col, Card } from 'antd';
|
||||
import { BankOutlined, CheckCircleOutlined, CloseCircleOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { Typography, Tabs, Tag, Descriptions, Spin, Empty, Row, Col, Card, Button } from 'antd';
|
||||
import { BankOutlined, CheckCircleOutlined, CloseCircleOutlined, ArrowLeftOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { companiesApi } from '@/api/companies';
|
||||
import { ServerEntity, WorkstationEntity, FiscalEntity } from '@/types/api';
|
||||
import ServerCard from '@/components/entities/ServerCard';
|
||||
import WorkstationCard from '@/components/entities/WorkstationCard';
|
||||
import FiscalCard from '@/components/entities/FiscalCard';
|
||||
import TicketTable from '@/components/tickets/TicketTable'; // Import
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -29,7 +30,6 @@ const CompanyPage: React.FC = () => {
|
||||
});
|
||||
|
||||
const company = companyRes?.data;
|
||||
// Не используем '|| []' здесь, чтобы ссылка на данные была стабильной для useMemo
|
||||
const rawInfrastructure = infraRes?.data;
|
||||
|
||||
// Группировка инфраструктуры
|
||||
@@ -38,7 +38,6 @@ const CompanyPage: React.FC = () => {
|
||||
const workstations: WorkstationEntity[] = [];
|
||||
const fiscals: FiscalEntity[] = [];
|
||||
|
||||
// Безопасно обрабатываем undefined внутри useMemo
|
||||
const list = rawInfrastructure || [];
|
||||
|
||||
list.forEach(item => {
|
||||
@@ -112,7 +111,17 @@ const CompanyPage: React.FC = () => {
|
||||
{
|
||||
key: 'tickets',
|
||||
label: 'Тикеты',
|
||||
children: <Empty description="Раздел в разработке" style={{ marginTop: 20 }} />,
|
||||
children: (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => console.log('Create Ticket')}>
|
||||
Создать тикет
|
||||
</Button>
|
||||
</div>
|
||||
{/* Передаем ID компании для фильтрации */}
|
||||
<TicketTable companyId={company.ID} limit={10} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'contracts',
|
||||
|
||||
144
src/pages/equipment/FiscalDetails.tsx
Normal file
144
src/pages/equipment/FiscalDetails.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card, Descriptions, Button, Space, Typography, Spin, Badge, Modal, Form, Input, message } from 'antd';
|
||||
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { equipmentApi } from '@/api/equipment';
|
||||
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||
import { formatRnm } from '@/utils/formatters';
|
||||
import { UpdateFiscalDTO } from '@/types/api';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const FiscalDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { data: fiscalRes, isLoading } = useQuery({
|
||||
queryKey: ['fiscal', id],
|
||||
queryFn: () => equipmentApi.getFiscal(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (values: UpdateFiscalDTO) => equipmentApi.updateFiscal(id!, values),
|
||||
onSuccess: () => {
|
||||
message.success('Данные обновлены');
|
||||
queryClient.invalidateQueries({ queryKey: ['fiscal', id] });
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: () => message.error('Ошибка обновления'),
|
||||
});
|
||||
|
||||
if (isLoading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
||||
if (!fiscalRes?.data) return <div>ФР не найден</div>;
|
||||
|
||||
const fiscal = fiscalRes.data;
|
||||
|
||||
// Логика цвета даты окончания ФН
|
||||
const getFnDateColor = (dateStr?: string) => {
|
||||
if (!dateStr) return undefined;
|
||||
const diff = dayjs(dateStr).diff(dayjs(), 'day');
|
||||
if (diff < 0) return 'red';
|
||||
if (diff < 30) return 'orange';
|
||||
return 'green';
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
form.setFieldsValue({
|
||||
description: fiscal.description,
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Space align="center">
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)} />
|
||||
<Space>
|
||||
<div style={{ fontSize: 24, color: '#1890ff' }}>{getEntityIcon('FiscalRegister')}</div>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>{fiscal.model_kkt || 'ККТ'}</Title>
|
||||
<Text type="secondary">{fiscal.serial_number}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Badge status={getStatusColor(fiscal.health_status)} text={fiscal.health_status} />
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>Редактировать</Button>
|
||||
<Button danger icon={<DeleteOutlined />}>Удалить</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
|
||||
{/* Main Info */}
|
||||
<Card title="Информация о ККТ" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="РНМ">
|
||||
<Text code>{formatRnm(fiscal.rn_kkt)}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Заводской номер">{fiscal.serial_number}</Descriptions.Item>
|
||||
<Descriptions.Item label="Модель">{fiscal.model_kkt}</Descriptions.Item>
|
||||
<Descriptions.Item label="Описание">{fiscal.description || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* FN Info */}
|
||||
<Card title="Фискальный Накопитель" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Номер ФН">{fiscal.fn_number || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Дата регистрации">
|
||||
{fiscal.fn_registration_date ? dayjs(fiscal.fn_registration_date).format('DD.MM.YYYY') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Дата окончания">
|
||||
<Text strong style={{ color: getFnDateColor(fiscal.fn_expire_date) }}>
|
||||
{fiscal.fn_expire_date ? dayjs(fiscal.fn_expire_date).format('DD.MM.YYYY') : '-'}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* Firmware Info */}
|
||||
<Card title="Прошивки и ПО" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={3}>
|
||||
<Descriptions.Item label="Прошивка ФР">{fiscal.fr_firmware || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Загрузчик">{fiscal.fr_downloader || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Драйвер">{fiscal.driver_version || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* Legal Info */}
|
||||
<Card title="Юридическое лицо" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Организация">{fiscal.organization_name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="ИНН">{fiscal.inn || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Адрес установки">{fiscal.address || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title="Редактирование ФР"
|
||||
open={isEditModalOpen}
|
||||
onCancel={() => setIsEditModalOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(values) => updateMutation.mutate(values)}>
|
||||
<Form.Item name="description" label="Описание / Заметки">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FiscalDetails;
|
||||
172
src/pages/equipment/ServerDetails.tsx
Normal file
172
src/pages/equipment/ServerDetails.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card, Descriptions, Button, Tag, Space, Typography, Spin, Badge, Modal, Form, Input, message } from 'antd';
|
||||
import { ArrowLeftOutlined, EditOutlined, SyncOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { equipmentApi } from '@/api/equipment';
|
||||
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||
import { formatDate } from '@/utils/formatters';
|
||||
import { UpdateServerDTO } from '@/types/api';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const ServerDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { data: serverRes, isLoading } = useQuery({
|
||||
queryKey: ['server', id],
|
||||
queryFn: () => equipmentApi.getServer(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (values: UpdateServerDTO) => equipmentApi.updateServer(id!, values),
|
||||
onSuccess: () => {
|
||||
message.success('Данные сервера обновлены');
|
||||
queryClient.invalidateQueries({ queryKey: ['server', id] });
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: () => message.error('Ошибка обновления'),
|
||||
});
|
||||
|
||||
const pollMutation = useMutation({
|
||||
mutationFn: () => equipmentApi.pollServer(id!),
|
||||
onSuccess: () => {
|
||||
message.success('Запрос на опрос отправлен');
|
||||
}
|
||||
});
|
||||
|
||||
if (isLoading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
||||
if (!serverRes?.data) return <div>Сервер не найден</div>;
|
||||
|
||||
const server = serverRes.data;
|
||||
|
||||
const handleEdit = () => {
|
||||
form.setFieldsValue({
|
||||
device_name: server.device_name,
|
||||
ip: server.ip,
|
||||
anydesk: server.anydesk,
|
||||
teamviewer: server.teamviewer,
|
||||
description: server.description,
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Space align="center">
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)} />
|
||||
<Space>
|
||||
<div style={{ fontSize: 24, color: '#1890ff' }}>{getEntityIcon('Server')}</div>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>{server.device_name || server.server_name || 'Server'}</Title>
|
||||
<Text type="secondary">{server.uuid}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Tag color={getStatusColor(server.operational_status) === 'success' ? 'green' : 'red'}>
|
||||
{server.operational_status?.toUpperCase()}
|
||||
</Tag>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
icon={<SyncOutlined spin={pollMutation.isPending} />}
|
||||
onClick={() => pollMutation.mutate()}
|
||||
>
|
||||
Опросить
|
||||
</Button>
|
||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>Редактировать</Button>
|
||||
<Button danger icon={<DeleteOutlined />}>Удалить</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 24 }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
|
||||
{/* Main Info */}
|
||||
<Card title="Основная информация" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="IP Адрес">
|
||||
<Paragraph copyable={{ text: server.ip }} style={{ margin: 0 }}>{server.ip || '-'}</Paragraph>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Health Status">
|
||||
<Badge status={getStatusColor(server.health_status)} text={server.health_status} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Unique ID">{server.unique_id || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="CRM ID">{server.crm_id || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Описание" span={2}>{server.description || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* Software Info */}
|
||||
<Card title="Программное обеспечение" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Версия сервера">{server.server_version || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Редакция">{server.server_edition || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Посл. опрос">{formatDate(server.last_polled_at)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* Access Info */}
|
||||
<Card title="Удаленный доступ" className="glass-panel" size="small">
|
||||
<Descriptions column={1} layout="vertical">
|
||||
<Descriptions.Item label="AnyDesk">
|
||||
{server.anydesk ? <Paragraph copyable>{server.anydesk}</Paragraph> : <Text type="secondary">-</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="TeamViewer">
|
||||
{server.teamviewer ? <Paragraph copyable>{server.teamviewer}</Paragraph> : <Text type="secondary">-</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="LiteManager">
|
||||
{server.litemanager ? <Paragraph copyable>{server.litemanager}</Paragraph> : <Text type="secondary">-</Text>}
|
||||
</Descriptions.Item>
|
||||
{server.partners_link && (
|
||||
<Descriptions.Item label="Кабинет">
|
||||
<Button type="link" href={server.partners_link} target="_blank" style={{ padding: 0 }}>
|
||||
Перейти в кабинет дилера
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
title="Редактирование сервера"
|
||||
open={isEditModalOpen}
|
||||
onCancel={() => setIsEditModalOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(values) => updateMutation.mutate(values)}>
|
||||
<Form.Item name="device_name" label="Имя устройства">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="ip" label="IP адрес">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="anydesk" label="AnyDesk">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="teamviewer" label="TeamViewer">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Описание">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDetails;
|
||||
114
src/pages/equipment/WorkstationDetails.tsx
Normal file
114
src/pages/equipment/WorkstationDetails.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card, Descriptions, Button, Space, Typography, Spin, Badge, Modal, Form, Input, message } from 'antd';
|
||||
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { equipmentApi } from '@/api/equipment';
|
||||
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||
import { UpdateWorkstationDTO } from '@/types/api';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const WorkstationDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { data: wsRes, isLoading } = useQuery({
|
||||
queryKey: ['workstation', id],
|
||||
queryFn: () => equipmentApi.getWorkstation(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (values: UpdateWorkstationDTO) => equipmentApi.updateWorkstation(id!, values),
|
||||
onSuccess: () => {
|
||||
message.success('Данные обновлены');
|
||||
queryClient.invalidateQueries({ queryKey: ['workstation', id] });
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: () => message.error('Ошибка обновления'),
|
||||
});
|
||||
|
||||
if (isLoading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
||||
if (!wsRes?.data) return <div>Рабочая станция не найдена</div>;
|
||||
|
||||
const ws = wsRes.data;
|
||||
|
||||
const handleEdit = () => {
|
||||
form.setFieldsValue({
|
||||
device_name: ws.device_name,
|
||||
anydesk: ws.anydesk,
|
||||
teamviewer: ws.teamviewer,
|
||||
description: ws.description,
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Space align="center">
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)} />
|
||||
<Space>
|
||||
<div style={{ fontSize: 24, color: '#1890ff' }}>{getEntityIcon('Workstation')}</div>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>{ws.device_name || 'Workstation'}</Title>
|
||||
<Text type="secondary">{ws.uuid}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Badge status={getStatusColor(ws.health_status)} text={ws.health_status} />
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>Редактировать</Button>
|
||||
<Button danger icon={<DeleteOutlined />}>Удалить</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card title="Детали рабочей станции" className="glass-panel" size="small">
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Описание">
|
||||
{ws.description || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="AnyDesk">
|
||||
{ws.anydesk ? <Paragraph copyable>{ws.anydesk}</Paragraph> : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="TeamViewer">
|
||||
{ws.teamviewer ? <Paragraph copyable>{ws.teamviewer}</Paragraph> : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="LiteManager">
|
||||
{ws.litemanager ? <Paragraph copyable>{ws.litemanager}</Paragraph> : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Редактирование РС"
|
||||
open={isEditModalOpen}
|
||||
onCancel={() => setIsEditModalOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(values) => updateMutation.mutate(values)}>
|
||||
<Form.Item name="device_name" label="Имя устройства">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="anydesk" label="AnyDesk">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="teamviewer" label="TeamViewer">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Описание">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkstationDetails;
|
||||
@@ -65,7 +65,6 @@ export interface ServerEntity {
|
||||
|
||||
crm_id?: string;
|
||||
|
||||
// Добавлено поле address
|
||||
address?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -84,7 +83,6 @@ export interface WorkstationEntity {
|
||||
health_status: 'ok' | 'attention_required' | 'locked';
|
||||
status_details?: unknown;
|
||||
|
||||
// Добавлено поле description
|
||||
description?: string;
|
||||
address?: string;
|
||||
|
||||
@@ -113,7 +111,6 @@ export interface FiscalEntity {
|
||||
health_status: 'ok' | 'attention_required' | 'locked';
|
||||
status_details?: unknown;
|
||||
|
||||
// Добавлено поле address
|
||||
address?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -163,4 +160,50 @@ export interface TaskResolutionPayload {
|
||||
new_owner_id?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
// --- Tickets DTO ---
|
||||
export interface TicketDTO {
|
||||
id: number;
|
||||
number: number;
|
||||
subject: string;
|
||||
description?: string;
|
||||
status: 'registered' | 'inprogress' | 'closed' | 'wait';
|
||||
priority?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
assignee?: {
|
||||
id: number;
|
||||
fullName: string;
|
||||
};
|
||||
company_id: string;
|
||||
}
|
||||
|
||||
export interface TicketListParams {
|
||||
company_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// DTO для обновления оборудования
|
||||
export interface UpdateServerDTO {
|
||||
device_name?: string;
|
||||
ip?: string;
|
||||
anydesk?: string;
|
||||
teamviewer?: string;
|
||||
description?: string;
|
||||
// Добавляем другие поля по мере необходимости
|
||||
}
|
||||
|
||||
export interface UpdateWorkstationDTO {
|
||||
device_name?: string;
|
||||
anydesk?: string;
|
||||
teamviewer?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateFiscalDTO {
|
||||
description?: string;
|
||||
// ККТ поля обычно read-only, т.к. приходят из железа, но description можно править
|
||||
}
|
||||
Reference in New Issue
Block a user