422 lines
13 KiB
JavaScript
422 lines
13 KiB
JavaScript
|
const express = require('express')
|
|||
|
const { Router } = require("express")
|
|||
|
const router = Router()
|
|||
|
const crypto = require('crypto')
|
|||
|
const path = require('path')
|
|||
|
const { getDB } = require('../../utils/mongo')
|
|||
|
const mongoose = require('mongoose')
|
|||
|
|
|||
|
// Используем одно определение модели
|
|||
|
const Questionnaire = (() => {
|
|||
|
// Если модель уже существует, используем её
|
|||
|
if (mongoose.models.Questionnaire) {
|
|||
|
return mongoose.models.Questionnaire;
|
|||
|
}
|
|||
|
|
|||
|
// Иначе создаем новую модель
|
|||
|
const questionnaireSchema = new mongoose.Schema({
|
|||
|
title: { type: String, required: true },
|
|||
|
description: { type: String },
|
|||
|
questions: [{
|
|||
|
text: { type: String, required: true },
|
|||
|
type: {
|
|||
|
type: String,
|
|||
|
enum: ['single_choice', 'multiple_choice', 'text', 'rating', 'tag_cloud', 'scale'],
|
|||
|
required: true
|
|||
|
},
|
|||
|
required: { type: Boolean, default: false },
|
|||
|
options: [{
|
|||
|
text: { type: String, required: true },
|
|||
|
count: { type: Number, default: 0 }
|
|||
|
}],
|
|||
|
scaleMin: { type: Number },
|
|||
|
scaleMax: { type: Number },
|
|||
|
scaleMinLabel: { type: String },
|
|||
|
scaleMaxLabel: { type: String },
|
|||
|
answers: [{ type: String }],
|
|||
|
scaleValues: [{ type: Number }],
|
|||
|
tags: [{
|
|||
|
text: { type: String },
|
|||
|
count: { type: Number, default: 1 }
|
|||
|
}]
|
|||
|
}],
|
|||
|
displayType: {
|
|||
|
type: String,
|
|||
|
enum: ['default', 'tag_cloud', 'voting', 'poll', 'step_by_step'],
|
|||
|
default: 'step_by_step'
|
|||
|
},
|
|||
|
createdAt: { type: Date, default: Date.now },
|
|||
|
updatedAt: { type: Date, default: Date.now },
|
|||
|
adminLink: { type: String, required: true },
|
|||
|
publicLink: { type: String, required: true }
|
|||
|
});
|
|||
|
|
|||
|
return mongoose.model('Questionnaire', questionnaireSchema);
|
|||
|
})();
|
|||
|
|
|||
|
// Middleware для парсинга JSON
|
|||
|
router.use(express.json());
|
|||
|
|
|||
|
// Обслуживание статичных файлов - проверяем правильность пути
|
|||
|
router.use('/static', express.static(path.join(__dirname, 'public', 'static')));
|
|||
|
|
|||
|
// Получить главную страницу
|
|||
|
router.get("/", (req, res) => {
|
|||
|
res.sendFile(path.join(__dirname, 'public/index.html'))
|
|||
|
})
|
|||
|
|
|||
|
// Страница создания нового опроса
|
|||
|
router.get("/create", (req, res) => {
|
|||
|
res.sendFile(path.join(__dirname, 'public/create.html'))
|
|||
|
})
|
|||
|
|
|||
|
// Страница редактирования опроса
|
|||
|
router.get("/edit/:adminLink", (req, res) => {
|
|||
|
res.sendFile(path.join(__dirname, 'public/edit.html'))
|
|||
|
})
|
|||
|
|
|||
|
// Страница администрирования опроса
|
|||
|
router.get("/admin/:adminLink", (req, res) => {
|
|||
|
res.sendFile(path.join(__dirname, 'public/admin.html'))
|
|||
|
})
|
|||
|
|
|||
|
// Страница голосования
|
|||
|
router.get("/poll/:publicLink", (req, res) => {
|
|||
|
res.sendFile(path.join(__dirname, 'public/poll.html'))
|
|||
|
})
|
|||
|
|
|||
|
// API для работы с опросами
|
|||
|
|
|||
|
// Создать новый опрос
|
|||
|
router.post("/api/questionnaires", async (req, res) => {
|
|||
|
try {
|
|||
|
// Проверка наличия нужных полей
|
|||
|
const { title, questions } = req.body;
|
|||
|
|
|||
|
if (!title || !Array.isArray(questions) || questions.length === 0) {
|
|||
|
return res.json({ success: false, error: 'Необходимо указать название и хотя бы один вопрос' });
|
|||
|
}
|
|||
|
|
|||
|
// Создаем уникальные ссылки
|
|||
|
const adminLink = crypto.randomBytes(6).toString('hex');
|
|||
|
const publicLink = crypto.randomBytes(6).toString('hex');
|
|||
|
|
|||
|
// Устанавливаем тип отображения step_by_step, если не указан
|
|||
|
if (!req.body.displayType) {
|
|||
|
req.body.displayType = 'step_by_step';
|
|||
|
}
|
|||
|
|
|||
|
// Создаем новый опросник
|
|||
|
const questionnaire = new Questionnaire({
|
|||
|
...req.body,
|
|||
|
adminLink,
|
|||
|
publicLink
|
|||
|
});
|
|||
|
|
|||
|
await questionnaire.save();
|
|||
|
|
|||
|
res.json({
|
|||
|
success: true,
|
|||
|
data: {
|
|||
|
adminLink,
|
|||
|
publicLink
|
|||
|
}
|
|||
|
});
|
|||
|
} catch (error) {
|
|||
|
console.error('Error creating questionnaire:', error);
|
|||
|
res.json({ success: false, error: error.message });
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// Получить все опросы
|
|||
|
router.get("/api/questionnaires", async (req, res) => {
|
|||
|
try {
|
|||
|
const questionnaires = await Questionnaire.find({}, {
|
|||
|
title: 1,
|
|||
|
description: 1,
|
|||
|
createdAt: 1,
|
|||
|
updatedAt: 1,
|
|||
|
_id: 1,
|
|||
|
adminLink: 1,
|
|||
|
publicLink: 1
|
|||
|
}).sort({ createdAt: -1 })
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
data: questionnaires
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error fetching questionnaires:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to fetch questionnaires'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Получить опрос по ID для админа
|
|||
|
router.get("/api/questionnaires/admin/:adminLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { adminLink } = req.params
|
|||
|
const questionnaire = await Questionnaire.findOne({ adminLink })
|
|||
|
|
|||
|
if (!questionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
data: questionnaire
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error fetching questionnaire:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to fetch questionnaire'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Получить опрос по публичной ссылке (для голосования)
|
|||
|
router.get("/api/questionnaires/public/:publicLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { publicLink } = req.params
|
|||
|
const questionnaire = await Questionnaire.findOne({ publicLink })
|
|||
|
|
|||
|
if (!questionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
data: questionnaire
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error fetching questionnaire:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to fetch questionnaire'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Обновить опрос
|
|||
|
router.put("/api/questionnaires/:adminLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { adminLink } = req.params
|
|||
|
const { title, description, questions, displayType } = req.body
|
|||
|
|
|||
|
const updatedQuestionnaire = await Questionnaire.findOneAndUpdate(
|
|||
|
{ adminLink },
|
|||
|
{
|
|||
|
title,
|
|||
|
description,
|
|||
|
questions,
|
|||
|
displayType,
|
|||
|
updatedAt: Date.now()
|
|||
|
},
|
|||
|
{ new: true }
|
|||
|
)
|
|||
|
|
|||
|
if (!updatedQuestionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
data: updatedQuestionnaire
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error updating questionnaire:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to update questionnaire'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Удалить опрос
|
|||
|
router.delete("/api/questionnaires/:adminLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { adminLink } = req.params
|
|||
|
|
|||
|
const deletedQuestionnaire = await Questionnaire.findOneAndDelete({ adminLink })
|
|||
|
|
|||
|
if (!deletedQuestionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
message: 'Questionnaire deleted successfully'
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error deleting questionnaire:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to delete questionnaire'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Голосование в опросе
|
|||
|
router.post("/api/vote/:publicLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { publicLink } = req.params
|
|||
|
const { answers } = req.body
|
|||
|
|
|||
|
const questionnaire = await Questionnaire.findOne({ publicLink })
|
|||
|
|
|||
|
if (!questionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
// Обновить счетчики голосов
|
|||
|
answers.forEach(answer => {
|
|||
|
const { questionIndex, optionIndices, textAnswer, scaleValue, tagTexts } = answer
|
|||
|
|
|||
|
// Обработка одиночного и множественного выбора
|
|||
|
if (Array.isArray(optionIndices)) {
|
|||
|
// Для множественного выбора
|
|||
|
optionIndices.forEach(optionIndex => {
|
|||
|
if (questionnaire.questions[questionIndex] &&
|
|||
|
questionnaire.questions[questionIndex].options[optionIndex]) {
|
|||
|
questionnaire.questions[questionIndex].options[optionIndex].count += 1
|
|||
|
}
|
|||
|
})
|
|||
|
} else if (typeof optionIndices === 'number') {
|
|||
|
// Для единичного выбора
|
|||
|
if (questionnaire.questions[questionIndex] &&
|
|||
|
questionnaire.questions[questionIndex].options[optionIndices]) {
|
|||
|
questionnaire.questions[questionIndex].options[optionIndices].count += 1
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Сохраняем текстовые ответы
|
|||
|
if (textAnswer && questionnaire.questions[questionIndex]) {
|
|||
|
if (!questionnaire.questions[questionIndex].answers) {
|
|||
|
questionnaire.questions[questionIndex].answers = [];
|
|||
|
}
|
|||
|
questionnaire.questions[questionIndex].answers.push(textAnswer);
|
|||
|
}
|
|||
|
|
|||
|
// Сохраняем ответы шкалы оценки
|
|||
|
if (scaleValue !== undefined && questionnaire.questions[questionIndex]) {
|
|||
|
if (!questionnaire.questions[questionIndex].scaleValues) {
|
|||
|
questionnaire.questions[questionIndex].scaleValues = [];
|
|||
|
}
|
|||
|
questionnaire.questions[questionIndex].scaleValues.push(scaleValue);
|
|||
|
}
|
|||
|
|
|||
|
// Сохраняем теги
|
|||
|
if (Array.isArray(tagTexts) && tagTexts.length > 0 && questionnaire.questions[questionIndex]) {
|
|||
|
if (!questionnaire.questions[questionIndex].tags) {
|
|||
|
questionnaire.questions[questionIndex].tags = [];
|
|||
|
}
|
|||
|
|
|||
|
tagTexts.forEach(tagText => {
|
|||
|
const existingTag = questionnaire.questions[questionIndex].tags.find(t => t.text === tagText);
|
|||
|
if (existingTag) {
|
|||
|
existingTag.count += 1;
|
|||
|
} else {
|
|||
|
questionnaire.questions[questionIndex].tags.push({ text: tagText, count: 1 });
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
await questionnaire.save()
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
message: 'Vote registered successfully'
|
|||
|
})
|
|||
|
} catch (error) {
|
|||
|
console.error('Error registering vote:', error)
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to register vote'
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Получить результаты опроса по публичной ссылке
|
|||
|
router.get("/api/results/:publicLink", async (req, res) => {
|
|||
|
try {
|
|||
|
const { publicLink } = req.params;
|
|||
|
const questionnaire = await Questionnaire.findOne({ publicLink });
|
|||
|
|
|||
|
if (!questionnaire) {
|
|||
|
return res.status(404).json({
|
|||
|
success: false,
|
|||
|
error: 'Questionnaire not found'
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// Формируем результаты для отправки
|
|||
|
const results = {
|
|||
|
title: questionnaire.title,
|
|||
|
description: questionnaire.description,
|
|||
|
questions: questionnaire.questions.map(question => {
|
|||
|
const result = {
|
|||
|
text: question.text,
|
|||
|
type: question.type
|
|||
|
};
|
|||
|
|
|||
|
// Добавляем варианты ответов, если они есть
|
|||
|
if (question.options && question.options.length > 0) {
|
|||
|
result.options = question.options;
|
|||
|
}
|
|||
|
|
|||
|
// Добавляем текстовые ответы, если они есть
|
|||
|
if (question.answers && question.answers.length > 0) {
|
|||
|
result.answers = question.answers;
|
|||
|
}
|
|||
|
|
|||
|
// Добавляем результаты шкалы, если они есть
|
|||
|
if (question.scaleValues && question.scaleValues.length > 0) {
|
|||
|
result.scaleValues = question.scaleValues;
|
|||
|
|
|||
|
// Считаем среднее значение
|
|||
|
result.scaleAverage = question.scaleValues.reduce((a, b) => a + b, 0) / question.scaleValues.length;
|
|||
|
}
|
|||
|
|
|||
|
// Добавляем теги, если они есть
|
|||
|
if (question.tags && question.tags.length > 0) {
|
|||
|
result.tags = question.tags;
|
|||
|
}
|
|||
|
|
|||
|
return result;
|
|||
|
})
|
|||
|
};
|
|||
|
|
|||
|
res.status(200).json({
|
|||
|
success: true,
|
|||
|
data: results
|
|||
|
});
|
|||
|
} catch (error) {
|
|||
|
console.error('Error fetching poll results:', error);
|
|||
|
res.status(500).json({
|
|||
|
success: false,
|
|||
|
error: 'Failed to fetch poll results'
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
module.exports = router
|