multy-stub/server/routers/questioneer/public/static/js/admin.js
Primakov Alexandr Alexandrovich f91f821f86 fix navigation elements
2025-03-12 09:12:09 +03:00

465 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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);
});