14.12.25 - Этап 2 на фронте. Написаны отдельные компоненты для оборудки, страница компании, немного поправил светлую тему
This commit is contained in:
26
src/App.tsx
26
src/App.tsx
@@ -9,6 +9,7 @@ import LoginPage from '@/pages/auth/LoginPage';
|
|||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
import SearchPage from '@/pages/SearchPage';
|
import SearchPage from '@/pages/SearchPage';
|
||||||
import TasksPage from '@/pages/TasksPage';
|
import TasksPage from '@/pages/TasksPage';
|
||||||
|
import CompanyPage from '@/pages/companies/CompanyPage';
|
||||||
|
|
||||||
import { useUiStore } from '@/store/uiStore';
|
import { useUiStore } from '@/store/uiStore';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
@@ -30,16 +31,16 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
|||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
// ReactNode валиден для возврата
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const themeMode = useUiStore((state) => state.themeMode);
|
const themeMode = useUiStore((state) => state.themeMode);
|
||||||
|
|
||||||
// Применение темы к body для смены общего фона (за пределами React компонентов)
|
// Обновляем логику фона: синхронизируем с themeConfig
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const colorBgLayout = themeMode === 'dark' ? '#0a111b' : '#f0f5ff';
|
// #000000 для темной, #f0f2f5 для светлой
|
||||||
|
const colorBgLayout = themeMode === 'dark' ? '#000000' : '#f0f2f5';
|
||||||
document.body.style.backgroundColor = colorBgLayout;
|
document.body.style.backgroundColor = colorBgLayout;
|
||||||
}, [themeMode]);
|
}, [themeMode]);
|
||||||
|
|
||||||
@@ -58,15 +59,24 @@ const App: React.FC = () => {
|
|||||||
<MainLayout />
|
<MainLayout />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}>
|
}>
|
||||||
{/* Вложенные роуты внутри MainLayout */}
|
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="search" element={<SearchPage />} />
|
<Route path="search" element={<SearchPage />} />
|
||||||
<Route path="tasks" element={<TasksPage />} />
|
<Route path="tasks" element={<TasksPage />} />
|
||||||
<Route path="tickets" element={<div>Тикеты</div>} />
|
<Route path="tickets" element={<div>Тикеты</div>} />
|
||||||
<Route path="companies" element={<div>Компании</div>} />
|
|
||||||
<Route path="servers" element={<div>Серверы</div>} />
|
<Route path="companies" element={<div>Список компаний</div>} />
|
||||||
<Route path="workstations" element={<div>РС</div>} />
|
<Route path="companies/:id" element={<CompanyPage />} />
|
||||||
<Route path="fiscals" element={<div>ФР</div>} />
|
|
||||||
|
{/* Роуты для оборудования */}
|
||||||
|
<Route path="servers" element={<div>Список серверов</div>} />
|
||||||
|
<Route path="servers/:id" element={<div>Детали сервера (в разработке)</div>} />
|
||||||
|
|
||||||
|
<Route path="workstations" element={<div>Список РС</div>} />
|
||||||
|
<Route path="workstations/:id" element={<div>Детали РС (в разработке)</div>} />
|
||||||
|
|
||||||
|
<Route path="fiscals" element={<div>Список ФР</div>} />
|
||||||
|
<Route path="fiscals/:id" element={<div>Детали ФР (в разработке)</div>} />
|
||||||
|
|
||||||
<Route path="admin" element={<div>Админка</div>} />
|
<Route path="admin" element={<div>Админка</div>} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
17
src/api/companies.ts
Normal file
17
src/api/companies.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import apiClient from './axios';
|
||||||
|
import { ApiResponse, CompanyModel, InfrastructureItem } from '@/types/api';
|
||||||
|
|
||||||
|
export const companiesApi = {
|
||||||
|
// Получение профиля компании
|
||||||
|
getCompany: async (id: string) => {
|
||||||
|
// В URL передаем ID компании
|
||||||
|
const response = await apiClient.get<ApiResponse<CompanyModel>>(`/companies/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получение инфраструктуры (CMDB)
|
||||||
|
getInfrastructure: async (id: string) => {
|
||||||
|
const response = await apiClient.get<ApiResponse<InfrastructureItem[]>>(`/companies/${id}/infrastructure`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
11
src/api/equipment.ts
Normal file
11
src/api/equipment.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import apiClient from './axios';
|
||||||
|
import { ApiResponse } from '@/types/api';
|
||||||
|
|
||||||
|
export const equipmentApi = {
|
||||||
|
// Принудительный опрос сервера
|
||||||
|
pollServer: async (uuid: string) => {
|
||||||
|
// Предполагаем, что бэкенд поддерживает этот эндпоинт
|
||||||
|
const response = await apiClient.post<ApiResponse<void>>(`/servers/${uuid}/poll`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
102
src/components/entities/FiscalCard.tsx
Normal file
102
src/components/entities/FiscalCard.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Badge, Space, Typography, Tag, Tooltip } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { FiscalEntity } from '@/types/api';
|
||||||
|
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||||
|
import { formatRnm } from '@/utils/formatters';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: FiscalEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const FiscalCard: React.FC<Props> = ({ data }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const renderFnInfo = (dateStr?: string) => {
|
||||||
|
if (!dateStr) return <Tag>Нет ФН</Tag>;
|
||||||
|
const expireDate = dayjs(dateStr);
|
||||||
|
const daysLeft = expireDate.diff(dayjs(), 'day');
|
||||||
|
|
||||||
|
let color = 'green';
|
||||||
|
let label = 'ФН OK';
|
||||||
|
|
||||||
|
if (daysLeft < 0) {
|
||||||
|
color = 'red';
|
||||||
|
label = 'ФН Истек';
|
||||||
|
} else if (daysLeft < 30) {
|
||||||
|
color = 'orange';
|
||||||
|
label = `ФН: ${daysLeft} дн.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space size={4}>
|
||||||
|
<Tag color={color} style={{ marginRight: 0 }}>{label}</Tag>
|
||||||
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
|
(до {expireDate.format('DD.MM.YYYY')})
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
navigate(`/fiscals/${data.uuid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
className="glass-panel"
|
||||||
|
hoverable
|
||||||
|
onClick={handleCardClick}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{getEntityIcon('FiscalRegister')}
|
||||||
|
<Text strong>{data.model_kkt || 'ККТ'}</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Tooltip title={`Статус здоровья: ${data.health_status}`}>
|
||||||
|
<Badge status={getStatusColor(data.health_status)} />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong style={{ display: 'block' }}>{data.organization_name}</Text>
|
||||||
|
{data.inn && <Text type="secondary" style={{ fontSize: 12, marginRight: 8 }}>ИНН: {data.inn}</Text>}
|
||||||
|
{/* address теперь типизирован как string | undefined, проверка безопасна */}
|
||||||
|
{data.address && <Text type="secondary" style={{ fontSize: 12 }}>| {data.address}</Text>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text type="secondary">РНМ:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.rn_kkt }} style={{ margin: 0, fontFamily: 'monospace' }}>
|
||||||
|
{formatRnm(data.rn_kkt)}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text type="secondary">SN:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.serial_number }} style={{ margin: 0, fontSize: 12 }}>
|
||||||
|
{data.serial_number}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 4 }}>
|
||||||
|
{renderFnInfo(data.fn_expire_date)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(data.driver_version || data.fr_firmware) && (
|
||||||
|
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8c8c', borderTop: '1px solid rgba(0,0,0,0.06)', paddingTop: 4 }}>
|
||||||
|
FW: {data.fr_firmware} {data.driver_version ? `| Drv: ${data.driver_version}` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FiscalCard;
|
||||||
208
src/components/entities/ServerCard.tsx
Normal file
208
src/components/entities/ServerCard.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Badge, Button, Space, Typography, Tooltip, message, Tag } from 'antd';
|
||||||
|
import { LinkOutlined, SyncOutlined, GlobalOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ServerEntity } from '@/types/api';
|
||||||
|
import { equipmentApi } from '@/api/equipment';
|
||||||
|
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||||
|
import { cleanWebUrl, formatServerEdition, formatDate } from '@/utils/formatters';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: ServerEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const ServerCard: React.FC<Props> = ({ data }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Определяем, является ли сервер облачным/веб (iikoWeb, Syrve)
|
||||||
|
const isCloud = (data.ip || '').toLowerCase().includes('iikoweb') ||
|
||||||
|
(data.ip || '').toLowerCase().includes('syrve');
|
||||||
|
|
||||||
|
const isOnline = data.operational_status === 'active';
|
||||||
|
|
||||||
|
// Мутация для опроса
|
||||||
|
const pollMutation = useMutation({
|
||||||
|
mutationFn: () => equipmentApi.pollServer(data.uuid),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('Запрос на опрос отправлен');
|
||||||
|
queryClient.invalidateQueries({ predicate: (query) =>
|
||||||
|
query.queryKey[0] === 'company' || query.queryKey[0] === 'search'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => message.error('Не удалось выполнить опрос'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePoll = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation(); // Чтобы не срабатывал клик по карточке
|
||||||
|
pollMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
navigate(`/servers/${data.uuid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Рендер для iikoWeb / Cloud серверов ---
|
||||||
|
if (isCloud) {
|
||||||
|
const webUrl = data.ip ? cleanWebUrl(data.ip) : '';
|
||||||
|
const fullUrl = `https://${webUrl}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
className="glass-panel"
|
||||||
|
hoverable
|
||||||
|
onClick={handleCardClick}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<GlobalOutlined style={{ color: '#1890ff' }} />
|
||||||
|
<Text strong>{data.device_name || 'Cloud Server'}</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
{/* Row 1: Address Copy + Link Button */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Paragraph copyable={{ text: webUrl }} style={{ margin: 0, maxWidth: 140 }} ellipsis>
|
||||||
|
{webUrl}
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
href={fullUrl}
|
||||||
|
target="_blank"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
>
|
||||||
|
iikoWeb
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: Version + Type */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text type="secondary">Версия:</Text>
|
||||||
|
<Text strong>{data.server_version || '-'} <Tag style={{ marginLeft: 4, marginRight: 0 }}>{formatServerEdition(data.server_edition) || 'Web'}</Tag></Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 3: UID */}
|
||||||
|
{data.unique_id && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text type="secondary">UID:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.unique_id }} style={{ margin: 0 }}>
|
||||||
|
{data.unique_id.substring(0, 15)}...
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action: Poll Button with Last Polled Date */}
|
||||||
|
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<SyncOutlined spin={pollMutation.isPending} />}
|
||||||
|
onClick={handlePoll}
|
||||||
|
loading={pollMutation.isPending}
|
||||||
|
>
|
||||||
|
{pollMutation.isPending ? 'Опрос...' : formatDate(data.last_polled_at)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Рендер для обычных серверов (RMS) ---
|
||||||
|
const renderAccessLink = (label: string, value?: string) => {
|
||||||
|
if (!value) return null;
|
||||||
|
return (
|
||||||
|
<Tooltip title={`Копировать ID/Link для ${label}`}>
|
||||||
|
<Paragraph copyable={{ text: value }} style={{ margin: 0 }}>
|
||||||
|
<Text type="secondary">{label}:</Text> {value}
|
||||||
|
</Paragraph>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
className="glass-panel"
|
||||||
|
hoverable
|
||||||
|
onClick={handleCardClick}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{getEntityIcon('Server')}
|
||||||
|
<Text strong>{data.device_name || data.server_name || 'Unknown Server'}</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Tooltip title={`Network: ${data.operational_status}`}>
|
||||||
|
<Badge status={isOnline ? 'success' : 'error'} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={`Health: ${data.health_status}`}>
|
||||||
|
<Badge status={getStatusColor(data.health_status)} />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="poll"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<SyncOutlined spin={pollMutation.isPending} />}
|
||||||
|
onClick={handlePoll}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{pollMutation.isPending ? 'Опрос...' : 'Обновить статус'}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||||
|
<Text type="secondary">IP:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.ip }} style={{ margin: 0 }}>
|
||||||
|
<Text strong>{data.ip || 'No IP'}</Text>
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text type="secondary">Версия:</Text>
|
||||||
|
<Text>{data.server_version} {data.server_edition ? `(${formatServerEdition(data.server_edition)})` : ''}</Text>
|
||||||
|
</div>
|
||||||
|
{data.partners_link && (
|
||||||
|
<div style={{ marginTop: 4, textAlign: 'right' }}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
href={data.partners_link}
|
||||||
|
target="_blank"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
style={{ paddingRight: 0 }}
|
||||||
|
>
|
||||||
|
Кабинет дилера
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ borderTop: '1px solid rgba(0,0,0,0.06)', paddingTop: 8 }}>
|
||||||
|
<Space direction="vertical" size={0} style={{ width: '100%' }}>
|
||||||
|
{renderAccessLink('AnyDesk', data.anydesk)}
|
||||||
|
{renderAccessLink('TeamViewer', data.teamviewer)}
|
||||||
|
{renderAccessLink('RDP', data.rdp)}
|
||||||
|
{renderAccessLink('LM', data.litemanager)}
|
||||||
|
|
||||||
|
{!data.anydesk && !data.teamviewer && !data.rdp && !data.litemanager && (
|
||||||
|
<Text type="secondary" italic>Нет данных для доступа</Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerCard;
|
||||||
79
src/components/entities/WorkstationCard.tsx
Normal file
79
src/components/entities/WorkstationCard.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Badge, Space, Typography, Tooltip } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { WorkstationEntity } from '@/types/api';
|
||||||
|
import { getEntityIcon, getStatusColor } from '@/utils/mappers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: WorkstationEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const WorkstationCard: React.FC<Props> = ({ data }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
navigate(`/workstations/${data.uuid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
className="glass-panel"
|
||||||
|
hoverable
|
||||||
|
onClick={handleCardClick}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{getEntityIcon('Workstation')}
|
||||||
|
<Text strong>{data.device_name || 'Workstation'}</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Tooltip title={`Health: ${data.health_status}`}>
|
||||||
|
<Badge status={getStatusColor(data.health_status)} text={data.health_status} />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||||
|
{/* description теперь типизирован как string | undefined */}
|
||||||
|
{data.description && (
|
||||||
|
<Paragraph
|
||||||
|
ellipsis={{ rows: 2, expandable: false }}
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
{data.description}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.anydesk && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text>AnyDesk:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.anydesk }} style={{ margin: 0 }}>{data.anydesk}</Paragraph>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.teamviewer && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text>TV:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.teamviewer }} style={{ margin: 0 }}>{data.teamviewer}</Paragraph>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.litemanager && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text>LM:</Text>
|
||||||
|
<Paragraph copyable={{ text: data.litemanager }} style={{ margin: 0 }}>{data.litemanager}</Paragraph>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!data.anydesk && !data.teamviewer && !data.litemanager && !data.description && (
|
||||||
|
<Text type="secondary" italic>Нет доп. информации</Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkstationCard;
|
||||||
@@ -9,20 +9,23 @@ body {
|
|||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Глобальный эффект стекла для компонентов AntD */
|
/*
|
||||||
|
Глобальный эффект стекла.
|
||||||
|
Убрали жесткий белый border, который был невиден на белом фоне.
|
||||||
|
Теперь полагаемся на настройки темы AntD для border-color.
|
||||||
|
*/
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Принудительно применяем эффект к карточкам и модалкам */
|
/* Принудительно применяем эффект к контенту AntD */
|
||||||
.ant-card, .ant-modal-content, .ant-drawer-content {
|
.ant-card, .ant-modal-content, .ant-drawer-content {
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Скроллбары для красоты */
|
/* Скроллбары */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -31,6 +34,9 @@ body {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #8c8c8c;
|
background: #bfbfbf; /* Более нейтральный цвет скролла */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #999;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams, Link } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Card, List, Typography, Spin, Empty, Badge, Button, Space } from 'antd';
|
import { Card, Typography, Spin, Empty, Space, Row, Col, Button } from 'antd';
|
||||||
import { searchApi } from '@/api/search';
|
import { searchApi } from '@/api/search';
|
||||||
import { getEntityIcon, getEntityLabel, getStatusColor } from '@/utils/mappers';
|
import { getEntityIcon } from '@/utils/mappers';
|
||||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
import { SearchFoundEntity, EntityData } from '@/types/api';
|
import { SearchFoundEntity, ServerEntity, WorkstationEntity, FiscalEntity } from '@/types/api';
|
||||||
|
import ServerCard from '@/components/entities/ServerCard';
|
||||||
|
import WorkstationCard from '@/components/entities/WorkstationCard';
|
||||||
|
import FiscalCard from '@/components/entities/FiscalCard';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
@@ -34,6 +37,19 @@ const SearchPage: React.FC = () => {
|
|||||||
|
|
||||||
const results = data?.data?.search_results || [];
|
const results = data?.data?.search_results || [];
|
||||||
|
|
||||||
|
const renderEntityCard = (item: SearchFoundEntity) => {
|
||||||
|
switch (item.entity_type) {
|
||||||
|
case 'Server':
|
||||||
|
return <ServerCard data={item.data as ServerEntity} />;
|
||||||
|
case 'Workstation':
|
||||||
|
return <WorkstationCard data={item.data as WorkstationEntity} />;
|
||||||
|
case 'FiscalRegister':
|
||||||
|
return <FiscalCard data={item.data as FiscalEntity} />;
|
||||||
|
default:
|
||||||
|
return <div>Unknown entity type</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title level={4}>Результаты поиска: "{term}"</Title>
|
<Title level={4}>Результаты поиска: "{term}"</Title>
|
||||||
@@ -46,53 +62,30 @@ const SearchPage: React.FC = () => {
|
|||||||
<Card
|
<Card
|
||||||
key={group.owner.uuid}
|
key={group.owner.uuid}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Link to={`/companies/${group.owner.uuid}`} style={{ color: 'inherit' }}>
|
||||||
{getEntityIcon('Company')}
|
<Space>
|
||||||
<Text strong>{group.owner.name}</Text>
|
{getEntityIcon('Company')}
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>{group.owner.address}</Text>
|
<Text strong style={{ cursor: 'pointer', textDecoration: 'underline' }}>
|
||||||
</Space>
|
{group.owner.name}
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>{group.owner.address}</Text>
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
}
|
}
|
||||||
className="glass-panel"
|
className="glass-panel"
|
||||||
extra={<Button type="link">Перейти к компании <ArrowRightOutlined /></Button>}
|
extra={
|
||||||
|
<Link to={`/companies/${group.owner.uuid}`}>
|
||||||
|
<Button type="link">Перейти к компании <ArrowRightOutlined /></Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<List
|
<Row gutter={[16, 16]}>
|
||||||
itemLayout="horizontal"
|
{group.found_entities.map((item, idx) => (
|
||||||
dataSource={group.found_entities}
|
<Col key={`${item.entity_type}-${idx}`} xs={24} md={12} lg={8} xl={6}>
|
||||||
renderItem={(item: SearchFoundEntity) => {
|
{renderEntityCard(item)}
|
||||||
const d = item.data as EntityData;
|
</Col>
|
||||||
const title = d.device_name || d.rn_kkt || d.uuid;
|
))}
|
||||||
const subtitle = d.ip || d.serial_number || '';
|
</Row>
|
||||||
const statusRaw = d.operational_status || d.health_status;
|
|
||||||
|
|
||||||
// getStatusColor теперь возвращает корректный Union Type для Badge
|
|
||||||
const badgeStatus = getStatusColor(statusRaw);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
avatar={
|
|
||||||
<div style={{ fontSize: 24, color: '#1890ff' }}>
|
|
||||||
{getEntityIcon(item.entity_type)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<Text strong>{title}</Text>
|
|
||||||
<Badge status={badgeStatus} text={statusRaw || 'unknown'} />
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Space split="|">
|
|
||||||
<Text type="secondary">{getEntityLabel(item.entity_type)}</Text>
|
|
||||||
<Text>{subtitle}</Text>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button size="small">Детали</Button>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
179
src/pages/companies/CompanyPage.tsx
Normal file
179
src/pages/companies/CompanyPage.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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 { 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';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
const CompanyPage: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
// Запрос профиля
|
||||||
|
const { data: companyRes, isLoading: loadingCompany } = useQuery({
|
||||||
|
queryKey: ['company', id],
|
||||||
|
queryFn: () => companiesApi.getCompany(id!),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Запрос инфраструктуры
|
||||||
|
const { data: infraRes, isLoading: loadingInfra } = useQuery({
|
||||||
|
queryKey: ['company', id, 'infra'],
|
||||||
|
queryFn: () => companiesApi.getInfrastructure(id!),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const company = companyRes?.data;
|
||||||
|
// Не используем '|| []' здесь, чтобы ссылка на данные была стабильной для useMemo
|
||||||
|
const rawInfrastructure = infraRes?.data;
|
||||||
|
|
||||||
|
// Группировка инфраструктуры
|
||||||
|
const groupedInfra = useMemo(() => {
|
||||||
|
const servers: ServerEntity[] = [];
|
||||||
|
const workstations: WorkstationEntity[] = [];
|
||||||
|
const fiscals: FiscalEntity[] = [];
|
||||||
|
|
||||||
|
// Безопасно обрабатываем undefined внутри useMemo
|
||||||
|
const list = rawInfrastructure || [];
|
||||||
|
|
||||||
|
list.forEach(item => {
|
||||||
|
if (item.entity_type === 'Server') servers.push(item.data as ServerEntity);
|
||||||
|
else if (item.entity_type === 'Workstation') workstations.push(item.data as WorkstationEntity);
|
||||||
|
else if (item.entity_type === 'FiscalRegister') fiscals.push(item.data as FiscalEntity);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { servers, workstations, fiscals };
|
||||||
|
}, [rawInfrastructure]);
|
||||||
|
|
||||||
|
if (loadingCompany) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
||||||
|
if (!company) return <Empty description="Компания не найдена" />;
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'infrastructure',
|
||||||
|
label: 'Инфраструктура',
|
||||||
|
children: (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
{loadingInfra ? <Spin /> : (
|
||||||
|
<>
|
||||||
|
{/* Секция Серверов */}
|
||||||
|
{groupedInfra.servers.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<Title level={5}>Серверы ({groupedInfra.servers.length})</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{groupedInfra.servers.map(srv => (
|
||||||
|
<Col key={srv.uuid} xs={24} md={12} lg={8} xl={6}>
|
||||||
|
<ServerCard data={srv} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Секция Касс */}
|
||||||
|
{groupedInfra.fiscals.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<Title level={5}>Фискальные регистраторы ({groupedInfra.fiscals.length})</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{groupedInfra.fiscals.map(fr => (
|
||||||
|
<Col key={fr.uuid} xs={24} md={12} lg={8} xl={6}>
|
||||||
|
<FiscalCard data={fr} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Секция Рабочих станций */}
|
||||||
|
{groupedInfra.workstations.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<Title level={5}>Рабочие станции ({groupedInfra.workstations.length})</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{groupedInfra.workstations.map(ws => (
|
||||||
|
<Col key={ws.uuid} xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<WorkstationCard data={ws} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!rawInfrastructure || rawInfrastructure.length === 0) && <Empty description="Оборудование не найдено" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tickets',
|
||||||
|
label: 'Тикеты',
|
||||||
|
children: <Empty description="Раздел в разработке" style={{ marginTop: 20 }} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'contracts',
|
||||||
|
label: 'Контракты',
|
||||||
|
children: <Empty description="Раздел в разработке" style={{ marginTop: 20 }} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Header Профиля */}
|
||||||
|
<Card className="glass-panel" style={{ marginBottom: 24 }}>
|
||||||
|
<Link to="/companies" style={{ display: 'inline-flex', alignItems: 'center', marginBottom: 16, color: '#8c8c8c' }}>
|
||||||
|
<ArrowLeftOutlined style={{ marginRight: 8 }} /> Назад к списку
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<div style={{
|
||||||
|
width: 64, height: 64,
|
||||||
|
background: '#e6f7ff',
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
marginRight: 16,
|
||||||
|
fontSize: 32, color: '#1890ff'
|
||||||
|
}}>
|
||||||
|
<BankOutlined />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Title level={3} style={{ margin: 0 }}>{company.Title}</Title>
|
||||||
|
<Text type="secondary">{company.Address}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
{company.ActiveContract ? (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">Контракт Активен</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag icon={<CloseCircleOutlined />} color="error">Нет контракта</Tag>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>ID: {company.ID}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Descriptions size="small" style={{ marginTop: 24 }} column={2}>
|
||||||
|
<Descriptions.Item label="Юр. название">{company.AdditionalName || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Родительская компания">
|
||||||
|
{company.ParentID ? <Link to={`/companies/${company.ParentID}`}>{company.ParentID}</Link> : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Обновлено">
|
||||||
|
{company.LastModifiedDate ? new Date(company.LastModifiedDate).toLocaleDateString() : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Основной контент */}
|
||||||
|
<Tabs defaultActiveKey="infrastructure" items={items} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyPage;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
// Исправленные импорты: разделяем значения и типы
|
|
||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
import type { ThemeConfig } from 'antd';
|
import type { ThemeConfig } from 'antd';
|
||||||
|
|
||||||
// Цветовые палитры для тем
|
// Цветовые палитры
|
||||||
const lightColors = {
|
const lightColors = {
|
||||||
primary: '#1890ff',
|
primary: '#1677ff', // Чуть более глубокий синий (AntD v5 default)
|
||||||
bgContainer: 'rgba(230, 247, 255, 0.65)',
|
bgContainer: 'rgba(255, 255, 255, 0.85)', // Почти белый, слегка прозрачный
|
||||||
bgLayout: '#f0f5ff',
|
bgLayout: '#f0f2f5a9', // Нейтральный светло-серый, приятный глазу
|
||||||
|
borderColor: '#d9d9d960', // Стандартный серый бордер
|
||||||
};
|
};
|
||||||
|
|
||||||
const darkColors = {
|
const darkColors = {
|
||||||
primary: '#177ddc',
|
primary: '#177ddc',
|
||||||
bgContainer: 'rgba(17, 29, 44, 0.65)',
|
bgContainer: 'rgba(20, 20, 20, 0.65)', // Темный, прозрачный
|
||||||
bgLayout: '#0a111b',
|
bgLayout: '#000000', // Глубокий черный для контраста в OLED стиле
|
||||||
|
borderColor: '#303030',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getThemeConfig = (mode: 'light' | 'dark'): ThemeConfig => {
|
export const getThemeConfig = (mode: 'light' | 'dark'): ThemeConfig => {
|
||||||
@@ -20,29 +21,32 @@ export const getThemeConfig = (mode: 'light' | 'dark'): ThemeConfig => {
|
|||||||
const colors = isDark ? darkColors : lightColors;
|
const colors = isDark ? darkColors : lightColors;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Теперь theme.darkAlgorithm и theme.defaultAlgorithm будут доступны
|
|
||||||
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: colors.primary,
|
colorPrimary: colors.primary,
|
||||||
colorBgContainer: colors.bgContainer,
|
colorBgContainer: colors.bgContainer,
|
||||||
colorBgLayout: colors.bgLayout,
|
colorBgLayout: colors.bgLayout,
|
||||||
|
colorBorder: colors.borderColor, // Явный цвет границ
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
wireframe: false,
|
wireframe: false,
|
||||||
|
// В светлой теме делаем тень мягче, но заметнее для отделения слоев
|
||||||
boxShadow: isDark
|
boxShadow: isDark
|
||||||
? '0 4px 12px rgba(0, 0, 0, 0.4)'
|
? '0 4px 12px rgba(0, 0, 0, 0.4)'
|
||||||
: '0 4px 12px rgba(24, 144, 255, 0.15)',
|
: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Layout: {
|
Layout: {
|
||||||
colorBgHeader: isDark ? 'rgba(20, 20, 20, 0.6)' : 'rgba(255, 255, 255, 0.6)',
|
// Хедер в светлой теме делаем чисто белым (или с легким блюром), чтобы отделить от серого фона
|
||||||
colorBgTrigger: colors.primary,
|
colorBgHeader: isDark ? 'rgba(20, 20, 20, 0.6)' : 'rgba(255, 255, 255, 0.7)',
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
colorBgContainer: 'transparent',
|
colorBgContainer: 'transparent',
|
||||||
},
|
},
|
||||||
Card: {
|
Card: {
|
||||||
colorBgContainer: colors.bgContainer,
|
colorBgContainer: colors.bgContainer,
|
||||||
lineWidth: 1,
|
// Убираем лишние обводки в светлой теме, полагаемся на тень,
|
||||||
|
// но оставляем тонкий бордер для четкости
|
||||||
|
lineWidth: 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
103
src/types/api.ts
103
src/types/api.ts
@@ -26,19 +26,105 @@ export interface CompanyOwner {
|
|||||||
parent_info?: { uuid: string; name: string };
|
parent_info?: { uuid: string; name: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тип для статуса бейджа Ant Design
|
export interface CompanyModel {
|
||||||
|
ID: string;
|
||||||
|
Title?: string;
|
||||||
|
Address?: string;
|
||||||
|
AdditionalName?: string;
|
||||||
|
ActiveContract?: boolean;
|
||||||
|
ParentID?: string;
|
||||||
|
LastModifiedDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type AntBadgeStatus = 'success' | 'processing' | 'error' | 'default' | 'warning';
|
export type AntBadgeStatus = 'success' | 'processing' | 'error' | 'default' | 'warning';
|
||||||
|
|
||||||
export interface EntityData {
|
// --- CMDB Entities (Rich DTOs) ---
|
||||||
|
|
||||||
|
export interface ServerEntity {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
external_uuid?: string;
|
||||||
|
unique_id?: string;
|
||||||
|
|
||||||
device_name?: string;
|
device_name?: string;
|
||||||
ip?: string; // Для серверов
|
server_name?: string;
|
||||||
rn_kkt?: string; // Для ФР
|
ip?: string;
|
||||||
serial_number?: string;
|
|
||||||
|
rdp?: string;
|
||||||
|
anydesk?: string;
|
||||||
|
teamviewer?: string;
|
||||||
|
litemanager?: string;
|
||||||
|
partners_link?: string;
|
||||||
|
|
||||||
operational_status?: 'active' | 'offline' | 'unknown';
|
operational_status?: 'active' | 'offline' | 'unknown';
|
||||||
health_status?: 'ok' | 'attention_required' | 'locked';
|
health_status: 'ok' | 'attention_required' | 'locked';
|
||||||
// Используем unknown вместо any для безопасной работы с динамическими полями
|
status_details?: unknown;
|
||||||
[key: string]: unknown;
|
last_polled_at?: string;
|
||||||
|
|
||||||
|
server_version?: string;
|
||||||
|
server_edition?: string;
|
||||||
|
|
||||||
|
crm_id?: string;
|
||||||
|
|
||||||
|
// Добавлено поле address
|
||||||
|
address?: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkstationEntity {
|
||||||
|
uuid: string;
|
||||||
|
external_uuid?: string;
|
||||||
|
device_name?: string;
|
||||||
|
|
||||||
|
anydesk?: string;
|
||||||
|
teamviewer?: string;
|
||||||
|
litemanager?: string;
|
||||||
|
|
||||||
|
health_status: 'ok' | 'attention_required' | 'locked';
|
||||||
|
status_details?: unknown;
|
||||||
|
|
||||||
|
// Добавлено поле description
|
||||||
|
description?: string;
|
||||||
|
address?: string;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FiscalEntity {
|
||||||
|
uuid: string;
|
||||||
|
external_uuid?: string;
|
||||||
|
|
||||||
|
model_kkt?: string;
|
||||||
|
serial_number?: string;
|
||||||
|
rn_kkt?: string;
|
||||||
|
|
||||||
|
fn_number?: string;
|
||||||
|
fn_registration_date?: string;
|
||||||
|
fn_expire_date?: string;
|
||||||
|
|
||||||
|
driver_version?: string;
|
||||||
|
fr_firmware?: string;
|
||||||
|
fr_downloader?: string;
|
||||||
|
|
||||||
|
organization_name?: string;
|
||||||
|
inn?: string;
|
||||||
|
|
||||||
|
health_status: 'ok' | 'attention_required' | 'locked';
|
||||||
|
status_details?: unknown;
|
||||||
|
|
||||||
|
// Добавлено поле address
|
||||||
|
address?: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityData = ServerEntity | WorkstationEntity | FiscalEntity;
|
||||||
|
|
||||||
|
export interface InfrastructureItem {
|
||||||
|
entity_type: 'Server' | 'Workstation' | 'FiscalRegister';
|
||||||
|
data: EntityData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Search DTO ---
|
// --- Search DTO ---
|
||||||
@@ -66,7 +152,6 @@ export interface TaskDTO {
|
|||||||
entity_type: string;
|
entity_type: string;
|
||||||
status: TaskStatus;
|
status: TaskStatus;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
// Детали могут быть любой структуры, но мы знаем, что это объект
|
|
||||||
details: Record<string, unknown>;
|
details: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/utils/formatters.ts
Normal file
29
src/utils/formatters.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export const formatRnm = (rnm?: string): string => {
|
||||||
|
if (!rnm) return '';
|
||||||
|
// Разбиваем по 4 цифры: 0000 1111 2222 3333
|
||||||
|
return rnm.replace(/\D/g, '').replace(/(\d{4})(?=\d)/g, '$1 ').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanWebUrl = (url?: string): string => {
|
||||||
|
if (!url) return '';
|
||||||
|
// Убираем протокол если есть, убираем порт и все после него
|
||||||
|
// co-mirine-co.iikoweb.ru:8080 -> co-mirine-co.iikoweb.ru
|
||||||
|
let clean = url.replace(/https?:\/\//, '');
|
||||||
|
clean = clean.split(':')[0];
|
||||||
|
return clean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatServerEdition = (edition?: string): string => {
|
||||||
|
if (!edition) return '';
|
||||||
|
const lower = edition.toLowerCase();
|
||||||
|
if (lower === 'default') return 'RMS';
|
||||||
|
if (lower === 'chain') return 'Chain';
|
||||||
|
return edition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (date?: string): string => {
|
||||||
|
if (!date) return '-';
|
||||||
|
return dayjs(date).format('DD.MM.YYYY HH:mm');
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user