5htp 0.3.5 → 0.3.8
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/package.json +4 -3
- package/skeleton/docker-compose.yml +7 -15
- package/skeleton/identity.yaml +12 -8
- package/skeleton/package.json +25 -7
- package/skeleton/src/client/assets/identity/logo.svg +13 -63
- package/skeleton/src/client/assets/identity/logoAndText.svg +9 -104
- package/skeleton/src/client/assets/identity/logoAndTextBlack.svg +11 -0
- package/skeleton/src/client/assets/illustration/landing/banner.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/candidate/employers.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/candidate/hero.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/candidate/mentors.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/headhunter/hero.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/hero.jpeg +0 -0
- package/skeleton/src/client/assets/illustration/landing/hero.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/hero.xcf +0 -0
- package/skeleton/src/client/assets/illustration/landing/recruiter/onboarding.webp +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/andre.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/emma.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/fei.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/gaetan.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/jordan.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/lery.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/mehdi.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/omkar.png +0 -0
- package/skeleton/src/client/assets/illustration/landing/team/thibaut.png +0 -0
- package/skeleton/src/client/assets/img/background/header-blur.png +0 -0
- package/skeleton/src/client/assets/img/partners/citron.svg +987 -0
- package/skeleton/src/client/assets/patterns/dots.png +0 -0
- package/skeleton/src/client/assets/theme.less +179 -226
- package/skeleton/src/client/assets/vars.less +54 -0
- package/skeleton/src/client/context.ts +23 -0
- package/skeleton/src/client/index.ts +59 -0
- package/skeleton/src/client/pages/_messages/400.tsx +45 -0
- package/skeleton/src/client/pages/_messages/401.tsx +39 -0
- package/skeleton/src/client/pages/_messages/403.tsx +43 -0
- package/skeleton/src/client/pages/_messages/404.tsx +42 -0
- package/skeleton/src/client/pages/_messages/500.tsx +42 -0
- package/skeleton/src/client/pages/platform/Header.less +12 -0
- package/skeleton/src/client/pages/platform/Header.tsx +119 -0
- package/skeleton/src/client/pages/platform/_layout/index.less +118 -0
- package/skeleton/src/client/pages/platform/_layout/index.tsx +131 -0
- package/skeleton/src/client/pages/platform/_layout/mobile.less +114 -0
- package/skeleton/src/client/pages/platform/_page.tsx +54 -0
- package/skeleton/src/client/pages/platform/headhunters/index.tsx +88 -0
- package/skeleton/src/client/pages/platform/index.tsx +58 -0
- package/skeleton/src/client/pages/platform/missions/index.tsx +149 -0
- package/skeleton/src/client/services/metrics/index.ts +59 -0
- package/skeleton/src/client/tsconfig.json +4 -1
- package/skeleton/src/common/config/router.ts +16 -0
- package/skeleton/src/common/forms/company/bookCall.ts +25 -0
- package/skeleton/src/common/forms/company/importJob.ts +26 -0
- package/skeleton/src/common/forms/company/signup.ts +31 -0
- package/skeleton/src/common/forms/headhunter/feedback.ts +31 -0
- package/skeleton/src/common/forms/headhunter/mission/cancel.ts +26 -0
- package/skeleton/src/common/forms/headhunter/mission/candidate/availability.ts +31 -0
- package/skeleton/src/common/forms/headhunter/mission/candidate/education.ts +19 -0
- package/skeleton/src/common/forms/headhunter/mission/candidate/identity.ts +31 -0
- package/skeleton/src/common/forms/headhunter/mission/candidate/others.ts +19 -0
- package/skeleton/src/common/forms/headhunter/mission/candidate/skills.ts +21 -0
- package/skeleton/src/common/forms/headhunter/mission/reject.ts +23 -0
- package/skeleton/src/common/forms/headhunter/mission/search.ts +78 -0
- package/skeleton/src/common/forms/headhunter/signup.ts +34 -0
- package/skeleton/src/common/libs/headhunter/candidate/index.ts +155 -0
- package/skeleton/src/common/libs/headhunter/mission/index.ts +30 -0
- package/skeleton/src/common/libs/hub/index.ts +41 -0
- package/skeleton/src/server/config/communication.ts +48 -0
- package/skeleton/src/server/config/crosspath.ts +9 -0
- package/skeleton/src/server/config/data.ts +34 -0
- package/skeleton/src/server/config/database.ts +26 -0
- package/skeleton/src/server/config/internal.ts +21 -0
- package/skeleton/src/server/config/user.ts +90 -0
- package/skeleton/src/server/index.ts +111 -23
- package/skeleton/src/server/libs/utils/slug.ts +11 -0
- package/skeleton/src/server/routes/global.ts +33 -0
- package/skeleton/src/server/routes/headhunters.ts +24 -0
- package/skeleton/src/server/routes/missions.ts +50 -0
- package/skeleton/src/server/services/Headhunter/index.ts +127 -0
- package/skeleton/src/server/services/Headhunter/service.json +6 -0
- package/skeleton/src/server/services/Mission/index.ts +174 -0
- package/skeleton/src/server/services/Mission/service.json +6 -0
- package/skeleton/src/server/services/email/sendgrid/index.ts +97 -0
- package/skeleton/src/server/services/email/sendgrid/service.json +6 -0
- package/skeleton/src/server/services/slack/index.ts +105 -0
- package/skeleton/src/server/services/slack/service.json +6 -0
- package/skeleton/src/server/services/users/index.ts +133 -0
- package/skeleton/src/server/services/users/service.json +6 -0
- package/skeleton/src/server/tsconfig.json +6 -8
- package/skeleton/var/typings/routes.d.ts +541 -0
- package/src/compiler/common/babel/index.ts +28 -25
- package/src/index.ts +1 -1
- package/src/utils/keyboard.ts +1 -1
- package/skeleton/package-lock.json +0 -6139
- package/skeleton/src/client/components/LoginModal.tsx +0 -45
- package/skeleton/src/client/pages/app/_layout/index.less +0 -20
- package/skeleton/src/client/pages/app/_layout/index.tsx +0 -33
- package/skeleton/src/client/pages/app/index.tsx +0 -57
- package/skeleton/src/client/pages/landing/_layout/index.less +0 -145
- package/skeleton/src/client/pages/landing/_layout/index.tsx +0 -63
- package/skeleton/src/client/pages/landing/index.tsx +0 -73
- package/skeleton/src/server/models.ts +0 -117
- package/skeleton/src/server/routes/general.ts +0 -66
- package/skeleton/src/server/services/auth/index.ts +0 -80
|
@@ -1,23 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import '
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPS
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
import { Application } from '@server/app';
|
|
5
|
+
import type { ServerBug } from '@common/errors';
|
|
6
|
+
|
|
7
|
+
/*----------------------------------
|
|
8
|
+
- APPLICATION
|
|
9
|
+
----------------------------------*/
|
|
10
|
+
export default class CrossPathCSM extends Application {
|
|
11
|
+
|
|
12
|
+
/*----------------------------------
|
|
13
|
+
- CORE ERVICES
|
|
14
|
+
----------------------------------*/
|
|
15
|
+
|
|
16
|
+
public Users = this.use('CrossPath/Users');
|
|
17
|
+
|
|
18
|
+
public Disks = this.use('Core/Disks', {
|
|
19
|
+
s3: this.use('Core/Disks/S3'),
|
|
20
|
+
local: this.use('Core/Disks/Local')
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
public Router = this.use('Core/Router', {
|
|
24
|
+
// Router services
|
|
25
|
+
auth: this.use('Core/Users/Router', {
|
|
26
|
+
users: this.Users
|
|
27
|
+
}),
|
|
28
|
+
schema: this.use('Core/Schema/Router')
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
public SQL = this.use('Core/Database/SQL');
|
|
32
|
+
|
|
33
|
+
public Email = this.use('Core/Email', {
|
|
34
|
+
sendgrid: this.use('CrossPath/Email/Sendgrid')
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
public Fetch = this.use('Core/Fetch', {
|
|
38
|
+
disks: this.Disks,
|
|
39
|
+
router: this.Router
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
public Cron = this.use('Core/Cron');
|
|
43
|
+
|
|
44
|
+
public AES = this.use('Core/Encryption/AES');
|
|
45
|
+
|
|
46
|
+
public Slack = this.use('CrossPath/Slack');
|
|
47
|
+
|
|
48
|
+
/*----------------------------------
|
|
49
|
+
- BUSINESS SERVICES
|
|
50
|
+
----------------------------------*/
|
|
51
|
+
|
|
52
|
+
public Mission = this.use('CrossPath/Mission');
|
|
53
|
+
|
|
54
|
+
public Headhunter = this.use('CrossPath/Headhunter');
|
|
55
|
+
|
|
56
|
+
/*----------------------------------
|
|
57
|
+
- LIFECYCLE
|
|
58
|
+
----------------------------------*/
|
|
59
|
+
|
|
60
|
+
public async launch() {
|
|
61
|
+
|
|
62
|
+
console.log("Launching CrossPath CSM Server");
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async ready() {
|
|
67
|
+
|
|
68
|
+
console.log("CrossPath CSM Server is ready");
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async reportBug( report: ServerBug ) {
|
|
73
|
+
|
|
74
|
+
if (this.env.name === 'local')
|
|
75
|
+
return console.warn(`Cancel bug report since we're in local.`);
|
|
76
|
+
|
|
77
|
+
this.Email.started && this.Email.send({
|
|
78
|
+
from: this.Email.config.bugReport.from,
|
|
79
|
+
to: this.Email.config.bugReport.to,
|
|
80
|
+
subject: "Bug on CSM server: " + (report.error.message),
|
|
81
|
+
html: `
|
|
82
|
+
<a href="${this.Router.http.publicUrl}/admin/activity/requests/${report.channelId}">
|
|
83
|
+
View Request details & console
|
|
84
|
+
</a>
|
|
85
|
+
<br/>
|
|
86
|
+
Channel Type: ${report.channelType}
|
|
87
|
+
Channel ID: ${report.channelId}
|
|
88
|
+
User: ${report.user}
|
|
89
|
+
IP: ${report.ip}
|
|
90
|
+
<br/>
|
|
91
|
+
${report.logs}
|
|
92
|
+
`
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
this.Slack.started && this.Slack.sendInChannel('engineering-alerts', `
|
|
96
|
+
*A bug occured on CSM server:* ${report.error.message}
|
|
97
|
+
Channel Type: ${report.channelType}
|
|
98
|
+
Channel ID: ${report.channelId}
|
|
99
|
+
User: ${report.user}
|
|
100
|
+
IP: ${report.ip}
|
|
101
|
+
Stacktrace:
|
|
102
|
+
${report.error.stack}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public async shutdown() {
|
|
107
|
+
|
|
108
|
+
console.log("CrossPath CSM Server will shutdown");
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
import slugify from 'slugify';
|
|
3
|
+
|
|
4
|
+
export const genSlug = (label: string) => slugify( label,{
|
|
5
|
+
replacement: '-', // replace spaces with replacement character, defaults to `-`
|
|
6
|
+
remove: undefined, // remove characters that match regex, defaults to `undefined`
|
|
7
|
+
lower: true, // convert to lower case, defaults to `false`
|
|
8
|
+
strict: false, // strip special characters except replacement, defaults to `false`
|
|
9
|
+
locale: 'vi', // language code of the locale to use
|
|
10
|
+
trim: true // trim leading and trailing replacement chars, defaults to `true`
|
|
11
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
|
|
7
|
+
// Core
|
|
8
|
+
import { Router, Users } from '@app';
|
|
9
|
+
|
|
10
|
+
/*----------------------------------
|
|
11
|
+
- TYPES
|
|
12
|
+
----------------------------------*/
|
|
13
|
+
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- SIGNUP
|
|
16
|
+
----------------------------------*/
|
|
17
|
+
|
|
18
|
+
Router.get('/health', { priority: 11 }, async ({ auth, request, response}) => {
|
|
19
|
+
return "ok";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
Router.get('/login', { priority: 11 }, async ({ schema, request, response }) => {
|
|
23
|
+
|
|
24
|
+
const { email, password } = schema.validate({
|
|
25
|
+
email: schema.email(),
|
|
26
|
+
password: schema.string(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await Users.login(email, password, request);
|
|
30
|
+
|
|
31
|
+
return response.redirect('/');
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
|
|
7
|
+
// Core
|
|
8
|
+
import { Router, Users, Headhunter } from '@app';
|
|
9
|
+
|
|
10
|
+
/*----------------------------------
|
|
11
|
+
- TYPES
|
|
12
|
+
----------------------------------*/
|
|
13
|
+
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- SIGNUP
|
|
16
|
+
----------------------------------*/
|
|
17
|
+
|
|
18
|
+
Router.get('/headhunters', async ({ schema, auth }) => {
|
|
19
|
+
|
|
20
|
+
const user = await auth.check('USER');
|
|
21
|
+
|
|
22
|
+
return await Headhunter.List();
|
|
23
|
+
|
|
24
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
|
|
7
|
+
// Core
|
|
8
|
+
import { Router, Users, Mission } from '@app';
|
|
9
|
+
|
|
10
|
+
/*----------------------------------
|
|
11
|
+
- TYPES
|
|
12
|
+
----------------------------------*/
|
|
13
|
+
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- SIGNUP
|
|
16
|
+
----------------------------------*/
|
|
17
|
+
|
|
18
|
+
Router.get('/missions', async ({ schema, auth }) => {
|
|
19
|
+
|
|
20
|
+
const user = await auth.check('USER');
|
|
21
|
+
|
|
22
|
+
return await Mission.List(user);
|
|
23
|
+
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
Router.get('/missions/applications', async ({ schema, auth }) => {
|
|
27
|
+
|
|
28
|
+
const user = await auth.check('USER');
|
|
29
|
+
|
|
30
|
+
const { missionId } = schema.validate({
|
|
31
|
+
missionId: schema.string({ opt: true }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return await Mission.Applications(user, missionId);
|
|
35
|
+
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
Router.post('/missions/applications/:response(approve|reject)', async ({ schema, auth, request }) => {
|
|
39
|
+
|
|
40
|
+
const user = await auth.check('USER');
|
|
41
|
+
|
|
42
|
+
const { missionId, headhunterId, response } = schema.validate({
|
|
43
|
+
missionId: schema.string(),
|
|
44
|
+
headhunterId: schema.string(),
|
|
45
|
+
response: schema.string()
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return await Mission.RespondApplication( response, missionId, headhunterId, user );
|
|
49
|
+
|
|
50
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import got from 'got';
|
|
7
|
+
|
|
8
|
+
// Services
|
|
9
|
+
import { SQL, Router, Fetch } from '@app';
|
|
10
|
+
|
|
11
|
+
// Core
|
|
12
|
+
import Service from '@server/app/service';
|
|
13
|
+
|
|
14
|
+
// App
|
|
15
|
+
import type { Headhunter } from '@/server/models';
|
|
16
|
+
|
|
17
|
+
/*----------------------------------
|
|
18
|
+
- SERVICE TYPES
|
|
19
|
+
----------------------------------*/
|
|
20
|
+
|
|
21
|
+
export type Config = {
|
|
22
|
+
debug?: boolean,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Hooks = {
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Services = {
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*----------------------------------
|
|
34
|
+
- TYPES
|
|
35
|
+
----------------------------------*/
|
|
36
|
+
|
|
37
|
+
export type HeadhunterWithKPIs = Headhunter & HeadhunterKPIs & {
|
|
38
|
+
url: string,
|
|
39
|
+
name: string,
|
|
40
|
+
onboarded: boolean,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type HeadhunterKPIs = {
|
|
44
|
+
|
|
45
|
+
activity: number,
|
|
46
|
+
reactivity: number,
|
|
47
|
+
quality: number,
|
|
48
|
+
performance: number,
|
|
49
|
+
|
|
50
|
+
bonus: number,
|
|
51
|
+
levelProgress: number,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*----------------------------------
|
|
55
|
+
- SERVICE
|
|
56
|
+
----------------------------------*/
|
|
57
|
+
export default class HeadhuntersService extends Service<Config, Hooks, CrossPathCSM, Services> {
|
|
58
|
+
|
|
59
|
+
// Actors
|
|
60
|
+
|
|
61
|
+
/*----------------------------------
|
|
62
|
+
- LIFECYCLE
|
|
63
|
+
----------------------------------*/
|
|
64
|
+
|
|
65
|
+
// Service start
|
|
66
|
+
protected async start() {
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async ready() {
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async shutdown() {
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/*----------------------------------
|
|
80
|
+
- ACTIONS
|
|
81
|
+
----------------------------------*/
|
|
82
|
+
|
|
83
|
+
public async List() {
|
|
84
|
+
return await SQL`
|
|
85
|
+
SELECT *,
|
|
86
|
+
CONCAT(firstName, ' ', lastName) as name
|
|
87
|
+
FROM Headhunter
|
|
88
|
+
ORDER BY created DESC
|
|
89
|
+
`.all();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* public async List(): Promise<HeadhunterWithKPIs[]> {
|
|
93
|
+
|
|
94
|
+
const headhunterRepo = Headhunting.user.repository;
|
|
95
|
+
|
|
96
|
+
// https://www.notion.so/Headhunter-KPIs-b430f02765d941058b3a64e89a04d8a2
|
|
97
|
+
return await SQL<HeadhunterWithKPIs>`
|
|
98
|
+
SELECT
|
|
99
|
+
CONCAT(firstName, ' ', lastName) as name,
|
|
100
|
+
CONCAT('/admin/headhunters/', airtableId) as url,
|
|
101
|
+
|
|
102
|
+
(password IS NOT NULL) as onboarded,
|
|
103
|
+
|
|
104
|
+
(${headhunterRepo.kpis.activity}) as activity,
|
|
105
|
+
(${headhunterRepo.kpis.reactivity}) as reactivity,
|
|
106
|
+
(${headhunterRepo.kpis.quality}) as quality,
|
|
107
|
+
(${headhunterRepo.kpis.performance}) as performance,
|
|
108
|
+
|
|
109
|
+
(${Headhunting.user.gamification.queries.bonus}) as bonus,
|
|
110
|
+
(${Headhunting.user.gamification.queries.levelProgress}) as levelProgress
|
|
111
|
+
|
|
112
|
+
FROM Headhunter
|
|
113
|
+
ORDER BY performance DESC, quality DESC, activity DESC, reactivity ASC
|
|
114
|
+
LIMIT 100
|
|
115
|
+
`.all();
|
|
116
|
+
}*/
|
|
117
|
+
|
|
118
|
+
public async Suggested() {
|
|
119
|
+
return await SQL<HeadhunterWithKPIs>`
|
|
120
|
+
SELECT
|
|
121
|
+
CONCAT(firstName, ' ', lastName) as name,
|
|
122
|
+
CONCAT('/admin/headhunters/', airtableId) as url
|
|
123
|
+
FROM Headhunter
|
|
124
|
+
LIMIT 20
|
|
125
|
+
`.all();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import got from 'got';
|
|
7
|
+
import { RemoteProvider } from '5htp-airtable';
|
|
8
|
+
|
|
9
|
+
// Services
|
|
10
|
+
import { SQL, Router, Email } from '@app';
|
|
11
|
+
|
|
12
|
+
// Core
|
|
13
|
+
import Service from '@server/app/service';
|
|
14
|
+
|
|
15
|
+
// App
|
|
16
|
+
|
|
17
|
+
/*----------------------------------
|
|
18
|
+
- SERVICE TYPES
|
|
19
|
+
----------------------------------*/
|
|
20
|
+
|
|
21
|
+
export const ApplicationResponses = ['approve', 'reject'] as const
|
|
22
|
+
export type TApplicationResponse = typeof ApplicationResponses[number]
|
|
23
|
+
|
|
24
|
+
export type Config = {
|
|
25
|
+
debug?: boolean,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Hooks = {
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Services = {
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*----------------------------------
|
|
37
|
+
- SERVICE
|
|
38
|
+
----------------------------------*/
|
|
39
|
+
export default class MissionsService extends Service<Config, Hooks, CrossPathCSM, Services> {
|
|
40
|
+
|
|
41
|
+
private provider = new RemoteProvider(this.app, "@recruiters/system/provider", "missions");
|
|
42
|
+
|
|
43
|
+
/*----------------------------------
|
|
44
|
+
- LIFECYCLE
|
|
45
|
+
----------------------------------*/
|
|
46
|
+
|
|
47
|
+
// Service start
|
|
48
|
+
protected async start() {
|
|
49
|
+
|
|
50
|
+
await this.provider.start();
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async ready() {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public async shutdown() {
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*----------------------------------
|
|
64
|
+
- ACTIONS
|
|
65
|
+
----------------------------------*/
|
|
66
|
+
|
|
67
|
+
public async List( user: User ) {
|
|
68
|
+
return await SQL`
|
|
69
|
+
SELECT *
|
|
70
|
+
FROM Mission
|
|
71
|
+
WHERE
|
|
72
|
+
customerSuccess = ${user.slug}
|
|
73
|
+
AND
|
|
74
|
+
Mission.status = 'Active'
|
|
75
|
+
`.all();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async Applications( user: User, missionId?: string ) {
|
|
79
|
+
return await SQL`
|
|
80
|
+
SELECT
|
|
81
|
+
|
|
82
|
+
dateStart,
|
|
83
|
+
mission as missionId,
|
|
84
|
+
headhunter as headhunterId,
|
|
85
|
+
|
|
86
|
+
CONCAT(Headhunter.firstName, ' ', Headhunter.lastName) as headhunter,
|
|
87
|
+
Mission.title as mission
|
|
88
|
+
FROM AcceptedMission
|
|
89
|
+
|
|
90
|
+
INNER JOIN Headhunter ON Headhunter.email = AcceptedMission.headhunter
|
|
91
|
+
INNER JOIN Mission ON Mission.airtableId = AcceptedMission.mission
|
|
92
|
+
|
|
93
|
+
WHERE
|
|
94
|
+
:${missionId === undefined
|
|
95
|
+
? `Mission.customerSuccess = "${user.slug}"`
|
|
96
|
+
: `Mission.airtableId = "${missionId}"`}
|
|
97
|
+
AND
|
|
98
|
+
AcceptedMission.status = 'approving'
|
|
99
|
+
|
|
100
|
+
`.all();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async RespondApplication(
|
|
104
|
+
action: TApplicationResponse,
|
|
105
|
+
missionId: string,
|
|
106
|
+
headhunterEmail: string
|
|
107
|
+
) {
|
|
108
|
+
|
|
109
|
+
// retrieve mission information
|
|
110
|
+
const mission = await SQL`
|
|
111
|
+
SELECT title, company
|
|
112
|
+
FROM Mission
|
|
113
|
+
WHERE airtableId = ${missionId};
|
|
114
|
+
`.firstOrFail("Unable to find mission info.");
|
|
115
|
+
|
|
116
|
+
// Update DB
|
|
117
|
+
await SQL.update('AcceptedMission', {
|
|
118
|
+
mission: missionId,
|
|
119
|
+
headhunter: headhunterEmail,
|
|
120
|
+
status: action === 'approve' ? 'accepted' : 'rejected'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (action === 'reject') {
|
|
124
|
+
// Send notification
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Retrieve the current list of headhunters for this mission
|
|
129
|
+
const headhuntersList = await SQL<{ headhunter: string }>`
|
|
130
|
+
SELECT Headhunter.airtableId as headhunter
|
|
131
|
+
FROM AcceptedMission
|
|
132
|
+
INNER JOIN Headhunter ON Headhunter.email = AcceptedMission.headhunter
|
|
133
|
+
WHERE
|
|
134
|
+
AcceptedMission.mission = ${missionId}
|
|
135
|
+
AND
|
|
136
|
+
AcceptedMission.status = 'accepted'
|
|
137
|
+
`.all().then(list =>
|
|
138
|
+
list.map(accepted => accepted.headhunter)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Add the currentheadhunter to the list of headhunterds for this mission
|
|
142
|
+
await this.provider.airtable.update([{
|
|
143
|
+
id: missionId,
|
|
144
|
+
'Headhunters Matched': [...headhuntersList]
|
|
145
|
+
}]);
|
|
146
|
+
|
|
147
|
+
// Send notification
|
|
148
|
+
const missionUrl = Router.url('@recruiters/mission/accepted/' + mission.company + '/' + missionId);
|
|
149
|
+
await Email.send( headhunterEmail,
|
|
150
|
+
`🎉 You've been selected to work on a new mission: ${mission.title}`,
|
|
151
|
+
`
|
|
152
|
+
Hey, I'm pleased to announce your application to work on the mission "${mission.title}" has been approved by our team!
|
|
153
|
+
|
|
154
|
+
You can start working on it starting from now, [directly on our platform](${missionUrl}).
|
|
155
|
+
|
|
156
|
+
Kind regards,
|
|
157
|
+
Your Cross Path Team
|
|
158
|
+
`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return true;
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public async RejectApplication( user: User, missionId: string, headhunterId: string ) {
|
|
166
|
+
|
|
167
|
+
// Update Db & Airtable
|
|
168
|
+
|
|
169
|
+
// Send notification
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
const sgMail = require('@sendgrid/mail');
|
|
7
|
+
|
|
8
|
+
// Core
|
|
9
|
+
import { Transporter, TCompleteEmail } from '@server/services/email';
|
|
10
|
+
import type { Application } from "@server/app/index";
|
|
11
|
+
|
|
12
|
+
/*----------------------------------
|
|
13
|
+
- CONFIG
|
|
14
|
+
----------------------------------*/
|
|
15
|
+
|
|
16
|
+
const LogPrefix = "[email][sendgrid]";
|
|
17
|
+
|
|
18
|
+
/*----------------------------------
|
|
19
|
+
- TYPES
|
|
20
|
+
----------------------------------*/
|
|
21
|
+
|
|
22
|
+
export type TConfig = {
|
|
23
|
+
debug: boolean,
|
|
24
|
+
api: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*----------------------------------
|
|
28
|
+
- TRABSPORTER
|
|
29
|
+
----------------------------------*/
|
|
30
|
+
|
|
31
|
+
export default class SendGridTransporter extends Transporter<TConfig> {
|
|
32
|
+
|
|
33
|
+
public constructor(
|
|
34
|
+
parent: Application,
|
|
35
|
+
config: TConfig,
|
|
36
|
+
services: {},
|
|
37
|
+
app: Application | 'self',
|
|
38
|
+
) {
|
|
39
|
+
super(parent, config, services, app);
|
|
40
|
+
|
|
41
|
+
sgMail.setApiKey( config.api );
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async send( emails: TCompleteEmail[] ) {
|
|
45
|
+
|
|
46
|
+
if (emails.length !== 1)
|
|
47
|
+
throw new Error(LogPrefix + ` Feature not supported by Mailchimp: can't send a different email for each person.`);
|
|
48
|
+
|
|
49
|
+
const email = emails[0];
|
|
50
|
+
|
|
51
|
+
const to = email.to.map( to => ({
|
|
52
|
+
email: to.email,
|
|
53
|
+
name: to.name,
|
|
54
|
+
type: 'to'
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
if (email.cc)
|
|
58
|
+
for (const person of email.cc)
|
|
59
|
+
to.push({
|
|
60
|
+
email: person.email,
|
|
61
|
+
name: person.name,
|
|
62
|
+
type: 'cc'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const body = {
|
|
66
|
+
key: this.config.api,
|
|
67
|
+
message: {
|
|
68
|
+
from_email: email.from.email,
|
|
69
|
+
from_name: email.from.name,
|
|
70
|
+
subject: email.subject,
|
|
71
|
+
html: email.html,
|
|
72
|
+
to
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.config.debug && console.log(LogPrefix, 'Send request to api:', body);
|
|
77
|
+
|
|
78
|
+
for (const email of emails)
|
|
79
|
+
await sgMail.send({
|
|
80
|
+
to: email.to,
|
|
81
|
+
from: email.from,
|
|
82
|
+
subject: email.subject,
|
|
83
|
+
html: email.html
|
|
84
|
+
}).then((res) => {
|
|
85
|
+
|
|
86
|
+
this.config.debug && console.log(LogPrefix, 'API request reponse:', body, res.statusCode, res.body);
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
|
|
90
|
+
}).catch( e => {
|
|
91
|
+
|
|
92
|
+
console.log(LogPrefix, "failed", body, e.response?.statusCode, e, e.response?.body);
|
|
93
|
+
throw new Error(e.response?.body?.errors[0]?.message || e.message || "Send email failed with MailChimp")
|
|
94
|
+
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|