@programisto/edrm-exams 0.2.4 → 0.2.5
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.
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +11 -3
- package/dist/modules/edrm-exams/models/candidate.models.js +27 -11
- package/dist/modules/edrm-exams/models/contact.model.d.ts +14 -0
- package/dist/modules/edrm-exams/models/contact.model.js +60 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +164 -34
- package/dist/modules/edrm-exams/routes/exams.router.js +29 -9
- package/package.json +1 -1
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { EnduranceSchema } from '@programisto/endurance-core';
|
|
2
|
+
import { Types } from 'mongoose';
|
|
3
|
+
export declare enum ExperienceLevel {
|
|
4
|
+
JUNIOR = "JUNIOR",
|
|
5
|
+
INTERMEDIATE = "INTERMEDIATE",
|
|
6
|
+
SENIOR = "SENIOR",
|
|
7
|
+
EXPERT = "EXPERT"
|
|
8
|
+
}
|
|
2
9
|
declare class Candidate extends EnduranceSchema {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
contact: Types.ObjectId;
|
|
11
|
+
experienceLevel: string;
|
|
12
|
+
yearsOfExperience: number;
|
|
13
|
+
skills: string[];
|
|
6
14
|
magicLinkToken?: string;
|
|
7
15
|
magicLinkExpiresAt?: Date;
|
|
8
16
|
authToken?: string;
|
|
@@ -8,10 +8,22 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
10
|
import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance-core';
|
|
11
|
+
import { Types } from 'mongoose';
|
|
12
|
+
// Enum pour les niveaux d'expérience
|
|
13
|
+
/* eslint-disable no-unused-vars */
|
|
14
|
+
export var ExperienceLevel;
|
|
15
|
+
(function (ExperienceLevel) {
|
|
16
|
+
ExperienceLevel["JUNIOR"] = "JUNIOR";
|
|
17
|
+
ExperienceLevel["INTERMEDIATE"] = "INTERMEDIATE";
|
|
18
|
+
ExperienceLevel["SENIOR"] = "SENIOR";
|
|
19
|
+
ExperienceLevel["EXPERT"] = "EXPERT";
|
|
20
|
+
})(ExperienceLevel || (ExperienceLevel = {}));
|
|
21
|
+
/* eslint-enable no-unused-vars */
|
|
11
22
|
let Candidate = class Candidate extends EnduranceSchema {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
contact;
|
|
24
|
+
experienceLevel;
|
|
25
|
+
yearsOfExperience;
|
|
26
|
+
skills;
|
|
15
27
|
magicLinkToken;
|
|
16
28
|
magicLinkExpiresAt;
|
|
17
29
|
authToken;
|
|
@@ -21,17 +33,21 @@ let Candidate = class Candidate extends EnduranceSchema {
|
|
|
21
33
|
}
|
|
22
34
|
};
|
|
23
35
|
__decorate([
|
|
24
|
-
EnduranceModelType.prop({ required: true }),
|
|
25
|
-
__metadata("design:type",
|
|
26
|
-
], Candidate.prototype, "
|
|
36
|
+
EnduranceModelType.prop({ required: true, ref: 'Contact' }),
|
|
37
|
+
__metadata("design:type", Types.ObjectId)
|
|
38
|
+
], Candidate.prototype, "contact", void 0);
|
|
27
39
|
__decorate([
|
|
28
|
-
EnduranceModelType.prop({ required:
|
|
40
|
+
EnduranceModelType.prop({ required: false, enum: ExperienceLevel, default: ExperienceLevel.JUNIOR }),
|
|
29
41
|
__metadata("design:type", String)
|
|
30
|
-
], Candidate.prototype, "
|
|
42
|
+
], Candidate.prototype, "experienceLevel", void 0);
|
|
31
43
|
__decorate([
|
|
32
|
-
EnduranceModelType.prop({ required:
|
|
33
|
-
__metadata("design:type",
|
|
34
|
-
], Candidate.prototype, "
|
|
44
|
+
EnduranceModelType.prop({ required: false, type: Number, default: 0 }),
|
|
45
|
+
__metadata("design:type", Number)
|
|
46
|
+
], Candidate.prototype, "yearsOfExperience", void 0);
|
|
47
|
+
__decorate([
|
|
48
|
+
EnduranceModelType.prop({ type: [String], required: true }),
|
|
49
|
+
__metadata("design:type", Array)
|
|
50
|
+
], Candidate.prototype, "skills", void 0);
|
|
35
51
|
__decorate([
|
|
36
52
|
EnduranceModelType.prop({ required: false, type: String }),
|
|
37
53
|
__metadata("design:type", String)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EnduranceSchema } from '@programisto/endurance-core';
|
|
2
|
+
import { Types } from 'mongoose';
|
|
3
|
+
declare class Contact extends EnduranceSchema {
|
|
4
|
+
firstname: string;
|
|
5
|
+
lastname: string;
|
|
6
|
+
email: string;
|
|
7
|
+
phone?: string;
|
|
8
|
+
linkedin?: string;
|
|
9
|
+
city: string;
|
|
10
|
+
notes: Types.ObjectId[];
|
|
11
|
+
static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof Contact, import("@typegoose/typegoose/lib/types").BeAnObject>;
|
|
12
|
+
}
|
|
13
|
+
declare const ContactModel: import("@typegoose/typegoose").ReturnModelType<typeof Contact, import("@typegoose/typegoose/lib/types").BeAnObject>;
|
|
14
|
+
export default ContactModel;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance-core';
|
|
11
|
+
import { Types } from 'mongoose';
|
|
12
|
+
let Contact = class Contact extends EnduranceSchema {
|
|
13
|
+
firstname;
|
|
14
|
+
lastname;
|
|
15
|
+
email;
|
|
16
|
+
phone;
|
|
17
|
+
linkedin;
|
|
18
|
+
city;
|
|
19
|
+
notes;
|
|
20
|
+
static getModel() {
|
|
21
|
+
return ContactModel;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
__decorate([
|
|
25
|
+
EnduranceModelType.prop({ required: true }),
|
|
26
|
+
__metadata("design:type", String)
|
|
27
|
+
], Contact.prototype, "firstname", void 0);
|
|
28
|
+
__decorate([
|
|
29
|
+
EnduranceModelType.prop({ required: true }),
|
|
30
|
+
__metadata("design:type", String)
|
|
31
|
+
], Contact.prototype, "lastname", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
EnduranceModelType.prop({ required: true }),
|
|
34
|
+
__metadata("design:type", String)
|
|
35
|
+
], Contact.prototype, "email", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
EnduranceModelType.prop(),
|
|
38
|
+
__metadata("design:type", String)
|
|
39
|
+
], Contact.prototype, "phone", void 0);
|
|
40
|
+
__decorate([
|
|
41
|
+
EnduranceModelType.prop(),
|
|
42
|
+
__metadata("design:type", String)
|
|
43
|
+
], Contact.prototype, "linkedin", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
EnduranceModelType.prop({ required: true }),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], Contact.prototype, "city", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
EnduranceModelType.prop({ type: [Types.ObjectId], ref: 'Note', default: [] }),
|
|
50
|
+
__metadata("design:type", Array)
|
|
51
|
+
], Contact.prototype, "notes", void 0);
|
|
52
|
+
Contact = __decorate([
|
|
53
|
+
EnduranceModelType.modelOptions({
|
|
54
|
+
options: {
|
|
55
|
+
allowMixed: EnduranceModelType.Severity.ALLOW
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
], Contact);
|
|
59
|
+
const ContactModel = EnduranceModelType.getModelForClass(Contact);
|
|
60
|
+
export default ContactModel;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance-core';
|
|
2
2
|
import CandidateModel from '../models/candidate.models.js';
|
|
3
|
+
import ContactModel from '../models/contact.model.js';
|
|
3
4
|
import TestResult from '../models/test-result.model.js';
|
|
4
5
|
import Test from '../models/test.model.js';
|
|
5
6
|
import jwt from 'jsonwebtoken';
|
|
@@ -14,16 +15,79 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
14
15
|
};
|
|
15
16
|
// Créer un nouveau candidat
|
|
16
17
|
this.post('/', authenticatedOptions, async (req, res) => {
|
|
17
|
-
const {
|
|
18
|
+
const { firstname, lastname, email, phone, linkedin, city, experienceLevel, yearsOfExperience, skills } = req.body;
|
|
18
19
|
console.log(req.body);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return res.status(400).json({ message: 'Error, firstName, lastName and email are required' });
|
|
20
|
+
if (!firstname || !lastname || !email || !city || !skills || skills.length === 0) {
|
|
21
|
+
return res.status(400).json({ message: 'Error, firstname, lastname, email, city and skills are required' });
|
|
22
22
|
}
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
// Vérifier si un contact existe déjà avec cette adresse email
|
|
25
|
+
const existingContact = await ContactModel.findOne({ email });
|
|
26
|
+
if (existingContact) {
|
|
27
|
+
// Vérifier si un candidat existe déjà avec ce contact
|
|
28
|
+
const existingCandidate = await CandidateModel.findOne({ contact: existingContact._id });
|
|
29
|
+
if (existingCandidate) {
|
|
30
|
+
// Contact et candidat existent déjà
|
|
31
|
+
return res.status(200).json({
|
|
32
|
+
message: 'Contact et candidat existent déjà avec cette adresse email',
|
|
33
|
+
status: 'EXISTING',
|
|
34
|
+
candidate: {
|
|
35
|
+
...existingCandidate.toObject(),
|
|
36
|
+
contact: existingContact.toObject()
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Le contact existe mais pas de candidat, créer le candidat
|
|
42
|
+
const newCandidate = new CandidateModel({
|
|
43
|
+
contact: existingContact._id,
|
|
44
|
+
experienceLevel: experienceLevel || 'JUNIOR',
|
|
45
|
+
yearsOfExperience: yearsOfExperience || 0,
|
|
46
|
+
skills
|
|
47
|
+
});
|
|
48
|
+
await newCandidate.save();
|
|
49
|
+
return res.status(201).json({
|
|
50
|
+
message: 'Candidat créé avec succès en utilisant le contact existant',
|
|
51
|
+
status: 'CREATED_WITH_EXISTING_CONTACT',
|
|
52
|
+
candidate: {
|
|
53
|
+
...newCandidate.toObject(),
|
|
54
|
+
contact: existingContact.toObject()
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Aucun contact existant, créer le contact et le candidat
|
|
60
|
+
const newContact = new ContactModel({
|
|
61
|
+
firstname,
|
|
62
|
+
lastname,
|
|
63
|
+
email,
|
|
64
|
+
phone,
|
|
65
|
+
linkedin,
|
|
66
|
+
city
|
|
67
|
+
});
|
|
68
|
+
await newContact.save();
|
|
69
|
+
// Créer ensuite le candidat avec la référence au contact
|
|
70
|
+
const newCandidate = new CandidateModel({
|
|
71
|
+
contact: newContact._id,
|
|
72
|
+
experienceLevel: experienceLevel || 'JUNIOR',
|
|
73
|
+
yearsOfExperience: yearsOfExperience || 0,
|
|
74
|
+
skills
|
|
75
|
+
});
|
|
25
76
|
await newCandidate.save();
|
|
26
|
-
|
|
77
|
+
// Récupérer le candidat et le contact séparément
|
|
78
|
+
const candidate = await CandidateModel.findById(newCandidate._id);
|
|
79
|
+
const contact = await ContactModel.findById(newContact._id);
|
|
80
|
+
if (!candidate || !contact) {
|
|
81
|
+
return res.status(500).json({ message: 'Erreur lors de la récupération des données' });
|
|
82
|
+
}
|
|
83
|
+
res.status(201).json({
|
|
84
|
+
message: 'Contact et candidat créés avec succès',
|
|
85
|
+
status: 'CREATED',
|
|
86
|
+
candidate: {
|
|
87
|
+
...candidate.toObject(),
|
|
88
|
+
contact: contact.toObject()
|
|
89
|
+
}
|
|
90
|
+
});
|
|
27
91
|
}
|
|
28
92
|
catch (err) {
|
|
29
93
|
console.error('error when creating candidate : ', err);
|
|
@@ -37,35 +101,78 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
37
101
|
const limit = parseInt(req.query.limit) || 10;
|
|
38
102
|
const skip = (page - 1) * limit;
|
|
39
103
|
const search = req.query.search || '';
|
|
40
|
-
const sortBy = req.query.sortBy || '
|
|
104
|
+
const sortBy = req.query.sortBy || 'lastname';
|
|
41
105
|
const sortOrder = req.query.sortOrder || 'asc';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Recherche sur firstName, lastName et email
|
|
106
|
+
let contactIds = [];
|
|
107
|
+
let total = 0;
|
|
45
108
|
if (search) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
109
|
+
// Recherche dans les contacts
|
|
110
|
+
const contactQuery = {
|
|
111
|
+
$or: [
|
|
112
|
+
{ firstname: { $regex: search, $options: 'i' } },
|
|
113
|
+
{ lastname: { $regex: search, $options: 'i' } },
|
|
114
|
+
{ email: { $regex: search, $options: 'i' } }
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
const contacts = await ContactModel.find(contactQuery);
|
|
118
|
+
contactIds = contacts.map(contact => contact._id);
|
|
119
|
+
// Compter les candidats avec ces contacts
|
|
120
|
+
total = await CandidateModel.countDocuments({ contact: { $in: contactIds } });
|
|
51
121
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
122
|
+
else {
|
|
123
|
+
// Pas de recherche, compter tous les candidats
|
|
124
|
+
total = await CandidateModel.countDocuments();
|
|
125
|
+
}
|
|
126
|
+
// Construction du tri pour les contacts
|
|
127
|
+
const allowedSortFields = ['firstname', 'lastname', 'email'];
|
|
128
|
+
const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'lastname';
|
|
129
|
+
let candidates;
|
|
130
|
+
if (search && contactIds.length > 0) {
|
|
131
|
+
// Récupérer les candidats avec les contacts trouvés
|
|
132
|
+
candidates = await CandidateModel.find({ contact: { $in: contactIds } })
|
|
61
133
|
.skip(skip)
|
|
62
134
|
.limit(limit)
|
|
63
|
-
.exec()
|
|
64
|
-
|
|
65
|
-
|
|
135
|
+
.exec();
|
|
136
|
+
}
|
|
137
|
+
else if (!search) {
|
|
138
|
+
// Récupérer tous les candidats
|
|
139
|
+
candidates = await CandidateModel.find()
|
|
140
|
+
.skip(skip)
|
|
141
|
+
.limit(limit)
|
|
142
|
+
.exec();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Aucun contact trouvé pour la recherche
|
|
146
|
+
candidates = [];
|
|
147
|
+
}
|
|
148
|
+
// Récupérer les contacts pour tous les candidats
|
|
149
|
+
const candidateContactIds = candidates.map(candidate => candidate.contact);
|
|
150
|
+
const contacts = await ContactModel.find({ _id: { $in: candidateContactIds } });
|
|
151
|
+
const contactsMap = new Map(contacts.map(contact => [contact._id.toString(), contact]));
|
|
152
|
+
// Combiner les candidats avec leurs contacts et trier
|
|
153
|
+
const candidatesWithContacts = candidates.map(candidate => {
|
|
154
|
+
const contact = contactsMap.get(candidate.contact.toString());
|
|
155
|
+
return {
|
|
156
|
+
...candidate.toObject(),
|
|
157
|
+
contact: contact ? contact.toObject() : null
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
// Trier les résultats côté serveur si nécessaire
|
|
161
|
+
if (sortField && candidatesWithContacts.length > 0) {
|
|
162
|
+
candidatesWithContacts.sort((a, b) => {
|
|
163
|
+
const aValue = a.contact ? a.contact[sortField] : '';
|
|
164
|
+
const bValue = b.contact ? b.contact[sortField] : '';
|
|
165
|
+
if (sortOrder === 'asc') {
|
|
166
|
+
return aValue.localeCompare(bValue);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
return bValue.localeCompare(aValue);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
66
173
|
const totalPages = Math.ceil(total / limit);
|
|
67
174
|
return res.json({
|
|
68
|
-
data:
|
|
175
|
+
data: candidatesWithContacts,
|
|
69
176
|
pagination: {
|
|
70
177
|
currentPage: page,
|
|
71
178
|
totalPages,
|
|
@@ -89,7 +196,18 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
89
196
|
if (!candidate) {
|
|
90
197
|
return res.status(404).json({ message: 'no candidate found with this id' });
|
|
91
198
|
}
|
|
92
|
-
|
|
199
|
+
// Récupérer le contact associé
|
|
200
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
201
|
+
if (!contact) {
|
|
202
|
+
return res.status(404).json({ message: 'contact not found for this candidate' });
|
|
203
|
+
}
|
|
204
|
+
res.status(200).json({
|
|
205
|
+
message: 'candidate : ',
|
|
206
|
+
data: {
|
|
207
|
+
...candidate.toObject(),
|
|
208
|
+
contact: contact.toObject()
|
|
209
|
+
}
|
|
210
|
+
});
|
|
93
211
|
}
|
|
94
212
|
catch (err) {
|
|
95
213
|
console.error('error when getting candidate : ', err);
|
|
@@ -100,12 +218,19 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
100
218
|
this.get('/email/:email', authenticatedOptions, async (req, res) => {
|
|
101
219
|
try {
|
|
102
220
|
const email = req.params.email;
|
|
103
|
-
|
|
221
|
+
// Chercher d'abord le contact par email
|
|
222
|
+
const contact = await ContactModel.findOne({ email });
|
|
223
|
+
if (!contact) {
|
|
224
|
+
return res.status(404).json({ message: 'Contact non trouvé' });
|
|
225
|
+
}
|
|
226
|
+
// Puis chercher le candidat avec ce contact
|
|
227
|
+
const candidate = await CandidateModel.findOne({ contact: contact._id });
|
|
104
228
|
if (!candidate) {
|
|
105
229
|
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
106
230
|
}
|
|
107
231
|
return res.json({
|
|
108
|
-
...candidate.toObject()
|
|
232
|
+
...candidate.toObject(),
|
|
233
|
+
contact: contact.toObject()
|
|
109
234
|
});
|
|
110
235
|
}
|
|
111
236
|
catch (error) {
|
|
@@ -120,7 +245,13 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
120
245
|
if (!email) {
|
|
121
246
|
return res.status(400).json({ message: 'Email requis' });
|
|
122
247
|
}
|
|
123
|
-
|
|
248
|
+
// Chercher d'abord le contact par email
|
|
249
|
+
const contact = await ContactModel.findOne({ email });
|
|
250
|
+
if (!contact) {
|
|
251
|
+
return res.status(404).json({ message: 'Contact non trouvé' });
|
|
252
|
+
}
|
|
253
|
+
// Puis chercher le candidat avec ce contact
|
|
254
|
+
const candidate = await CandidateModel.findOne({ contact: contact._id });
|
|
124
255
|
if (!candidate) {
|
|
125
256
|
return res.status(404).json({ message: 'Candidat non trouvé' });
|
|
126
257
|
}
|
|
@@ -195,8 +326,7 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
195
326
|
candidate: {
|
|
196
327
|
id: candidate._id,
|
|
197
328
|
email: decoded.email,
|
|
198
|
-
|
|
199
|
-
lastName: candidate.lastName
|
|
329
|
+
contact: candidate.contact
|
|
200
330
|
}
|
|
201
331
|
});
|
|
202
332
|
}
|
|
@@ -4,6 +4,7 @@ import TestQuestion from '../models/test-question.model.js';
|
|
|
4
4
|
import TestResult from '../models/test-result.model.js';
|
|
5
5
|
import TestCategory from '../models/test-category.models.js';
|
|
6
6
|
import Candidate from '../models/candidate.models.js';
|
|
7
|
+
import ContactModel from '../models/contact.model.js';
|
|
7
8
|
import { generateLiveMessage } from '../lib/openai.js';
|
|
8
9
|
class ExamsRouter extends EnduranceRouter {
|
|
9
10
|
constructor() {
|
|
@@ -545,7 +546,12 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
545
546
|
if (!candidate) {
|
|
546
547
|
return res.status(404).json({ message: 'Candidate not found' });
|
|
547
548
|
}
|
|
548
|
-
|
|
549
|
+
// Récupérer le contact pour obtenir l'email
|
|
550
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
551
|
+
if (!contact) {
|
|
552
|
+
return res.status(404).json({ message: 'Contact not found' });
|
|
553
|
+
}
|
|
554
|
+
const email = contact.email;
|
|
549
555
|
// Construire le lien d'invitation
|
|
550
556
|
const testLink = process.env.TEST_INVITATION_LINK || '';
|
|
551
557
|
// Récupérer les credentials d'envoi
|
|
@@ -907,20 +913,29 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
907
913
|
maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
|
|
908
914
|
}
|
|
909
915
|
// Combiner les résultats avec les données des candidats
|
|
910
|
-
const resultsWithCandidates = results.map(result => {
|
|
916
|
+
const resultsWithCandidates = await Promise.all(results.map(async (result) => {
|
|
911
917
|
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
918
|
+
if (!candidate) {
|
|
919
|
+
return {
|
|
920
|
+
...result.toObject(),
|
|
921
|
+
candidate: null,
|
|
922
|
+
maxScore
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
// Récupérer le contact pour obtenir les informations personnelles
|
|
926
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
912
927
|
return {
|
|
913
928
|
...result.toObject(),
|
|
914
|
-
candidate:
|
|
929
|
+
candidate: contact
|
|
915
930
|
? {
|
|
916
|
-
firstName:
|
|
917
|
-
lastName:
|
|
918
|
-
email:
|
|
931
|
+
firstName: contact.firstname,
|
|
932
|
+
lastName: contact.lastname,
|
|
933
|
+
email: contact.email
|
|
919
934
|
}
|
|
920
935
|
: null,
|
|
921
936
|
maxScore
|
|
922
937
|
};
|
|
923
|
-
});
|
|
938
|
+
}));
|
|
924
939
|
const totalPages = Math.ceil(total / limit);
|
|
925
940
|
return res.json({
|
|
926
941
|
data: resultsWithCandidates,
|
|
@@ -947,17 +962,22 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
947
962
|
if (!result) {
|
|
948
963
|
return res.status(404).json({ message: 'Result not found' });
|
|
949
964
|
}
|
|
950
|
-
// Récupérer
|
|
965
|
+
// Récupérer le candidat et son contact
|
|
951
966
|
const candidate = await Candidate.findById(result.candidateId);
|
|
952
967
|
if (!candidate) {
|
|
953
968
|
return res.status(404).json({ message: 'Candidate not found' });
|
|
954
969
|
}
|
|
970
|
+
// Récupérer le contact pour obtenir l'email
|
|
971
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
972
|
+
if (!contact) {
|
|
973
|
+
return res.status(404).json({ message: 'Contact not found' });
|
|
974
|
+
}
|
|
955
975
|
// Récupérer les informations du test
|
|
956
976
|
const test = await Test.findById(result.testId);
|
|
957
977
|
if (!test) {
|
|
958
978
|
return res.status(404).json({ message: 'Test not found' });
|
|
959
979
|
}
|
|
960
|
-
const email =
|
|
980
|
+
const email = contact.email;
|
|
961
981
|
const emailUser = process.env.EMAIL_USER;
|
|
962
982
|
const emailPassword = process.env.EMAIL_PASSWORD;
|
|
963
983
|
// Construire le lien d'invitation
|