групповая привязка событий к постам по площадкам
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2025-09-29 15:50:57 +03:00
parent 99c5a3b55a
commit 8c02770fe3
2 changed files with 316 additions and 0 deletions
+68
View File
@@ -1215,6 +1215,74 @@ def api_get_all_interlinks():
finally: finally:
db.close() db.close()
# Новые API endpoints для работы с площадками
@bp.route('/api/venues')
@login_required
def api_venues():
"""Получает список всех уникальных площадок с количеством событий"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
SELECT unit_name as name, COUNT(*) as event_count
FROM events
WHERE unit_name IS NOT NULL AND unit_name != ''
GROUP BY unit_name
ORDER BY unit_name
""")
venues = cursor.fetchall()
venues_list = []
for venue in venues:
venues_list.append({
'name': venue['name'],
'event_count': venue['event_count']
})
return jsonify(venues_list)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/venues/<venue_name>/events')
@login_required
def api_venue_events(venue_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,
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.unit_name = %s
ORDER BY e.name
""", (post_id, venue_name))
events = cursor.fetchall()
events_list = []
for event in events:
events_list.append({
'id': event['id'],
'name': event['name'],
'number': event['number'],
'unit_name': event['unit_name'],
'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():
+248
View File
@@ -430,6 +430,9 @@
<span>Выбранное событие: </span> <span>Выбранное событие: </span>
<span id="post-selected-event-name"></span> <span id="post-selected-event-name"></span>
<button class="btn btn-sm btn-outline-primary ms-2" id="post-link-event-btn">Привязать</button> <button class="btn btn-sm btn-outline-primary ms-2" id="post-link-event-btn">Привязать</button>
<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>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -624,6 +627,53 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Модальное окно для выбора площадок -->
<div class="modal fade" id="venuesModal" 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="venues-list" style="max-height: 60vh; overflow-y: auto;">
<!-- Список площадок будет загружен динамически -->
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора событий площадки -->
<div class="modal fade" id="eventsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">События площадки: <span id="selected-venue-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-events">
<i class="fas fa-check-square me-1"></i> Выбрать все
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselect-all-events">
<i class="fas fa-square me-1"></i> Снять все
</button>
</div>
<div class="list-group" id="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-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">
@@ -1588,6 +1638,8 @@
} }
// Обновляем информацию о выбранном посте на вкладке событий // Обновляем информацию о выбранном посте на вкладке событий
updateSelectedPostInfo(); updateSelectedPostInfo();
// Обновляем информацию о выбранном событии на вкладке постов (для показа кнопки "Площадки")
updateSelectedEventInfo();
} }
// Показать детали поста // Показать детали поста
async function showPostDetails(postId) { async function showPostDetails(postId) {
@@ -1865,12 +1917,21 @@
function updateSelectedEventInfo() { function updateSelectedEventInfo() {
const eventInfoElement = document.getElementById('post-selected-event-info'); const eventInfoElement = document.getElementById('post-selected-event-info');
const eventNameElement = document.getElementById('post-selected-event-name'); const eventNameElement = document.getElementById('post-selected-event-name');
const venuesBtn = document.getElementById('post-venues-btn');
if (selectedEventData) { if (selectedEventData) {
eventNameElement.textContent = selectedEventData.name || `Событие ${selectedEventData.id}`; eventNameElement.textContent = selectedEventData.name || `Событие ${selectedEventData.id}`;
eventInfoElement.style.display = 'flex'; eventInfoElement.style.display = 'flex';
// Показываем кнопку "Площадки" только если выбран пост
if (selectedPostId) {
venuesBtn.style.display = 'inline-block';
} else {
venuesBtn.style.display = 'none';
}
updateInterlinkInfo(); updateInterlinkInfo();
} else { } else {
eventInfoElement.style.display = 'none'; eventInfoElement.style.display = 'none';
venuesBtn.style.display = 'none';
} }
} }
// Добавляем функции для отвязывания постов от событий и наоборот // Добавляем функции для отвязывания постов от событий и наоборот
@@ -2563,6 +2624,193 @@ document.addEventListener('DOMContentLoaded', () => {
adjustPanelWidthsOnResize(); adjustPanelWidthsOnResize();
}, 100); // Задержка 100ms }, 100); // Задержка 100ms
}); });
// Функции для работы с площадками
let selectedVenueName = '';
let selectedVenueEvents = [];
let currentPostId = null;
// Обработчик клика по кнопке "Площадки"
document.getElementById('post-venues-btn').addEventListener('click', async () => {
if (!selectedPostId) {
alert('Сначала выберите пост');
return;
}
currentPostId = selectedPostId;
await loadVenues();
const venuesModal = new bootstrap.Modal(document.getElementById('venuesModal'));
venuesModal.show();
});
// Загрузка списка площадок
async function loadVenues() {
try {
const response = await fetch(`${PREFIX}/api/venues`);
const venues = await response.json();
const venuesList = document.getElementById('venues-list');
venuesList.innerHTML = '';
if (venues.length === 0) {
venuesList.innerHTML = '<div class="list-group-item text-center text-muted">Площадки не найдены</div>';
return;
}
venues.forEach(venue => {
const venueItem = document.createElement('div');
venueItem.className = 'list-group-item list-group-item-action';
venueItem.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${venue.name}</h6>
<small class="text-muted">${venue.event_count} событий</small>
</div>
`;
venueItem.addEventListener('click', () => {
selectedVenueName = venue.name;
loadVenueEvents(venue.name);
});
venuesList.appendChild(venueItem);
});
} catch (error) {
console.error('Ошибка загрузки площадок:', error);
alert('Ошибка загрузки площадок');
}
}
// Загрузка событий выбранной площадки
async function loadVenueEvents(venueName) {
try {
const response = await fetch(`${PREFIX}/api/venues/${encodeURIComponent(venueName)}/events?post_id=${currentPostId}`);
const events = await response.json();
selectedVenueEvents = events;
// Закрываем модальное окно площадок
const venuesModal = bootstrap.Modal.getInstance(document.getElementById('venuesModal'));
venuesModal.hide();
// Показываем модальное окно событий
document.getElementById('selected-venue-name').textContent = venueName;
renderEventsList(events);
const eventsModal = new bootstrap.Modal(document.getElementById('eventsModal'));
eventsModal.show();
} catch (error) {
console.error('Ошибка загрузки событий площадки:', error);
alert('Ошибка загрузки событий площадки');
}
}
// Отображение списка событий с чекбоксами
function renderEventsList(events) {
const eventsList = document.getElementById('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="event-${event.id}" ${event.linked ? 'checked' : ''}>
<label class="form-check-label" for="event-${event.id}">
<strong>${event.name}</strong>
${event.number ? `<br><small class="text-muted">№ ${event.number}</small>` : ''}
</label>
</div>
`;
eventsList.appendChild(eventItem);
});
}
// Обработчики кнопок "Выбрать все" и "Снять все"
document.getElementById('select-all-events').addEventListener('click', () => {
document.querySelectorAll('#events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
});
});
document.getElementById('deselect-all-events').addEventListener('click', () => {
document.querySelectorAll('#events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
});
// Обработчик кнопки "ОК" для подтверждения привязки/отвязки событий
document.getElementById('confirm-events-link').addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('#events-list input[type="checkbox"]');
const promises = [];
for (const checkbox of checkboxes) {
const eventId = parseInt(checkbox.value);
const isChecked = checkbox.checked;
const wasLinked = selectedVenueEvents.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 eventsModal = bootstrap.Modal.getInstance(document.getElementById('eventsModal'));
eventsModal.hide();
// Обновляем информацию о связях
updateInterlinkInfo();
} catch (error) {
console.error('Ошибка при сохранении изменений:', error);
alert('Ошибка при сохранении изменений');
}
});
// Функция привязки события к посту
async function linkEventToPost(postId, eventId) {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
post_id: postId,
event_id: eventId
})
});
if (!response.ok) {
throw new Error('Ошибка привязки события к посту');
}
}
// Функция отвязки события от поста
async function unlinkEventFromPost(postId, eventId) {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
post_id: postId,
event_id: eventId
})
});
if (!response.ok) {
throw new Error('Ошибка отвязки события от поста');
}
}
}); });
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML // Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
window.resetLayout = resetLayout; window.resetLayout = resetLayout;