diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx
index 398b00c..1ddf6ba 100644
--- a/frontend/src/BillingManager.jsx
+++ b/frontend/src/BillingManager.jsx
@@ -7,6 +7,7 @@ import Pagination from './components/Pagination.jsx';
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 ErrorAlert from './components/ErrorAlert.jsx';
import {
IconPlus,
@@ -1622,15 +1623,16 @@ function AddBillingForm({ item, onItemChange, servers }) {
/>
-
Связанный сервер (опционально)
+ onItemChange({ ...item, serverId: value })}
- options={serverOptions}
- helpText="Используется для связи с разделом «Серверы»"
+ onChange={(value) => onItemChange({ ...item, serverId: value || '' })}
+ servers={servers}
+ placeholder="Поиск по IP, DNS, провайдеру..."
/>
+
+ Используется для связи с разделом «Серверы». Можно оставить пустым.
+
-
Связанный сервер (опционально)
+ onItemChange({ ...item, serverId: value })}
- options={serverOptions}
- helpText="Используется для связи с разделом «Серверы»"
+ onChange={(value) => onItemChange({ ...item, serverId: value || '' })}
+ servers={servers}
+ placeholder="Поиск по IP, DNS, провайдеру..."
/>
+
+ Используется для связи с разделом «Серверы». Можно оставить пустым.
+
{
+ const q = String(value || '').toLowerCase();
+
+ const list = (servers || []).map((s) => {
+ const id = s.id || s.ip;
+ const labelIp = s.ip || '—';
+ const labelDns = s.dns || '';
+ const labelProvider = s.provider || '';
+ const label =
+ labelDns && labelProvider
+ ? `${labelIp} (${labelDns}) – ${labelProvider}`
+ : labelDns
+ ? `${labelIp} (${labelDns})`
+ : labelProvider
+ ? `${labelIp} – ${labelProvider}`
+ : `${labelIp}`;
+
+ return {
+ raw: s,
+ value: id,
+ label,
+ search: [
+ id,
+ s.ip,
+ s.extIp,
+ s.internalIp,
+ s.dns,
+ s.provider,
+ s.country,
+ s.gateway,
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase(),
+ };
+ });
+
+ if (!q) return list.slice(0, maxSuggestions);
+ const filtered = list.filter((item) => item.search.includes(q));
+ return filtered.slice(0, maxSuggestions);
+ }, [value, servers, maxSuggestions]);
+
+ useEffect(() => {
+ const handleOutside = (e) => {
+ if (!containerRef.current) return;
+ if (!containerRef.current.contains(e.target)) setOpen(false);
+ };
+ document.addEventListener('click', handleOutside);
+ return () => document.removeEventListener('click', handleOutside);
+ }, []);
+
+ const selectItem = (item) => {
+ onChange(item.value);
+ if (onSelectMeta) onSelectMeta(item.raw);
+ setOpen(false);
+ setActiveIndex(-1);
+ if (inputRef.current) inputRef.current.focus();
+ };
+
+ const handleKeyDown = (e) => {
+ if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
+ setOpen(true);
+ return;
+ }
+ if (!open) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setActiveIndex((prev) => Math.max(prev - 1, 0));
+ } else if (e.key === 'Enter') {
+ if (activeIndex >= 0 && activeIndex < suggestions.length) {
+ e.preventDefault();
+ selectItem(suggestions[activeIndex]);
+ }
+ } else if (e.key === 'Escape') {
+ setOpen(false);
+ setActiveIndex(-1);
+ }
+ };
+
+ // Текст в input: ищем label по выбранному serverId
+ const displayValue = useMemo(() => {
+ if (!value) return '';
+ const found = (servers || []).find((s) => (s.id || s.ip) === value);
+ if (!found) return '';
+ const ip = found.ip || '—';
+ const dns = found.dns || '';
+ const provider = found.provider || '';
+ if (dns && provider) return `${ip} (${dns}) – ${provider}`;
+ if (dns) return `${ip} (${dns})`;
+ if (provider) return `${ip} – ${provider}`;
+ return ip;
+ }, [value, servers]);
+
+ return (
+
+
+
+
+
+ {
+ onChange(e.target.value);
+ setOpen(true);
+ }}
+ onFocus={() => setOpen(true)}
+ onKeyDown={handleKeyDown}
+ autoComplete="off"
+ />
+
+
+ {open && suggestions.length > 0 && (
+
+
+
+ {suggestions.map((s, idx) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default ServerAutocompleteInput;
+
+
+