init Questionnaire

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-03-11 23:50:50 +03:00
parent 41dbe81001
commit 1fcc5ed70d
15 changed files with 5085 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,294 @@
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
$(document).ready(function() {
const adminLink = window.location.pathname.split('/').pop();
let questionnaireData = null;
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (admin/:adminLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
};
// Загрузка данных опроса
const loadQuestionnaire = () => {
$.ajax({
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
method: 'GET',
success: function(result) {
if (result.success) {
questionnaireData = result.data;
renderQuestionnaire();
} else {
$('#loading').text(`Ошибка: ${result.error}`);
}
},
error: function(error) {
console.error('Error loading questionnaire:', error);
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
}
});
};
// Отображение данных опроса
const renderQuestionnaire = () => {
// Заполняем основные данные
$('#questionnaire-title').text(questionnaireData.title);
$('#questionnaire-description').text(questionnaireData.description || 'Нет описания');
// Формируем ссылки
const baseUrl = window.location.origin;
const baseQuestionnairePath = window.location.pathname.split('/admin')[0];
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
$('#public-link').val(publicUrl);
$('#admin-link').val(adminUrl);
// Отображаем статистику
renderStats(questionnaireData.questions);
// Показываем контейнер с данными
$('#loading').hide();
$('#questionnaire-container').show();
};
// Отображение статистики опроса
const renderStats = (questions) => {
const $statsContainer = $('#stats-container');
$statsContainer.empty();
// Проверяем, есть ли ответы
let hasAnyResponses = false;
// Проверяем наличие ответов для каждого типа вопросов
for (const question of questions) {
if (question.type === 'single' || question.type === 'multiple') {
if (question.options && question.options.some(option => option.votes && option.votes > 0)) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'tagcloud') {
if (question.tags && question.tags.some(tag => tag.count && tag.count > 0)) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'scale' || question.type === 'rating') {
if (question.responses && question.responses.length > 0) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'text') {
if (question.textAnswers && question.textAnswers.length > 0) {
hasAnyResponses = true;
break;
}
}
}
if (!hasAnyResponses) {
$statsContainer.html('<div class="no-stats">Пока нет ответов на опрос</div>');
return;
}
// Для каждого вопроса создаем блок статистики
questions.forEach((question, index) => {
const $questionStats = $('<div>', { class: 'question-stats' });
const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
$questionStats.append($questionTitle);
// В зависимости от типа вопроса отображаем разную статистику
if (question.type === 'single' || question.type === 'multiple') {
// Для вопросов с выбором вариантов
const totalVotes = question.options.reduce((sum, option) => sum + (option.votes || 0), 0);
if (totalVotes === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
} else {
const $table = $('<table>', { class: 'stats-table' });
const $thead = $('<thead>').append(
$('<tr>').append(
$('<th>', { text: 'Вариант' }),
$('<th>', { text: 'Голоса' }),
$('<th>', { text: '%' }),
$('<th>', { text: 'Визуализация' })
)
);
const $tbody = $('<tbody>');
question.options.forEach(option => {
const votes = option.votes || 0;
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
const $tr = $('<tr>').append(
$('<td>', { text: option.text }),
$('<td>', { text: votes }),
$('<td>', { text: `${percent}%` }),
$('<td>').append(
$('<div>', { class: 'bar-container' }).append(
$('<div>', {
class: 'bar',
css: { width: `${percent}%` }
})
)
)
);
$tbody.append($tr);
});
$table.append($thead, $tbody);
$questionStats.append($table);
$questionStats.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
}
} else if (question.type === 'tagcloud') {
// Для облака тегов
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
} else {
const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
// Находим максимальное количество для масштабирования
const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
// Сортируем теги по популярности
const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
sortedTags.forEach(tag => {
if (tag.count && tag.count > 0) {
const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
$tagCloud.append(
$('<span>', {
class: 'tag-item',
text: `${tag.text} (${tag.count})`,
css: { fontSize: `${fontSize}em` }
})
);
}
});
$questionStats.append($tagCloud);
}
} else if (question.type === 'scale' || question.type === 'rating') {
// Для шкалы и рейтинга
if (!question.responses || question.responses.length === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
} else {
const values = question.responses;
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
const $scaleStats = $('<div>', { class: 'scale-stats' });
$scaleStats.append(
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
$('<span>', { class: 'stat-value', text: avg.toFixed(1) })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Минимум:' }),
$('<span>', { class: 'stat-value', text: min })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Максимум:' }),
$('<span>', { class: 'stat-value', text: max })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
$('<span>', { class: 'stat-value', text: values.length })
)
);
$questionStats.append($scaleStats);
}
} else if (question.type === 'text') {
// Для текстовых ответов
if (!question.textAnswers || question.textAnswers.length === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
} else {
const $textAnswers = $('<div>', { class: 'text-answers-list' });
question.textAnswers.forEach((answer, i) => {
$textAnswers.append(
$('<div>', { class: 'text-answer-item' }).append(
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
$('<div>', { class: 'answer-text', text: answer })
)
);
});
$questionStats.append($textAnswers);
}
}
$statsContainer.append($questionStats);
});
};
// Копирование ссылок
$('#copy-public-link').on('click', function() {
$('#public-link').select();
document.execCommand('copy');
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование', null, true);
});
$('#copy-admin-link').on('click', function() {
$('#admin-link').select();
document.execCommand('copy');
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование', null, true);
});
// Отображение QR-кода
$('#show-qr-code').on('click', function() {
const publicUrl = $('#public-link').val();
showQRCodeModal(publicUrl, 'QR-код для голосования');
});
// Редактирование опроса
$('#edit-questionnaire').on('click', function() {
const basePath = window.location.pathname.split('/admin')[0];
window.location.href = `${basePath}/edit/${adminLink}`;
});
// Удаление опроса
$('#delete-questionnaire').on('click', function() {
showConfirm('Вы уверены, что хотите удалить опрос? Все ответы будут удалены безвозвратно.', function(confirmed) {
if (confirmed) {
deleteQuestionnaire();
}
}, 'Удаление опроса');
});
// Функция удаления опроса
const deleteQuestionnaire = () => {
$.ajax({
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
method: 'DELETE',
success: function(result) {
if (result.success) {
showAlert('Опрос успешно удален', 'Удаление опроса', function() {
window.location.href = window.location.pathname.split('/admin')[0];
}, true);
} else {
showAlert(`Ошибка при удалении опроса: ${result.error}`, 'Ошибка');
}
},
error: function(error) {
console.error('Error deleting questionnaire:', error);
showAlert('Не удалось удалить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
}
});
};
// Инициализация
loadQuestionnaire();
// Обновление данных каждые 10 секунд
setInterval(loadQuestionnaire, 10000);
});

View File

@@ -0,0 +1,236 @@
/* global $, document */
// Функция для создания модального окна
function createModal(options) {
// Если модальное окно уже существует, удаляем его
$('.modal-overlay').remove();
// Опции по умолчанию
const defaultOptions = {
title: 'Сообщение',
content: '',
closeText: 'Закрыть',
onClose: null,
showCancel: false,
cancelText: 'Отмена',
confirmText: 'Подтвердить',
onConfirm: null,
onCancel: null,
size: 'normal', // 'normal', 'large', 'small'
customClass: '',
autoClose: false, // Автоматическое закрытие по таймеру
autoCloseTime: 2000 // Время до автоматического закрытия (2 секунды)
};
// Объединяем пользовательские опции с опциями по умолчанию
const settings = $.extend({}, defaultOptions, options);
// Создаем структуру модального окна
const $modalOverlay = $('<div>', { class: 'modal-overlay' });
const $modal = $('<div>', { class: `modal ${settings.customClass}` });
// Устанавливаем ширину в зависимости от размера
if (settings.size === 'large') {
$modal.css('max-width', '700px');
} else if (settings.size === 'small') {
$modal.css('max-width', '400px');
}
// Создаем заголовок
const $modalHeader = $('<div>', { class: 'modal-header' });
const $modalTitle = $('<h3>', { text: settings.title });
const $modalClose = $('<button>', {
class: 'modal-close',
html: '&times;',
click: function() {
closeModal();
if (typeof settings.onClose === 'function') {
settings.onClose();
}
}
});
$modalHeader.append($modalTitle, $modalClose);
// Создаем тело
const $modalBody = $('<div>', { class: 'modal-body' });
if (typeof settings.content === 'string') {
$modalBody.html(settings.content);
} else {
$modalBody.append(settings.content);
}
// Создаем футер
const $modalFooter = $('<div>', { class: 'modal-footer' });
// Если нужно показать кнопку отмены
if (settings.showCancel) {
const $cancelButton = $('<button>', {
class: 'btn btn-secondary',
text: settings.cancelText,
click: function() {
closeModal();
if (typeof settings.onCancel === 'function') {
settings.onCancel();
}
}
});
$modalFooter.append($cancelButton);
}
// Кнопка подтверждения/закрытия
const $confirmButton = $('<button>', {
class: settings.showCancel ? 'btn btn-primary' : 'btn',
text: settings.showCancel ? settings.confirmText : settings.closeText,
click: function() {
closeModal();
if (settings.showCancel && typeof settings.onConfirm === 'function') {
settings.onConfirm();
} else if (!settings.showCancel && typeof settings.onClose === 'function') {
settings.onClose();
}
}
});
$modalFooter.append($confirmButton);
// Добавляем прогресс-бар, если включено автоматическое закрытие
if (settings.autoClose) {
const $progressBar = $('<div>', { class: 'modal-progress' });
$modal.append($progressBar);
}
// Собираем модальное окно
$modal.append($modalHeader, $modalBody, $modalFooter);
$modalOverlay.append($modal);
// Добавляем модальное окно в DOM
$('body').append($modalOverlay);
// Закрытие по клику на фоне
$modalOverlay.on('click', function(e) {
if (e.target === this) {
closeModal();
if (typeof settings.onClose === 'function') {
settings.onClose();
}
}
});
// Функция закрытия модального окна
function closeModal() {
$modalOverlay.removeClass('active');
setTimeout(function() {
$modalOverlay.remove();
}, 300);
}
// Активируем модальное окно
setTimeout(function() {
$modalOverlay.addClass('active');
// Активируем прогресс-бар и запускаем таймер закрытия, если включено автоматическое закрытие
if (settings.autoClose) {
const $progressBar = $modal.find('.modal-progress');
setTimeout(() => {
$progressBar.addClass('active');
}, 50);
setTimeout(() => {
closeModal();
if (typeof settings.onClose === 'function') {
settings.onClose();
}
}, settings.autoCloseTime);
}
}, 10);
// Возвращаем объект модального окна
return {
$modal: $modal,
$overlay: $modalOverlay,
close: closeModal
};
}
// Функция для отображения модального окна с сообщением (замена alert)
function showAlert(message, title, callback, autoClose = false) {
return createModal({
title: title || 'Сообщение',
content: message,
onClose: callback,
autoClose: autoClose,
autoCloseTime: 2000
});
}
// Функция для отображения модального окна с подтверждением (замена confirm)
function showConfirm(message, callback, title) {
return createModal({
title: title || 'Подтверждение',
content: message,
showCancel: true,
onConfirm: function() {
if (typeof callback === 'function') {
callback(true);
}
},
onCancel: function() {
if (typeof callback === 'function') {
callback(false);
}
}
});
}
// Функция для генерации QR-кода
function generateQRCode(data, size) {
const typeNumber = 0; // Автоматическое определение
const errorCorrectionLevel = 'L'; // Низкий уровень коррекции ошибок
const qr = qrcode(typeNumber, errorCorrectionLevel);
qr.addData(data);
qr.make();
return qr.createImgTag(size || 8, 0);
}
// Функция для отображения QR-кода в модальном окне
function showQRCodeModal(url, title) {
const qrCode = generateQRCode(url);
const content = `
<div class="qr-container">
<div class="qr-code">
${qrCode}
</div>
<div class="qr-link-container">
<input type="text" class="qr-link-input" value="${url}" readonly>
<button class="btn btn-copy-link">Копировать</button>
</div>
</div>
`;
const modal = createModal({
title: title || 'QR-код для доступа',
content: content,
size: 'large'
});
// Добавляем обработчик для кнопки копирования
modal.$modal.find('.btn-copy-link').on('click', function() {
const input = modal.$modal.find('.qr-link-input');
input.select();
document.execCommand('copy');
// Показываем уведомление о копировании
const $button = $(this);
const originalText = $button.text();
$button.text('Скопировано!');
$button.addClass('copied');
setTimeout(function() {
$button.text(originalText);
$button.removeClass('copied');
}, 1500);
});
return modal;
}

View File

@@ -0,0 +1,343 @@
/* 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;
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последнюю часть пути (create)
pathParts.pop();
return pathParts.join('/') + '/api';
};
// Добавление нового вопроса
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) {
// Перенаправление на страницу администрирования опроса
const basePath = window.location.pathname.split('/create')[0];
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();
});

View File

@@ -0,0 +1,332 @@
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
$(document).ready(function() {
const form = $('#edit-questionnaire-form');
const questionsList = $('#questions-list');
const addQuestionBtn = $('#add-question');
const adminLink = window.location.pathname.split('/').pop();
let questionCount = 0;
let questionnaireData = null;
// Получаем базовый путь API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (edit/:adminLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
};
// Загрузка данных опроса
const loadQuestionnaire = () => {
$.ajax({
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
method: 'GET',
success: function(result) {
if (result.success) {
questionnaireData = result.data;
fillFormData();
$('#loading').hide();
$('#edit-form-container').show();
} else {
$('#loading').text(`Ошибка: ${result.error}`);
}
},
error: function(error) {
console.error('Error loading questionnaire:', error);
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
}
});
};
// Заполнение формы данными опроса
const fillFormData = () => {
// Заполняем основные данные
$('#title').val(questionnaireData.title);
$('#description').val(questionnaireData.description || '');
$('#display-type').val(questionnaireData.displayType);
// Формируем ссылки
const baseUrl = window.location.origin;
const baseQuestionnairePath = window.location.pathname.split('/edit')[0];
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
$('#public-link').val(publicUrl);
$('#admin-link').val(adminUrl);
// Добавляем вопросы
questionsList.empty();
if (questionnaireData.questions && questionnaireData.questions.length > 0) {
questionnaireData.questions.forEach((question, index) => {
addQuestion(question);
});
} else {
// Если нет вопросов, добавляем пустой
addQuestion();
}
renumberQuestions();
};
// Добавление нового вопроса
addQuestionBtn.on('click', function() {
addQuestion();
renumberQuestions();
});
// Обработка отправки формы
form.on('submit', function(e) {
e.preventDefault();
saveQuestionnaire();
});
// Делегирование событий для динамических элементов
questionsList.on('click', '.delete-question', function() {
// Удаление вопроса
const questionItem = $(this).closest('.question-item');
questionItem.remove();
renumberQuestions();
});
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();
});
// Делегирование для изменения типа вопроса
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);
}
});
// Копирование ссылок
$('#copy-public-link').on('click', function() {
$('#public-link').select();
document.execCommand('copy');
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование');
});
$('#copy-admin-link').on('click', function() {
$('#admin-link').select();
document.execCommand('copy');
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование');
});
// Отображение QR-кода
$('#show-qr-code').on('click', function() {
const publicUrl = $('#public-link').val();
showQRCodeModal(publicUrl, 'QR-код для голосования');
});
// Возврат к админке
$('#back-to-admin').on('click', function(e) {
e.preventDefault();
const basePath = window.location.pathname.split('/edit')[0];
window.location.href = `${basePath}/admin/${adminLink}`;
});
// Функция для добавления нового вопроса
function addQuestion(questionData) {
const template = $('#question-template').html();
const index = questionCount++;
// Заменяем плейсхолдеры в шаблоне
let questionHtml = template
.replace(/\{\{index\}\}/g, index)
.replace(/\{\{number\}\}/g, index + 1);
questionsList.append(questionHtml);
// Если есть данные вопроса - заполняем поля
if (questionData) {
$(`#question-text-${index}`).val(questionData.text);
$(`#question-type-${index}`).val(questionData.type);
if (questionData.required) {
$(`input[name="questions[${index}][required]"]`).prop('checked', true);
}
// Добавляем варианты ответа если они есть
if (questionData.options && questionData.options.length > 0) {
questionData.options.forEach(option => {
addOption(index, option.text);
});
}
// Заполняем настройки шкалы если нужно
if (questionData.scaleMax) {
$(`#scale-max-${index}`).val(questionData.scaleMax);
}
}
// Показываем/скрываем контейнеры в зависимости от типа вопроса
const questionType = $(`#question-type-${index}`).val();
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
$(`#options-container-${index}`).show();
$(`#scale-container-${index}`).hide();
// Если нет вариантов и не загружены данные, добавляем два
if (!questionData && $(`#options-list-${index}`).children().length === 0) {
addOption(index);
addOption(index);
}
} else if (questionType === 'scale') {
$(`#options-container-${index}`).hide();
$(`#scale-container-${index}`).show();
} else {
$(`#options-container-${index}`).hide();
$(`#scale-container-${index}`).hide();
}
}
// Функция для добавления варианта ответа
function addOption(questionIndex, optionText) {
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);
// Если есть текст варианта - устанавливаем его
if (optionText) {
optionsList.children().last().find('input[type="text"]').val(optionText);
}
// Проверяем, видим ли контейнер опций
const optionsContainer = $(`#options-container-${questionIndex}`);
if (optionsContainer.is(':hidden')) {
// Если контейнер скрыт, отключаем required у полей ввода
optionsList.find('input[type="text"]').prop('required', false);
}
}
// Перенумерация вопросов
function renumberQuestions() {
$('.question-item').each(function(index) {
$(this).find('h3').text(`Вопрос ${index + 1}`);
});
}
// Сохранение опроса
function saveQuestionnaire() {
const questionnaire = {
title: $('#title').val(),
description: $('#description').val(),
displayType: $('#display-type').val(),
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.scaleMax = parseInt($(`#scale-max-${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) {
// Сохраняем количество голосов из старых данных
let count = 0;
const optionIndex = $(this).data('index');
if (questionnaireData &&
questionnaireData.questions[index] &&
questionnaireData.questions[index].options &&
questionnaireData.questions[index].options[optionIndex]) {
count = questionnaireData.questions[index].options[optionIndex].count || 0;
}
question.options.push({
text: optionText,
count: count
});
}
});
}
questionnaire.questions.push(question);
});
// Отправка на сервер
$.ajax({
url: `${getApiPath()}/questionnaires/${adminLink}`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify(questionnaire),
success: function(result) {
if (result.success) {
showAlert('Опрос успешно обновлен', 'Успешно', function() {
const basePath = window.location.pathname.split('/edit')[0];
window.location.href = `${basePath}/admin/${adminLink}`;
});
} else {
showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
}
},
error: function(error) {
console.error('Error updating questionnaire:', error);
showAlert('Не удалось обновить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
}
});
}
// Инициализация
loadQuestionnaire();
});

View File

@@ -0,0 +1,67 @@
/* global $, window, document */
$(document).ready(function() {
// Функция для получения базового пути API
const getApiPath = () => {
// Извлекаем базовый путь из URL страницы
const pathParts = window.location.pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
};
// Функция для загрузки списка опросов
const loadQuestionnaires = () => {
$.ajax({
url: getApiPath() + '/questionnaires',
method: 'GET',
success: function(result) {
if (result.success) {
renderQuestionnaires(result.data);
} else {
$('#questionnaires-container').html(`<p class="error">Ошибка: ${result.error}</p>`);
}
},
error: function(error) {
console.error('Error loading questionnaires:', error);
$('#questionnaires-container').html('<p class="error">Не удалось загрузить опросы. Пожалуйста, попробуйте позже.</p>');
}
});
};
// Функция для отображения списка опросов
const renderQuestionnaires = (questionnaires) => {
if (!questionnaires || questionnaires.length === 0) {
$('#questionnaires-container').html('<p>У вас еще нет созданных опросов.</p>');
return;
}
// Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
const basePath = window.location.pathname.endsWith('/')
? window.location.pathname
: window.location.pathname + '/';
const questionnairesHTML = questionnaires.map(q => `
<div class="questionnaire-item">
<h3>${q.title}</h3>
<p>${q.description || 'Нет описания'}</p>
<p>Создан: ${new Date(q.createdAt).toLocaleString()}</p>
<div class="questionnaire-links">
<a href="${basePath}admin/${q.adminLink}" class="btn btn-small">Редактировать</a>
<a href="${basePath}poll/${q.publicLink}" class="btn btn-small btn-primary" target="_blank">Смотреть как участник</a>
</div>
</div>
`).join('');
$('#questionnaires-container').html(questionnairesHTML);
};
// Инициализация страницы
loadQuestionnaires();
// Обновление данных каждые 30 секунд
setInterval(loadQuestionnaires, 30000);
});

File diff suppressed because it is too large Load Diff