multy-stub/server/routers/questioneer/index.js
Primakov Alexandr Alexandrovich 1fcc5ed70d init Questionnaire
2025-03-11 23:50:50 +03:00

422 lines
13 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.

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