diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 5d69065..53efe00 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -21,7 +21,9 @@ import { IconSettings, IconDeviceFloppy, IconHash, - IconRocket + IconRocket, + IconSortAscending, + IconSortDescending } from '@tabler/icons-react'; const API_URL = '/api'; @@ -57,13 +59,25 @@ function FilterManager() { const [editServerError, setEditServerError] = useState(''); // Новое состояние для режима - const [mode, setMode] = useState('simple'); // 'simple' | 'advanced' + const [mode, setMode] = useState('advanced'); // 'simple' | 'advanced' const [simpleFilters, setSimpleFilters] = useState([]); const [simpleAddFilter, setSimpleAddFilter] = useState({ community: '', gateway: '', description: '' }); const [simpleError, setSimpleError] = useState(''); const [simpleSuccess, setSimpleSuccess] = useState(''); const [simpleLoading, setSimpleLoading] = useState(false); + // Состояние поиска/сортировки в расширенном режиме + const [filterSearch, setFilterSearch] = useState(''); + const [sortBy, setSortBy] = useState('community'); // community | gateway | description + const [sortDir, setSortDir] = useState('asc'); // asc | desc + + // Состояние для инлайн-редактирования и массовых действий + const [editingKey, setEditingKey] = useState(null); + const [editingDraft, setEditingDraft] = useState({ community: '', gateway: '', description: '' }); + const [selectedFilterKeys, setSelectedFilterKeys] = useState(new Set()); + + const filterKey = (f) => `${String(f.community || '')}||${String(f.gateway || '')}`; + useEffect(() => { fetchServers(); fetchSimpleFilters(); @@ -571,6 +585,113 @@ function FilterManager() { const enabledServers = servers.filter(s => s.enabled).length; const serversWithFilters = servers.filter(s => s.id === selectedServer?.id ? serverFilters.length > 0 : false).length; + // Преобразованные фильтры для таблицы (поиск + сортировка) + const processedFilters = (() => { + const search = filterSearch.trim().toLowerCase(); + const filtered = search + ? serverFilters.filter((f) => { + const c = String(f.community || '').toLowerCase(); + const g = String(f.gateway || '').toLowerCase(); + const d = String(f.description || '').toLowerCase(); + return c.includes(search) || g.includes(search) || d.includes(search); + }) + : serverFilters.slice(); + const sorted = filtered.sort((a, b) => { + const av = String(a[sortBy] || '').toLowerCase(); + const bv = String(b[sortBy] || '').toLowerCase(); + if (av < bv) return sortDir === 'asc' ? -1 : 1; + if (av > bv) return sortDir === 'asc' ? 1 : -1; + return 0; + }); + return sorted; + })(); + + const toggleSort = (field) => { + if (sortBy === field) { + setSortDir((prev) => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortBy(field); + setSortDir('asc'); + } + }; + + // Убрано сохранение режима в localStorage по просьбе пользователя + + const startInlineEdit = (filter) => { + setEditingKey(filterKey(filter)); + setEditingDraft({ + community: filter.community || '', + gateway: filter.gateway || '', + description: filter.description || '' + }); + }; + + const cancelInlineEdit = () => { + setEditingKey(null); + setEditingDraft({ community: '', gateway: '', description: '' }); + }; + + const saveInlineEdit = async () => { + if (!editingDraft.community.trim() || !editingDraft.gateway.trim()) { + setError('Поля Community и Gateway должны быть заполнены.'); + return; + } + const updated = serverFilters.map((f) => + filterKey(f) === editingKey + ? { community: editingDraft.community.trim(), gateway: editingDraft.gateway.trim(), description: editingDraft.description } + : f + ); + setServerFilters(updated); + cancelInlineEdit(); + setSelectedFilterKeys(new Set()); + try { + await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated }); + setSuccess('Фильтр успешно обновлен!'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error saving server filters:', error); + setError('Не удалось сохранить фильтр.'); + } + }; + + const toggleSelectFilter = (key) => { + setSelectedFilterKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + }; + + const areAllProcessedSelected = processedFilters.length > 0 && processedFilters.every((f) => selectedFilterKeys.has(filterKey(f))); + const toggleSelectAllProcessed = () => { + setSelectedFilterKeys((prev) => { + const next = new Set(prev); + if (areAllProcessedSelected) { + processedFilters.forEach((f) => next.delete(filterKey(f))); + } else { + processedFilters.forEach((f) => next.add(filterKey(f))); + } + return next; + }); + }; + + const deleteSelectedFilters = async () => { + if (selectedFilterKeys.size === 0) return; + const confirmed = window.confirm(`Удалить выбранные фильтры (${selectedFilterKeys.size})?`); + if (!confirmed) return; + const updated = serverFilters.filter((f) => !selectedFilterKeys.has(filterKey(f))); + setServerFilters(updated); + setSelectedFilterKeys(new Set()); + try { + await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated }); + setSuccess('Выбранные фильтры удалены.'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error deleting selected filters:', error); + setError('Не удалось удалить выбранные фильтры.'); + } + }; + // Функция для применения фильтров из упрощённого режима на все сервера const handleDeploySimpleFiltersToAll = async () => { if (servers.length === 0) { @@ -950,7 +1071,7 @@ function FilterManager() {
-
+

@@ -963,7 +1084,24 @@ function FilterManager() { Управление фильтрами community/gateway для генерации конфигурации MikroTik

-
+
+
+ + + + setFilterSearch(e.target.value)} + /> + {filterSearch && ( + + )} +
+
+
) : ( + <>
- - - + + + + - {serverFilters.map((filter, index) => ( - + {processedFilters.map((filter, index) => { + const key = filterKey(filter); + const isEditing = editingKey === key; + const isSelected = selectedFilterKeys.has(key); + return ( + + - ))} + );})}
COMMUNITYGATEWAYОПИСАНИЕ + + toggleSort('community')}> + COMMUNITY + {sortBy === 'community' && (sortDir === 'asc' ? : )} + toggleSort('gateway')}> + GATEWAY + {sortBy === 'gateway' && (sortDir === 'asc' ? : )} + toggleSort('description')}> + ОПИСАНИЕ + {sortBy === 'description' && (sortDir === 'asc' ? : )} + ДЕЙСТВИЯ
-
- - - - {filter.community} -
+ toggleSelectFilter(key)} + />
-
- - - - {filter.gateway} -
+ {isEditing ? ( + setEditingDraft({ ...editingDraft, community: e.target.value })} + autoFocus + /> + ) : ( +
+ + + + {filter.community} +
+ )}
-
- {filter.description ? ( - {filter.description} - ) : ( - Без описания - )} -
+ {isEditing ? ( + setEditingDraft({ ...editingDraft, gateway: e.target.value })} + /> + ) : ( +
+ + + + {filter.gateway} +
+ )} +
+ {isEditing ? ( + setEditingDraft({ ...editingDraft, description: e.target.value })} + /> + ) : ( +
+ {filter.description ? ( + {filter.description} + ) : ( + Без описания + )} +
+ )}
- - + {isEditing ? ( + <> + + + + ) : ( + <> + + + + )}
+ {selectedFilterKeys.size > 0 && ( +
+
Выбрано: {selectedFilterKeys.size}
+
+ + +
+
+ )} + )}