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