update
This commit is contained in:
17
server/routers/assessment-tools/index.js
Normal file
17
server/routers/assessment-tools/index.js
Normal 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;
|
||||
39
server/routers/assessment-tools/models/Criteria.js
Normal file
39
server/routers/assessment-tools/models/Criteria.js
Normal 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);
|
||||
|
||||
44
server/routers/assessment-tools/models/Event.js
Normal file
44
server/routers/assessment-tools/models/Event.js
Normal 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);
|
||||
|
||||
38
server/routers/assessment-tools/models/Expert.js
Normal file
38
server/routers/assessment-tools/models/Expert.js
Normal 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);
|
||||
|
||||
59
server/routers/assessment-tools/models/Rating.js
Normal file
59
server/routers/assessment-tools/models/Rating.js
Normal 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);
|
||||
|
||||
47
server/routers/assessment-tools/models/Team.js
Normal file
47
server/routers/assessment-tools/models/Team.js
Normal 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);
|
||||
|
||||
14
server/routers/assessment-tools/models/index.js
Normal file
14
server/routers/assessment-tools/models/index.js
Normal 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
|
||||
};
|
||||
|
||||
117
server/routers/assessment-tools/routes/criteria.js
Normal file
117
server/routers/assessment-tools/routes/criteria.js
Normal 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;
|
||||
|
||||
108
server/routers/assessment-tools/routes/event.js
Normal file
108
server/routers/assessment-tools/routes/event.js
Normal 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;
|
||||
|
||||
104
server/routers/assessment-tools/routes/experts.js
Normal file
104
server/routers/assessment-tools/routes/experts.js
Normal 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;
|
||||
|
||||
215
server/routers/assessment-tools/routes/ratings.js
Normal file
215
server/routers/assessment-tools/routes/ratings.js
Normal 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;
|
||||
|
||||
180
server/routers/assessment-tools/routes/teams.js
Normal file
180
server/routers/assessment-tools/routes/teams.js
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user