465 lines
18 KiB
JavaScript
465 lines
18 KiB
JavaScript
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
|
||
$(document).ready(function() {
|
||
const adminLink = window.location.pathname.split('/').pop();
|
||
let questionnaireData = null;
|
||
|
||
// Функция для получения базового пути API
|
||
const getApiPath = () => {
|
||
// Проверяем, содержит ли путь /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 {
|
||
// Для локальной разработки: формируем путь к API без учета текущей страницы
|
||
// Извлекаем базовый путь из URL страницы до /admin/[adminLink]
|
||
const basePath = pathname.split('/admin')[0];
|
||
|
||
// Путь до API приложения
|
||
return basePath + '/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 isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||
|
||
let baseQuestionnairePath;
|
||
if (isMsPath) {
|
||
// Для продакшна: используем /ms/questioneer
|
||
baseQuestionnairePath = '/ms/questioneer';
|
||
} else {
|
||
// Для локальной разработки: используем текущий путь
|
||
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) {
|
||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||
const questionType = normalizeQuestionType(question.type);
|
||
|
||
if (questionType === 'single' || questionType === 'multiple') {
|
||
if (question.options && question.options.some(option => (option.votes > 0 || option.count > 0))) {
|
||
hasAnyResponses = true;
|
||
break;
|
||
}
|
||
} else if (questionType === 'tagcloud') {
|
||
if (question.tags && question.tags.some(tag => tag.count > 0)) {
|
||
hasAnyResponses = true;
|
||
break;
|
||
}
|
||
} else if (questionType === 'scale' || questionType === 'rating') {
|
||
// Проверяем оба возможных поля для данных шкалы
|
||
const hasScaleValues = question.scaleValues && question.scaleValues.length > 0;
|
||
const hasResponses = question.responses && question.responses.length > 0;
|
||
if (hasScaleValues || hasResponses) {
|
||
hasAnyResponses = true;
|
||
break;
|
||
}
|
||
} else if (questionType === 'text') {
|
||
// Проверяем оба возможных поля для текстовых ответов
|
||
const hasTextAnswers = question.textAnswers && question.textAnswers.length > 0;
|
||
const hasAnswers = question.answers && question.answers.length > 0;
|
||
if (hasTextAnswers || hasAnswers) {
|
||
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);
|
||
|
||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||
const questionType = normalizeQuestionType(question.type);
|
||
|
||
// В зависимости от типа вопроса отображаем разную статистику
|
||
if (questionType === 'single' || questionType === 'multiple') {
|
||
// Для вопросов с выбором вариантов
|
||
renderChoiceStats(question, $questionStats);
|
||
} else if (questionType === 'tagcloud') {
|
||
// Для облака тегов
|
||
renderTagCloudStats(question, $questionStats);
|
||
} else if (questionType === 'scale' || questionType === 'rating') {
|
||
// Для шкалы и рейтинга
|
||
renderScaleStats(question, $questionStats);
|
||
} else if (questionType === 'text') {
|
||
// Для текстовых ответов
|
||
renderTextStats(question, $questionStats);
|
||
}
|
||
|
||
$statsContainer.append($questionStats);
|
||
});
|
||
};
|
||
|
||
// Приводит тип вопроса к стандартному формату
|
||
const normalizeQuestionType = (type) => {
|
||
const typeMap = {
|
||
'single_choice': 'single',
|
||
'multiple_choice': 'multiple',
|
||
'tag_cloud': 'tagcloud',
|
||
'single': 'single',
|
||
'multiple': 'multiple',
|
||
'tagcloud': 'tagcloud',
|
||
'scale': 'scale',
|
||
'rating': 'rating',
|
||
'text': 'text'
|
||
};
|
||
return typeMap[type] || type;
|
||
};
|
||
|
||
// Отображение статистики для вопросов с выбором
|
||
const renderChoiceStats = (question, $container) => {
|
||
// Преобразуем опции к единому формату
|
||
const options = question.options.map(option => ({
|
||
text: option.text,
|
||
votes: option.votes || option.count || 0
|
||
}));
|
||
|
||
const totalVotes = options.reduce((sum, option) => sum + option.votes, 0);
|
||
|
||
if (totalVotes === 0) {
|
||
$container.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
|
||
return;
|
||
}
|
||
|
||
const $table = $('<table>', { class: 'stats-table' });
|
||
const $thead = $('<thead>').append(
|
||
$('<tr>').append(
|
||
$('<th>', { text: 'Вариант' }),
|
||
$('<th>', { text: 'Голоса' }),
|
||
$('<th>', { text: '%' }),
|
||
$('<th>', { text: 'Визуализация' })
|
||
)
|
||
);
|
||
|
||
const $tbody = $('<tbody>');
|
||
|
||
options.forEach(option => {
|
||
const votes = option.votes;
|
||
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);
|
||
$container.append($table);
|
||
$container.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
|
||
};
|
||
|
||
// Отображение статистики для облака тегов
|
||
const renderTagCloudStats = (question, $container) => {
|
||
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
|
||
$container.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
|
||
return;
|
||
}
|
||
|
||
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` }
|
||
})
|
||
);
|
||
}
|
||
});
|
||
|
||
$container.append($tagCloud);
|
||
};
|
||
|
||
// Отображение статистики для шкалы и рейтинга
|
||
const renderScaleStats = (question, $container) => {
|
||
// Используем scaleValues или responses, в зависимости от того, что доступно
|
||
const values = question.responses && question.responses.length > 0
|
||
? question.responses
|
||
: (question.scaleValues || []);
|
||
|
||
if (values.length === 0) {
|
||
$container.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
|
||
return;
|
||
}
|
||
|
||
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-summary' }).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 })
|
||
)
|
||
)
|
||
);
|
||
|
||
// Создаем таблицу для визуализации распределения голосов
|
||
const $table = $('<table>', { class: 'stats-table' });
|
||
const $thead = $('<thead>').append(
|
||
$('<tr>').append(
|
||
$('<th>', { text: 'Значение' }),
|
||
$('<th>', { text: 'Голоса' }),
|
||
$('<th>', { text: '%' }),
|
||
$('<th>', { text: 'Визуализация' })
|
||
)
|
||
);
|
||
|
||
const $tbody = $('<tbody>');
|
||
|
||
// Определяем минимальное и максимальное значение шкалы из самих данных
|
||
// либо используем значения из настроек вопроса, если они есть
|
||
const scaleMin = question.scaleMin !== undefined ? question.scaleMin : min;
|
||
const scaleMax = question.scaleMax !== undefined ? question.scaleMax : max;
|
||
|
||
// Создаем счетчик для каждого возможного значения шкалы
|
||
const countByValue = {};
|
||
for (let i = scaleMin; i <= scaleMax; i++) {
|
||
countByValue[i] = 0;
|
||
}
|
||
|
||
// Подсчитываем количество голосов для каждого значения
|
||
values.forEach(value => {
|
||
if (countByValue[value] !== undefined) {
|
||
countByValue[value]++;
|
||
}
|
||
});
|
||
|
||
// Создаем строки таблицы для каждого значения шкалы
|
||
for (let value = scaleMin; value <= scaleMax; value++) {
|
||
const count = countByValue[value] || 0;
|
||
const percent = values.length > 0 ? Math.round((count / values.length) * 100) : 0;
|
||
|
||
const $tr = $('<tr>').append(
|
||
$('<td>', { text: value }),
|
||
$('<td>', { text: count }),
|
||
$('<td>', { text: `${percent}%` }),
|
||
$('<td>').append(
|
||
$('<div>', { class: 'bar-container' }).append(
|
||
$('<div>', {
|
||
class: 'bar',
|
||
css: { width: `${percent}%` }
|
||
})
|
||
)
|
||
)
|
||
);
|
||
|
||
$tbody.append($tr);
|
||
}
|
||
|
||
$table.append($thead, $tbody);
|
||
$scaleStats.append($table);
|
||
|
||
$container.append($scaleStats);
|
||
};
|
||
|
||
// Отображение статистики для текстовых ответов
|
||
const renderTextStats = (question, $container) => {
|
||
// Используем textAnswers или answers, в зависимости от того, что доступно
|
||
const answers = question.textAnswers && question.textAnswers.length > 0
|
||
? question.textAnswers
|
||
: (question.answers || []);
|
||
|
||
if (answers.length === 0) {
|
||
$container.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
|
||
return;
|
||
}
|
||
|
||
const $textAnswers = $('<div>', { class: 'text-answers-list' });
|
||
|
||
answers.forEach((answer, i) => {
|
||
$textAnswers.append(
|
||
$('<div>', { class: 'text-answer-item' }).append(
|
||
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
|
||
$('<div>', { class: 'answer-text', text: answer })
|
||
)
|
||
);
|
||
});
|
||
|
||
$container.append($textAnswers);
|
||
};
|
||
|
||
// Копирование ссылок
|
||
$('#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 isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||
let basePath;
|
||
|
||
if (isMsPath) {
|
||
// Для продакшна: используем /ms/questioneer
|
||
basePath = '/ms/questioneer';
|
||
} else {
|
||
// Для локальной разработки: используем текущий путь
|
||
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/${adminLink}`,
|
||
method: 'DELETE',
|
||
success: function(result) {
|
||
if (result.success) {
|
||
showAlert('Опрос успешно удален', 'Удаление опроса', function() {
|
||
// Получаем базовый путь с учетом /ms в продакшен-версии
|
||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||
let basePath;
|
||
|
||
if (isMsPath) {
|
||
// Для продакшна: используем /ms/questioneer
|
||
basePath = '/ms/questioneer';
|
||
} else {
|
||
// Для локальной разработки: используем текущий путь
|
||
basePath = window.location.pathname.split('/admin')[0];
|
||
}
|
||
|
||
// Перенаправляем на главную страницу
|
||
window.location.href = basePath;
|
||
}, true);
|
||
} else {
|
||
showAlert(`Ошибка при удалении опроса: ${result.error}`, 'Ошибка');
|
||
}
|
||
},
|
||
error: function(error) {
|
||
console.error('Error deleting questionnaire:', error);
|
||
showAlert('Не удалось удалить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||
}
|
||
});
|
||
};
|
||
|
||
// Инициализация
|
||
loadQuestionnaire();
|
||
|
||
// Обновление данных каждые 10 секунд
|
||
setInterval(loadQuestionnaire, 10000);
|
||
});
|