From 7602c60bd69ab93c602d62b6ac3fcc18aaf8e1af Mon Sep 17 00:00:00 2001 From: gitadmin Date: Mon, 29 Sep 2025 19:51:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=BF=D1=80=D0=B8=D0=B2=D1=8F=D0=B7=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=82=D1=8D=D0=B3=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_edit.py | 83 +++++++++++++++++ templates/index.html | 208 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) diff --git a/db_edit.py b/db_edit.py index aa06191..d28878d 100644 --- a/db_edit.py +++ b/db_edit.py @@ -1283,6 +1283,89 @@ def api_venue_events(venue_name): finally: db.close() +# API endpoints для работы с тэгами +@bp.route('/api/tags') +@login_required +def api_tags(): + """Получает список всех уникальных тэгов с количеством событий""" + db = get_db() + try: + with db.conn.cursor() as cursor: + # Получаем все тэги и разбиваем их по пробелам + cursor.execute(""" + SELECT tags + FROM events + WHERE tags IS NOT NULL AND tags != '' + """) + events = cursor.fetchall() + + # Собираем все уникальные тэги + tag_counts = {} + for event in events: + tags = event['tags'].split() + for tag in tags: + tag = tag.strip() + if tag: + tag_counts[tag] = tag_counts.get(tag, 0) + 1 + + # Сортируем по количеству событий (убывание), затем по названию + sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0])) + + tags_list = [] + for tag, count in sorted_tags: + tags_list.append({ + 'name': tag, + 'event_count': count + }) + + return jsonify(tags_list) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/tags//events') +@login_required +def api_tag_events(tag_name): + """Получает список событий для указанного тэга с информацией о привязке к посту""" + post_id = request.args.get('post_id') + if not post_id: + return jsonify({'error': 'post_id parameter is required'}), 400 + + db = get_db() + try: + with db.conn.cursor() as cursor: + # Получаем события с указанным тэгом с информацией о привязке к посту + cursor.execute(""" + SELECT e.id, e.name, e.number, e.unit_name, e.tags, + CASE WHEN i.post_id IS NOT NULL THEN 1 ELSE 0 END as linked + FROM events e + LEFT JOIN interlinks i ON e.id = i.event_id AND i.post_id = %s + WHERE e.tags LIKE %s + ORDER BY e.name + """, (post_id, f'%{tag_name}%')) + events = cursor.fetchall() + + events_list = [] + for event in events: + # Проверяем, что тэг действительно присутствует (не подстрока другого тэга) + tags = event['tags'].split() if event['tags'] else [] + if tag_name in tags: + events_list.append({ + 'id': event['id'], + 'name': event['name'], + 'number': event['number'], + 'unit_name': event['unit_name'], + 'tags': event['tags'], + 'linked': bool(event['linked']) + }) + + return jsonify(events_list) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + @bp.route('/image_proxy') @login_required def image_proxy(): diff --git a/templates/index.html b/templates/index.html index ce219c5..d3ef577 100644 --- a/templates/index.html +++ b/templates/index.html @@ -434,6 +434,9 @@ +
@@ -674,6 +677,52 @@
+ + + + + +
@@ -1938,10 +1987,13 @@ // Обновление видимости кнопки "Площадки" function updateVenuesButtonVisibility() { const venuesBtn = document.getElementById('post-venues-btn'); + const tagsBtn = document.getElementById('post-tags-btn'); if (selectedPostId) { venuesBtn.style.display = 'inline-block'; + tagsBtn.style.display = 'inline-block'; } else { venuesBtn.style.display = 'none'; + tagsBtn.style.display = 'none'; } } // Добавляем функции для отвязывания постов от событий и наоборот @@ -2826,6 +2878,162 @@ document.addEventListener('DOMContentLoaded', () => { throw new Error('Ошибка отвязки события от поста'); } } + + // Функции для работы с тэгами + let selectedTagName = ''; + let selectedTagEvents = []; + + // Обработчик клика по кнопке "Тэги" + document.getElementById('post-tags-btn').addEventListener('click', async () => { + if (!selectedPostId) { + alert('Сначала выберите пост'); + return; + } + currentPostId = selectedPostId; + await loadTags(); + const tagsModal = new bootstrap.Modal(document.getElementById('tagsModal')); + tagsModal.show(); + }); + + // Загрузка списка тэгов + async function loadTags() { + try { + const response = await fetch(`${PREFIX}/api/tags`); + const tags = await response.json(); + + const tagsList = document.getElementById('tags-list'); + tagsList.innerHTML = ''; + + if (tags.length === 0) { + tagsList.innerHTML = '
Тэги не найдены
'; + return; + } + + tags.forEach(tag => { + const tagItem = document.createElement('div'); + tagItem.className = 'list-group-item list-group-item-action'; + tagItem.innerHTML = ` +
+
${tag.name}
+ ${tag.event_count} событий +
+ `; + tagItem.addEventListener('click', () => { + selectedTagName = tag.name; + loadTagEvents(tag.name); + }); + tagsList.appendChild(tagItem); + }); + } catch (error) { + console.error('Ошибка загрузки тэгов:', error); + alert('Ошибка загрузки тэгов'); + } + } + + // Загрузка событий выбранного тэга + async function loadTagEvents(tagName) { + try { + const response = await fetch(`${PREFIX}/api/tags/${encodeURIComponent(tagName)}/events?post_id=${currentPostId}`); + const events = await response.json(); + + selectedTagEvents = events; + + // Закрываем модальное окно тэгов + const tagsModal = bootstrap.Modal.getInstance(document.getElementById('tagsModal')); + tagsModal.hide(); + + // Показываем модальное окно событий + document.getElementById('selected-tag-name').textContent = tagName; + renderTagEventsList(events); + + const tagEventsModal = new bootstrap.Modal(document.getElementById('tagEventsModal')); + tagEventsModal.show(); + } catch (error) { + console.error('Ошибка загрузки событий тэга:', error); + alert('Ошибка загрузки событий тэга'); + } + } + + // Отображение списка событий с чекбоксами для тэгов + function renderTagEventsList(events) { + const eventsList = document.getElementById('tag-events-list'); + eventsList.innerHTML = ''; + + if (events.length === 0) { + eventsList.innerHTML = '
События не найдены
'; + return; + } + + events.forEach(event => { + const eventItem = document.createElement('div'); + eventItem.className = 'list-group-item'; + eventItem.innerHTML = ` +
+ + +
+ `; + eventsList.appendChild(eventItem); + }); + } + + // Обработчики кнопок "Выбрать все" и "Снять все" для тэгов + document.getElementById('select-all-tag-events').addEventListener('click', () => { + document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => { + checkbox.checked = true; + }); + }); + + document.getElementById('deselect-all-tag-events').addEventListener('click', () => { + document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => { + checkbox.checked = false; + }); + }); + + // Обработчик кнопки "ОК" для подтверждения привязки/отвязки событий по тэгу + document.getElementById('confirm-tag-events-link').addEventListener('click', async () => { + const checkboxes = document.querySelectorAll('#tag-events-list input[type="checkbox"]'); + const promises = []; + + for (const checkbox of checkboxes) { + const eventId = parseInt(checkbox.value); + const isChecked = checkbox.checked; + const wasLinked = selectedTagEvents.find(e => e.id === eventId)?.linked || false; + + // Если состояние изменилось, выполняем соответствующее действие + if (isChecked && !wasLinked) { + // Привязываем событие к посту + promises.push(linkEventToPost(currentPostId, eventId)); + } else if (!isChecked && wasLinked) { + // Отвязываем событие от поста + promises.push(unlinkEventFromPost(currentPostId, eventId)); + } + } + + try { + await Promise.all(promises); + alert('Изменения сохранены'); + + // Закрываем модальное окно + const tagEventsModal = bootstrap.Modal.getInstance(document.getElementById('tagEventsModal')); + tagEventsModal.hide(); + + // Обновляем информацию о связях + updateInterlinkInfo(); + + // Обновляем детали выбранного поста, если он открыт + if (currentPostId) { + await showPostDetails(currentPostId); + } + } catch (error) { + console.error('Ошибка при сохранении изменений:', error); + alert('Ошибка при сохранении изменений'); + } + }); }); // Убедимся, что функция доступна глобально, если потребуется вызвать из HTML window.resetLayout = resetLayout;