This commit is contained in:
+83
@@ -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/<tag_name>/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():
|
||||
|
||||
@@ -434,6 +434,9 @@
|
||||
<button class="btn btn-sm btn-outline-success ms-2" id="post-venues-btn" style="display: none;">
|
||||
<i class="fas fa-map-marker-alt me-1"></i> Площадки
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info ms-2" id="post-tags-btn" style="display: none;">
|
||||
<i class="fas fa-tags me-1"></i> Тэги
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Левая панель - список постов -->
|
||||
@@ -674,6 +677,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для выбора тэгов -->
|
||||
<div class="modal fade" id="tagsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Выбор тэга</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="list-group" id="tags-list" style="max-height: 60vh; overflow-y: auto;">
|
||||
<!-- Список тэгов будет загружен динамически -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для выбора событий по тэгу -->
|
||||
<div class="modal fade" id="tagEventsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">События с тэгом: <span id="selected-tag-name"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" id="select-all-tag-events">
|
||||
<i class="fas fa-check-square me-1"></i> Выбрать все
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselect-all-tag-events">
|
||||
<i class="fas fa-square me-1"></i> Снять все
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-group" id="tag-events-list" style="max-height: 50vh; overflow-y: auto;">
|
||||
<!-- Список событий будет загружен динамически -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary" id="confirm-tag-events-link">ОК</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сообщение о процессе выполнения -->
|
||||
<div id="processing-message" class="alert alert-info">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -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 = '<div class="list-group-item text-center text-muted">Тэги не найдены</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tags.forEach(tag => {
|
||||
const tagItem = document.createElement('div');
|
||||
tagItem.className = 'list-group-item list-group-item-action';
|
||||
tagItem.innerHTML = `
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">${tag.name}</h6>
|
||||
<small class="text-muted">${tag.event_count} событий</small>
|
||||
</div>
|
||||
`;
|
||||
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 = '<div class="list-group-item text-center text-muted">События не найдены</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
events.forEach(event => {
|
||||
const eventItem = document.createElement('div');
|
||||
eventItem.className = 'list-group-item';
|
||||
eventItem.innerHTML = `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="${event.id}" id="tag-event-${event.id}" ${event.linked ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="tag-event-${event.id}">
|
||||
<strong>${event.name}</strong>
|
||||
${event.number ? `<br><small class="text-muted">№ ${event.number}</small>` : ''}
|
||||
${event.unit_name ? `<br><small class="text-muted">Площадка: ${event.unit_name}</small>` : ''}
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user