add algam

This commit is contained in:
2025-09-22 11:54:42 +05:00
parent 3c418a97b4
commit 5b9c55a4dc
34 changed files with 4700 additions and 426 deletions

View File

@@ -0,0 +1,304 @@
// API конфигурация
const API_BASE_URL = 'http://localhost:8000/api/v1';
// Класс для работы с API
class API {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
// Общий метод для HTTP запросов
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
// Для некоторых запросов может быть пустой ответ
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return response;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// GET запрос
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url);
}
// POST запрос
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
// PUT запрос
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
// DELETE запрос
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
// Загрузка файла
async uploadFile(endpoint, formData) {
return this.request(endpoint, {
method: 'POST',
body: formData,
headers: {} // Убираем Content-Type для FormData
});
}
// === Методы для работы с тестами ===
async getTests(params = {}) {
return this.get('/tests', params);
}
async getTestById(id) {
return this.get(`/tests/${id}`);
}
async createTest(testData) {
return this.post('/tests', testData);
}
async updateTest(id, testData) {
return this.put(`/tests/${id}`, testData);
}
async deleteTest(id) {
return this.delete(`/tests/${id}`);
}
// === Методы для работы с категориями ===
async getCategories() {
return this.get('/test-categories');
}
async getCategoryById(id) {
return this.get(`/test-categories/${id}`);
}
async createCategory(categoryData) {
return this.post('/test-categories', categoryData);
}
async updateCategory(id, categoryData) {
return this.put(`/test-categories/${id}`, categoryData);
}
async deleteCategory(id) {
return this.delete(`/test-categories/${id}`);
}
// === Методы для работы с вопросами ===
async getQuestionsByTest(testId) {
return this.get(`/questions/test/${testId}`);
}
async getQuestionById(id) {
return this.get(`/questions/${id}`);
}
async createQuestion(questionData) {
return this.post('/questions', questionData);
}
async updateQuestion(id, questionData) {
return this.put(`/questions/${id}`, questionData);
}
async deleteQuestion(id) {
return this.delete(`/questions/${id}`);
}
// === Методы для работы с вариантами ответов ===
async getAnswerOptionsByQuestion(questionId) {
return this.get(`/answer-options/question/${questionId}`);
}
async createAnswerOption(optionData) {
return this.post('/answer-options', optionData);
}
async updateAnswerOption(id, optionData) {
return this.put(`/answer-options/${id}`, optionData);
}
async deleteAnswerOption(id) {
return this.delete(`/answer-options/${id}`);
}
// === Методы для работы с изображениями ===
async uploadImage(file, altText = null) {
const formData = new FormData();
formData.append('file', file);
if (altText) {
formData.append('alt_text', altText);
}
return this.uploadFile('/images/upload', formData);
}
async getImage(imageId) {
return this.request(`/images/${imageId}`, {
headers: {}
});
}
async deleteImage(id) {
return this.delete(`/images/${id}`);
}
// === Служебные методы ===
async checkHealth() {
return this.get('/health');
}
}
// Создаем глобальный экземпляр API
const api = new API(API_BASE_URL);
// Утилиты для работы с API
// Показать уведомление об ошибке
function showApiError(error, customMessage = null) {
const message = customMessage || error.message || 'Произошла ошибка при обращении к API';
showNotification(message, 'danger');
console.error('API Error:', error);
}
// Показать уведомление об успехе
function showApiSuccess(message) {
showNotification(message, 'success');
}
// Показать загрузку
function showLoading(show = true) {
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.style.display = show ? 'block' : 'none';
}
}
// Функция для показа уведомлений
function showNotification(message, type = 'info', duration = 5000) {
const notificationsContainer = document.getElementById('notifications');
if (!notificationsContainer) return;
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
notificationsContainer.appendChild(alertDiv);
// Автоматически удаляем уведомление через заданное время
setTimeout(() => {
if (alertDiv && alertDiv.parentNode) {
alertDiv.remove();
}
}, duration);
}
// Форматирование даты
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Получение URL изображения
function getImageUrl(imageId) {
return `${API_BASE_URL}/images/${imageId}`;
}
// Валидация данных формы
function validateForm(formId) {
const form = document.getElementById(formId);
if (!form) return false;
const requiredFields = form.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
field.classList.remove('is-invalid');
if (!field.value.trim()) {
field.classList.add('is-invalid');
isValid = false;
}
});
return isValid;
}
// Очистка формы
function clearForm(formId) {
const form = document.getElementById(formId);
if (!form) return;
form.reset();
// Удаляем классы валидации
const fields = form.querySelectorAll('.is-invalid');
fields.forEach(field => field.classList.remove('is-invalid'));
// Очищаем скрытые поля
const hiddenFields = form.querySelectorAll('input[type="hidden"]');
hiddenFields.forEach(field => field.value = '');
}
// Экспорт для использования в других скриптах
window.api = api;
window.showApiError = showApiError;
window.showApiSuccess = showApiSuccess;
window.showLoading = showLoading;
window.showNotification = showNotification;
window.formatDate = formatDate;
window.getImageUrl = getImageUrl;
window.validateForm = validateForm;
window.clearForm = clearForm;

View File

@@ -0,0 +1,959 @@
// Основное приложение для управления тестами
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 = `
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
Тесты не найдены
</td>
</tr>
`;
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 = `
<td>${test.id}</td>
<td>
<strong>${test.title}</strong>
${test.description ? `<br><small class="text-muted">${test.description}</small>` : ''}
</td>
<td>
<span class="badge bg-secondary">${categoryName}</span>
</td>
<td>
<span class="badge ${test.is_active ? 'bg-success' : 'bg-secondary'}">
${test.is_active ? 'Активный' : 'Неактивный'}
</span>
</td>
<td>${test.time_limit_minutes || '-'}</td>
<td>
<span class="badge bg-info" id="questions-count-${test.id}">0</span>
</td>
<td>
<small class="text-muted">${formatDate(test.created_at)}</small>
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="testManager.editTest(${test.id})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info" onclick="testManager.viewQuestions(${test.id})">
<i class="bi bi-question-circle"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="testManager.confirmDeleteTest(${test.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
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 = `
<tr>
<td colspan="6" class="text-center text-muted py-4">
<i class="bi bi-tags fs-1 d-block mb-2"></i>
Категории не найдены
</td>
</tr>
`;
return;
}
this.categories.forEach(category => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${category.id}</td>
<td>
<strong>${category.name}</strong>
</td>
<td>
${category.description || '-'}
</td>
<td>
<span class="badge bg-info" id="category-tests-count-${category.id}">0</span>
</td>
<td>
<small class="text-muted">${formatDate(category.created_at)}</small>
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="testManager.editCategory(${category.id})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="testManager.confirmDeleteCategory(${category.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
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 = `
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
В этом тесте пока нет вопросов. Добавьте первый вопрос.
</div>
`;
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 = `
<div class="question-header">
<div>
<span class="badge bg-primary me-2">${question.order_number}</span>
<span class="badge bg-secondary me-2">${this.getQuestionTypeLabel(question.question_type)}</span>
<span class="badge bg-info">${question.points} балл${question.points === 1 ? '' : question.points < 5 ? 'а' : 'ов'}</span>
</div>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="testManager.editQuestion(${question.id})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="testManager.confirmDeleteQuestion(${question.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="question-text">${question.question_text}</div>
${question.image_id ? `<img src="${getImageUrl(question.image_id)}" class="question-image" alt="Изображение к вопросу">` : ''}
<div class="question-options" id="options-${question.id}">
<div class="text-muted">Загрузка вариантов ответов...</div>
</div>
`;
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 = '<div class="text-muted"><i>Нет вариантов ответов</i></div>';
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 = `
<i class="bi bi-${option.is_correct ? 'check-circle-fill text-success' : 'circle'} me-2"></i>
${option.option_text}
`;
container.appendChild(optionDiv);
});
} catch (error) {
console.error('Error loading question options:', error);
const container = document.getElementById(`options-${questionId}`);
container.innerHTML = '<div class="text-danger"><i>Ошибка загрузки вариантов</i></div>';
}
}
getQuestionTypeLabel(type) {
const labels = {
'single_choice': 'Один ответ',
'multiple_choice': 'Несколько ответов',
'text_input': 'Текстовый ввод'
};
return labels[type] || type;
}
showQuestionsEmptyState() {
document.getElementById('questions-content').innerHTML = `
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Выберите тест для просмотра и редактирования вопросов
</div>
`;
}
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 = `
<div class="option-controls">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="option-correct-${optionIndex}" ${optionData?.is_correct ? 'checked' : ''}>
<label class="form-check-label" for="option-correct-${optionIndex}">
Правильный ответ
</label>
</div>
<input type="text" class="form-control option-text" placeholder="Текст варианта ответа" value="${optionData?.option_text || ''}" required>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.parentElement.parentElement.remove()">
<i class="bi bi-trash"></i>
</button>
</div>
<input type="hidden" class="option-id" value="${optionData?.id || ''}">
`;
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;