групповая привязка событий к постам по площадкам
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:
|
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():
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user