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
|