@programisto/edrm-exams 0.2.3 → 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.
@@ -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,12 +546,17 @@ 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
552
- const emailUser = process.env.EMAIL_USER_TURING;
553
- const emailPassword = process.env.EMAIL_PASSWORD_TURING;
558
+ const emailUser = process.env.EMAIL_USER;
559
+ const emailPassword = process.env.EMAIL_PASSWORD;
554
560
  // Envoyer l'email via l'event emitter
555
561
  await emitter.emit(eventTypes.SEND_EMAIL, {
556
562
  template: 'test-invitation',
@@ -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: candidate
929
+ candidate: contact
915
930
  ? {
916
- firstName: candidate.firstName,
917
- lastName: candidate.lastName,
918
- email: candidate.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,19 +962,24 @@ 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 l'email du candidat
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 = candidate.email;
961
- const emailUser = process.env.EMAIL_USER_TURING;
962
- const emailPassword = process.env.EMAIL_PASSWORD_TURING;
980
+ const email = contact.email;
981
+ const emailUser = process.env.EMAIL_USER;
982
+ const emailPassword = process.env.EMAIL_PASSWORD;
963
983
  // Construire le lien d'invitation
964
984
  const testLink = process.env.TEST_INVITATION_LINK || '';
965
985
  // Envoyer l'email via l'event emitter
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.2.3",
4
+ "version": "0.2.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },