multy-stub/server/routers/questioneer/index.js

422 lines
13 KiB
JavaScript
Raw Normal View History

2025-03-11 23:50:50 +03:00
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