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