// Основное приложение для управления тестами class TestManager { constructor() { this.currentPanel = 'tests'; this.categories = []; this.tests = []; this.questions = []; this.selectedTestId = null; this.init(); } async init() { this.setupEventListeners(); await this.loadInitialData(); this.showPanel('tests'); } setupEventListeners() { // Навигация document.getElementById('nav-tests').addEventListener('click', (e) => { e.preventDefault(); this.showPanel('tests'); }); document.getElementById('nav-categories').addEventListener('click', (e) => { e.preventDefault(); this.showPanel('categories'); }); document.getElementById('nav-questions').addEventListener('click', (e) => { e.preventDefault(); this.showPanel('questions'); }); // Кнопки добавления document.getElementById('add-test-btn').addEventListener('click', () => { this.showTestModal(); }); document.getElementById('add-category-btn').addEventListener('click', () => { this.showCategoryModal(); }); document.getElementById('add-question-btn').addEventListener('click', () => { this.showQuestionModal(); }); // Кнопки сохранения document.getElementById('saveTestBtn').addEventListener('click', () => { this.saveTest(); }); document.getElementById('saveCategoryBtn').addEventListener('click', () => { this.saveCategory(); }); document.getElementById('saveQuestionBtn').addEventListener('click', () => { this.saveQuestion(); }); // Фильтры document.getElementById('category-filter').addEventListener('change', () => { this.loadTests(); }); document.getElementById('status-filter').addEventListener('change', () => { this.loadTests(); }); document.getElementById('search-input').addEventListener('input', () => { this.filterTests(); }); // Выбор теста для вопросов document.getElementById('test-select').addEventListener('change', (e) => { this.selectedTestId = e.target.value; if (this.selectedTestId) { this.loadQuestions(this.selectedTestId); document.getElementById('add-question-btn').disabled = false; } else { document.getElementById('add-question-btn').disabled = true; this.showQuestionsEmptyState(); } }); // Подтверждение удаления document.getElementById('confirmDeleteBtn').addEventListener('click', () => { this.executeDelete(); }); // Работа с вариантами ответов document.getElementById('addOptionBtn').addEventListener('click', () => { this.addAnswerOption(); }); // Загрузка изображения document.getElementById('questionImage').addEventListener('change', (e) => { this.previewImage(e.target.files[0]); }); document.getElementById('removeImageBtn').addEventListener('click', () => { this.removeImage(); }); // Тип вопроса document.getElementById('questionType').addEventListener('change', (e) => { this.toggleAnswerOptions(e.target.value); }); } // === Управление панелями === showPanel(panelName) { // Скрываем все панели document.querySelectorAll('.content-panel').forEach(panel => { panel.style.display = 'none'; }); // Показываем выбранную панель document.getElementById(`${panelName}-panel`).style.display = 'block'; // Обновляем навигацию document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); }); document.getElementById(`nav-${panelName}`).classList.add('active'); this.currentPanel = panelName; // Загружаем данные для панели this.loadPanelData(panelName); } async loadPanelData(panelName) { switch (panelName) { case 'tests': await this.loadTests(); break; case 'categories': await this.loadCategories(); break; case 'questions': await this.loadTestsForSelect(); this.showQuestionsEmptyState(); break; } } // === Загрузка начальных данных === async loadInitialData() { try { showLoading(true); // Проверяем подключение к API await api.checkHealth(); // Загружаем категории для фильтров this.categories = await api.getCategories(); this.populateCategoryFilters(); } catch (error) { showApiError(error, 'Ошибка при загрузке начальных данных'); } finally { showLoading(false); } } populateCategoryFilters() { const categoryFilter = document.getElementById('category-filter'); const testCategorySelect = document.getElementById('testCategory'); // Очищаем существующие опции (кроме первой) while (categoryFilter.children.length > 1) { categoryFilter.removeChild(categoryFilter.lastChild); } while (testCategorySelect.children.length > 1) { testCategorySelect.removeChild(testCategorySelect.lastChild); } // Добавляем категории this.categories.forEach(category => { const option1 = new Option(category.name, category.id); const option2 = new Option(category.name, category.id); categoryFilter.appendChild(option1); testCategorySelect.appendChild(option2); }); } // === Управление тестами === async loadTests() { try { const categoryId = document.getElementById('category-filter').value; const isActive = document.getElementById('status-filter').value; const params = {}; if (categoryId) params.category_id = categoryId; if (isActive !== '') params.is_active = isActive === 'true'; this.tests = await api.getTests(params); this.renderTestsTable(); } catch (error) { showApiError(error, 'Ошибка при загрузке тестов'); } } renderTestsTable() { const tbody = document.getElementById('tests-table-body'); tbody.innerHTML = ''; if (this.tests.length === 0) { tbody.innerHTML = ` Тесты не найдены `; return; } this.tests.forEach(test => { const category = this.categories.find(c => c.id === test.category_id); const categoryName = category ? category.name : 'Без категории'; const row = document.createElement('tr'); row.innerHTML = ` ${test.id} ${test.title} ${test.description ? `
${test.description}` : ''} ${categoryName} ${test.is_active ? 'Активный' : 'Неактивный'} ${test.time_limit_minutes || '-'} 0 ${formatDate(test.created_at)}
`; tbody.appendChild(row); // Загружаем количество вопросов для каждого теста this.loadQuestionsCount(test.id); }); } async loadQuestionsCount(testId) { try { const questions = await api.getQuestionsByTest(testId); const countElement = document.getElementById(`questions-count-${testId}`); if (countElement) { countElement.textContent = questions.length; } } catch (error) { console.error('Error loading questions count:', error); } } filterTests() { const searchTerm = document.getElementById('search-input').value.toLowerCase(); const rows = document.querySelectorAll('#tests-table-body tr'); rows.forEach(row => { const title = row.cells[1]?.textContent.toLowerCase() || ''; const visible = title.includes(searchTerm); row.style.display = visible ? '' : 'none'; }); } showTestModal(test = null) { const modal = new bootstrap.Modal(document.getElementById('testModal')); const title = document.getElementById('testModalTitle'); if (test) { title.textContent = 'Редактировать тест'; this.fillTestForm(test); } else { title.textContent = 'Добавить тест'; clearForm('testForm'); } modal.show(); } fillTestForm(test) { document.getElementById('testId').value = test.id; document.getElementById('testTitle').value = test.title; document.getElementById('testDescription').value = test.description || ''; document.getElementById('testCategory').value = test.category_id || ''; document.getElementById('testTimeLimit').value = test.time_limit_minutes || ''; document.getElementById('testIsActive').checked = test.is_active; } async saveTest() { if (!validateForm('testForm')) { showNotification('Заполните все обязательные поля', 'warning'); return; } const testId = document.getElementById('testId').value; const testData = { title: document.getElementById('testTitle').value.trim(), description: document.getElementById('testDescription').value.trim() || null, category_id: document.getElementById('testCategory').value || null, time_limit_minutes: document.getElementById('testTimeLimit').value || null, is_active: document.getElementById('testIsActive').checked }; try { showLoading(true); if (testId) { await api.updateTest(testId, testData); showApiSuccess('Тест успешно обновлен'); } else { await api.createTest(testData); showApiSuccess('Тест успешно создан'); } const modal = bootstrap.Modal.getInstance(document.getElementById('testModal')); modal.hide(); await this.loadTests(); } catch (error) { showApiError(error, 'Ошибка при сохранении теста'); } finally { showLoading(false); } } async editTest(testId) { try { const test = await api.getTestById(testId); this.showTestModal(test); } catch (error) { showApiError(error, 'Ошибка при загрузке теста'); } } confirmDeleteTest(testId) { this.showDeleteConfirmation( 'test', testId, 'Вы уверены, что хотите удалить этот тест? Все связанные вопросы и ответы также будут удалены.' ); } async deleteTest(testId) { try { await api.deleteTest(testId); showApiSuccess('Тест успешно удален'); await this.loadTests(); } catch (error) { showApiError(error, 'Ошибка при удалении теста'); } } viewQuestions(testId) { this.selectedTestId = testId; document.getElementById('test-select').value = testId; this.showPanel('questions'); this.loadQuestions(testId); document.getElementById('add-question-btn').disabled = false; } // === Управление категориями === async loadCategories() { try { this.categories = await api.getCategories(); this.renderCategoriesTable(); this.populateCategoryFilters(); } catch (error) { showApiError(error, 'Ошибка при загрузке категорий'); } } renderCategoriesTable() { const tbody = document.getElementById('categories-table-body'); tbody.innerHTML = ''; if (this.categories.length === 0) { tbody.innerHTML = ` Категории не найдены `; return; } this.categories.forEach(category => { const row = document.createElement('tr'); row.innerHTML = ` ${category.id} ${category.name} ${category.description || '-'} 0 ${formatDate(category.created_at)}
`; tbody.appendChild(row); // Подсчитываем количество тестов в категории this.loadCategoryTestsCount(category.id); }); } loadCategoryTestsCount(categoryId) { const testsCount = this.tests.filter(test => test.category_id === categoryId).length; const countElement = document.getElementById(`category-tests-count-${categoryId}`); if (countElement) { countElement.textContent = testsCount; } } showCategoryModal(category = null) { const modal = new bootstrap.Modal(document.getElementById('categoryModal')); const title = document.getElementById('categoryModalTitle'); if (category) { title.textContent = 'Редактировать категорию'; this.fillCategoryForm(category); } else { title.textContent = 'Добавить категорию'; clearForm('categoryForm'); } modal.show(); } fillCategoryForm(category) { document.getElementById('categoryId').value = category.id; document.getElementById('categoryName').value = category.name; document.getElementById('categoryDescription').value = category.description || ''; } async saveCategory() { if (!validateForm('categoryForm')) { showNotification('Заполните все обязательные поля', 'warning'); return; } const categoryId = document.getElementById('categoryId').value; const categoryData = { name: document.getElementById('categoryName').value.trim(), description: document.getElementById('categoryDescription').value.trim() || null }; try { showLoading(true); if (categoryId) { await api.updateCategory(categoryId, categoryData); showApiSuccess('Категория успешно обновлена'); } else { await api.createCategory(categoryData); showApiSuccess('Категория успешно создана'); } const modal = bootstrap.Modal.getInstance(document.getElementById('categoryModal')); modal.hide(); await this.loadCategories(); } catch (error) { showApiError(error, 'Ошибка при сохранении категории'); } finally { showLoading(false); } } async editCategory(categoryId) { try { const category = await api.getCategoryById(categoryId); this.showCategoryModal(category); } catch (error) { showApiError(error, 'Ошибка при загрузке категории'); } } confirmDeleteCategory(categoryId) { this.showDeleteConfirmation( 'category', categoryId, 'Вы уверены, что хотите удалить эту категорию? Все тесты в ней останутся без категории.' ); } async deleteCategory(categoryId) { try { await api.deleteCategory(categoryId); showApiSuccess('Категория успешно удалена'); await this.loadCategories(); } catch (error) { showApiError(error, 'Ошибка при удалении категории'); } } // === Управление вопросами === async loadTestsForSelect() { try { const tests = await api.getTests({ is_active: true }); const select = document.getElementById('test-select'); // Очищаем существующие опции (кроме первой) while (select.children.length > 1) { select.removeChild(select.lastChild); } tests.forEach(test => { const option = new Option(test.title, test.id); select.appendChild(option); }); } catch (error) { showApiError(error, 'Ошибка при загрузке списка тестов'); } } async loadQuestions(testId) { try { this.questions = await api.getQuestionsByTest(testId); this.renderQuestions(); } catch (error) { showApiError(error, 'Ошибка при загрузке вопросов'); } } renderQuestions() { const container = document.getElementById('questions-content'); if (this.questions.length === 0) { container.innerHTML = `
В этом тесте пока нет вопросов. Добавьте первый вопрос.
`; return; } container.innerHTML = ''; this.questions .sort((a, b) => a.order_number - b.order_number) .forEach(async (question, index) => { const questionDiv = document.createElement('div'); questionDiv.className = 'question-item'; questionDiv.innerHTML = `
${question.order_number} ${this.getQuestionTypeLabel(question.question_type)} ${question.points} балл${question.points === 1 ? '' : question.points < 5 ? 'а' : 'ов'}
${question.question_text}
${question.image_id ? `Изображение к вопросу` : ''}
Загрузка вариантов ответов...
`; container.appendChild(questionDiv); // Загружаем варианты ответов this.loadQuestionOptions(question.id); }); } async loadQuestionOptions(questionId) { try { const options = await api.getAnswerOptionsByQuestion(questionId); const container = document.getElementById(`options-${questionId}`); if (options.length === 0) { container.innerHTML = '
Нет вариантов ответов
'; return; } container.innerHTML = ''; options .sort((a, b) => a.order_number - b.order_number) .forEach(option => { const optionDiv = document.createElement('div'); optionDiv.className = `option-item ${option.is_correct ? 'correct-option' : 'incorrect-option'}`; optionDiv.innerHTML = ` ${option.option_text} `; container.appendChild(optionDiv); }); } catch (error) { console.error('Error loading question options:', error); const container = document.getElementById(`options-${questionId}`); container.innerHTML = '
Ошибка загрузки вариантов
'; } } getQuestionTypeLabel(type) { const labels = { 'single_choice': 'Один ответ', 'multiple_choice': 'Несколько ответов', 'text_input': 'Текстовый ввод' }; return labels[type] || type; } showQuestionsEmptyState() { document.getElementById('questions-content').innerHTML = `
Выберите тест для просмотра и редактирования вопросов
`; } showQuestionModal(question = null) { const modal = new bootstrap.Modal(document.getElementById('questionModal')); const title = document.getElementById('questionModalTitle'); if (question) { title.textContent = 'Редактировать вопрос'; this.fillQuestionForm(question); } else { title.textContent = 'Добавить вопрос'; clearForm('questionForm'); document.getElementById('questionTestId').value = this.selectedTestId; // Устанавливаем следующий порядковый номер const nextOrder = Math.max(...this.questions.map(q => q.order_number), 0) + 1; document.getElementById('questionOrder').value = nextOrder; this.resetAnswerOptions(); } this.toggleAnswerOptions(document.getElementById('questionType').value); modal.show(); } fillQuestionForm(question) { document.getElementById('questionId').value = question.id; document.getElementById('questionTestId').value = question.test_id; document.getElementById('questionText').value = question.question_text; document.getElementById('questionType').value = question.question_type; document.getElementById('questionOrder').value = question.order_number; document.getElementById('questionPoints').value = question.points; if (question.image_id) { document.getElementById('currentImage').style.display = 'block'; document.getElementById('imagePreview').src = getImageUrl(question.image_id); document.getElementById('imagePreview').dataset.imageId = question.image_id; } else { document.getElementById('currentImage').style.display = 'none'; } // Загружаем варианты ответов this.loadQuestionOptionsForEdit(question.id); } async loadQuestionOptionsForEdit(questionId) { try { const options = await api.getAnswerOptionsByQuestion(questionId); this.resetAnswerOptions(); options .sort((a, b) => a.order_number - b.order_number) .forEach(option => { this.addAnswerOption(option); }); } catch (error) { console.error('Error loading options for edit:', error); } } toggleAnswerOptions(questionType) { const optionsContainer = document.getElementById('answerOptions'); if (questionType === 'text_input') { optionsContainer.style.display = 'none'; } else { optionsContainer.style.display = 'block'; // Если нет вариантов, добавляем первый if (document.getElementById('optionsList').children.length === 0) { this.addAnswerOption(); } } } resetAnswerOptions() { document.getElementById('optionsList').innerHTML = ''; document.getElementById('currentImage').style.display = 'none'; document.getElementById('questionImage').value = ''; } addAnswerOption(optionData = null) { const optionsList = document.getElementById('optionsList'); const optionIndex = optionsList.children.length; const optionDiv = document.createElement('div'); optionDiv.className = 'answer-option'; optionDiv.innerHTML = `
`; optionsList.appendChild(optionDiv); } previewImage(file) { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { document.getElementById('currentImage').style.display = 'block'; document.getElementById('imagePreview').src = e.target.result; document.getElementById('imagePreview').dataset.imageId = ''; }; reader.readAsDataURL(file); } removeImage() { document.getElementById('currentImage').style.display = 'none'; document.getElementById('questionImage').value = ''; document.getElementById('imagePreview').dataset.imageId = ''; } async saveQuestion() { if (!validateForm('questionForm')) { showNotification('Заполните все обязательные поля', 'warning'); return; } const questionId = document.getElementById('questionId').value; const imageFile = document.getElementById('questionImage').files[0]; let imageId = document.getElementById('imagePreview').dataset.imageId || null; try { showLoading(true); // Загружаем изображение, если выбрано новое if (imageFile) { const imageResult = await api.uploadImage(imageFile); imageId = imageResult.id; } const questionData = { test_id: parseInt(document.getElementById('questionTestId').value), question_text: document.getElementById('questionText').value.trim(), question_type: document.getElementById('questionType').value, order_number: parseInt(document.getElementById('questionOrder').value), points: parseInt(document.getElementById('questionPoints').value), image_id: imageId ? parseInt(imageId) : null }; let savedQuestion; if (questionId) { savedQuestion = await api.updateQuestion(questionId, questionData); showApiSuccess('Вопрос успешно обновлен'); } else { savedQuestion = await api.createQuestion(questionData); showApiSuccess('Вопрос успешно создан'); } // Сохраняем варианты ответов if (questionData.question_type !== 'text_input') { await this.saveAnswerOptions(savedQuestion.id || questionId); } const modal = bootstrap.Modal.getInstance(document.getElementById('questionModal')); modal.hide(); await this.loadQuestions(this.selectedTestId); } catch (error) { showApiError(error, 'Ошибка при сохранении вопроса'); } finally { showLoading(false); } } async saveAnswerOptions(questionId) { const optionElements = document.querySelectorAll('#optionsList .answer-option'); for (let i = 0; i < optionElements.length; i++) { const element = optionElements[i]; const optionId = element.querySelector('.option-id').value; const optionText = element.querySelector('.option-text').value.trim(); const isCorrect = element.querySelector(`#option-correct-${i}`).checked; if (!optionText) continue; const optionData = { question_id: questionId, option_text: optionText, is_correct: isCorrect, order_number: i + 1 }; try { if (optionId) { await api.updateAnswerOption(optionId, optionData); } else { await api.createAnswerOption(optionData); } } catch (error) { console.error('Error saving answer option:', error); } } } async editQuestion(questionId) { try { const question = await api.getQuestionById(questionId); this.showQuestionModal(question); } catch (error) { showApiError(error, 'Ошибка при загрузке вопроса'); } } confirmDeleteQuestion(questionId) { this.showDeleteConfirmation( 'question', questionId, 'Вы уверены, что хотите удалить этот вопрос? Все варианты ответов также будут удалены.' ); } async deleteQuestion(questionId) { try { await api.deleteQuestion(questionId); showApiSuccess('Вопрос успешно удален'); await this.loadQuestions(this.selectedTestId); } catch (error) { showApiError(error, 'Ошибка при удалении вопроса'); } } // === Подтверждение удаления === showDeleteConfirmation(type, id, message) { this.deleteType = type; this.deleteId = id; document.getElementById('deleteConfirmText').textContent = message; const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal')); modal.show(); } async executeDelete() { try { showLoading(true); switch (this.deleteType) { case 'test': await this.deleteTest(this.deleteId); break; case 'category': await this.deleteCategory(this.deleteId); break; case 'question': await this.deleteQuestion(this.deleteId); break; } const modal = bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal')); modal.hide(); } catch (error) { showApiError(error, 'Ошибка при удалении'); } finally { showLoading(false); } } } // Создаем глобальный экземпляр менеджера тестов let testManager; // Инициализация при загрузке страницы document.addEventListener('DOMContentLoaded', () => { testManager = new TestManager(); }); // Экспорт для использования в HTML window.testManager = testManager;