1
|
|
|
import React, { useState, useRef } from "react"; |
2
|
|
|
import { FormattedMessage, defineMessages, useIntl } from "react-intl"; |
3
|
|
|
import * as Yup from "yup"; |
4
|
|
|
import { Formik, Form, FastField } from "formik"; |
5
|
|
|
import nprogress from "nprogress"; |
6
|
|
|
import { Job, Department } from "../../../models/types"; |
7
|
|
|
import { emptyJob } from "../../../models/jobUtil"; |
8
|
|
|
import Modal from "../../Modal"; |
9
|
|
|
import { validationMessages } from "../../Form/Messages"; |
10
|
|
|
import TextAreaInput from "../../Form/TextAreaInput"; |
11
|
|
|
import { find } from "../../../helpers/queries"; |
12
|
|
|
import { getLocale } from "../../../helpers/localize"; |
13
|
|
|
|
14
|
|
|
interface JobImpactProps { |
15
|
|
|
/** Optional Job to prepopulate form values from. */ |
16
|
|
|
job: Job | null; |
17
|
|
|
/** The list of known departments. Used to determine the Department statement. */ |
18
|
|
|
departments: Department[]; |
19
|
|
|
/** Function to run after successful form validation. |
20
|
|
|
* It must return true if the submission was successful, false otherwise. |
21
|
|
|
*/ |
22
|
|
|
handleSubmit: (values: Job) => Promise<boolean>; |
23
|
|
|
// The function to run when user clicks Prev Page |
24
|
|
|
handleReturn: () => void; |
25
|
|
|
/** Function to run when modal cancel is clicked. */ |
26
|
|
|
handleModalCancel: () => void; |
27
|
|
|
/** Function to run when modal confirm is clicked. */ |
28
|
|
|
handleModalConfirm: () => void; |
29
|
|
|
|
30
|
|
|
jobIsComplete: boolean; |
31
|
|
|
handleSkipToReview: () => Promise<void>; |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
interface ImpactFormValues { |
35
|
|
|
teamImpact: string; |
36
|
|
|
hireImpact: string; |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
const messages = defineMessages({ |
40
|
|
|
hireLabel: { |
41
|
|
|
id: "jobBuilder.impact.hireLabel", |
42
|
|
|
defaultMessage: "Hire Impact Statement", |
43
|
|
|
description: "Label for hire impact statement text area", |
44
|
|
|
}, |
45
|
|
|
teamLabel: { |
46
|
|
|
id: "jobBuilder.impact.teamLabel", |
47
|
|
|
defaultMessage: "Team Impact Statement", |
48
|
|
|
description: "Label for team impact statement text area", |
49
|
|
|
}, |
50
|
|
|
hirePlaceholder: { |
51
|
|
|
id: "jobBuilder.impact.hirePlaceholder", |
52
|
|
|
defaultMessage: "Remember, don't use Government speak...", |
53
|
|
|
description: "", |
54
|
|
|
}, |
55
|
|
|
teamPlaceholder: { |
56
|
|
|
id: "jobBuilder.impact.teamPlaceholder", |
57
|
|
|
defaultMessage: "Try for a casual, frank, friendly tone...", |
58
|
|
|
description: "", |
59
|
|
|
}, |
60
|
|
|
}); |
61
|
|
|
|
62
|
|
|
const updateJobWithValues = ( |
63
|
|
|
initialJob: Job, |
64
|
|
|
locale: "en" | "fr", |
65
|
|
|
{ teamImpact, hireImpact }: ImpactFormValues, |
66
|
|
|
deptImpacts: { en: string; fr: string }, |
67
|
|
|
): Job => ({ |
68
|
|
|
...initialJob, |
69
|
|
|
// Adding this until the impact statement for the office privacy canada gets added |
70
|
|
|
dept_impact: |
71
|
|
|
deptImpacts.en && deptImpacts.fr ? deptImpacts : { en: "N/A", fr: "S/O" }, |
72
|
|
|
team_impact: { |
73
|
|
|
...initialJob.team_impact, |
74
|
|
|
[locale]: teamImpact, |
75
|
|
|
}, |
76
|
|
|
hire_impact: { |
77
|
|
|
...initialJob.hire_impact, |
78
|
|
|
[locale]: hireImpact, |
79
|
|
|
}, |
80
|
|
|
}); |
81
|
|
|
|
82
|
|
|
const determineDeptImpact = ( |
83
|
|
|
departments: Department[], |
84
|
|
|
job: Job | null, |
85
|
|
|
): { en: string; fr: string } => { |
86
|
|
|
if (job === null || job.department_id === null) { |
87
|
|
|
return { en: "", fr: "" }; |
88
|
|
|
} |
89
|
|
|
const dept = find(departments, job.department_id); |
90
|
|
|
if (dept === null) { |
91
|
|
|
return { en: "", fr: "" }; |
92
|
|
|
} |
93
|
|
|
return { |
94
|
|
|
en: dept.impact.en, |
95
|
|
|
fr: dept.impact.fr, |
96
|
|
|
}; |
97
|
|
|
}; |
98
|
|
|
|
99
|
|
|
const deptImpactStatement = ( |
100
|
|
|
departments: Department[], |
101
|
|
|
job: Job | null, |
102
|
|
|
deptImpacts: { en: string; fr: string }, |
103
|
|
|
locale: "en" | "fr", |
104
|
|
|
): React.ReactElement => { |
105
|
|
|
if (job === null || job.department_id === null) { |
106
|
|
|
return ( |
107
|
|
|
<p data-c-margin="bottom(double)"> |
108
|
|
|
<FormattedMessage |
109
|
|
|
id="jobBuilder.impact.selectDepartment" |
110
|
|
|
defaultMessage="You must select a Department for this Job." |
111
|
|
|
description="Message warning user that they must have a department selected to complete impact statements." |
112
|
|
|
/> |
113
|
|
|
</p> |
114
|
|
|
); |
115
|
|
|
} |
116
|
|
|
if (departments.length === 0) { |
117
|
|
|
return ( |
118
|
|
|
<p data-c-margin="bottom(double)"> |
119
|
|
|
<i |
120
|
|
|
aria-hidden="true" |
121
|
|
|
className="fa fa-spinner fa-spin" |
122
|
|
|
data-c-margin="right" |
123
|
|
|
/> |
124
|
|
|
<FormattedMessage |
125
|
|
|
id="jobBuilder.impact.departmentsLoading" |
126
|
|
|
defaultMessage="Loading department data..." |
127
|
|
|
description="Placeholder message while department data is being retrieved from the server." |
128
|
|
|
/> |
129
|
|
|
</p> |
130
|
|
|
); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
return ( |
134
|
|
|
<p id="deptImpactStatement" data-c-margin="bottom(double)"> |
135
|
|
|
{deptImpacts[locale] || ( |
136
|
|
|
<FormattedMessage |
137
|
|
|
id="jobBuilder.impact.unknownDepartment" |
138
|
|
|
defaultMessage="Error: Unknown Department selected." |
139
|
|
|
description="Error message shown when the job has a department selected for which data has not been passed to this component." |
140
|
|
|
/> |
141
|
|
|
)} |
142
|
|
|
</p> |
143
|
|
|
); |
144
|
|
|
}; |
145
|
|
|
|
146
|
|
|
const JobImpact: React.FunctionComponent<JobImpactProps> = ({ |
147
|
|
|
departments, |
148
|
|
|
job, |
149
|
|
|
handleSubmit, |
150
|
|
|
handleReturn, |
151
|
|
|
handleModalCancel, |
152
|
|
|
handleModalConfirm, |
153
|
|
|
jobIsComplete, |
154
|
|
|
handleSkipToReview, |
155
|
|
|
}): React.ReactElement => { |
156
|
|
|
const intl = useIntl(); |
157
|
|
|
const locale = getLocale(intl.locale); |
158
|
|
|
const modalId = "impact-dialog"; |
159
|
|
|
const [isModalVisible, setIsModalVisible] = useState(false); |
160
|
|
|
const modalParentRef = useRef<HTMLDivElement>(null); |
161
|
|
|
|
162
|
|
|
const initialTeamImpact = job ? job.team_impact[locale] : null; |
163
|
|
|
const initialHireImpact = job ? job.hire_impact[locale] : null; |
164
|
|
|
const initialValues: ImpactFormValues = { |
165
|
|
|
teamImpact: initialTeamImpact || "", |
166
|
|
|
hireImpact: initialHireImpact || "", |
167
|
|
|
}; |
168
|
|
|
|
169
|
|
|
const validationSchema = Yup.object().shape({ |
170
|
|
|
teamImpact: Yup.string().required( |
171
|
|
|
intl.formatMessage(validationMessages.required), |
172
|
|
|
), |
173
|
|
|
hireImpact: Yup.string().required( |
174
|
|
|
intl.formatMessage(validationMessages.required), |
175
|
|
|
), |
176
|
|
|
}); |
177
|
|
|
|
178
|
|
|
const deptImpacts: { en: string; fr: string } = determineDeptImpact( |
179
|
|
|
departments, |
180
|
|
|
job, |
181
|
|
|
); |
182
|
|
|
|
183
|
|
|
const updateValuesAndReturn = (values: ImpactFormValues): void => { |
184
|
|
|
// The following only triggers after validations pass |
185
|
|
|
nprogress.start(); |
186
|
|
|
handleSubmit( |
187
|
|
|
updateJobWithValues(job || emptyJob(), locale, values, deptImpacts), |
188
|
|
|
).then((isSuccessful: boolean): void => { |
189
|
|
|
if (isSuccessful) { |
190
|
|
|
nprogress.done(); |
191
|
|
|
handleReturn(); |
192
|
|
|
} |
193
|
|
|
}); |
194
|
|
|
}; |
195
|
|
|
|
196
|
|
|
return ( |
197
|
|
|
<section ref={modalParentRef}> |
198
|
|
|
<div data-c-container="form" data-c-padding="top(triple) bottom(triple)"> |
199
|
|
|
<h3 |
200
|
|
|
data-c-font-size="h3" |
201
|
|
|
data-c-font-weight="bold" |
202
|
|
|
data-c-margin="bottom(double)" |
203
|
|
|
> |
204
|
|
|
<FormattedMessage |
205
|
|
|
id="jobBuilder.impact.title" |
206
|
|
|
defaultMessage="Create an Impact Statement" |
207
|
|
|
description="Header of Job Poster Builder Impact Step" |
208
|
|
|
/> |
209
|
|
|
</h3> |
210
|
|
|
<ul data-c-margin="bottom(double)"> |
211
|
|
|
<li> |
212
|
|
|
<FormattedMessage |
213
|
|
|
id="jobBuilder.impact.points.opportunity" |
214
|
|
|
defaultMessage="Working in the federal government offers an important opportunity to have a broad impact for Canadians." |
215
|
|
|
description="Bullet Point on Job Poster Builder Impact Step" |
216
|
|
|
/> |
217
|
|
|
</li> |
218
|
|
|
<li> |
219
|
|
|
<FormattedMessage |
220
|
|
|
id="jobBuilder.impact.points.highlight" |
221
|
|
|
defaultMessage="This is your chance to highlight what makes your work valuable and interesting." |
222
|
|
|
description="Bullet Point on Job Poster Builder Impact Step" |
223
|
|
|
/> |
224
|
|
|
</li> |
225
|
|
|
<li> |
226
|
|
|
<FormattedMessage |
227
|
|
|
id="jobBuilder.impact.points.counts" |
228
|
|
|
defaultMessage="Your impact statement is the first thing that applicants will see when they click your job poster so make sure it counts!" |
229
|
|
|
description="Bullet Point on Job Poster Builder Impact Step" |
230
|
|
|
/> |
231
|
|
|
</li> |
232
|
|
|
</ul> |
233
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
234
|
|
|
<FormattedMessage |
235
|
|
|
id="jobBuilder.impact.header.department" |
236
|
|
|
defaultMessage="How our department makes an impact:" |
237
|
|
|
description="Header of Department Impact Section on Job Poster Builder Impact Step" |
238
|
|
|
/> |
239
|
|
|
</p> |
240
|
|
|
{deptImpactStatement && |
241
|
|
|
deptImpactStatement(departments, job, deptImpacts, locale)} |
242
|
|
|
<Formik |
243
|
|
|
enableReinitialize |
244
|
|
|
initialValues={initialValues} |
245
|
|
|
validationSchema={validationSchema} |
246
|
|
|
onSubmit={(values, actions): void => { |
247
|
|
|
nprogress.start(); |
248
|
|
|
// The following only triggers after validations pass |
249
|
|
|
handleSubmit( |
250
|
|
|
updateJobWithValues( |
251
|
|
|
job || emptyJob(), |
252
|
|
|
locale, |
253
|
|
|
values, |
254
|
|
|
deptImpacts, |
255
|
|
|
), |
256
|
|
|
) |
257
|
|
|
.then((isSuccessful: boolean): void => { |
258
|
|
|
if (isSuccessful) { |
259
|
|
|
nprogress.done(); |
260
|
|
|
setIsModalVisible(true); |
261
|
|
|
} |
262
|
|
|
}) |
263
|
|
|
.finally((): void => { |
264
|
|
|
actions.setSubmitting(false); // Required by Formik to finish the submission cycle |
265
|
|
|
}); |
266
|
|
|
}} |
267
|
|
|
> |
268
|
|
|
{({ values, isSubmitting }): React.ReactElement => ( |
269
|
|
|
<> |
270
|
|
|
<Form id="form" data-c-grid="gutter"> |
271
|
|
|
<div data-c-grid-item="base(1of1)" data-c-input="textarea"> |
272
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
273
|
|
|
<FormattedMessage |
274
|
|
|
id="jobBuilder.impact.teamHeader" |
275
|
|
|
defaultMessage="How our team makes an impact:" |
276
|
|
|
description="Header of Job Poster Builder Team Impact Section" |
277
|
|
|
/> |
278
|
|
|
</p> |
279
|
|
|
<p data-c-margin="bottom(normal)"> |
280
|
|
|
<FormattedMessage |
281
|
|
|
id="jobBuilder.impact.teamBody" |
282
|
|
|
defaultMessage="Describe the value your team/service/initiative brings to Canadians. It doesn’t matter if your work is direct to citizens or back office, innovative or maintenance, top priority or ongoing. Describe how it contributes to making Canada better the way you would to someone who knows nothing about your work." |
283
|
|
|
description="Body of Job Poster Builder Team Impact Section" |
284
|
|
|
/> |
285
|
|
|
</p> |
286
|
|
|
<div> |
287
|
|
|
<FastField |
288
|
|
|
name="teamImpact" |
289
|
|
|
id="TeamImpact" |
290
|
|
|
placeholder={intl.formatMessage(messages.teamPlaceholder)} |
291
|
|
|
label={intl.formatMessage(messages.teamLabel)} |
292
|
|
|
required |
293
|
|
|
component={TextAreaInput} |
294
|
|
|
/> |
295
|
|
|
</div> |
296
|
|
|
</div> |
297
|
|
|
<div data-c-grid-item="base(1of1)" data-c-input="textarea"> |
298
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
299
|
|
|
<FormattedMessage |
300
|
|
|
id="jobBuilder.impact.hireHeader" |
301
|
|
|
defaultMessage="How the new hire makes an impact:" |
302
|
|
|
description="Header of Job Poster Builder Hire Impact Section" |
303
|
|
|
/> |
304
|
|
|
</p> |
305
|
|
|
<p data-c-margin="bottom(normal)"> |
306
|
|
|
<FormattedMessage |
307
|
|
|
id="jobBuilder.impact.hireBody" |
308
|
|
|
defaultMessage="Describe how the new hire will contribute in this role. Focus on the value they’ll bring, not on specific tasks (you’ll provide these later on). For example “In this role, you’ll contribute to…” or, “As a member of this team, you’ll be responsible for helping us…”" |
309
|
|
|
description="Body of Job Poster Builder Hire Impact Section" |
310
|
|
|
/> |
311
|
|
|
</p> |
312
|
|
|
<div> |
313
|
|
|
<FastField |
314
|
|
|
id="HireImpact" |
315
|
|
|
name="hireImpact" |
316
|
|
|
label={intl.formatMessage(messages.hireLabel)} |
317
|
|
|
placeholder={intl.formatMessage(messages.hirePlaceholder)} |
318
|
|
|
required |
319
|
|
|
component={TextAreaInput} |
320
|
|
|
/> |
321
|
|
|
</div> |
322
|
|
|
</div> |
323
|
|
|
<div data-c-grid="gutter" data-c-grid-item="base(1of1)"> |
324
|
|
|
<div data-c-grid-item="base(1of1)"> |
325
|
|
|
<hr data-c-margin="top(normal) bottom(normal)" /> |
326
|
|
|
</div> |
327
|
|
|
<div |
328
|
|
|
data-c-alignment="base(centre) tp(left)" |
329
|
|
|
data-c-grid-item="tp(1of2)" |
330
|
|
|
> |
331
|
|
|
<button |
332
|
|
|
data-c-button="outline(c2)" |
333
|
|
|
data-c-radius="rounded" |
334
|
|
|
type="button" |
335
|
|
|
disabled={isSubmitting} |
336
|
|
|
onClick={(): void => { |
337
|
|
|
updateValuesAndReturn(values); |
338
|
|
|
}} |
339
|
|
|
> |
340
|
|
|
<FormattedMessage |
341
|
|
|
id="jobBuilder.impact.button.return" |
342
|
|
|
defaultMessage="Save & Return to Work Environment" |
343
|
|
|
description="Label for Save & Return button on Impact form." |
344
|
|
|
/> |
345
|
|
|
</button> |
346
|
|
|
</div> |
347
|
|
|
<div |
348
|
|
|
data-c-alignment="base(centre) tp(right)" |
349
|
|
|
data-c-grid-item="tp(1of2)" |
350
|
|
|
> |
351
|
|
|
<button |
352
|
|
|
data-c-button="solid(c1)" |
353
|
|
|
data-c-radius="rounded" |
354
|
|
|
type="submit" |
355
|
|
|
disabled={isSubmitting} |
356
|
|
|
> |
357
|
|
|
<FormattedMessage |
358
|
|
|
id="jobBuilder.impact.button.next" |
359
|
|
|
defaultMessage="Save & Preview" |
360
|
|
|
description="Label for Save & Preview button on Impact form." |
361
|
|
|
/> |
362
|
|
|
</button> |
363
|
|
|
</div> |
364
|
|
|
</div> |
365
|
|
|
</Form> |
366
|
|
|
<Modal |
367
|
|
|
id={modalId} |
368
|
|
|
parentElement={modalParentRef.current} |
369
|
|
|
visible={isModalVisible} |
370
|
|
|
onModalConfirm={(): void => { |
371
|
|
|
handleModalConfirm(); |
372
|
|
|
setIsModalVisible(false); |
373
|
|
|
}} |
374
|
|
|
onModalCancel={(): void => { |
375
|
|
|
handleModalCancel(); |
376
|
|
|
setIsModalVisible(false); |
377
|
|
|
}} |
378
|
|
|
onModalMiddle={(): void => { |
379
|
|
|
handleSkipToReview().finally((): void => |
380
|
|
|
setIsModalVisible(false), |
381
|
|
|
); |
382
|
|
|
}} |
383
|
|
|
> |
384
|
|
|
<Modal.Header> |
385
|
|
|
<div |
386
|
|
|
data-c-background="c1(100)" |
387
|
|
|
data-c-border="bottom(thin, solid, black)" |
388
|
|
|
data-c-padding="normal" |
389
|
|
|
> |
390
|
|
|
<h5 |
391
|
|
|
data-c-colour="white" |
392
|
|
|
data-c-font-size="h4" |
393
|
|
|
id={`${modalId}-title`} |
394
|
|
|
> |
395
|
|
|
<FormattedMessage |
396
|
|
|
id="jobBuilder.impact.modalTitle" |
397
|
|
|
defaultMessage="Awesome work!" |
398
|
|
|
description="Title of modal dialog for Impact review." |
399
|
|
|
/> |
400
|
|
|
</h5> |
401
|
|
|
</div> |
402
|
|
|
</Modal.Header> |
403
|
|
|
<Modal.Body> |
404
|
|
|
<div |
405
|
|
|
data-c-border="bottom(thin, solid, black)" |
406
|
|
|
data-c-padding="normal" |
407
|
|
|
id={`${modalId}-description`} |
408
|
|
|
> |
409
|
|
|
<p> |
410
|
|
|
<FormattedMessage |
411
|
|
|
id="jobBuilder.impact.modalDescription" |
412
|
|
|
defaultMessage="Here's a preview of the Impact Statement you just entered. Feel free to go back and edit things or move to the next step if you're happy with it." |
413
|
|
|
description="Description of modal dialog for Impact review." |
414
|
|
|
/> |
415
|
|
|
</p> |
416
|
|
|
</div> |
417
|
|
|
<div |
418
|
|
|
data-c-background="grey(20)" |
419
|
|
|
data-c-border="bottom(thin, solid, black)" |
420
|
|
|
data-c-padding="normal" |
421
|
|
|
> |
422
|
|
|
<div |
423
|
|
|
className="manager-job-card" |
424
|
|
|
data-c-background="white(100)" |
425
|
|
|
data-c-padding="normal" |
426
|
|
|
data-c-radius="rounded" |
427
|
|
|
> |
428
|
|
|
<h4 |
429
|
|
|
data-c-border="bottom(thin, solid, black)" |
430
|
|
|
data-c-font-size="h4" |
431
|
|
|
data-c-font-weight="600" |
432
|
|
|
data-c-margin="bottom(normal)" |
433
|
|
|
data-c-padding="bottom(normal)" |
434
|
|
|
> |
435
|
|
|
<FormattedMessage |
436
|
|
|
id="jobBuilder.impactPreview.title" |
437
|
|
|
defaultMessage="Impact" |
438
|
|
|
description="Heading for Impact preview on modal dialog." |
439
|
|
|
/> |
440
|
|
|
</h4> |
441
|
|
|
<p id="deptImpactPreview" data-c-margin="bottom(normal)"> |
442
|
|
|
{deptImpacts[locale] || ( |
443
|
|
|
<FormattedMessage |
444
|
|
|
id="jobBuilder.impact.unknownDepartment" |
445
|
|
|
defaultMessage="Error: Unknown Department selected." |
446
|
|
|
description="Error message shown when the job has a department selected for which data has not been passed to this component." |
447
|
|
|
/> |
448
|
|
|
)} |
449
|
|
|
</p> |
450
|
|
|
<p data-c-margin="bottom(normal)">{values.teamImpact}</p> |
451
|
|
|
<p>{values.hireImpact}</p> |
452
|
|
|
</div> |
453
|
|
|
</div> |
454
|
|
|
</Modal.Body> |
455
|
|
|
<Modal.Footer> |
456
|
|
|
<Modal.FooterCancelBtn> |
457
|
|
|
<FormattedMessage |
458
|
|
|
id="jobBuilder.impact.button.goBack" |
459
|
|
|
defaultMessage="Go Back" |
460
|
|
|
description="Label for Go Back button on Impact review modal." |
461
|
|
|
/> |
462
|
|
|
</Modal.FooterCancelBtn> |
463
|
|
|
{jobIsComplete && ( |
464
|
|
|
<Modal.FooterMiddleBtn> |
465
|
|
|
<FormattedMessage |
466
|
|
|
id="jobBuilder.impact.button.skipToReview" |
467
|
|
|
defaultMessage="Skip to Review" |
468
|
|
|
description="Label for Skip to Review button on Impact review modal." |
469
|
|
|
/> |
470
|
|
|
</Modal.FooterMiddleBtn> |
471
|
|
|
)} |
472
|
|
|
<Modal.FooterConfirmBtn> |
473
|
|
|
<FormattedMessage |
474
|
|
|
id="jobBuilder.impact.button.nextStep" |
475
|
|
|
defaultMessage="Next Step" |
476
|
|
|
description="Label for Next Step button on Impact review modal." |
477
|
|
|
/> |
478
|
|
|
</Modal.FooterConfirmBtn> |
479
|
|
|
</Modal.Footer> |
480
|
|
|
</Modal> |
481
|
|
|
</> |
482
|
|
|
)} |
483
|
|
|
</Formik> |
484
|
|
|
</div> |
485
|
|
|
<div data-c-dialog-overlay={isModalVisible ? "active" : ""} /> |
486
|
|
|
</section> |
487
|
|
|
); |
488
|
|
|
}; |
489
|
|
|
|
490
|
|
|
export default JobImpact; |
491
|
|
|
|