@programisto/edrm-storage 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +135 -0
  2. package/dist/bin/www.d.ts +2 -0
  3. package/dist/bin/www.js +13 -0
  4. package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +9 -0
  5. package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +6 -0
  6. package/dist/modules/edrm-exams/lib/openai.d.ts +37 -0
  7. package/dist/modules/edrm-exams/lib/openai.js +135 -0
  8. package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
  9. package/dist/modules/edrm-exams/listeners/correct.listener.js +167 -0
  10. package/dist/modules/edrm-exams/models/candidate.model.d.ts +21 -0
  11. package/dist/modules/edrm-exams/models/candidate.model.js +75 -0
  12. package/dist/modules/edrm-exams/models/candidate.models.d.ts +21 -0
  13. package/dist/modules/edrm-exams/models/candidate.models.js +75 -0
  14. package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
  15. package/dist/modules/edrm-exams/models/company.model.js +34 -0
  16. package/dist/modules/edrm-exams/models/contact.model.d.ts +14 -0
  17. package/dist/modules/edrm-exams/models/contact.model.js +60 -0
  18. package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
  19. package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
  20. package/dist/modules/edrm-exams/models/test-job.model.d.ts +7 -0
  21. package/dist/modules/edrm-exams/models/test-job.model.js +29 -0
  22. package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
  23. package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
  24. package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
  25. package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
  26. package/dist/modules/edrm-exams/models/test.model.d.ts +47 -0
  27. package/dist/modules/edrm-exams/models/test.model.js +133 -0
  28. package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
  29. package/dist/modules/edrm-exams/models/user.model.js +73 -0
  30. package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
  31. package/dist/modules/edrm-exams/routes/company.router.js +108 -0
  32. package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
  33. package/dist/modules/edrm-exams/routes/exams-candidate.router.js +448 -0
  34. package/dist/modules/edrm-exams/routes/exams.router.d.ts +8 -0
  35. package/dist/modules/edrm-exams/routes/exams.router.js +1343 -0
  36. package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
  37. package/dist/modules/edrm-exams/routes/result.router.js +370 -0
  38. package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
  39. package/dist/modules/edrm-exams/routes/user.router.js +96 -0
  40. package/dist/modules/edrm-storage/config/edrm-storage.config.d.ts +29 -0
  41. package/dist/modules/edrm-storage/config/edrm-storage.config.js +31 -0
  42. package/dist/modules/edrm-storage/config/environment.example.d.ts +54 -0
  43. package/dist/modules/edrm-storage/config/environment.example.js +130 -0
  44. package/dist/modules/edrm-storage/examples/usage.example.d.ts +52 -0
  45. package/dist/modules/edrm-storage/examples/usage.example.js +156 -0
  46. package/dist/modules/edrm-storage/index.d.ts +5 -0
  47. package/dist/modules/edrm-storage/index.js +8 -0
  48. package/dist/modules/edrm-storage/integration/edrm-storage-integration.d.ts +53 -0
  49. package/dist/modules/edrm-storage/integration/edrm-storage-integration.js +132 -0
  50. package/dist/modules/edrm-storage/interfaces/storage-provider.interface.d.ts +35 -0
  51. package/dist/modules/edrm-storage/interfaces/storage-provider.interface.js +1 -0
  52. package/dist/modules/edrm-storage/migrations/edrm-storage.migration.d.ts +6 -0
  53. package/dist/modules/edrm-storage/migrations/edrm-storage.migration.js +151 -0
  54. package/dist/modules/edrm-storage/models/file.model.d.ts +78 -0
  55. package/dist/modules/edrm-storage/models/file.model.js +190 -0
  56. package/dist/modules/edrm-storage/providers/s3-storage.provider.d.ts +18 -0
  57. package/dist/modules/edrm-storage/providers/s3-storage.provider.js +95 -0
  58. package/dist/modules/edrm-storage/routes/edrm-storage.router.d.ts +8 -0
  59. package/dist/modules/edrm-storage/routes/edrm-storage.router.js +155 -0
  60. package/dist/modules/edrm-storage/scripts/quick-start.d.ts +7 -0
  61. package/dist/modules/edrm-storage/scripts/quick-start.js +114 -0
  62. package/dist/modules/edrm-storage/services/edrm-storage.service.d.ts +29 -0
  63. package/dist/modules/edrm-storage/services/edrm-storage.service.js +188 -0
  64. package/dist/modules/edrm-storage/tests/edrm-storage.service.test.d.ts +1 -0
  65. package/dist/modules/edrm-storage/tests/edrm-storage.service.test.js +143 -0
  66. package/dist/modules/edrm-storage/tests/integration.test.d.ts +1 -0
  67. package/dist/modules/edrm-storage/tests/integration.test.js +141 -0
  68. package/package.json +81 -0
@@ -0,0 +1,1343 @@
1
+ import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter as emitter, enduranceEventTypes as eventTypes } from '@programisto/endurance-core';
2
+ import Test from '../models/test.model.js';
3
+ import TestQuestion from '../models/test-question.model.js';
4
+ import TestResult from '../models/test-result.model.js';
5
+ import TestCategory from '../models/test-category.models.js';
6
+ import TestJob from '../models/test-job.model.js';
7
+ import Candidate from '../models/candidate.model.js';
8
+ import ContactModel from '../models/contact.model.js';
9
+ import { generateLiveMessage, generateLiveMessageAssistant } from '../lib/openai.js';
10
+ // Fonction utilitaire pour récupérer le nom du job
11
+ async function getJobName(targetJob) {
12
+ // Si c'est déjà une string (ancien format), on la retourne directement
13
+ if (typeof targetJob === 'string') {
14
+ return targetJob;
15
+ }
16
+ // Si c'est un ObjectId, on récupère le job
17
+ if (targetJob && typeof targetJob === 'object' && targetJob._id) {
18
+ const job = await TestJob.findById(targetJob._id);
19
+ return job ? job.name : 'Job inconnu';
20
+ }
21
+ // Si c'est juste un ObjectId
22
+ if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
23
+ const job = await TestJob.findById(targetJob);
24
+ return job ? job.name : 'Job inconnu';
25
+ }
26
+ return 'Job inconnu';
27
+ }
28
+ // Fonction pour migrer automatiquement un test si nécessaire
29
+ async function migrateTestIfNeeded(test) {
30
+ if (typeof test.targetJob === 'string') {
31
+ await test.migrateTargetJob();
32
+ }
33
+ }
34
+ class ExamsRouter extends EnduranceRouter {
35
+ constructor() {
36
+ super(EnduranceAuthMiddleware.getInstance());
37
+ }
38
+ async generateAndSaveQuestion(test, categoryInfo, useAssistant = false) {
39
+ try {
40
+ const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
41
+ if (!categoryDoc) {
42
+ console.error('Catégorie non trouvée:', categoryInfo.categoryId);
43
+ return null;
44
+ }
45
+ // Récupérer les questions existantes pour éviter les doublons
46
+ const otherQuestionsIds = test.questions.map(question => question.questionId);
47
+ const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
48
+ const jobName = await getJobName(test.targetJob);
49
+ const questionParams = {
50
+ job: jobName,
51
+ seniority: test.seniorityLevel,
52
+ category: categoryDoc.name,
53
+ questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
54
+ expertiseLevel: categoryInfo.expertiseLevel,
55
+ otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
56
+ };
57
+ let generatedQuestion;
58
+ if (useAssistant) {
59
+ generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', questionParams, true);
60
+ }
61
+ else {
62
+ generatedQuestion = await generateLiveMessage('createQuestion', questionParams, true);
63
+ }
64
+ if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
65
+ console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
66
+ return null;
67
+ }
68
+ const question = new TestQuestion(JSON.parse(generatedQuestion));
69
+ await question.save();
70
+ // Ajouter la question au test et sauvegarder immédiatement
71
+ test.questions.push({ questionId: question._id, order: test.questions.length });
72
+ await test.save();
73
+ return question;
74
+ }
75
+ catch (error) {
76
+ console.error('Erreur lors de la génération/sauvegarde de la question:', error);
77
+ return null;
78
+ }
79
+ }
80
+ setupRoutes() {
81
+ const authenticatedOptions = {
82
+ requireAuth: false,
83
+ permissions: []
84
+ };
85
+ // Créer une catégorie
86
+ this.post('/categories', authenticatedOptions, async (req, res) => {
87
+ const { name } = req.body;
88
+ if (!name) {
89
+ return res.status(400).json({ message: 'Error, all params are required' });
90
+ }
91
+ try {
92
+ const newCategory = new TestCategory({ name });
93
+ await newCategory.save();
94
+ res.status(201).json({ message: 'category created with sucess', category: newCategory });
95
+ }
96
+ catch (err) {
97
+ console.error('error when creating category : ', err);
98
+ res.status(500).json({ message: 'Internal server error' });
99
+ }
100
+ });
101
+ // Lister toutes les catégories
102
+ this.get('/categories', authenticatedOptions, async (req, res) => {
103
+ try {
104
+ const categories = await TestCategory.find();
105
+ res.status(200).json({ array: categories });
106
+ }
107
+ catch (err) {
108
+ console.error('error when creating category : ', err);
109
+ res.status(500).json({ message: 'Internal server error' });
110
+ }
111
+ });
112
+ // Obtenir une catégorie par son ID
113
+ this.get('/categorie/:id', authenticatedOptions, async (req, res) => {
114
+ const { id } = req.params;
115
+ try {
116
+ const category = await TestCategory.findById(id);
117
+ if (!category) {
118
+ return res.status(404).json({ message: 'no category founded with this id' });
119
+ }
120
+ res.status(200).json({ array: category });
121
+ }
122
+ catch (err) {
123
+ console.error('error when creating category : ', err);
124
+ res.status(500).json({ message: 'Internal server error' });
125
+ }
126
+ });
127
+ // Créer un job type
128
+ this.post('/jobs', authenticatedOptions, async (req, res) => {
129
+ const { name } = req.body;
130
+ if (!name) {
131
+ return res.status(400).json({ message: 'Error, name is required' });
132
+ }
133
+ try {
134
+ const newJob = new TestJob({ name });
135
+ await newJob.save();
136
+ res.status(201).json({ message: 'job created with success', job: newJob });
137
+ }
138
+ catch (err) {
139
+ console.error('error when creating job : ', err);
140
+ res.status(500).json({ message: 'Internal server error' });
141
+ }
142
+ });
143
+ // Lister tous les jobs
144
+ this.get('/jobs', authenticatedOptions, async (req, res) => {
145
+ try {
146
+ const jobs = await TestJob.find();
147
+ res.status(200).json({ array: jobs });
148
+ }
149
+ catch (err) {
150
+ console.error('error when getting jobs : ', err);
151
+ res.status(500).json({ message: 'Internal server error' });
152
+ }
153
+ });
154
+ // Obtenir un job par son ID
155
+ this.get('/jobs/:id', authenticatedOptions, async (req, res) => {
156
+ const { id } = req.params;
157
+ try {
158
+ const job = await TestJob.findById(id);
159
+ if (!job) {
160
+ return res.status(404).json({ message: 'no job founded with this id' });
161
+ }
162
+ res.status(200).json({ array: job });
163
+ }
164
+ catch (err) {
165
+ console.error('error when getting job : ', err);
166
+ res.status(500).json({ message: 'Internal server error' });
167
+ }
168
+ });
169
+ // Migrer tous les tests avec l'ancien format targetJob
170
+ this.post('/migrate-targetjobs', authenticatedOptions, async (req, res) => {
171
+ try {
172
+ const tests = await Test.find();
173
+ let migratedCount = 0;
174
+ let errorCount = 0;
175
+ for (const test of tests) {
176
+ try {
177
+ // Vérifier si le test a besoin de migration
178
+ if (typeof test.targetJob === 'string') {
179
+ await test.migrateTargetJob();
180
+ migratedCount++;
181
+ }
182
+ }
183
+ catch (error) {
184
+ console.error(`Erreur lors de la migration du test ${test._id}:`, error);
185
+ errorCount++;
186
+ }
187
+ }
188
+ res.status(200).json({
189
+ message: `Migration terminée. ${migratedCount} tests migrés, ${errorCount} erreurs.`,
190
+ migratedCount,
191
+ errorCount
192
+ });
193
+ }
194
+ catch (err) {
195
+ console.error('Erreur lors de la migration :', err);
196
+ res.status(500).json({ message: 'Erreur interne du serveur' });
197
+ }
198
+ });
199
+ // Créer un test
200
+ this.post('/test', authenticatedOptions, async (req, res) => {
201
+ const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
202
+ const user = req.user;
203
+ if (!title || !targetJob || !seniorityLevel) {
204
+ return res.status(400).json({ message: 'Error, all params are required' });
205
+ }
206
+ try {
207
+ const companyId = user?.companyId;
208
+ const userId = user?._id;
209
+ // Traiter le targetJob - si c'est une string, on cherche ou crée le TestJob
210
+ let targetJobId;
211
+ if (typeof targetJob === 'string') {
212
+ let existingJob = await TestJob.findOne({ name: targetJob });
213
+ if (!existingJob) {
214
+ existingJob = new TestJob({ name: targetJob });
215
+ await existingJob.save();
216
+ }
217
+ targetJobId = existingJob._id;
218
+ }
219
+ else {
220
+ targetJobId = targetJob;
221
+ }
222
+ const processedCategories = await Promise.all(categories?.map(async (category) => {
223
+ let existingCategory = await TestCategory.findOne({ name: category.name });
224
+ if (!existingCategory) {
225
+ existingCategory = await TestCategory.create({ name: category.name });
226
+ }
227
+ return {
228
+ categoryId: existingCategory._id,
229
+ expertiseLevel: category.expertiseLevel
230
+ };
231
+ }) || []);
232
+ const newTest = new Test({
233
+ companyId,
234
+ userId,
235
+ title,
236
+ description,
237
+ targetJob: targetJobId,
238
+ seniorityLevel,
239
+ state,
240
+ categories: processedCategories
241
+ });
242
+ await newTest.save();
243
+ res.status(201).json({ message: 'test created with sucess', data: newTest });
244
+ }
245
+ catch (err) {
246
+ console.error('error when creating test : ', err);
247
+ res.status(500).json({ message: 'Internal server error' });
248
+ }
249
+ });
250
+ // Modifier un test
251
+ this.put('/test/:id', authenticatedOptions, async (req, res) => {
252
+ const { id } = req.params;
253
+ const { title, description, targetJob, seniorityLevel, categories, state, questions } = req.body;
254
+ try {
255
+ const test = await Test.findById(id);
256
+ if (!test) {
257
+ return res.status(404).json({ message: 'Test non trouvé' });
258
+ }
259
+ if (title)
260
+ test.title = title;
261
+ if (description)
262
+ test.description = description;
263
+ if (targetJob) {
264
+ // Traiter le targetJob - si c'est une string, on cherche ou crée le TestJob
265
+ if (typeof targetJob === 'string') {
266
+ let existingJob = await TestJob.findOne({ name: targetJob });
267
+ if (!existingJob) {
268
+ existingJob = new TestJob({ name: targetJob });
269
+ await existingJob.save();
270
+ }
271
+ test.targetJob = existingJob._id;
272
+ }
273
+ else {
274
+ test.targetJob = targetJob;
275
+ }
276
+ }
277
+ if (seniorityLevel)
278
+ test.seniorityLevel = seniorityLevel;
279
+ if (state)
280
+ test.state = state;
281
+ if (categories) {
282
+ const processedCategories = await Promise.all(categories.map(async (category) => {
283
+ let existingCategory = await TestCategory.findOne({ name: category.name });
284
+ if (!existingCategory) {
285
+ existingCategory = await TestCategory.create({ name: category.name });
286
+ }
287
+ return {
288
+ categoryId: existingCategory._id,
289
+ expertiseLevel: category.expertiseLevel
290
+ };
291
+ }));
292
+ test.categories = processedCategories;
293
+ }
294
+ if (questions) {
295
+ // Vérifier que toutes les questions existent
296
+ const questionIds = questions.map((q) => q.questionId);
297
+ const existingQuestions = await TestQuestion.find({ _id: { $in: questionIds } });
298
+ if (existingQuestions.length !== questionIds.length) {
299
+ return res.status(400).json({
300
+ message: 'Certaines questions spécifiées n\'existent pas',
301
+ providedQuestions: questionIds.length,
302
+ foundQuestions: existingQuestions.length
303
+ });
304
+ }
305
+ // Mettre à jour les questions avec leur ordre
306
+ test.questions = questions.map((q) => ({
307
+ questionId: q.questionId,
308
+ order: q.order || 0
309
+ }));
310
+ }
311
+ await test.save();
312
+ res.status(200).json({ message: 'Test modifié avec succès', data: test });
313
+ }
314
+ catch (err) {
315
+ console.error('Erreur lors de la modification du test : ', err);
316
+ res.status(500).json({ message: 'Erreur interne du serveur' });
317
+ }
318
+ });
319
+ // Supprimer un test
320
+ this.delete('/test/:id', authenticatedOptions, async (req, res) => {
321
+ const { id } = req.params;
322
+ try {
323
+ const test = await Test.findById(id);
324
+ if (!test) {
325
+ return res.status(404).json({ message: 'Test not found' });
326
+ }
327
+ for (let i = 0; i < test.questions.length; i++) {
328
+ await TestQuestion.findByIdAndDelete(test.questions[i].questionId);
329
+ }
330
+ await TestResult.deleteMany({ testId: id });
331
+ await Test.findByIdAndDelete(id);
332
+ res.status(200).json({ message: 'test deleted with sucess' });
333
+ }
334
+ catch (err) {
335
+ console.error('error when deleting user : ', err);
336
+ res.status(500).json({ message: 'Internal server error' });
337
+ }
338
+ });
339
+ // Obtenir un test par son ID
340
+ this.get('/test/:id', authenticatedOptions, async (req, res) => {
341
+ const { id } = req.params;
342
+ try {
343
+ const test = await Test.findById(id);
344
+ if (!test) {
345
+ return res.status(404).json({ message: 'no test founded with this id' });
346
+ }
347
+ // Migration automatique si nécessaire
348
+ await migrateTestIfNeeded(test);
349
+ const questions = [];
350
+ for (const questionRef of test.questions) {
351
+ console.log(questionRef);
352
+ const question = await TestQuestion.findById(questionRef.questionId);
353
+ if (question) {
354
+ console.log(question);
355
+ questions.push(question);
356
+ }
357
+ }
358
+ // Récupérer le nom du job pour l'affichage
359
+ const testObj = test.toObject();
360
+ testObj.targetJobName = await getJobName(testObj.targetJob);
361
+ res.status(200).json({ test: testObj, questions });
362
+ }
363
+ catch (err) {
364
+ console.error('error when geting test : ', err);
365
+ res.status(500).json({ message: 'Internal server error' });
366
+ }
367
+ });
368
+ // Lister tous les tests
369
+ this.get('/', authenticatedOptions, async (req, res) => {
370
+ try {
371
+ const page = parseInt(req.query.page) || 1;
372
+ const limit = parseInt(req.query.limit) || 10;
373
+ const skip = (page - 1) * limit;
374
+ const search = req.query.search || '';
375
+ const targetJob = req.query.targetJob || 'all';
376
+ const seniorityLevel = req.query.seniorityLevel || 'all';
377
+ const state = req.query.state || 'all';
378
+ const sortBy = req.query.sortBy || 'updatedAt';
379
+ const sortOrder = req.query.sortOrder || 'desc';
380
+ // Construction de la requête de recherche
381
+ const query = {};
382
+ // Filtres
383
+ if (targetJob !== 'all') {
384
+ // Si on filtre par targetJob, on cherche d'abord le TestJob correspondant
385
+ const jobType = await TestJob.findOne({ name: targetJob });
386
+ if (jobType) {
387
+ query.targetJob = jobType._id;
388
+ }
389
+ else {
390
+ // Si le job n'existe pas, on ne retourne aucun résultat
391
+ query.targetJob = null;
392
+ }
393
+ }
394
+ if (seniorityLevel !== 'all') {
395
+ query.seniorityLevel = seniorityLevel;
396
+ }
397
+ if (state !== 'all') {
398
+ query.state = state;
399
+ }
400
+ // Recherche sur testName et targetJob
401
+ if (search) {
402
+ // Pour la recherche sur targetJob, on cherche d'abord les jobs qui correspondent
403
+ const matchingJobs = await TestJob.find({ name: { $regex: search, $options: 'i' } });
404
+ const jobIds = matchingJobs.map(job => job._id);
405
+ query.$or = [
406
+ { title: { $regex: search, $options: 'i' } },
407
+ { description: { $regex: search, $options: 'i' } },
408
+ { targetJob: { $in: jobIds } },
409
+ { seniorityLevel: { $regex: search, $options: 'i' } }
410
+ ];
411
+ }
412
+ // Construction du tri
413
+ const allowedSortFields = ['testName', 'targetJob', 'seniorityLevel', 'updatedAt'];
414
+ const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'updatedAt';
415
+ const sortOptions = {
416
+ [sortField]: sortOrder === 'asc' ? 1 : -1
417
+ };
418
+ const [tests, total] = await Promise.all([
419
+ Test.find(query)
420
+ .sort(sortOptions)
421
+ .skip(skip)
422
+ .limit(limit)
423
+ .exec(),
424
+ Test.countDocuments(query)
425
+ ]);
426
+ // Récupérer les noms des catégories et des jobs pour chaque test
427
+ const testsWithCategories = await Promise.all(tests.map(async (test) => {
428
+ // Migration automatique si nécessaire
429
+ await migrateTestIfNeeded(test);
430
+ const testObj = test.toObject();
431
+ // Récupérer le nom du job
432
+ testObj.targetJobName = await getJobName(testObj.targetJob);
433
+ if (testObj.categories && testObj.categories.length > 0) {
434
+ const categoriesWithNames = await Promise.all(testObj.categories.map(async (category) => {
435
+ const categoryDoc = await TestCategory.findById(category.categoryId);
436
+ return {
437
+ ...category,
438
+ categoryName: categoryDoc?.name || 'Catégorie inconnue'
439
+ };
440
+ }));
441
+ testObj.categories = categoriesWithNames;
442
+ }
443
+ return testObj;
444
+ }));
445
+ const totalPages = Math.ceil(total / limit);
446
+ return res.json({
447
+ data: testsWithCategories,
448
+ pagination: {
449
+ currentPage: page,
450
+ totalPages,
451
+ totalItems: total,
452
+ itemsPerPage: limit,
453
+ hasNextPage: page < totalPages,
454
+ hasPreviousPage: page > 1
455
+ }
456
+ });
457
+ }
458
+ catch (err) {
459
+ console.error('error when geting tests : ', err);
460
+ res.status(500).json({ message: 'Internal server error' });
461
+ }
462
+ });
463
+ // Supprimer une catégorie d'un test
464
+ this.delete('/test/removeCategory/:testId', authenticatedOptions, async (req, res) => {
465
+ const { testId } = req.params;
466
+ const { categoryName } = req.body;
467
+ try {
468
+ const category = await TestCategory.findOne({ name: categoryName });
469
+ if (!category)
470
+ return res.status(404).json({ message: 'Category not found' });
471
+ const test = await Test.findByIdAndUpdate(testId, { $pull: { categories: { categoryId: category._id } } }, { new: true });
472
+ if (!test)
473
+ return res.status(404).json({ message: 'Test not found' });
474
+ res.status(200).json({ message: 'Category removed', test });
475
+ }
476
+ catch (err) {
477
+ console.error('Error when removing category from test:', err);
478
+ res.status(500).json({ message: 'Internal server error' });
479
+ }
480
+ });
481
+ // Ajouter une catégorie à un test
482
+ this.put('/test/addCategory/:testId', authenticatedOptions, async (req, res) => {
483
+ const { testId } = req.params;
484
+ const { categoryName, expertiseLevel } = req.body;
485
+ try {
486
+ let category = await TestCategory.findOne({ name: categoryName });
487
+ if (!category) {
488
+ category = new TestCategory({ name: categoryName });
489
+ await category.save();
490
+ }
491
+ const test = await Test.findById(testId);
492
+ if (!test) {
493
+ return res.status(404).json({ message: 'Test not found' });
494
+ }
495
+ const categoryExists = test.categories.some(cat => cat.categoryId.equals(category._id));
496
+ if (categoryExists) {
497
+ return res.status(200).json({ message: 'Category already exists in the test' });
498
+ }
499
+ test.categories.push({ categoryId: category._id, expertiseLevel });
500
+ await test.save();
501
+ res.status(200).json({ message: 'Category added successfully', data: test });
502
+ }
503
+ catch (err) {
504
+ console.error('Error when adding category to test:', err);
505
+ res.status(500).json({ message: 'Internal server error' });
506
+ }
507
+ });
508
+ // Obtenir une question par son ID
509
+ this.get('/test/question/:questionId', authenticatedOptions, async (req, res) => {
510
+ const { questionId } = req.params;
511
+ const question = await TestQuestion.findById(questionId);
512
+ if (!question) {
513
+ return res.status(404).json({ message: 'no question founded with this id' });
514
+ }
515
+ res.status(200).json({ data: question });
516
+ });
517
+ // Obtenir toutes les questions d'un test
518
+ this.get('/test/questions/:testId', authenticatedOptions, async (req, res) => {
519
+ const { testId } = req.params;
520
+ try {
521
+ const test = await Test.findById(testId);
522
+ if (!test) {
523
+ return res.status(404).json({ message: 'Test not found' });
524
+ }
525
+ const questions = [];
526
+ for (const questionId of test.questions) {
527
+ const question = await TestQuestion.findById(questionId);
528
+ if (question) {
529
+ questions.push(question);
530
+ }
531
+ }
532
+ res.status(200).json({ array: questions });
533
+ }
534
+ catch (err) {
535
+ console.error('Error when getting question:', err);
536
+ res.status(500).json({ message: 'Internal server error' });
537
+ }
538
+ });
539
+ // Supprimer une question d'un test
540
+ this.delete('/test/question/:testId/:questionId', authenticatedOptions, async (req, res) => {
541
+ const { testId, questionId } = req.params;
542
+ const question = await TestQuestion.findByIdAndDelete(questionId);
543
+ const test = await Test.findById(testId);
544
+ if (!question) {
545
+ return res.status(404).json({ message: 'no question founded with this id' });
546
+ }
547
+ if (!test) {
548
+ return res.status(404).json({ message: 'no test founded with this id' });
549
+ }
550
+ // Supprimer la question du tableau questions en filtrant par questionId
551
+ test.questions = test.questions.filter(q => q.questionId.toString() !== questionId);
552
+ // Recalculer les ordres pour que ça se suive
553
+ test.questions.forEach((q, index) => {
554
+ q.order = index + 1;
555
+ });
556
+ await test.save();
557
+ res.status(200).json({ message: 'question deleted with sucess' });
558
+ });
559
+ // Supprimer toutes les questions d'un test
560
+ this.delete('/test/questions/:testId', authenticatedOptions, async (req, res) => {
561
+ const { testId } = req.params;
562
+ const test = await Test.findById(testId);
563
+ if (!test) {
564
+ return res.status(404).json({ message: 'no test founded with this id' });
565
+ }
566
+ for (const questionId of test.questions) {
567
+ await TestQuestion.findByIdAndDelete(questionId);
568
+ }
569
+ test.questions = [];
570
+ await test.save();
571
+ res.status(200).json({ message: 'questions deleted with sucess' });
572
+ });
573
+ // Modifier une question
574
+ this.put('/test/modifyQuestion/:id', authenticatedOptions, async (req, res) => {
575
+ const { id } = req.params;
576
+ const { instruction, maxScore, time, possibleResponses, textType } = req.body;
577
+ try {
578
+ const question = await TestQuestion.findById(id);
579
+ if (!question) {
580
+ return res.status(404).json({ message: 'no question founded with this id' });
581
+ }
582
+ if (instruction) {
583
+ question.instruction = instruction;
584
+ }
585
+ if (maxScore) {
586
+ question.maxScore = maxScore;
587
+ }
588
+ if (time) {
589
+ question.time = time;
590
+ }
591
+ if (textType) {
592
+ question.textType = textType;
593
+ }
594
+ if (possibleResponses) {
595
+ question.possibleResponses = possibleResponses;
596
+ }
597
+ await question.save();
598
+ res.status(200).json({ message: 'question modified with sucess' });
599
+ }
600
+ catch (err) {
601
+ console.error('error when modify question : ', err);
602
+ res.status(500).json({ message: 'Internal server error' });
603
+ }
604
+ });
605
+ // Ajouter une question à un test
606
+ this.put('/test/addCustomQuestion/:id', authenticatedOptions, async (req, res) => {
607
+ const { id } = req.params;
608
+ const { questionType, instruction, maxScore, time } = req.body;
609
+ try {
610
+ const test = await Test.findById(id);
611
+ if (!test) {
612
+ return res.status(404).json({ message: 'no test founded with this id' });
613
+ }
614
+ const question = new TestQuestion({
615
+ questionType,
616
+ instruction,
617
+ maxScore,
618
+ time
619
+ });
620
+ await question.save();
621
+ test.questions.push({ questionId: question._id, order: test.questions.length });
622
+ await test.save();
623
+ res.status(200).json({ message: 'question added in test', test });
624
+ }
625
+ catch (err) {
626
+ console.error('error when add question in test : ', err);
627
+ res.status(500).json({ message: 'Internal server error' });
628
+ }
629
+ });
630
+ // Ajouter une question à un test
631
+ this.put('/test/addQuestion/:id', authenticatedOptions, async (req, res) => {
632
+ const { id } = req.params;
633
+ const { questionType, category, expertiseLevel } = req.body;
634
+ try {
635
+ const test = await Test.findById(id);
636
+ if (!test) {
637
+ return res.status(404).json({ message: 'no test founded with this id' });
638
+ }
639
+ const otherQuestionsIds = test.questions.map(question => question.questionId);
640
+ const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
641
+ const jobName = await getJobName(test.targetJob);
642
+ const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
643
+ job: jobName,
644
+ seniority: test.seniorityLevel,
645
+ questionType,
646
+ category,
647
+ expertiseLevel,
648
+ otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
649
+ }, true);
650
+ const question = new TestQuestion(JSON.parse(generatedQuestion));
651
+ await question.save();
652
+ test.questions.push({ questionId: question._id, order: test.questions.length });
653
+ await test.save();
654
+ res.status(200).json({ message: 'question added in test', test });
655
+ }
656
+ catch (err) {
657
+ console.error('error when add question in test : ', err);
658
+ res.status(500).json({ message: 'Internal server error' });
659
+ }
660
+ });
661
+ // Mélanger les questions d'un test
662
+ this.get('/test/shuffle/:testId', authenticatedOptions, async (req, res) => {
663
+ const { testId } = req.params;
664
+ try {
665
+ const test = await Test.findById(testId);
666
+ if (!test) {
667
+ return res.status(404).json({ message: 'Test not found' });
668
+ }
669
+ for (let i = test.questions.length - 1; i > 0; i--) {
670
+ const j = Math.floor(Math.random() * (i + 1));
671
+ [test.questions[i], test.questions[j]] = [test.questions[j], test.questions[i]];
672
+ }
673
+ await test.save();
674
+ res.status(200).json({ message: 'Questions shuffled', test });
675
+ }
676
+ catch (err) {
677
+ console.error('Error when shuffling questions:', err);
678
+ res.status(500).json({ message: 'Internal server error' });
679
+ }
680
+ });
681
+ // Ajouter un texte d'invitation à un test
682
+ this.put('/test/addInvitationText/:id', authenticatedOptions, async (req, res) => {
683
+ const { id } = req.params;
684
+ const { invitationText } = req.body;
685
+ try {
686
+ const test = await Test.findById(id);
687
+ if (!test) {
688
+ return res.status(404).json({ message: 'no test founded with this id' });
689
+ }
690
+ test.invitationText = invitationText;
691
+ await test.save();
692
+ res.status(200).json({
693
+ message: 'invitation text added in test',
694
+ invitationText
695
+ });
696
+ }
697
+ catch (err) {
698
+ console.error('error when add invitation text in test : ', err);
699
+ res.status(500).json({ message: 'Internal server error' });
700
+ }
701
+ });
702
+ // Obtenir un résultat par son ID
703
+ this.get('/result/:id', authenticatedOptions, async (req, res) => {
704
+ const { id } = req.params;
705
+ try {
706
+ const result = await TestResult.findById(id);
707
+ if (!result) {
708
+ return res.status(404).json({ message: 'no result founded with this id' });
709
+ }
710
+ res.status(200).json({ message: 'result', data: result });
711
+ }
712
+ catch (err) {
713
+ console.error('error when geting result : ', err);
714
+ res.status(500).json({ message: 'Internal server error' });
715
+ }
716
+ });
717
+ // Lister tous les résultats
718
+ this.get('/results/', authenticatedOptions, async (req, res) => {
719
+ try {
720
+ const results = await TestResult.find();
721
+ if (!results) {
722
+ return res.status(404).json({ message: 'no results founded' });
723
+ }
724
+ res.status(200).json({ array: results });
725
+ }
726
+ catch (err) {
727
+ console.error('error when geting results : ', err);
728
+ res.status(500).json({ message: 'Internal server error' });
729
+ }
730
+ });
731
+ // Créer un résultat
732
+ this.post('/invite', authenticatedOptions, async (req, res) => {
733
+ const { candidateId, testId } = req.body;
734
+ if (!candidateId || !testId) {
735
+ return res.status(400).json({ message: 'Error, all params are required' });
736
+ }
737
+ try {
738
+ const test = await Test.findById(testId);
739
+ if (!test) {
740
+ return res.status(404).json({ message: 'Test not found' });
741
+ }
742
+ const categories = test.categories.map(cat => ({ categoryId: cat.categoryId }));
743
+ const newResult = new TestResult({
744
+ candidateId,
745
+ testId,
746
+ categories,
747
+ state: 'pending',
748
+ invitationDate: Date.now()
749
+ });
750
+ await newResult.save();
751
+ // Récupérer l'email du candidat
752
+ const candidate = await Candidate.findById(candidateId);
753
+ if (!candidate) {
754
+ return res.status(404).json({ message: 'Candidate not found' });
755
+ }
756
+ // Récupérer le contact pour obtenir l'email
757
+ const contact = await ContactModel.findById(candidate.contact);
758
+ if (!contact) {
759
+ return res.status(404).json({ message: 'Contact not found' });
760
+ }
761
+ const email = contact.email;
762
+ // Construire le lien d'invitation
763
+ const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
764
+ // Récupérer les credentials d'envoi
765
+ const emailUser = process.env.EMAIL_USER;
766
+ const emailPassword = process.env.EMAIL_PASSWORD;
767
+ // Envoyer l'email via l'event emitter
768
+ await emitter.emit(eventTypes.SEND_EMAIL, {
769
+ template: 'test-invitation',
770
+ to: email,
771
+ from: emailUser,
772
+ emailUser,
773
+ emailPassword,
774
+ data: {
775
+ firstname: contact.firstname,
776
+ testName: test?.title || '',
777
+ testLink
778
+ }
779
+ });
780
+ res.status(201).json({ message: 'result created with sucess', data: newResult });
781
+ }
782
+ catch (err) {
783
+ console.error('error when creating result : ', err);
784
+ res.status(500).json({ message: 'Internal server error' });
785
+ }
786
+ });
787
+ // Obtenir la question suivante
788
+ this.get('/result/getNextQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
789
+ const { id, idCurrentQuestion } = req.params;
790
+ try {
791
+ const result = await TestResult.findById(id);
792
+ if (!result) {
793
+ return res.status(404).json({ message: 'Result not found' });
794
+ }
795
+ const test = await Test.findById(result.testId);
796
+ if (!test) {
797
+ return res.status(404).json({ message: 'Test not found' });
798
+ }
799
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
800
+ if (questionIndex < test.questions.length) {
801
+ const nextQuestion = test.questions[questionIndex + 1];
802
+ res.status(200).json({ data: nextQuestion });
803
+ }
804
+ else {
805
+ res.status(200).json({ data: null });
806
+ }
807
+ }
808
+ catch (err) {
809
+ console.error('error when geting the next question : ', err);
810
+ res.status(500).json({ message: 'Internal server error' });
811
+ }
812
+ });
813
+ // Vérifier si c'est la dernière question
814
+ this.get('/result/isLastQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
815
+ const { id, idCurrentQuestion } = req.params;
816
+ try {
817
+ const result = await TestResult.findById(id);
818
+ if (!result) {
819
+ return res.status(404).json({ message: 'Result not found' });
820
+ }
821
+ const test = await Test.findById(result.testId);
822
+ if (!test) {
823
+ return res.status(404).json({ message: 'Test not found' });
824
+ }
825
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
826
+ if (questionIndex === test.questions.length - 1) {
827
+ res.status(200).json({ data: true });
828
+ }
829
+ else {
830
+ res.status(200).json({ data: false });
831
+ }
832
+ }
833
+ catch (err) {
834
+ console.error('error when geting the next question : ', err);
835
+ res.status(500).json({ message: 'Internal server error' });
836
+ }
837
+ });
838
+ // Obtenir une question
839
+ this.get('/result/question/:questionId', authenticatedOptions, async (req, res) => {
840
+ const { questionId } = req.params;
841
+ try {
842
+ const question = await TestQuestion.findById(questionId);
843
+ if (!question) {
844
+ return res.status(404).json({ message: 'not found' });
845
+ }
846
+ res.status(200).json({ data: question });
847
+ }
848
+ catch (err) {
849
+ console.error('error when geting the question : ', err);
850
+ res.status(500).json({ message: 'Internal server error' });
851
+ }
852
+ });
853
+ // Envoyer une réponse
854
+ this.put('/result/sendResponse/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
855
+ const { id, idCurrentQuestion } = req.params;
856
+ const { candidateResponse } = req.body;
857
+ try {
858
+ const result = await TestResult.findById(id);
859
+ if (!result) {
860
+ return res.status(404).json({ message: 'Result not found' });
861
+ }
862
+ const test = await Test.findById(result.testId);
863
+ if (!test) {
864
+ return res.status(404).json({ message: 'Test not found' });
865
+ }
866
+ if (!result.responses) {
867
+ result.state = 'inProgress';
868
+ result.responses = [];
869
+ }
870
+ result.responses.push({
871
+ questionId: idCurrentQuestion,
872
+ response: candidateResponse,
873
+ score: 0,
874
+ comment: ' '
875
+ });
876
+ await result.save();
877
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
878
+ if (questionIndex === test.questions.length - 1) {
879
+ emitter.emit(eventTypes.CORRECT_TEST, result);
880
+ result.state = 'finish';
881
+ await result.save();
882
+ }
883
+ res.status(200).json({ response: candidateResponse });
884
+ }
885
+ catch (err) {
886
+ console.error('error when sending result : ', err);
887
+ res.status(500).json({ message: 'Internal server error' });
888
+ }
889
+ });
890
+ // Corriger un test
891
+ this.post('/result/correct/:id', authenticatedOptions, async (req, res) => {
892
+ const { id } = req.params;
893
+ try {
894
+ const result = await TestResult.findById(id);
895
+ if (!result) {
896
+ return res.status(404).json({ message: 'Result not found' });
897
+ }
898
+ emitter.emit(eventTypes.CORRECT_TEST, result);
899
+ res.status(200).json({ message: 'Result in correction' });
900
+ }
901
+ catch (err) {
902
+ console.error('error when correcting result : ', err);
903
+ res.status(500).json({ message: 'Internal server error' });
904
+ }
905
+ });
906
+ // Calculer le score
907
+ this.put('/result/calculateScore/:id', authenticatedOptions, async (req, res) => {
908
+ const { id } = req.params;
909
+ try {
910
+ const result = await TestResult.findById(id);
911
+ if (!result) {
912
+ return res.status(404).json({ message: 'Result not found' });
913
+ }
914
+ result.state = 'finish';
915
+ let finalscore = 0;
916
+ for (const response of result.responses) {
917
+ const question = await TestQuestion.findById(response.questionId);
918
+ if (!question)
919
+ continue;
920
+ const score = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
921
+ question: {
922
+ _id: question._id.toString(),
923
+ instruction: question.instruction,
924
+ possibleResponses: question.possibleResponses,
925
+ questionType: question.questionType,
926
+ maxScore: question.maxScore
927
+ },
928
+ result: {
929
+ responses: [{
930
+ questionId: response.questionId.toString(),
931
+ response: response.response
932
+ }]
933
+ }
934
+ }, true);
935
+ const parsedResult = JSON.parse(score);
936
+ finalscore += parsedResult.score;
937
+ response.score = parsedResult.score;
938
+ response.comment = parsedResult.comment;
939
+ }
940
+ result.score = finalscore;
941
+ await result.save();
942
+ res.status(200).json({ data: finalscore });
943
+ }
944
+ catch (err) {
945
+ console.error('error when calculate the score : ', err);
946
+ res.status(500).json({ message: 'Internal server error' });
947
+ }
948
+ });
949
+ // Obtenir le score maximum
950
+ this.get('/maxscore/:resultId', authenticatedOptions, async (req, res) => {
951
+ const { resultId } = req.params;
952
+ try {
953
+ const result = await TestResult.findById(resultId);
954
+ if (!result) {
955
+ return res.status(404).json({ message: 'Result not found' });
956
+ }
957
+ const test = await Test.findById(result.testId);
958
+ if (!test) {
959
+ return res.status(404).json({ message: 'Test not found' });
960
+ }
961
+ let maxScore = 0;
962
+ for (const questionId of test.questions) {
963
+ const question = await TestQuestion.findById(questionId);
964
+ if (question) {
965
+ maxScore += question.maxScore;
966
+ }
967
+ }
968
+ res.status(200).json({ data: maxScore });
969
+ }
970
+ catch (err) {
971
+ console.error('error when geting score : ', err);
972
+ res.status(500).json({ message: 'Internal server error' });
973
+ }
974
+ });
975
+ // Obtenir le score d'un résultat
976
+ this.get('/result/score/:id', authenticatedOptions, async (req, res) => {
977
+ const { id } = req.params;
978
+ try {
979
+ const result = await TestResult.findById(id);
980
+ if (!result) {
981
+ return res.status(404).json({ message: 'Result not found' });
982
+ }
983
+ res.status(200).json({ data: result.score });
984
+ }
985
+ catch (err) {
986
+ console.error('error when geting score : ', err);
987
+ res.status(500).json({ message: 'Internal server error' });
988
+ }
989
+ });
990
+ // Générer plusieurs questions pour un test
991
+ this.put('/test/generateQuestions/:id', authenticatedOptions, async (req, res) => {
992
+ const { id } = req.params;
993
+ const { numberOfQuestions, category } = req.body;
994
+ if (!numberOfQuestions || numberOfQuestions <= 0) {
995
+ return res.status(400).json({ message: 'Le nombre de questions doit être positif' });
996
+ }
997
+ try {
998
+ const test = await Test.findById(id);
999
+ if (!test) {
1000
+ return res.status(404).json({ message: 'Test non trouvé' });
1001
+ }
1002
+ let categoriesToUse = [];
1003
+ if (category && category !== 'ALL') {
1004
+ const categoryInfo = test.categories.find(cat => cat.categoryId.toString() === category);
1005
+ if (categoryInfo) {
1006
+ categoriesToUse = [{
1007
+ categoryId: categoryInfo.categoryId.toString(),
1008
+ expertiseLevel: categoryInfo.expertiseLevel.toString()
1009
+ }];
1010
+ }
1011
+ }
1012
+ else {
1013
+ // Si category est 'ALL' ou absent, on utilise toutes les catégories du test
1014
+ categoriesToUse = test.categories.map(cat => ({
1015
+ categoryId: cat.categoryId.toString(),
1016
+ expertiseLevel: cat.expertiseLevel.toString()
1017
+ }));
1018
+ }
1019
+ if (categoriesToUse.length === 0) {
1020
+ return res.status(400).json({ message: 'Aucune catégorie disponible pour générer des questions' });
1021
+ }
1022
+ const generatedQuestions = [];
1023
+ let questionsGenerated = 0;
1024
+ let attempts = 0;
1025
+ const maxAttempts = numberOfQuestions * 3; // Limite pour éviter les boucles infinies
1026
+ // Si on spécifie une catégorie, on génère toutes les questions pour cette catégorie
1027
+ if (category && category !== 'ALL') {
1028
+ const categoryInfo = categoriesToUse[0];
1029
+ while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
1030
+ attempts++;
1031
+ const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
1032
+ if (question) {
1033
+ generatedQuestions.push(question);
1034
+ questionsGenerated++;
1035
+ }
1036
+ }
1037
+ }
1038
+ else {
1039
+ // Pour ALL, répartition aléatoire sur toutes les catégories
1040
+ const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
1041
+ while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
1042
+ attempts++;
1043
+ // Sélectionner une catégorie aléatoire
1044
+ const randomCategoryIndex = Math.floor(Math.random() * shuffledCategories.length);
1045
+ const categoryInfo = shuffledCategories[randomCategoryIndex];
1046
+ const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
1047
+ if (question) {
1048
+ generatedQuestions.push(question);
1049
+ questionsGenerated++;
1050
+ }
1051
+ }
1052
+ }
1053
+ // Vérifier qu'au moins une question a été générée
1054
+ if (generatedQuestions.length === 0) {
1055
+ return res.status(500).json({
1056
+ message: 'Aucune question n\'a pu être générée. Veuillez réessayer plus tard.'
1057
+ });
1058
+ }
1059
+ res.status(200).json({
1060
+ message: `${generatedQuestions.length} question(s) générée(s) avec succès`,
1061
+ questions: generatedQuestions,
1062
+ test
1063
+ });
1064
+ }
1065
+ catch (err) {
1066
+ console.error('Erreur lors de la génération des questions : ', err);
1067
+ res.status(500).json({ message: 'Erreur interne du serveur' });
1068
+ }
1069
+ });
1070
+ // Lister tous les candidats invités à un test
1071
+ this.get('/test/:testId/candidates', authenticatedOptions, async (req, res) => {
1072
+ const { testId } = req.params;
1073
+ const page = parseInt(req.query.page) || 1;
1074
+ const limit = parseInt(req.query.limit) || 10;
1075
+ const skip = (page - 1) * limit;
1076
+ const search = req.query.search || '';
1077
+ const state = req.query.state || 'all';
1078
+ const sortBy = req.query.sortBy || 'invitationDate';
1079
+ const sortOrder = req.query.sortOrder || 'desc';
1080
+ try {
1081
+ const test = await Test.findById(testId);
1082
+ if (!test) {
1083
+ return res.status(404).json({ message: 'Test non trouvé' });
1084
+ }
1085
+ // Construction de la requête
1086
+ const query = { testId };
1087
+ if (state !== 'all') {
1088
+ query.state = state;
1089
+ }
1090
+ // Recherche sur les candidats via leurs contacts
1091
+ if (search) {
1092
+ // D'abord, rechercher dans les contacts
1093
+ const contacts = await ContactModel.find({
1094
+ $or: [
1095
+ { firstname: { $regex: search, $options: 'i' } },
1096
+ { lastname: { $regex: search, $options: 'i' } },
1097
+ { email: { $regex: search, $options: 'i' } }
1098
+ ]
1099
+ });
1100
+ // Ensuite, récupérer les candidats qui ont ces contacts
1101
+ const contactIds = contacts.map(c => c._id);
1102
+ const candidates = await Candidate.find({
1103
+ contact: { $in: contactIds }
1104
+ });
1105
+ const candidateIds = candidates.map(c => c._id);
1106
+ query.candidateId = { $in: candidateIds };
1107
+ }
1108
+ // Déterminer l'ordre de tri
1109
+ const sortDirection = sortOrder === 'asc' ? 1 : -1;
1110
+ // Si on trie par lastName, on récupère tous les résultats puis on trie après
1111
+ // Sinon on peut trier directement dans la requête MongoDB
1112
+ let results, total;
1113
+ if (sortBy === 'lastName') {
1114
+ // Récupérer tous les résultats sans pagination pour pouvoir trier par lastName
1115
+ const allResults = await TestResult.find(query).exec();
1116
+ total = allResults.length;
1117
+ // Récupérer les données des candidats pour le tri
1118
+ const candidateIds = allResults.map(result => result.candidateId);
1119
+ const candidates = await Candidate.find({ _id: { $in: candidateIds } });
1120
+ const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
1121
+ // Combiner les résultats avec les données des candidats et trier
1122
+ const resultsWithCandidates = await Promise.all(allResults.map(async (result) => {
1123
+ const candidate = candidatesMap.get(result.candidateId.toString());
1124
+ if (!candidate) {
1125
+ return {
1126
+ ...result.toObject(),
1127
+ candidate: null,
1128
+ lastName: ''
1129
+ };
1130
+ }
1131
+ const contact = await ContactModel.findById(candidate.contact);
1132
+ return {
1133
+ ...result.toObject(),
1134
+ candidate: contact
1135
+ ? {
1136
+ firstName: contact.firstname,
1137
+ lastName: contact.lastname,
1138
+ email: contact.email
1139
+ }
1140
+ : null,
1141
+ lastName: contact ? contact.lastname : ''
1142
+ };
1143
+ }));
1144
+ // Trier par lastName
1145
+ resultsWithCandidates.sort((a, b) => {
1146
+ const lastNameA = (a.lastName || '').toLowerCase();
1147
+ const lastNameB = (b.lastName || '').toLowerCase();
1148
+ return sortDirection === 1
1149
+ ? lastNameA.localeCompare(lastNameB)
1150
+ : lastNameB.localeCompare(lastNameA);
1151
+ });
1152
+ // Appliquer la pagination
1153
+ results = resultsWithCandidates.slice(skip, skip + limit);
1154
+ }
1155
+ else {
1156
+ // Tri direct dans MongoDB pour invitationDate
1157
+ const sortObject = {};
1158
+ sortObject[sortBy] = sortDirection;
1159
+ [results, total] = await Promise.all([
1160
+ TestResult.find(query)
1161
+ .sort(sortObject)
1162
+ .skip(skip)
1163
+ .limit(limit)
1164
+ .exec(),
1165
+ TestResult.countDocuments(query)
1166
+ ]);
1167
+ }
1168
+ // Calculer le maxScore du test
1169
+ let maxScore = 0;
1170
+ if (test.questions && test.questions.length > 0) {
1171
+ const questionIds = test.questions.map((q) => q.questionId || q);
1172
+ const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
1173
+ maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
1174
+ }
1175
+ // Si on a déjà traité les candidats pour le tri par lastName, on utilise directement les résultats
1176
+ let resultsWithCandidates;
1177
+ if (sortBy === 'lastName') {
1178
+ // Les résultats sont déjà traités avec les données des candidats
1179
+ resultsWithCandidates = results.map(result => ({
1180
+ ...result,
1181
+ maxScore
1182
+ }));
1183
+ }
1184
+ else {
1185
+ // Récupérer les données des candidats
1186
+ const candidateIds = results.map(result => result.candidateId);
1187
+ const candidates = await Candidate.find({ _id: { $in: candidateIds } });
1188
+ const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
1189
+ // Combiner les résultats avec les données des candidats
1190
+ resultsWithCandidates = await Promise.all(results.map(async (result) => {
1191
+ const candidate = candidatesMap.get(result.candidateId.toString());
1192
+ if (!candidate) {
1193
+ return {
1194
+ ...result.toObject(),
1195
+ candidate: null,
1196
+ maxScore
1197
+ };
1198
+ }
1199
+ // Récupérer le contact pour obtenir les informations personnelles
1200
+ const contact = await ContactModel.findById(candidate.contact);
1201
+ return {
1202
+ ...result.toObject(),
1203
+ candidate: contact
1204
+ ? {
1205
+ firstName: contact.firstname,
1206
+ lastName: contact.lastname,
1207
+ email: contact.email
1208
+ }
1209
+ : null,
1210
+ maxScore
1211
+ };
1212
+ }));
1213
+ }
1214
+ const totalPages = Math.ceil(total / limit);
1215
+ return res.json({
1216
+ data: resultsWithCandidates,
1217
+ pagination: {
1218
+ currentPage: page,
1219
+ totalPages,
1220
+ totalItems: total,
1221
+ itemsPerPage: limit,
1222
+ hasNextPage: page < totalPages,
1223
+ hasPreviousPage: page > 1
1224
+ }
1225
+ });
1226
+ }
1227
+ catch (err) {
1228
+ console.error('Erreur lors de la récupération des candidats : ', err);
1229
+ res.status(500).json({ message: 'Erreur interne du serveur' });
1230
+ }
1231
+ });
1232
+ // Renvoyer l'email d'invitation à un candidat
1233
+ this.post('/reinvite/:resultId', authenticatedOptions, async (req, res) => {
1234
+ const { resultId } = req.params;
1235
+ try {
1236
+ const result = await TestResult.findById(resultId);
1237
+ if (!result) {
1238
+ return res.status(404).json({ message: 'Result not found' });
1239
+ }
1240
+ // Récupérer le candidat et son contact
1241
+ const candidate = await Candidate.findById(result.candidateId);
1242
+ if (!candidate) {
1243
+ return res.status(404).json({ message: 'Candidate not found' });
1244
+ }
1245
+ // Récupérer le contact pour obtenir l'email
1246
+ const contact = await ContactModel.findById(candidate.contact);
1247
+ if (!contact) {
1248
+ return res.status(404).json({ message: 'Contact not found' });
1249
+ }
1250
+ // Récupérer les informations du test
1251
+ const test = await Test.findById(result.testId);
1252
+ if (!test) {
1253
+ return res.status(404).json({ message: 'Test not found' });
1254
+ }
1255
+ const email = contact.email;
1256
+ const emailUser = process.env.EMAIL_USER;
1257
+ const emailPassword = process.env.EMAIL_PASSWORD;
1258
+ // Construire le lien d'invitation
1259
+ const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
1260
+ // Envoyer l'email via l'event emitter
1261
+ await emitter.emit(eventTypes.SEND_EMAIL, {
1262
+ template: 'test-invitation',
1263
+ to: email,
1264
+ from: emailUser,
1265
+ emailUser,
1266
+ emailPassword,
1267
+ data: {
1268
+ testLink
1269
+ }
1270
+ });
1271
+ // Mettre à jour la date d'invitation
1272
+ result.set('invitationDate', new Date());
1273
+ await result.save();
1274
+ res.status(200).json({ message: 'Invitation email sent successfully' });
1275
+ }
1276
+ catch (err) {
1277
+ console.error('Error when resending invitation : ', err);
1278
+ res.status(500).json({ message: 'Internal server error' });
1279
+ }
1280
+ });
1281
+ // Correction manuelle d'une réponse à une question d'un testResult
1282
+ this.put('/result/:testResultId/response/:questionId', authenticatedOptions, async (req, res) => {
1283
+ try {
1284
+ const { testResultId, questionId } = req.params;
1285
+ const { score, comment } = req.body;
1286
+ // Récupérer le résultat de test
1287
+ const result = await TestResult.findById(testResultId);
1288
+ if (!result) {
1289
+ return res.status(404).json({ message: 'TestResult non trouvé' });
1290
+ }
1291
+ // Trouver la réponse à corriger
1292
+ const response = (result.responses || []).find((r) => r.questionId.toString() === questionId);
1293
+ if (!response) {
1294
+ return res.status(404).json({ message: 'Réponse à cette question non trouvée dans ce testResult' });
1295
+ }
1296
+ // Récupérer la question pour vérifier le maxScore
1297
+ const question = await TestQuestion.findById(questionId);
1298
+ if (!question) {
1299
+ return res.status(404).json({ message: 'Question non trouvée' });
1300
+ }
1301
+ const maxScore = question.maxScore;
1302
+ if (typeof score === 'number' && score > maxScore) {
1303
+ return res.status(400).json({ message: `Le score ne peut pas dépasser le maximum autorisé (${maxScore}) pour cette question.` });
1304
+ }
1305
+ // Surcharger le score et le commentaire
1306
+ if (typeof score === 'number')
1307
+ response.score = score;
1308
+ if (typeof comment === 'string')
1309
+ response.comment = comment;
1310
+ // Recalculer le score global
1311
+ result.score = (result.responses || []).reduce((sum, r) => sum + (r.score || 0), 0);
1312
+ await result.save();
1313
+ return res.status(200).json({
1314
+ message: 'Correction manuelle enregistrée',
1315
+ response,
1316
+ scoreGlobal: result.score
1317
+ });
1318
+ }
1319
+ catch (err) {
1320
+ console.error('Erreur lors de la correction manuelle :', err);
1321
+ res.status(500).json({ message: 'Erreur interne du serveur' });
1322
+ }
1323
+ });
1324
+ // Supprimer un test result
1325
+ this.delete('/result/:id', authenticatedOptions, async (req, res) => {
1326
+ const { id } = req.params;
1327
+ try {
1328
+ const testResult = await TestResult.findById(id);
1329
+ if (!testResult) {
1330
+ return res.status(404).json({ message: 'TestResult not found' });
1331
+ }
1332
+ await TestResult.findByIdAndDelete(id);
1333
+ res.status(200).json({ message: 'TestResult deleted with success' });
1334
+ }
1335
+ catch (err) {
1336
+ console.error('error when deleting testResult : ', err);
1337
+ res.status(500).json({ message: 'Internal server error' });
1338
+ }
1339
+ });
1340
+ }
1341
+ }
1342
+ const router = new ExamsRouter();
1343
+ export default router;