From e03fb9a872e8e48cf0f2573670cacc4a7338d033 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 25 Dec 2025 20:15:02 +0300 Subject: [PATCH] =?UTF-8?q?[=D0=92=D0=9E=D0=9B=D0=9A]=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=9F=D0=BB=D0=BE=D1=89=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_edit.py | 82 ++++++++ templates/index.html | 434 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 484 insertions(+), 32 deletions(-) diff --git a/db_edit.py b/db_edit.py index ee2217e..1ea710e 100644 --- a/db_edit.py +++ b/db_edit.py @@ -1092,6 +1092,88 @@ def api_load_categories(): log_event(error_msg) return jsonify({'success': False, 'error': error_msg}), 500 +@bp.route('/api/categories') +@login_required +def api_categories(): + """Получает список всех категорий (площадок) из таблицы categories""" + db = get_db() + try: + with db.conn.cursor() as cursor: + cursor.execute(""" + SELECT AUTO_ID, ID, TITLE, description + FROM categories + ORDER BY TITLE, ID + """) + categories = cursor.fetchall() + + categories_list = [] + for cat in categories: + categories_list.append({ + 'auto_id': cat['AUTO_ID'], + 'id': cat['ID'], + 'title': cat['TITLE'] or '', + 'description': cat['description'] or '' + }) + + return jsonify(categories_list) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/categories/') +@login_required +def api_category_details(category_id): + """Получает детали категории по ID""" + db = get_db() + try: + with db.conn.cursor() as cursor: + cursor.execute(""" + SELECT AUTO_ID, ID, TITLE, description + FROM categories + WHERE ID = %s + """, (category_id,)) + category = cursor.fetchone() + + if not category: + return jsonify({'error': 'Category not found'}), 404 + + # Получаем количество событий для этой категории + cursor.execute(""" + SELECT COUNT(*) as event_count + FROM events + WHERE unit_name = %s + """, (category_id,)) + event_count_result = cursor.fetchone() + event_count = event_count_result['event_count'] if event_count_result else 0 + + return jsonify({ + 'auto_id': category['AUTO_ID'], + 'id': category['ID'], + 'title': category['TITLE'] or '', + 'description': category['description'] or '', + 'event_count': event_count + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/events/count') +@login_required +def api_events_count(): + """Получает количество событий в таблице events""" + db = get_db() + try: + with db.conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) as count FROM events") + result = cursor.fetchone() + return jsonify({'count': result['count'] if result else 0}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + @bp.route('/api/run_volk_rescan', methods=['POST']) @login_required def api_run_volk_rescan(): diff --git a/templates/index.html b/templates/index.html index c9b0e95..0b4e1e2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -451,6 +451,13 @@ События + {% if g.workmode == 'VOLK' %} + + {% endif %}
@@ -573,11 +580,6 @@ - {% if g.workmode == 'VOLK' %} - - {% endif %}
+ {% if g.workmode == 'VOLK' %} + +
+
+ +
+
+ +
+
+
+
Список площадок
+
+
+ + + +
+ +
+
+
+ +
+ + + + + + + +
IDНазвание
+
+ +
+ + + + + + +
+
+ Загрузка... +
+

Загрузка данных...

+
+
+
+
+
+ +
+
+
+
Детали площадки
+
+
+
+
+ +

Выберите площадку для просмотра деталей

+
+
+
+
+
+
+ {% endif %} @@ -866,6 +945,21 @@ syncColumnWidths('events'); }, 50); }); + {% if g.workmode == 'VOLK' %} + const categoriesTab = document.getElementById('categories-tab'); + if (categoriesTab) { + categoriesTab.addEventListener('click', () => { + if (document.getElementById('categories-table').children.length === 1) { + loadCategoriesList(); + } + // Принудительно обновляем размеры панелей и синхронизируем колонки + setTimeout(() => { + adjustPanelWidthsOnResize(); + syncColumnWidths('categories'); + }, 50); + }); + } + {% endif %} // Новые обработчики для кнопок событий {% if g.workmode == 'VOLK' %} const volkRescanBtn = document.getElementById('volk-rescan-btn'); @@ -885,6 +979,10 @@ if (loadCategoriesBtn) { loadCategoriesBtn.addEventListener('click', loadCategories); } + const refreshCategoriesBtn = document.getElementById('refresh-categories-btn'); + if (refreshCategoriesBtn) { + refreshCategoriesBtn.addEventListener('click', loadCategoriesList); + } {% endif %} // Обработчики для кнопок привязки document.getElementById('post-link-event-btn').addEventListener('click', togglePostEventLink); @@ -1034,28 +1132,76 @@ } // Функция для загрузки категорий function loadCategories() { - showProcessingMessage("Загрузка категорий..."); - fetch(`${PREFIX}/api/load_categories`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => response.json()) - .then(data => { - hideProcessingMessage(); - if (data.success) { - alert('Категории загружены успешно!'); - } else { - alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка')); - } - }) - .catch(error => { - hideProcessingMessage(); - console.error('Ошибка загрузки категорий:', error); - alert('Произошла ошибка при загрузке категорий'); - }); + // Сначала проверяем наличие событий + fetch(`${PREFIX}/api/events/count`) + .then(response => response.json()) + .then(data => { + const eventCount = data.count || 0; + if (eventCount > 0) { + // Если есть события, спрашиваем подтверждение + if (!confirm(`В таблице событий есть ${eventCount} записей. Обновление данных площадок может повлиять на существующие события. Вы уверены, что хотите продолжить?`)) { + return; + } + } + // Продолжаем загрузку категорий + showProcessingMessage("Загрузка категорий..."); + fetch(`${PREFIX}/api/load_categories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(data => { + hideProcessingMessage(); + if (data.success) { + alert('Категории загружены успешно!'); + // Обновляем список категорий, если мы на вкладке площадок + const categoriesTab = document.getElementById('categories-tab'); + if (categoriesTab && categoriesTab.classList.contains('active')) { + loadCategoriesList(); + } + } else { + alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка')); + } + }) + .catch(error => { + hideProcessingMessage(); + console.error('Ошибка загрузки категорий:', error); + alert('Произошла ошибка при загрузке категорий'); + }); + }) + .catch(error => { + console.error('Ошибка проверки количества событий:', error); + // В случае ошибки все равно продолжаем загрузку + showProcessingMessage("Загрузка категорий..."); + fetch(`${PREFIX}/api/load_categories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(data => { + hideProcessingMessage(); + if (data.success) { + alert('Категории загружены успешно!'); + const categoriesTab = document.getElementById('categories-tab'); + if (categoriesTab && categoriesTab.classList.contains('active')) { + loadCategoriesList(); + } + } else { + alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка')); + } + }) + .catch(err => { + hideProcessingMessage(); + console.error('Ошибка загрузки категорий:', err); + alert('Произошла ошибка при загрузке категорий'); + }); + }); } // Функция для публикации конкретного события function publishCurrentEvent(eventId) { @@ -1680,6 +1826,142 @@ console.error('Ошибка загрузки деталей события:', error); } } + // Функции для работы с категориями (площадками) + let selectedCategoryId = null; + async function loadCategoriesList() { + const tableBody = document.getElementById('categories-table'); + if (!tableBody) return; + tableBody.innerHTML = ` + + +
+ Загрузка... +
+ Загрузка площадок... + + `; + try { + const response = await fetch(`${PREFIX}/api/categories`); + const categories = await response.json(); + if (categories.length === 0) { + tableBody.innerHTML = ` + + + +

Нет доступных площадок

+ + `; + return; + } + tableBody.innerHTML = ''; + categories.forEach(category => { + const row = ` + + ${category.id || '-'} + ${category.title || 'Без названия'} + `; + tableBody.innerHTML += row; + }); + // Клик по строке категории + document.querySelectorAll('.category-row').forEach(row => { + row.addEventListener('click', (e) => { + const categoryId = row.dataset.id; + showCategoryDetails(categoryId); + highlightSelectedCategory(categoryId); + }); + }); + // Переинициализируем разделители колонок для категорий + setTimeout(() => { + syncColumnWidths('categories'); + reinitializeColumnResizers('categories'); + }, 0); + } catch (error) { + tableBody.innerHTML = ` + + + +

Ошибка загрузки данных

+ + + `; + console.error('Ошибка загрузки категорий:', error); + } + } + // Выделение выбранной категории + function highlightSelectedCategory(categoryId) { + // Снимаем выделение со всех категорий + document.querySelectorAll('.category-row').forEach(row => { + row.classList.remove('active'); + }); + // Находим нужную категорию + const selectedRow = document.querySelector(`.category-row[data-id="${categoryId}"]`); + if (selectedRow) { + // Выделяем категорию + selectedRow.classList.add('active'); + selectedCategoryId = categoryId; + // Прокручиваем к выбранной категории + selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + // Показать детали категории + async function showCategoryDetails(categoryId) { + const detailsPanel = document.getElementById('category-details'); + if (!detailsPanel) return; + detailsPanel.innerHTML = ` +
+
+ Загрузка... +
+

Загрузка данных площадки...

+
`; + try { + const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}`); + if (!response.ok) { + throw new Error('Площадка не найдена'); + } + const category = await response.json(); + // Создаем HTML для деталей + let detailsHtml = ` +
+
+
+ ID: ${category.id || 'Не указано'} +
+
+ Название: ${category.title || 'Не указано'} +
+
+ Количество событий: ${category.event_count || 0} +
+
`; + if (category.description) { + detailsHtml += ` +
+ Описание: +
${category.description}
+
`; + } + detailsHtml += `
`; + detailsPanel.innerHTML = detailsHtml; + // Обновляем заголовок панели + const header = document.getElementById('category-details-header'); + if (header) { + header.textContent = `Площадка: ${category.title || category.id || 'Без названия'}`; + } + } catch (error) { + detailsPanel.innerHTML = ` +
+ + Ошибка загрузки данных площадки: ${error.message} + +
`; + console.error('Ошибка загрузки деталей категории:', error); + } + } // Функция загрузки постов async function loadPosts() { const tableBody = document.getElementById('posts-table'); @@ -2550,11 +2832,17 @@ function setupColumnResizer(resizer, header, tableType = 'posts') { } // Применяем ширину к соответствующим ячейкам в теле таблицы - const bodyTable = document.querySelector( - tableType === 'posts' - ? '#posts-tab-pane .scrollable-table-body table' - : '#events-tab-pane .scrollable-table-body table' - ); + let bodyTableSelector; + if (tableType === 'posts') { + bodyTableSelector = '#posts-tab-pane .scrollable-table-body table'; + } else if (tableType === 'events') { + bodyTableSelector = '#events-tab-pane .scrollable-table-body table'; + } else if (tableType === 'categories') { + bodyTableSelector = '#categories-tab-pane .scrollable-table-body table'; + } else { + bodyTableSelector = '#posts-tab-pane .scrollable-table-body table'; + } + const bodyTable = document.querySelector(bodyTableSelector); if (bodyTable) { const bodyRows = bodyTable.querySelectorAll('tbody tr'); @@ -2595,6 +2883,9 @@ function syncColumnWidths(tableType) { } else if (tableType === 'events') { headerContainerSelector = '#events-tab-pane .table-header-container table'; bodyContainerSelector = '#events-tab-pane .scrollable-table-body table'; + } else if (tableType === 'categories') { + headerContainerSelector = '#categories-tab-pane .table-header-container table'; + bodyContainerSelector = '#categories-tab-pane .scrollable-table-body table'; } else { console.warn('Неизвестный тип таблицы для синхронизации:', tableType); return; @@ -3088,6 +3379,8 @@ document.addEventListener('DOMContentLoaded', () => { syncColumnWidths('posts'); } else if (tabId === 'events-tab-pane') { syncColumnWidths('events'); + } else if (tabId === 'categories-tab-pane') { + syncColumnWidths('categories'); } } }, 0); @@ -3541,6 +3834,56 @@ document.addEventListener('DOMContentLoaded', () => { } // Простой поиск по событиям (без тэгов) + {% if g.workmode == 'VOLK' %} + // Функция поиска по категориям + function searchCategories(query) { + const tableBody = document.getElementById('categories-table'); + if (!tableBody) return; + const rows = tableBody.querySelectorAll('.category-row'); + const searchTerm = query.toLowerCase().trim(); + let foundCount = 0; + let firstMatch = null; + rows.forEach((row, index) => { + const id = row.querySelector('td:first-child').textContent.toLowerCase(); + const name = row.querySelector('.category-name-preview').textContent.toLowerCase(); + const matches = id.includes(searchTerm) || name.includes(searchTerm); + if (matches) { + row.style.display = ''; + if (foundCount === 0) { + firstMatch = row; + } + foundCount++; + } else { + row.style.display = 'none'; + } + }); + // Выделяем первый найденный результат + if (firstMatch && searchTerm) { + const categoryId = firstMatch.dataset.id; + highlightSelectedCategory(categoryId); + showCategoryDetails(categoryId); + firstMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + // Навигация по результатам поиска категорий + let currentCategorySearchIndex = -1; + function navigateCategoriesSearch(direction) { + const tableBody = document.getElementById('categories-table'); + if (!tableBody) return; + const visibleRows = Array.from(tableBody.querySelectorAll('.category-row')).filter(row => row.style.display !== 'none'); + if (visibleRows.length === 0) return; + if (direction === 'next') { + currentCategorySearchIndex = (currentCategorySearchIndex + 1) % visibleRows.length; + } else { + currentCategorySearchIndex = (currentCategorySearchIndex - 1 + visibleRows.length) % visibleRows.length; + } + const selectedRow = visibleRows[currentCategorySearchIndex]; + const categoryId = selectedRow.dataset.id; + highlightSelectedCategory(categoryId); + showCategoryDetails(categoryId); + selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + {% endif %} function searchEventsSimple(query) { const events = document.querySelectorAll('.event-row'); eventsSearchResults = []; @@ -3673,6 +4016,33 @@ document.addEventListener('DOMContentLoaded', () => { } }); + {% if g.workmode == 'VOLK' %} + const categoriesSearchInput = document.getElementById('categories-search'); + if (categoriesSearchInput) { + categoriesSearchInput.addEventListener('input', (e) => { + searchCategories(e.target.value); + currentCategorySearchIndex = -1; + }); + document.getElementById('categories-search-prev').addEventListener('click', () => { + navigateCategoriesSearch('prev'); + }); + document.getElementById('categories-search-next').addEventListener('click', () => { + navigateCategoriesSearch('next'); + }); + categoriesSearchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + navigateCategoriesSearch('next'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + navigateCategoriesSearch('prev'); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + navigateCategoriesSearch('next'); + } + }); + } + {% endif %} document.getElementById('events-search').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault();