This commit is contained in:
+83
@@ -1283,6 +1283,89 @@ def api_venue_events(venue_name):
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
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')
|
@bp.route('/image_proxy')
|
||||||
@login_required
|
@login_required
|
||||||
def image_proxy():
|
def image_proxy():
|
||||||
|
|||||||
@@ -434,6 +434,9 @@
|
|||||||
<button class="btn btn-sm btn-outline-success ms-2" id="post-venues-btn" style="display: none;">
|
<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> Площадки
|
<i class="fas fa-map-marker-alt me-1"></i> Площадки
|
||||||
</button>
|
</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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Левая панель - список постов -->
|
<!-- Левая панель - список постов -->
|
||||||
@@ -674,6 +677,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 id="processing-message" class="alert alert-info">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@@ -1938,10 +1987,13 @@
|
|||||||
// Обновление видимости кнопки "Площадки"
|
// Обновление видимости кнопки "Площадки"
|
||||||
function updateVenuesButtonVisibility() {
|
function updateVenuesButtonVisibility() {
|
||||||
const venuesBtn = document.getElementById('post-venues-btn');
|
const venuesBtn = document.getElementById('post-venues-btn');
|
||||||
|
const tagsBtn = document.getElementById('post-tags-btn');
|
||||||
if (selectedPostId) {
|
if (selectedPostId) {
|
||||||
venuesBtn.style.display = 'inline-block';
|
venuesBtn.style.display = 'inline-block';
|
||||||
|
tagsBtn.style.display = 'inline-block';
|
||||||
} else {
|
} else {
|
||||||
venuesBtn.style.display = 'none';
|
venuesBtn.style.display = 'none';
|
||||||
|
tagsBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Добавляем функции для отвязывания постов от событий и наоборот
|
// Добавляем функции для отвязывания постов от событий и наоборот
|
||||||
@@ -2826,6 +2878,162 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
throw new Error('Ошибка отвязки события от поста');
|
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
|
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
|
||||||
window.resetLayout = resetLayout;
|
window.resetLayout = resetLayout;
|
||||||
|
|||||||
Reference in New Issue
Block a user