This commit is contained in:
2025-11-21 16:19:47 +03:00
parent 2480f7c376
commit fa860921da
13 changed files with 1020 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
const router = require('express').Router();
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
const mongoose = require('../../utils/mongoose');
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
router.use(timer());
// Подключение маршрутов - прямые пути без path.join и __dirname
router.use('/events', require('./routes/event'));
router.use('/teams', require('./routes/teams'));
router.use('/experts', require('./routes/experts'));
router.use('/criteria', require('./routes/criteria'));
router.use('/ratings', require('./routes/ratings'));
module.exports = router;

View File

@@ -0,0 +1,39 @@
const mongoose = require('../../../utils/mongoose');
const criterionItemSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
maxScore: {
type: Number,
default: 5,
min: 0,
max: 10
}
}, { _id: false });
const criteriaSchema = new mongoose.Schema({
blockName: {
type: String,
required: true
},
criteria: [criterionItemSchema],
order: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Criteria', criteriaSchema);

View File

@@ -0,0 +1,44 @@
const mongoose = require('../../../utils/mongoose');
const eventSchema = new mongoose.Schema({
name: {
type: String,
required: true,
default: 'Новое мероприятие'
},
description: {
type: String,
default: ''
},
eventDate: {
type: Date,
required: true,
default: Date.now
},
location: {
type: String,
default: ''
},
status: {
type: String,
enum: ['draft', 'ready', 'active', 'completed'],
default: 'draft'
},
votingEnabled: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Event', eventSchema);

View File

@@ -0,0 +1,38 @@
const mongoose = require('../../../utils/mongoose');
const crypto = require('crypto');
const expertSchema = new mongoose.Schema({
fullName: {
type: String,
required: true
},
token: {
type: String,
unique: true
},
qrCodeUrl: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Generate unique token before saving
expertSchema.pre('save', function(next) {
if (!this.token) {
this.token = crypto.randomBytes(16).toString('hex');
}
next();
});
module.exports = mongoose.model('Expert', expertSchema);

View File

@@ -0,0 +1,59 @@
const mongoose = require('../../../utils/mongoose');
const ratingItemSchema = new mongoose.Schema({
criteriaId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Criteria',
required: true
},
criterionName: {
type: String,
required: true
},
score: {
type: Number,
required: true,
min: 0,
max: 5
}
}, { _id: false });
const ratingSchema = new mongoose.Schema({
expertId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Expert',
required: true
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Team',
required: true
},
ratings: [ratingItemSchema],
totalScore: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Calculate total score before saving
ratingSchema.pre('save', function(next) {
this.totalScore = this.ratings.reduce((sum, item) => sum + item.score, 0);
next();
});
// Ensure unique combination of expert and team
ratingSchema.index({ expertId: 1, teamId: 1 }, { unique: true });
module.exports = mongoose.model('Rating', ratingSchema);

View File

@@ -0,0 +1,47 @@
const mongoose = require('../../../utils/mongoose');
const teamSchema = new mongoose.Schema({
type: {
type: String,
enum: ['team', 'participant'],
required: true
},
name: {
type: String,
required: true
},
projectName: {
type: String,
default: ''
},
caseDescription: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
},
votingStatus: {
type: String,
enum: ['not_evaluated', 'evaluating', 'evaluated'],
default: 'not_evaluated'
},
isActiveForVoting: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Team', teamSchema);

View File

@@ -0,0 +1,14 @@
const Event = require('./Event');
const Team = require('./Team');
const Expert = require('./Expert');
const Criteria = require('./Criteria');
const Rating = require('./Rating');
module.exports = {
Event,
Team,
Expert,
Criteria,
Rating
};

View File

@@ -0,0 +1,117 @@
const router = require('express').Router();
const { Criteria } = require('../models');
// Критерии по умолчанию из hack.md
const DEFAULT_CRITERIA = [
{
blockName: 'Оценка проекта',
criteria: [
{ name: 'Соответствие решения поставленной задаче', maxScore: 5 },
{ name: 'Оригинальность - использование нестандартных технических и проектных подходов', maxScore: 5 },
{ name: 'Работоспособность решения', maxScore: 1 },
{ name: 'Технологическая сложность решения', maxScore: 2 },
{ name: 'Объем функциональных возможностей решения', maxScore: 2 },
{ name: 'Аргументация способа выбранного решения', maxScore: 5 },
{ name: 'Качество предоставления информации', maxScore: 5 },
{ name: 'Наличие удобного UX/UI', maxScore: 5 },
{ name: 'Наличие не менее 5 AI-агентов', maxScore: 5 }
],
order: 0
}
];
// GET /api/criteria - получить все блоки критериев
router.get('/', async (req, res) => {
try {
const criteria = await Criteria.find().sort({ order: 1 });
res.json(criteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/criteria/:id - получить блок критериев по ID
router.get('/:id', async (req, res) => {
try {
const criteria = await Criteria.findById(req.params.id);
if (!criteria) {
return res.status(404).json({ error: 'Criteria not found' });
}
res.json(criteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/criteria - создать блок критериев
router.post('/', async (req, res) => {
try {
const { blockName, criteria, order } = req.body;
if (!blockName || !criteria || !Array.isArray(criteria)) {
return res.status(400).json({ error: 'Block name and criteria array are required' });
}
const criteriaBlock = await Criteria.create({
blockName,
criteria,
order: order !== undefined ? order : 0
});
res.status(201).json(criteriaBlock);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/criteria/default - загрузить критерии по умолчанию из hack.md
router.post('/default', async (req, res) => {
try {
// Удаляем все существующие критерии
await Criteria.deleteMany({});
// Создаем критерии по умолчанию
const createdCriteria = await Criteria.insertMany(DEFAULT_CRITERIA);
res.status(201).json(createdCriteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/criteria/:id - редактировать блок
router.put('/:id', async (req, res) => {
try {
const { blockName, criteria, order } = req.body;
const criteriaBlock = await Criteria.findById(req.params.id);
if (!criteriaBlock) {
return res.status(404).json({ error: 'Criteria not found' });
}
if (blockName !== undefined) criteriaBlock.blockName = blockName;
if (criteria !== undefined) criteriaBlock.criteria = criteria;
if (order !== undefined) criteriaBlock.order = order;
await criteriaBlock.save();
res.json(criteriaBlock);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/criteria/:id - удалить блок
router.delete('/:id', async (req, res) => {
try {
const criteria = await Criteria.findByIdAndDelete(req.params.id);
if (!criteria) {
return res.status(404).json({ error: 'Criteria not found' });
}
res.json({ message: 'Criteria deleted successfully', criteria });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,108 @@
const router = require('express').Router();
const { Event } = require('../models');
// GET /api/events - получить все мероприятия
router.get('/', async (req, res) => {
try {
const events = await Event.find().sort({ eventDate: -1 });
res.json(events);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/events/:id - получить одно мероприятие
router.get('/:id', async (req, res) => {
try {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/events - создать новое мероприятие
router.post('/', async (req, res) => {
try {
const { name, description, eventDate, location, status } = req.body;
const event = await Event.create({
name: name || 'Новое мероприятие',
description: description || '',
eventDate: eventDate || new Date(),
location: location || '',
status: status || 'draft',
votingEnabled: false
});
res.status(201).json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/events/:id - обновить мероприятие
router.put('/:id', async (req, res) => {
try {
const { name, description, eventDate, location, status } = req.body;
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
if (name !== undefined) event.name = name;
if (description !== undefined) event.description = description;
if (eventDate !== undefined) event.eventDate = eventDate;
if (location !== undefined) event.location = location;
if (status !== undefined) event.status = status;
await event.save();
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/events/:id - удалить мероприятие
router.delete('/:id', async (req, res) => {
try {
const event = await Event.findByIdAndDelete(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
res.json({ message: 'Мероприятие удалено', event });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/events/:id/toggle-voting - вкл/выкл оценку
router.patch('/:id/toggle-voting', async (req, res) => {
try {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
event.votingEnabled = !event.votingEnabled;
await event.save();
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,104 @@
const router = require('express').Router();
const { Expert } = require('../models');
// GET /api/experts - список экспертов
router.get('/', async (req, res) => {
try {
const experts = await Expert.find().sort({ createdAt: -1 });
res.json(experts);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/experts/by-token/:token - получить данные эксперта по токену
router.get('/by-token/:token', async (req, res) => {
try {
const expert = await Expert.findOne({ token: req.params.token });
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/experts/:id - получить эксперта по ID
router.get('/:id', async (req, res) => {
try {
const expert = await Expert.findById(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/experts - создать эксперта (с генерацией уникальной ссылки)
router.post('/', async (req, res) => {
try {
const { fullName } = req.body;
if (!fullName) {
return res.status(400).json({ error: 'Full name is required' });
}
// Создаем нового эксперта
const expert = new Expert({
fullName
});
// Сохраняем эксперта (токен генерируется в pre-save хуке)
await expert.save();
// Формируем URL для QR кода ПОСЛЕ сохранения, когда токен уже сгенерирован
const baseUrl = req.protocol + '://' + req.get('host');
expert.qrCodeUrl = `${baseUrl}/assessment-tools/expert/${expert.token}`;
// Сохраняем еще раз с обновленным qrCodeUrl
await expert.save();
res.status(201).json(expert);
} catch (error) {
console.error('Error creating expert:', error);
res.status(500).json({ error: error.message });
}
});
// PUT /api/experts/:id - редактировать эксперта
router.put('/:id', async (req, res) => {
try {
const { fullName } = req.body;
const expert = await Expert.findById(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
if (fullName !== undefined) expert.fullName = fullName;
await expert.save();
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/experts/:id - удалить эксперта
router.delete('/:id', async (req, res) => {
try {
const expert = await Expert.findByIdAndDelete(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json({ message: 'Expert deleted successfully', expert });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,215 @@
const router = require('express').Router();
const { Rating, Team, Expert, Criteria } = require('../models');
// GET /api/ratings - получить все оценки (с фильтрами)
router.get('/', async (req, res) => {
try {
const { expertId, teamId } = req.query;
const filter = {};
if (expertId) filter.expertId = expertId;
if (teamId) filter.teamId = teamId;
const ratings = await Rating.find(filter)
.populate('expertId', 'fullName')
.populate('teamId', 'name type')
.sort({ createdAt: -1 });
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/team/:teamId - оценки конкретной команды
router.get('/team/:teamId', async (req, res) => {
try {
const ratings = await Rating.find({ teamId: req.params.teamId })
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/expert/:expertId - оценки конкретного эксперта
router.get('/expert/:expertId', async (req, res) => {
try {
const ratings = await Rating.find({ expertId: req.params.expertId })
.populate('teamId', 'name type projectName');
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/statistics - статистика с группировкой по командам
router.get('/statistics', async (req, res) => {
try {
const { type } = req.query;
// Получаем все команды
const teamFilter = type ? { type, isActive: true } : { isActive: true };
const teams = await Team.find(teamFilter);
// Получаем все оценки
const ratings = await Rating.find()
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
// Группируем оценки по командам
const statistics = teams.map(team => {
const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString());
// Считаем средние оценки по критериям
const criteriaStats = {};
teamRatings.forEach(rating => {
rating.ratings.forEach(item => {
if (!criteriaStats[item.criterionName]) {
criteriaStats[item.criterionName] = {
name: item.criterionName,
scores: [],
average: 0
};
}
criteriaStats[item.criterionName].scores.push(item.score);
});
});
// Вычисляем средние значения
Object.keys(criteriaStats).forEach(key => {
const scores = criteriaStats[key].scores;
criteriaStats[key].average = scores.reduce((sum, s) => sum + s, 0) / scores.length;
});
// Считаем общий балл команды (среднее от всех экспертов)
const totalScore = teamRatings.length > 0
? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length
: 0;
return {
team: {
_id: team._id,
name: team.name,
type: team.type,
projectName: team.projectName
},
ratings: teamRatings.map(r => ({
expert: r.expertId ? r.expertId.fullName : 'Unknown',
criteria: r.ratings,
totalScore: r.totalScore
})),
criteriaStats: Object.values(criteriaStats),
totalScore: totalScore,
ratingsCount: teamRatings.length
};
});
res.json(statistics);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/top3 - топ-3 команды/участники
router.get('/top3', async (req, res) => {
try {
const { type } = req.query;
// Получаем статистику
const teamFilter = type ? { type, isActive: true } : { isActive: true };
const teams = await Team.find(teamFilter);
const ratings = await Rating.find()
.populate('teamId', 'name type projectName');
// Группируем и считаем средние баллы
const teamScores = teams.map(team => {
const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString());
const totalScore = teamRatings.length > 0
? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length
: 0;
return {
team: {
_id: team._id,
name: team.name,
type: team.type,
projectName: team.projectName
},
totalScore: totalScore,
ratingsCount: teamRatings.length
};
});
// Сортируем по баллам и берем топ-3
const top3 = teamScores
.filter(t => t.ratingsCount > 0)
.sort((a, b) => b.totalScore - a.totalScore)
.slice(0, 3);
res.json(top3);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/ratings - создать/обновить оценку эксперта
router.post('/', async (req, res) => {
try {
const { expertId, teamId, ratings } = req.body;
if (!expertId || !teamId || !ratings || !Array.isArray(ratings)) {
return res.status(400).json({ error: 'Expert ID, team ID, and ratings array are required' });
}
// Проверяем существование эксперта и команды
const expert = await Expert.findById(expertId);
const team = await Team.findById(teamId);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
// Проверяем, активна ли команда
if (!team.isActive) {
return res.status(400).json({ error: 'Team voting is disabled' });
}
// Ищем существующую оценку
let rating = await Rating.findOne({ expertId, teamId });
if (rating) {
// Обновляем существующую оценку
rating.ratings = ratings;
await rating.save();
} else {
// Создаем новую оценку
rating = await Rating.create({
expertId,
teamId,
ratings
});
}
// Возвращаем с populate
rating = await Rating.findById(rating._id)
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
res.status(rating ? 200 : 201).json(rating);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,180 @@
const router = require('express').Router();
const { Team } = require('../models');
// GET /api/teams - список всех команд
router.get('/', async (req, res) => {
try {
const { type } = req.query;
const filter = type ? { type } : {};
const teams = await Team.find(filter).sort({ createdAt: -1 });
res.json(teams);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/teams/active/voting - получить активную для оценки команду (ДОЛЖЕН БЫТЬ ПЕРЕД /:id)
router.get('/active/voting', async (req, res) => {
try {
const team = await Team.findOne({ isActiveForVoting: true });
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/stop-all-voting/global - остановить все оценивания (ДОЛЖЕН БЫТЬ ПЕРЕД /:id)
router.patch('/stop-all-voting/global', async (req, res) => {
try {
// Находим все команды, которые сейчас оцениваются
const result = await Team.updateMany(
{ isActiveForVoting: true },
{
isActiveForVoting: false,
votingStatus: 'evaluated'
}
);
res.json({
message: 'All voting stopped successfully',
modifiedCount: result.modifiedCount
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/teams/:id - получить команду по ID
router.get('/:id', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/teams - создать команду/участника
router.post('/', async (req, res) => {
try {
const { type, name, projectName, caseDescription } = req.body;
if (!type || !name) {
return res.status(400).json({ error: 'Type and name are required' });
}
const team = await Team.create({
type,
name,
projectName: projectName || '',
caseDescription: caseDescription || '',
isActive: true
});
res.status(201).json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/teams/:id - редактировать команду
router.put('/:id', async (req, res) => {
try {
const { type, name, projectName, caseDescription } = req.body;
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
if (type !== undefined) team.type = type;
if (name !== undefined) team.name = name;
if (projectName !== undefined) team.projectName = projectName;
if (caseDescription !== undefined) team.caseDescription = caseDescription;
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/teams/:id - удалить команду
router.delete('/:id', async (req, res) => {
try {
const team = await Team.findByIdAndDelete(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
res.json({ message: 'Team deleted successfully', team });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/activate-for-voting - активировать команду для оценки
router.patch('/:id/activate-for-voting', async (req, res) => {
try {
// Деактивируем все команды и сохраняем их статус
const previouslyActive = await Team.findOne({ isActiveForVoting: true });
if (previouslyActive) {
previouslyActive.isActiveForVoting = false;
previouslyActive.votingStatus = 'evaluated';
await previouslyActive.save();
}
// Активируем выбранную команду
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
team.isActiveForVoting = true;
team.votingStatus = 'evaluating';
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/stop-voting - остановить оценивание конкретной команды
router.patch('/:id/stop-voting', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
team.isActiveForVoting = false;
team.votingStatus = 'evaluated';
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/toggle-active - остановить оценку команды
router.patch('/:id/toggle-active', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
team.isActive = !team.isActive;
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,38 @@
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
const mongoose = require('../../../utils/mongoose');
const { Event } = require('../models');
async function recreateTestUser() {
try {
// Ждем, пока подключение будет готово
if (mongoose.connection.readyState !== 1) {
await new Promise(resolve => {
mongoose.connection.once('connected', resolve);
});
}
console.log('Connected to MongoDB');
// Создаем тестовое мероприятие если его нет
let event = await Event.findOne();
if (!event) {
event = await Event.create({
name: 'Tatar san',
status: 'draft',
votingEnabled: false
});
console.log('Test event created:', event.name);
}
console.log('Database initialized successfully');
await mongoose.disconnect();
console.log('Disconnected from MongoDB');
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
recreateTestUser();