@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.
@@ -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
- firstName: string;
4
- lastName: string;
5
- email: string;
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
- firstName;
13
- lastName;
14
- email;
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", String)
26
- ], Candidate.prototype, "firstName", void 0);
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: true }),
40
+ EnduranceModelType.prop({ required: false, enum: ExperienceLevel, default: ExperienceLevel.JUNIOR }),
29
41
  __metadata("design:type", String)
30
- ], Candidate.prototype, "lastName", void 0);
42
+ ], Candidate.prototype, "experienceLevel", void 0);
31
43
  __decorate([
32
- EnduranceModelType.prop({ required: true, unique: true }),
33
- __metadata("design:type", String)
34
- ], Candidate.prototype, "email", void 0);
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 { firstName, lastName, email } = req.body;
18
+ const { firstname, lastname, email, phone, linkedin, city, experienceLevel, yearsOfExperience, skills } = req.body;
18
19
  console.log(req.body);
19
- console.log(firstName, lastName, email);
20
- if (!firstName || !lastName || !email) {
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
- const newCandidate = new CandidateModel({ firstName, lastName, email });
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
- res.status(201).json({ message: 'candidate created with success', candidate: newCandidate });
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 || 'lastName';
104
+ const sortBy = req.query.sortBy || 'lastname';
41
105
  const sortOrder = req.query.sortOrder || 'asc';
42
- // Construction de la requête de recherche
43
- const query = {};
44
- // Recherche sur firstName, lastName et email
106
+ let contactIds = [];
107
+ let total = 0;
45
108
  if (search) {
46
- query.$or = [
47
- { firstName: { $regex: search, $options: 'i' } },
48
- { lastName: { $regex: search, $options: 'i' } },
49
- { email: { $regex: search, $options: 'i' } }
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
- // Construction du tri
53
- const allowedSortFields = ['firstName', 'lastName', 'email'];
54
- const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'lastName';
55
- const sortOptions = {
56
- [sortField]: sortOrder === 'asc' ? 1 : -1
57
- };
58
- const [candidates, total] = await Promise.all([
59
- CandidateModel.find(query)
60
- .sort(sortOptions)
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
- CandidateModel.countDocuments(query)
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: candidates,
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
- res.status(200).json({ message: 'candidate : ', data: candidate });
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
- const candidate = await CandidateModel.findOne({ email });
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
- const candidate = await CandidateModel.findOne({ email });
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
- firstName: candidate.firstName,
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
- const email = candidate.email;
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
- const candidates = await Candidate.find({
888
+ // D'abord, rechercher dans les contacts
889
+ const contacts = await ContactModel.find({
881
890
  $or: [
882
- { firstName: { $regex: search, $options: 'i' } },
883
- { lastName: { $regex: search, $options: 'i' } },
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
- const [results, total] = await Promise.all([
891
- TestResult.find(query)
892
- .sort({ invitationDate: -1 })
893
- .skip(skip)
894
- .limit(limit)
895
- .exec(),
896
- TestResult.countDocuments(query)
897
- ]);
898
- // Récupérer les données des candidats
899
- const candidateIds = results.map(result => result.candidateId);
900
- const candidates = await Candidate.find({ _id: { $in: candidateIds } });
901
- const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
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
- // Combiner les résultats avec les données des candidats
910
- const resultsWithCandidates = results.map(result => {
911
- const candidate = candidatesMap.get(result.candidateId.toString());
912
- return {
913
- ...result.toObject(),
914
- candidate: candidate
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 l'email du candidat
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 = candidate.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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.2.4",
4
+ "version": "0.2.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },