@programisto/edrm-exams 0.2.4 → 0.2.6
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 +124 -33
- 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
|
|
@@ -865,6 +871,8 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
865
871
|
const skip = (page - 1) * limit;
|
|
866
872
|
const search = req.query.search || '';
|
|
867
873
|
const state = req.query.state || 'all';
|
|
874
|
+
const sortBy = req.query.sortBy || 'invitationDate';
|
|
875
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
868
876
|
try {
|
|
869
877
|
const test = await Test.findById(testId);
|
|
870
878
|
if (!test) {
|
|
@@ -875,30 +883,84 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
875
883
|
if (state !== 'all') {
|
|
876
884
|
query.state = state;
|
|
877
885
|
}
|
|
878
|
-
// Recherche sur les candidats
|
|
886
|
+
// Recherche sur les candidats via leurs contacts
|
|
879
887
|
if (search) {
|
|
880
|
-
|
|
888
|
+
// D'abord, rechercher dans les contacts
|
|
889
|
+
const contacts = await ContactModel.find({
|
|
881
890
|
$or: [
|
|
882
|
-
{
|
|
883
|
-
{
|
|
891
|
+
{ firstname: { $regex: search, $options: 'i' } },
|
|
892
|
+
{ lastname: { $regex: search, $options: 'i' } },
|
|
884
893
|
{ email: { $regex: search, $options: 'i' } }
|
|
885
894
|
]
|
|
886
895
|
});
|
|
896
|
+
// Ensuite, récupérer les candidats qui ont ces contacts
|
|
897
|
+
const contactIds = contacts.map(c => c._id);
|
|
898
|
+
const candidates = await Candidate.find({
|
|
899
|
+
contact: { $in: contactIds }
|
|
900
|
+
});
|
|
887
901
|
const candidateIds = candidates.map(c => c._id);
|
|
888
902
|
query.candidateId = { $in: candidateIds };
|
|
889
903
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
904
|
+
// Déterminer l'ordre de tri
|
|
905
|
+
const sortDirection = sortOrder === 'asc' ? 1 : -1;
|
|
906
|
+
// Si on trie par lastName, on récupère tous les résultats puis on trie après
|
|
907
|
+
// Sinon on peut trier directement dans la requête MongoDB
|
|
908
|
+
let results, total;
|
|
909
|
+
if (sortBy === 'lastName') {
|
|
910
|
+
// Récupérer tous les résultats sans pagination pour pouvoir trier par lastName
|
|
911
|
+
const allResults = await TestResult.find(query).exec();
|
|
912
|
+
total = allResults.length;
|
|
913
|
+
// Récupérer les données des candidats pour le tri
|
|
914
|
+
const candidateIds = allResults.map(result => result.candidateId);
|
|
915
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
916
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
917
|
+
// Combiner les résultats avec les données des candidats et trier
|
|
918
|
+
const resultsWithCandidates = await Promise.all(allResults.map(async (result) => {
|
|
919
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
920
|
+
if (!candidate) {
|
|
921
|
+
return {
|
|
922
|
+
...result.toObject(),
|
|
923
|
+
candidate: null,
|
|
924
|
+
lastName: ''
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
928
|
+
return {
|
|
929
|
+
...result.toObject(),
|
|
930
|
+
candidate: contact
|
|
931
|
+
? {
|
|
932
|
+
firstName: contact.firstname,
|
|
933
|
+
lastName: contact.lastname,
|
|
934
|
+
email: contact.email
|
|
935
|
+
}
|
|
936
|
+
: null,
|
|
937
|
+
lastName: contact ? contact.lastname : ''
|
|
938
|
+
};
|
|
939
|
+
}));
|
|
940
|
+
// Trier par lastName
|
|
941
|
+
resultsWithCandidates.sort((a, b) => {
|
|
942
|
+
const lastNameA = (a.lastName || '').toLowerCase();
|
|
943
|
+
const lastNameB = (b.lastName || '').toLowerCase();
|
|
944
|
+
return sortDirection === 1
|
|
945
|
+
? lastNameA.localeCompare(lastNameB)
|
|
946
|
+
: lastNameB.localeCompare(lastNameA);
|
|
947
|
+
});
|
|
948
|
+
// Appliquer la pagination
|
|
949
|
+
results = resultsWithCandidates.slice(skip, skip + limit);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
// Tri direct dans MongoDB pour invitationDate
|
|
953
|
+
const sortObject = {};
|
|
954
|
+
sortObject[sortBy] = sortDirection;
|
|
955
|
+
[results, total] = await Promise.all([
|
|
956
|
+
TestResult.find(query)
|
|
957
|
+
.sort(sortObject)
|
|
958
|
+
.skip(skip)
|
|
959
|
+
.limit(limit)
|
|
960
|
+
.exec(),
|
|
961
|
+
TestResult.countDocuments(query)
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
902
964
|
// Calculer le maxScore du test
|
|
903
965
|
let maxScore = 0;
|
|
904
966
|
if (test.questions && test.questions.length > 0) {
|
|
@@ -906,21 +968,45 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
906
968
|
const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
|
|
907
969
|
maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
|
|
908
970
|
}
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
? {
|
|
916
|
-
firstName: candidate.firstName,
|
|
917
|
-
lastName: candidate.lastName,
|
|
918
|
-
email: candidate.email
|
|
919
|
-
}
|
|
920
|
-
: null,
|
|
971
|
+
// Si on a déjà traité les candidats pour le tri par lastName, on utilise directement les résultats
|
|
972
|
+
let resultsWithCandidates;
|
|
973
|
+
if (sortBy === 'lastName') {
|
|
974
|
+
// Les résultats sont déjà traités avec les données des candidats
|
|
975
|
+
resultsWithCandidates = results.map(result => ({
|
|
976
|
+
...result,
|
|
921
977
|
maxScore
|
|
922
|
-
};
|
|
923
|
-
}
|
|
978
|
+
}));
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
// Récupérer les données des candidats
|
|
982
|
+
const candidateIds = results.map(result => result.candidateId);
|
|
983
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
984
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
985
|
+
// Combiner les résultats avec les données des candidats
|
|
986
|
+
resultsWithCandidates = await Promise.all(results.map(async (result) => {
|
|
987
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
988
|
+
if (!candidate) {
|
|
989
|
+
return {
|
|
990
|
+
...result.toObject(),
|
|
991
|
+
candidate: null,
|
|
992
|
+
maxScore
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
// Récupérer le contact pour obtenir les informations personnelles
|
|
996
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
997
|
+
return {
|
|
998
|
+
...result.toObject(),
|
|
999
|
+
candidate: contact
|
|
1000
|
+
? {
|
|
1001
|
+
firstName: contact.firstname,
|
|
1002
|
+
lastName: contact.lastname,
|
|
1003
|
+
email: contact.email
|
|
1004
|
+
}
|
|
1005
|
+
: null,
|
|
1006
|
+
maxScore
|
|
1007
|
+
};
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
924
1010
|
const totalPages = Math.ceil(total / limit);
|
|
925
1011
|
return res.json({
|
|
926
1012
|
data: resultsWithCandidates,
|
|
@@ -947,17 +1033,22 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
947
1033
|
if (!result) {
|
|
948
1034
|
return res.status(404).json({ message: 'Result not found' });
|
|
949
1035
|
}
|
|
950
|
-
// Récupérer
|
|
1036
|
+
// Récupérer le candidat et son contact
|
|
951
1037
|
const candidate = await Candidate.findById(result.candidateId);
|
|
952
1038
|
if (!candidate) {
|
|
953
1039
|
return res.status(404).json({ message: 'Candidate not found' });
|
|
954
1040
|
}
|
|
1041
|
+
// Récupérer le contact pour obtenir l'email
|
|
1042
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
1043
|
+
if (!contact) {
|
|
1044
|
+
return res.status(404).json({ message: 'Contact not found' });
|
|
1045
|
+
}
|
|
955
1046
|
// Récupérer les informations du test
|
|
956
1047
|
const test = await Test.findById(result.testId);
|
|
957
1048
|
if (!test) {
|
|
958
1049
|
return res.status(404).json({ message: 'Test not found' });
|
|
959
1050
|
}
|
|
960
|
-
const email =
|
|
1051
|
+
const email = contact.email;
|
|
961
1052
|
const emailUser = process.env.EMAIL_USER;
|
|
962
1053
|
const emailPassword = process.env.EMAIL_PASSWORD;
|
|
963
1054
|
// Construire le lien d'invitation
|