diff --git a/db_edit.py b/db_edit.py index 2a7b8b2..361cd9d 100644 --- a/db_edit.py +++ b/db_edit.py @@ -1166,6 +1166,157 @@ def api_category_details(category_id): finally: db.close() +@bp.route('/api/categories', methods=['POST']) +@login_required +def api_create_category(): + """Создает новую категорию""" + db = get_db() + try: + data = request.json + with db.conn.cursor() as cursor: + cursor.execute(""" + INSERT INTO categories (ID, TITLE, description, image_url, tg_id, marked_for_publication) + VALUES (%s, %s, %s, %s, %s, %s) + """, ( + data.get('id'), + data.get('title', ''), + data.get('description', ''), + data.get('image_url', ''), + data.get('tg_id'), + bool(data.get('marked_for_publication', False)) + )) + db.conn.commit() + client_ip = get_client_ip() + log_event(f"Создана площадка: {data.get('id')}, IP {client_ip}") + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/categories/', methods=['PUT']) +@login_required +def api_update_category(category_id): + """Обновляет категорию""" + db = get_db() + try: + data = request.json + with db.conn.cursor() as cursor: + cursor.execute(""" + UPDATE categories + SET TITLE = %s, description = %s, image_url = %s, tg_id = %s, marked_for_publication = %s + WHERE ID = %s + """, ( + data.get('title', ''), + data.get('description', ''), + data.get('image_url', ''), + data.get('tg_id'), + bool(data.get('marked_for_publication', False)), + category_id + )) + db.conn.commit() + if cursor.rowcount > 0: + client_ip = get_client_ip() + log_event(f"Обновлена площадка: {category_id}, IP {client_ip}") + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': 'Category not found'}), 404 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/categories/', methods=['DELETE']) +@login_required +def api_delete_category(category_id): + """Удаляет категорию""" + db = get_db() + try: + with db.conn.cursor() as cursor: + # Проверяем наличие привязанных событий + cursor.execute(""" + SELECT COUNT(*) as event_count + FROM events + WHERE unit_name = %s + """, (category_id,)) + event_count_result = cursor.fetchone() + event_count = event_count_result['event_count'] if event_count_result else 0 + + if event_count > 0: + return jsonify({ + 'success': False, + 'error': 'Cannot delete category with linked events', + 'event_count': event_count + }), 400 + + # Получаем название категории для лога + cursor.execute("SELECT TITLE FROM categories WHERE ID = %s", (category_id,)) + category = cursor.fetchone() + category_title = category['TITLE'] if category else category_id + + cursor.execute("DELETE FROM categories WHERE ID = %s", (category_id,)) + db.conn.commit() + + if cursor.rowcount > 0: + client_ip = get_client_ip() + log_event(f"Удалена площадка: {category_title}, ID: {category_id}, IP {client_ip}") + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': 'Category not found'}), 404 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/categories//toggle_publication', methods=['POST']) +@login_required +def api_toggle_category_publication(category_id): + """Переключает статус публикации категории""" + db = get_db() + try: + with db.conn.cursor() as cursor: + # Получаем текущее значение + cursor.execute("SELECT marked_for_publication FROM categories WHERE ID = %s", (category_id,)) + category = cursor.fetchone() + if not category: + return jsonify({'success': False, 'error': 'Category not found'}), 404 + + # Переключаем значение + new_value = not bool(category['marked_for_publication']) + cursor.execute(""" + UPDATE categories + SET marked_for_publication = %s + WHERE ID = %s + """, (new_value, category_id)) + db.conn.commit() + + client_ip = get_client_ip() + log_event(f"Изменен статус публикации площадки: {category_id}, новый статус: {new_value}, IP {client_ip}") + return jsonify({'success': True, 'marked_for_publication': new_value}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/categories//events_count') +@login_required +def api_category_events_count(category_id): + """Получает количество событий, привязанных к категории""" + db = get_db() + try: + with db.conn.cursor() as cursor: + cursor.execute(""" + SELECT COUNT(*) as event_count + FROM events + WHERE unit_name = %s + """, (category_id,)) + result = cursor.fetchone() + return jsonify({'count': result['event_count'] if result else 0}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + @bp.route('/api/events/count') @login_required def api_events_count(): @@ -1599,6 +1750,29 @@ def edit_event_page(event_id): def add_event_page(): return render_template('edit_event.html', event_id=None) +@bp.route('/edit_category/') +@login_required +def edit_category_page(category_id): + db = get_db() + try: + with db.conn.cursor() as cursor: + cursor.execute("SELECT ID, TITLE FROM categories WHERE ID = %s", (category_id,)) + category = cursor.fetchone() + if not category: + flash('Площадка не найдена', 'danger') + return redirect(url_for('zilant.index')) + + client_ip = get_client_ip() + log_event(f"Открыта площадка на редактирование: {category['TITLE']}, ID: {category_id}, IP {client_ip}") + finally: + db.close() + return render_template('edit_category.html', category_id=category_id) + +@bp.route('/add_category') +@login_required +def add_category_page(): + return render_template('edit_category.html', category_id=None) + # Регистрируем Blueprint в приложении app.register_blueprint(bp) diff --git a/templates/edit_category.html b/templates/edit_category.html new file mode 100644 index 0000000..dd9d190 --- /dev/null +++ b/templates/edit_category.html @@ -0,0 +1,277 @@ + + + + + + {% if category_id %}Редактирование площадки{% else %}Добавление площадки{% endif %} + + + + + +
+
+

{% if category_id %}Редактирование площадки{% else %}Добавление площадки{% endif %}

+ + Назад + +
+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+ + +
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+
+ + + + + + diff --git a/templates/index.html b/templates/index.html index e90b69f..a424af7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -702,6 +702,7 @@ ID Название Статус + Действия @@ -711,7 +712,7 @@ - - @@ -1872,15 +1902,42 @@ + `; tableBody.innerHTML += row; }); + // Добавляем обработчики событий для кнопок + document.querySelectorAll('.edit-category-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const categoryId = e.target.closest('tr').dataset.id; + window.location.href = `${PREFIX}/edit_category/${encodeURIComponent(categoryId)}`; + }); + }); + document.querySelectorAll('.delete-category-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const categoryId = e.target.closest('tr').dataset.id; + document.getElementById('delete-category-id').value = categoryId; + // Проверяем наличие привязанных событий + checkCategoryEventsBeforeDelete(categoryId); + }); + }); // Клик по строке категории document.querySelectorAll('.category-row').forEach(row => { row.addEventListener('click', (e) => { - const categoryId = row.dataset.id; - showCategoryDetails(categoryId); - highlightSelectedCategory(categoryId); + if (!e.target.closest('.action-buttons')) { + const categoryId = row.dataset.id; + showCategoryDetails(categoryId); + highlightSelectedCategory(categoryId); + } }); }); // Переинициализируем разделители колонок для категорий @@ -1891,7 +1948,7 @@ } catch (error) { tableBody.innerHTML = ` -
+
Загрузка...
@@ -783,6 +784,26 @@ + + +
+

Нет доступных площадок

${category.id || '-'} ${category.title || 'Без названия'} ${publicationBadge}${publishedBadge} + + +
+

Ошибка загрузки данных

+ + +
@@ -1995,6 +2065,14 @@ detailsHtml += `
`; detailsPanel.innerHTML = detailsHtml; + // Обработчик для кнопки переключения публикации категории + const togglePubBtn = document.getElementById('toggle-category-pub-btn'); + if (togglePubBtn) { + togglePubBtn.addEventListener('click', () => { + toggleCategoryPublication(category.id); + }); + } + // Проверяем, загрузилось ли изображение if (category.image_url) { const img = document.getElementById('category-image'); @@ -2028,6 +2106,88 @@ console.error('Ошибка загрузки деталей категории:', error); } } + // Переключение статуса публикации категории + async function toggleCategoryPublication(categoryId) { + try { + const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}/toggle_publication`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }); + const data = await response.json(); + if (data.success) { + // Обновляем детали категории + showCategoryDetails(categoryId); + // Обновляем список категорий + loadCategoriesList(); + } else { + alert('Ошибка изменения статуса публикации: ' + (data.error || 'неизвестная ошибка')); + } + } catch (error) { + console.error('Ошибка переключения статуса публикации:', error); + alert('Произошла ошибка при изменении статуса публикации'); + } + } + // Проверка наличия событий перед удалением категории + async function checkCategoryEventsBeforeDelete(categoryId) { + try { + const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}/events_count`); + const data = await response.json(); + const eventCount = data.count || 0; + + if (eventCount > 0) { + const message = `У площадки есть ${eventCount} привязанных событий. Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.`; + document.getElementById('delete-category-message').textContent = message; + } else { + document.getElementById('delete-category-message').textContent = 'Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.'; + } + + const deleteModal = new bootstrap.Modal(document.getElementById('deleteCategoryModal')); + deleteModal.show(); + } catch (error) { + console.error('Ошибка проверки событий:', error); + // Показываем модальное окно без проверки + document.getElementById('delete-category-message').textContent = 'Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.'; + const deleteModal = new bootstrap.Modal(document.getElementById('deleteCategoryModal')); + deleteModal.show(); + } + } + // Удаление категории + async function deleteCategory(categoryId) { + try { + const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }); + const result = await response.json(); + + if (result.success) { + const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteCategoryModal')); + deleteModal.hide(); + loadCategoriesList(); + // Сбрасываем детали категории + document.getElementById('category-details').innerHTML = ` +
+ +

Выберите площадку для просмотра деталей

+
`; + } else { + if (result.error === 'Cannot delete category with linked events') { + alert(`Невозможно удалить площадку: у неё есть ${result.event_count} привязанных событий.`); + } else { + alert('Ошибка удаления площадки: ' + (result.error || 'неизвестная ошибка')); + } + } + } catch (error) { + console.error('Ошибка удаления площадки:', error); + alert('Произошла ошибка при удалении площадки'); + } + } // Функция загрузки постов async function loadPosts() { const tableBody = document.getElementById('posts-table');