2025-03-11 23:50:50 +03:00
|
|
|
|
/* global $, window, document, alert, showAlert, showConfirm */
|
|
|
|
|
$(document).ready(function() {
|
|
|
|
|
const form = $('#create-questionnaire-form');
|
|
|
|
|
const questionsList = $('#questions-list');
|
|
|
|
|
const addQuestionBtn = $('#add-question');
|
|
|
|
|
|
|
|
|
|
let questionCount = 0;
|
|
|
|
|
|
2025-03-12 00:09:36 +03:00
|
|
|
|
// Функция для получения базового пути API
|
2025-03-11 23:50:50 +03:00
|
|
|
|
const getApiPath = () => {
|
2025-03-12 00:09:36 +03:00
|
|
|
|
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
|
|
|
|
const pathname = window.location.pathname;
|
|
|
|
|
const isMsPath = pathname.includes('/ms/questioneer');
|
|
|
|
|
|
|
|
|
|
if (isMsPath) {
|
|
|
|
|
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
|
|
|
|
return '/ms/questioneer/api';
|
|
|
|
|
} else {
|
2025-03-12 00:37:32 +03:00
|
|
|
|
// Для локальной разработки: формируем путь к API без учета текущей страницы
|
|
|
|
|
// Извлекаем базовый путь из URL страницы до /create
|
|
|
|
|
const basePath = pathname.split('/create')[0];
|
2025-03-12 00:09:36 +03:00
|
|
|
|
|
2025-03-12 00:37:32 +03:00
|
|
|
|
// Путь до API приложения
|
|
|
|
|
return basePath + '/api';
|
2025-03-12 00:09:36 +03:00
|
|
|
|
}
|
2025-03-11 23:50:50 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Добавление нового вопроса
|
|
|
|
|
addQuestionBtn.on('click', function() {
|
|
|
|
|
addQuestion();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Обработка отправки формы
|
|
|
|
|
form.on('submit', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
saveQuestionnaire();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Делегирование событий для динамических элементов
|
|
|
|
|
questionsList.on('click', '.delete-question', function() {
|
|
|
|
|
// Удаление вопроса
|
|
|
|
|
const questionItem = $(this).closest('.question-item');
|
|
|
|
|
showConfirm('Вы уверены, что хотите удалить этот вопрос?', function(confirmed) {
|
|
|
|
|
if (confirmed) {
|
|
|
|
|
questionItem.remove();
|
|
|
|
|
renumberQuestions();
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
questionsList.on('click', '.add-option', function() {
|
|
|
|
|
// Добавление варианта ответа
|
|
|
|
|
const questionIndex = $(this).data('question-index');
|
|
|
|
|
addOption(questionIndex);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
questionsList.on('click', '.delete-option', function() {
|
|
|
|
|
// Удаление варианта ответа
|
|
|
|
|
$(this).closest('.option-item').remove();
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Делегирование для изменения типа вопроса
|
|
|
|
|
questionsList.on('change', '.question-type-select', function() {
|
|
|
|
|
const questionItem = $(this).closest('.question-item');
|
|
|
|
|
const questionIndex = questionItem.data('index');
|
|
|
|
|
const optionsContainer = $(`#options-container-${questionIndex}`);
|
|
|
|
|
const scaleContainer = $(`#scale-container-${questionIndex}`);
|
|
|
|
|
|
|
|
|
|
// Скрыть/показать варианты ответа в зависимости от типа вопроса
|
|
|
|
|
const questionType = $(this).val();
|
|
|
|
|
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
|
|
|
|
optionsContainer.show();
|
|
|
|
|
scaleContainer.hide();
|
|
|
|
|
|
|
|
|
|
// Если нет вариантов, добавляем два
|
|
|
|
|
const optionsList = $(`#options-list-${questionIndex}`);
|
|
|
|
|
if (optionsList.children().length === 0) {
|
|
|
|
|
addOption(questionIndex);
|
|
|
|
|
addOption(questionIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Включаем required для полей ввода вариантов
|
|
|
|
|
optionsList.find('input[type="text"]').prop('required', true);
|
|
|
|
|
} else if (questionType === 'scale') {
|
|
|
|
|
optionsContainer.hide();
|
|
|
|
|
scaleContainer.show();
|
|
|
|
|
// Отключаем required для скрытых полей
|
|
|
|
|
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
|
|
|
|
} else {
|
|
|
|
|
optionsContainer.hide();
|
|
|
|
|
scaleContainer.hide();
|
|
|
|
|
// Отключаем required для скрытых полей
|
|
|
|
|
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Функция для добавления нового вопроса
|
|
|
|
|
function addQuestion() {
|
|
|
|
|
const template = $('#question-template').html();
|
|
|
|
|
const index = questionCount++;
|
|
|
|
|
|
|
|
|
|
// Заменяем плейсхолдеры в шаблоне
|
|
|
|
|
let questionHtml = template
|
|
|
|
|
.replace(/\{\{index\}\}/g, index)
|
|
|
|
|
.replace(/\{\{number\}\}/g, index + 1);
|
|
|
|
|
|
|
|
|
|
questionsList.append(questionHtml);
|
|
|
|
|
|
|
|
|
|
// Показываем/скрываем контейнер вариантов в зависимости от типа вопроса
|
|
|
|
|
const questionType = $(`#question-type-${index}`).val();
|
|
|
|
|
if (!['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
|
|
|
|
$(`#options-container-${index}`).hide();
|
|
|
|
|
// Отключаем required для скрытых полей
|
|
|
|
|
$(`#options-list-${index}`).find('input[type="text"]').prop('required', false);
|
|
|
|
|
} else {
|
|
|
|
|
// Добавляем пару начальных вариантов ответа
|
|
|
|
|
addOption(index);
|
|
|
|
|
addOption(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (questionType === 'scale') {
|
|
|
|
|
$(`#scale-container-${index}`).show();
|
|
|
|
|
} else {
|
|
|
|
|
$(`#scale-container-${index}`).hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Функция для добавления варианта ответа
|
|
|
|
|
function addOption(questionIndex) {
|
|
|
|
|
const optionsList = $(`#options-list-${questionIndex}`);
|
|
|
|
|
const template = $('#option-template').html();
|
|
|
|
|
|
|
|
|
|
const optionIndex = optionsList.children().length;
|
|
|
|
|
|
|
|
|
|
// Заменяем плейсхолдеры в шаблоне
|
|
|
|
|
let optionHtml = template
|
|
|
|
|
.replace(/\{\{questionIndex\}\}/g, questionIndex)
|
|
|
|
|
.replace(/\{\{optionIndex\}\}/g, optionIndex);
|
|
|
|
|
|
|
|
|
|
optionsList.append(optionHtml);
|
|
|
|
|
|
|
|
|
|
// Проверяем, видим ли контейнер опций
|
|
|
|
|
const optionsContainer = $(`#options-container-${questionIndex}`);
|
|
|
|
|
if (optionsContainer.is(':hidden')) {
|
|
|
|
|
// Если контейнер скрыт, отключаем required у полей ввода
|
|
|
|
|
optionsList.find('input[type="text"]').prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Перенумерация вопросов
|
|
|
|
|
function renumberQuestions() {
|
|
|
|
|
$('.question-item').each(function(index) {
|
|
|
|
|
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Функция для обновления нумерации вопросов
|
|
|
|
|
function updateQuestionNumbers() {
|
|
|
|
|
$('.question-item').each(function(index) {
|
|
|
|
|
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Сохранение опроса
|
|
|
|
|
function saveQuestionnaire() {
|
|
|
|
|
const questionnaire = {
|
|
|
|
|
title: $('#title').val(),
|
|
|
|
|
description: $('#description').val(),
|
|
|
|
|
displayType: 'step_by_step', // Всегда устанавливаем пошаговый режим
|
|
|
|
|
questions: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Собираем данные о вопросах
|
|
|
|
|
$('.question-item').each(function() {
|
|
|
|
|
const index = $(this).data('index');
|
|
|
|
|
const questionType = $(`#question-type-${index}`).val();
|
|
|
|
|
|
|
|
|
|
const question = {
|
|
|
|
|
text: $(`#question-text-${index}`).val(),
|
|
|
|
|
type: questionType,
|
|
|
|
|
required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
|
|
|
|
|
options: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Добавляем настройки шкалы если нужно
|
|
|
|
|
if (questionType === 'scale') {
|
|
|
|
|
question.scaleMin = parseInt($(`#scale-min-${index}`).val()) || 0;
|
|
|
|
|
question.scaleMax = parseInt($(`#scale-max-${index}`).val()) || 10;
|
|
|
|
|
question.scaleMinLabel = $(`#scale-min-label-${index}`).val() || 'Минимум';
|
|
|
|
|
question.scaleMaxLabel = $(`#scale-max-label-${index}`).val() || 'Максимум';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Собираем варианты ответа если это не текстовый вопрос или шкала
|
|
|
|
|
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
|
|
|
|
$(`#options-list-${index} .option-item`).each(function() {
|
|
|
|
|
const optionText = $(this).find('input[type="text"]').val();
|
|
|
|
|
|
|
|
|
|
if (optionText) {
|
|
|
|
|
question.options.push({
|
|
|
|
|
text: optionText,
|
|
|
|
|
count: 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
questionnaire.questions.push(question);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Отправка на сервер
|
|
|
|
|
$.ajax({
|
|
|
|
|
url: `${getApiPath()}/questionnaires`,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
contentType: 'application/json',
|
|
|
|
|
data: JSON.stringify(questionnaire),
|
|
|
|
|
success: function(result) {
|
|
|
|
|
if (result.success) {
|
2025-03-12 00:09:36 +03:00
|
|
|
|
// Перенаправляем на страницу администратора
|
|
|
|
|
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
|
|
|
|
let basePath;
|
|
|
|
|
|
|
|
|
|
if (isMsPath) {
|
|
|
|
|
// Для продакшна: используем /ms/questioneer
|
|
|
|
|
basePath = '/ms/questioneer';
|
|
|
|
|
} else {
|
|
|
|
|
// Для локальной разработки: используем текущий путь
|
|
|
|
|
basePath = window.location.pathname.split('/create')[0];
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 23:50:50 +03:00
|
|
|
|
window.location.href = `${basePath}/admin/${result.data.adminLink}`;
|
|
|
|
|
} else {
|
|
|
|
|
showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error: function(error) {
|
|
|
|
|
console.error('Error creating questionnaire:', error);
|
|
|
|
|
showAlert('Не удалось создать опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Функция для обновления атрибута required в зависимости от видимости полей
|
|
|
|
|
function updateRequiredAttributes() {
|
|
|
|
|
// Для полей вопросов
|
|
|
|
|
$('.question-item').each(function() {
|
|
|
|
|
const questionType = $(this).find('.question-type-select').val();
|
|
|
|
|
const textInput = $(this).find('.question-text');
|
|
|
|
|
const optionsContainer = $(this).find('.options-container');
|
|
|
|
|
|
|
|
|
|
// Обновляем required для текстового поля вопроса
|
|
|
|
|
if (textInput.is(':visible')) {
|
|
|
|
|
textInput.prop('required', true);
|
|
|
|
|
} else {
|
|
|
|
|
textInput.prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Обновляем required для полей опций
|
|
|
|
|
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
|
|
|
|
|
optionsContainer.find('input[type="text"]').each(function() {
|
|
|
|
|
if ($(this).is(':visible')) {
|
|
|
|
|
$(this).prop('required', true);
|
|
|
|
|
} else {
|
|
|
|
|
$(this).prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
optionsContainer.find('input[type="text"]').prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Для шкалы оценки
|
|
|
|
|
if (questionType === 'scale') {
|
|
|
|
|
const minInput = $(this).find('.scale-min');
|
|
|
|
|
const maxInput = $(this).find('.scale-max');
|
|
|
|
|
const minLabelInput = $(this).find('.scale-min-label');
|
|
|
|
|
const maxLabelInput = $(this).find('.scale-max-label');
|
|
|
|
|
|
|
|
|
|
if (minInput.is(':visible')) minInput.prop('required', true);
|
|
|
|
|
else minInput.prop('required', false);
|
|
|
|
|
|
|
|
|
|
if (maxInput.is(':visible')) maxInput.prop('required', true);
|
|
|
|
|
else maxInput.prop('required', false);
|
|
|
|
|
|
|
|
|
|
if (minLabelInput.is(':visible')) minLabelInput.prop('required', true);
|
|
|
|
|
else minLabelInput.prop('required', false);
|
|
|
|
|
|
|
|
|
|
if (maxLabelInput.is(':visible')) maxLabelInput.prop('required', true);
|
|
|
|
|
else maxLabelInput.prop('required', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Для основных полей формы
|
|
|
|
|
const titleInput = $('#title');
|
|
|
|
|
const descriptionInput = $('#description');
|
|
|
|
|
|
|
|
|
|
if (titleInput.is(':visible')) titleInput.prop('required', true);
|
|
|
|
|
else titleInput.prop('required', false);
|
|
|
|
|
|
|
|
|
|
if (descriptionInput.is(':visible')) descriptionInput.prop('required', false); // Описание не обязательно
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Инициализация с одним вопросом
|
|
|
|
|
addQuestion();
|
|
|
|
|
|
|
|
|
|
// Обработчик отправки формы
|
|
|
|
|
$('#create-questionnaire-form').on('submit', function(e) {
|
|
|
|
|
// Обновляем атрибуты required перед отправкой
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
|
|
|
|
|
// Проверяем валидность формы
|
|
|
|
|
if (!this.checkValidity()) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
// Находим первый невалидный элемент и прокручиваем к нему
|
|
|
|
|
const firstInvalid = $(this).find(':invalid').first();
|
|
|
|
|
if (firstInvalid.length) {
|
|
|
|
|
$('html, body').animate({
|
|
|
|
|
scrollTop: firstInvalid.offset().top - 100
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
// Добавляем класс ошибки к родительскому элементу вопроса
|
|
|
|
|
firstInvalid.closest('.question-item').addClass('error');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
firstInvalid.closest('.question-item').removeClass('error');
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$(this).addClass('was-validated');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Инициализируем атрибуты required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Обработчик удаления вопроса
|
|
|
|
|
$(document).on('click', '.remove-question', function() {
|
|
|
|
|
$(this).closest('.question-item').remove();
|
|
|
|
|
updateQuestionNumbers();
|
|
|
|
|
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Обработчик удаления опции
|
|
|
|
|
$(document).on('click', '.remove-option', function() {
|
|
|
|
|
$(this).closest('.option-item').remove();
|
|
|
|
|
|
|
|
|
|
// Вызываем функцию обновления атрибутов required
|
|
|
|
|
updateRequiredAttributes();
|
|
|
|
|
});
|