15.12.25 - Этап 3. Детализация карточек и тикеты
All checks were successful
Mirror frontend to backend repo / mirror (push) Successful in 6s

This commit is contained in:
2025-12-15 07:29:25 +03:00
parent 0c17971baf
commit 33e579b402
9 changed files with 634 additions and 16 deletions

View File

@@ -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>

View File

@@ -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
View 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;
},
};

View 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;

View File

@@ -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',

View 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;

View 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;

View 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;

View File

@@ -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 можно править
}