diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx
index 1ddf6ba..30b4cdb 100644
--- a/frontend/src/BillingManager.jsx
+++ b/frontend/src/BillingManager.jsx
@@ -8,6 +8,7 @@ import PageHeader from './components/PageHeader.jsx';
import PageHeaderActions from './components/PageHeaderActions.jsx';
import FormField from './components/FormField.jsx';
import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
+import PurposeSelectInput, { PURPOSES as PURPOSE_OPTIONS } from './components/PurposeSelectInput.jsx';
import ErrorAlert from './components/ErrorAlert.jsx';
import {
IconPlus,
@@ -1577,17 +1578,6 @@ function BillingManager() {
// Компоненты форм для модальных окон
function AddBillingForm({ item, onItemChange, servers }) {
- const purposeOptions = [
- { value: '', label: 'Выберите назначение' },
- { value: 'relay', label: 'Relay VPS' },
- { value: 'bgp', label: 'BGP сервер' },
- { value: 'monitoring', label: 'Мониторинг' },
- { value: 'dns', label: 'DNS сервер' },
- { value: 'proxy', label: 'Прокси сервер' },
- { value: 'backup', label: 'Backup сервер' },
- { value: 'other', label: 'Другое' }
- ];
-
const statusOptions = [
{ value: 'active', label: 'Активен' },
{ value: 'inactive', label: 'Неактивен' }
@@ -1635,13 +1625,10 @@ function AddBillingForm({ item, onItemChange, servers }) {
-
Назначение
+ onItemChange({ ...item, purpose: value })}
- options={purposeOptions}
/>
@@ -1731,17 +1718,6 @@ function AddBillingForm({ item, onItemChange, servers }) {
}
function EditBillingForm({ item, onItemChange, servers }) {
- const purposeOptions = [
- { value: '', label: 'Выберите назначение' },
- { value: 'relay', label: 'Relay VPS' },
- { value: 'bgp', label: 'BGP сервер' },
- { value: 'monitoring', label: 'Мониторинг' },
- { value: 'dns', label: 'DNS сервер' },
- { value: 'proxy', label: 'Прокси сервер' },
- { value: 'backup', label: 'Backup сервер' },
- { value: 'other', label: 'Другое' }
- ];
-
const statusOptions = [
{ value: 'active', label: 'Активен' },
{ value: 'inactive', label: 'Неактивен' }
@@ -1789,13 +1765,10 @@ function EditBillingForm({ item, onItemChange, servers }) {
-
Назначение
+ onItemChange({ ...item, purpose: value })}
- options={purposeOptions}
/>
diff --git a/frontend/src/components/PurposeSelectInput.jsx b/frontend/src/components/PurposeSelectInput.jsx
new file mode 100644
index 0000000..c2aa8f3
--- /dev/null
+++ b/frontend/src/components/PurposeSelectInput.jsx
@@ -0,0 +1,150 @@
+import { useMemo, useRef, useState, useEffect } from 'react';
+import {
+ IconRoute,
+ IconTopologyStar3,
+ IconActivity,
+ IconWorld,
+ IconShield,
+ IconDatabaseExport,
+ IconDots,
+} from '@tabler/icons-react';
+
+const PURPOSES = [
+ {
+ value: 'relay',
+ label: 'Relay VPS',
+ description: 'Трафик/ретрансляция',
+ icon: IconRoute,
+ },
+ {
+ value: 'bgp',
+ label: 'BGP сервер',
+ description: 'Маршрутизация и объявления префиксов',
+ icon: IconTopologyStar3,
+ },
+ {
+ value: 'monitoring',
+ label: 'Мониторинг',
+ description: 'Сбор метрик и алертов',
+ icon: IconActivity,
+ },
+ {
+ value: 'dns',
+ label: 'DNS сервер',
+ description: 'Резолвинг и обслуживание DNS-зон',
+ icon: IconWorld,
+ },
+ {
+ value: 'proxy',
+ label: 'Прокси сервер',
+ description: 'Проксирование HTTP(S)/SOCKS и т.п.',
+ icon: IconShield,
+ },
+ {
+ value: 'backup',
+ label: 'Backup сервер',
+ description: 'Резервные копии и архивы',
+ icon: IconDatabaseExport,
+ },
+ {
+ value: 'other',
+ label: 'Другое',
+ description: 'Свободное назначение',
+ icon: IconDots,
+ },
+];
+
+function PurposeSelectInput({ value, onChange, placeholder = 'Выберите назначение' }) {
+ const containerRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+
+ const selected = useMemo(
+ () => PURPOSES.find((p) => p.value === value) || null,
+ [value]
+ );
+
+ useEffect(() => {
+ const handler = (e) => {
+ if (!containerRef.current) return;
+ if (!containerRef.current.contains(e.target)) setOpen(false);
+ };
+ document.addEventListener('click', handler);
+ return () => document.removeEventListener('click', handler);
+ }, []);
+
+ const handleSelect = (p) => {
+ onChange(p ? p.value : '');
+ setOpen(false);
+ };
+
+ return (
+
+
+
+ {open && (
+
+
+ {PURPOSES.map((p, idx) => {
+ const Icon = p.icon;
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+export { PURPOSES };
+export default PurposeSelectInput;
+
+