групповая привязка событий к постам по площадкам
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+68
@@ -1215,6 +1215,74 @@ def api_get_all_interlinks():
|
||||
finally:
|
||||
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')
|
||||
@login_required
|
||||
def image_proxy():
|
||||
|
||||
@@ -430,6 +430,9 @@
|
||||
<span>Выбранное событие: </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-success ms-2" id="post-venues-btn" style="display: none;">
|
||||
<i class="fas fa-map-marker-alt me-1"></i> Площадки
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -624,6 +627,53 @@
|
||||
</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 class="d-flex align-items-center">
|
||||
@@ -1588,6 +1638,8 @@
|
||||
}
|
||||
// Обновляем информацию о выбранном посте на вкладке событий
|
||||
updateSelectedPostInfo();
|
||||
// Обновляем информацию о выбранном событии на вкладке постов (для показа кнопки "Площадки")
|
||||
updateSelectedEventInfo();
|
||||
}
|
||||
// Показать детали поста
|
||||
async function showPostDetails(postId) {
|
||||
@@ -1865,12 +1917,21 @@
|
||||
function updateSelectedEventInfo() {
|
||||
const eventInfoElement = document.getElementById('post-selected-event-info');
|
||||
const eventNameElement = document.getElementById('post-selected-event-name');
|
||||
const venuesBtn = document.getElementById('post-venues-btn');
|
||||
|
||||
if (selectedEventData) {
|
||||
eventNameElement.textContent = selectedEventData.name || `Событие ${selectedEventData.id}`;
|
||||
eventInfoElement.style.display = 'flex';
|
||||
// Показываем кнопку "Площадки" только если выбран пост
|
||||
if (selectedPostId) {
|
||||
venuesBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
venuesBtn.style.display = 'none';
|
||||
}
|
||||
updateInterlinkInfo();
|
||||
} else {
|
||||
eventInfoElement.style.display = 'none';
|
||||
venuesBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// Добавляем функции для отвязывания постов от событий и наоборот
|
||||
@@ -2563,6 +2624,193 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
adjustPanelWidthsOnResize();
|
||||
}, 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
|
||||
window.resetLayout = resetLayout;
|
||||
|
||||
Reference in New Issue
Block a user