Passed
Push — dev ( 6272c1...da0f63 )
by Tristan
09:01
created

resources/assets/js/components/JobBuilder/Details/JobDetails.tsx   A

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1108
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 894
mnd 21
bc 21
fnc 0
dl 0
loc 1108
rs 9.706
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
/* eslint-disable jsx-a11y/label-has-associated-control, camelcase, @typescript-eslint/camelcase */
2
import React, { useState, useRef } from "react";
3
import {
4
  FormattedMessage,
5
  MessageDescriptor,
6
  IntlShape,
7
  useIntl,
8
} from "react-intl";
9
import { Formik, Form, Field, FastField } from "formik";
10
import nprogress from "nprogress";
11
import * as Yup from "yup";
12
import { connect } from "react-redux";
13
import RadioGroup from "../../Form/RadioGroup";
14
import TextInput from "../../Form/TextInput";
15
import NumberInput from "../../Form/NumberInput";
16
import SelectInput from "../../Form/SelectInput";
17
import JobPreview from "../../JobPreview";
18
import Modal from "../../Modal";
19
import { RootState } from "../../../store/store";
20
import { getJob as selectJob } from "../../../store/Job/jobSelector";
21
import { Job } from "../../../models/types";
22
import { DispatchType } from "../../../configureStore";
23
import { updateJob, createJob } from "../../../store/Job/jobActions";
24
import { validationMessages } from "../../Form/Messages";
25
import RadioInput from "../../Form/RadioInput";
26
import {
27
  LanguageRequirementId,
28
  SecurityClearanceId,
29
  ProvinceId,
30
  FrequencyId,
31
  TravelRequirementId,
32
  OvertimeRequirementId,
33
  ClassificationId,
34
  getKeyByValue,
35
} from "../../../models/lookupConstants";
36
import { emptyJob } from "../../../models/jobUtil";
37
import {
38
  securityClearance,
39
  languageRequirement,
40
  provinceName,
41
  frequencyName,
42
  travelRequirementDescription,
43
  overtimeRequirementDescription,
44
  classificationCodeOption,
45
} from "../../../models/localizedConstants";
46
import ContextBlockItem from "../../ContextBlock/ContextBlockItem";
47
import CopyToClipboardButton from "../../CopyToClipboardButton";
48
import TextAreaInput from "../../Form/TextAreaInput";
49
import { formMessages, educationMessages } from "./JobDetailsMessages";
50
import { hasKey } from "../../../helpers/queries";
51
import { localizeField, getLocale } from "../../../helpers/localize";
52
53
interface JobDetailsProps {
54
  // Optional Job to prepopulate form values from.
55
  job: Job | null;
56
  // Function to run after successful form validation.
57
  // It must return true if the submission was successful, false otherwise.
58
  handleSubmit: (values: Job) => Promise<boolean>;
59
  // The function to run when user clicks Prev Page
60
  handleReturn: () => void;
61
  // Function to run when modal cancel is clicked.
62
  handleModalCancel: () => void;
63
  // Function to run when modal confirm is clicked.
64
  handleModalConfirm: () => void;
65
  jobIsComplete: boolean;
66
  handleSkipToReview: () => Promise<void>;
67
}
68
69
type RemoteWorkType = "remoteWorkNone" | "remoteWorkCanada" | "remoteWorkWorld";
70
71
const remoteWorkMessages = {
72
  remoteWorkWorld: formMessages.remoteWorkWorldLabel,
73
  remoteWorkCanada: formMessages.remoteWorkCanadaLabel,
74
  remoteWorkNone: formMessages.remoteWorkNoneLabel,
75
};
76
77
type TeleworkOptionType =
78
  | "teleworkNever"
79
  | "teleworkOccasionally"
80
  | "teleworkSometimes"
81
  | "teleworkFrequently"
82
  | "teleworkAlways";
83
84
const teleworkMessages: {
85
  [key in TeleworkOptionType]: MessageDescriptor;
86
} = {
87
  teleworkNever: frequencyName(FrequencyId.never),
88
  teleworkOccasionally: frequencyName(FrequencyId.rarely),
89
  teleworkSometimes: frequencyName(FrequencyId.sometimes),
90
  teleworkFrequently: frequencyName(FrequencyId.often),
91
  teleworkAlways: frequencyName(FrequencyId.always),
92
};
93
94
const teleworkFrequencies: TeleworkOptionType[] = Object.keys(
95
  teleworkMessages,
96
) as TeleworkOptionType[];
97
98
type FlexHourOptionType =
99
  | "flexHoursNever"
100
  | "flexHoursOccasionally"
101
  | "flexHoursSometimes"
102
  | "flexHoursFrequently"
103
  | "flexHoursAlways";
104
105
const flexHourMessages: {
106
  [key in FlexHourOptionType]: MessageDescriptor;
107
} = {
108
  flexHoursNever: frequencyName(FrequencyId.never),
109
  flexHoursOccasionally: frequencyName(FrequencyId.sometimes),
110
  flexHoursSometimes: frequencyName(FrequencyId.rarely),
111
  flexHoursFrequently: frequencyName(FrequencyId.often),
112
  flexHoursAlways: frequencyName(FrequencyId.always),
113
};
114
const flexHourFrequencies: FlexHourOptionType[] = Object.keys(
115
  flexHourMessages,
116
) as FlexHourOptionType[];
117
118
type TravelOptionType =
119
  | "travelFrequently"
120
  | "travelOpportunitiesAvailable"
121
  | "travelNoneRequired";
122
123
const travelMessages: {
124
  [key in TravelOptionType]: MessageDescriptor;
125
} = {
126
  travelFrequently: travelRequirementDescription(
127
    TravelRequirementId.frequently,
128
  ),
129
  travelOpportunitiesAvailable: travelRequirementDescription(
130
    TravelRequirementId.available,
131
  ),
132
  travelNoneRequired: travelRequirementDescription(TravelRequirementId.none),
133
};
134
const travelRequirements: TravelOptionType[] = Object.keys(
135
  travelMessages,
136
) as TravelOptionType[];
137
138
type OvertimeOptionType =
139
  | "overtimeFrequently"
140
  | "overtimeOpportunitiesAvailable"
141
  | "overtimeNoneRequired";
142
143
const overtimeMessages: {
144
  [key in OvertimeOptionType]: MessageDescriptor;
145
} = {
146
  overtimeFrequently: overtimeRequirementDescription(
147
    OvertimeRequirementId.frequently,
148
  ),
149
  overtimeOpportunitiesAvailable: overtimeRequirementDescription(
150
    OvertimeRequirementId.available,
151
  ),
152
  overtimeNoneRequired: overtimeRequirementDescription(
153
    OvertimeRequirementId.none,
154
  ),
155
};
156
const overtimeRequirements: OvertimeOptionType[] = Object.keys(
157
  overtimeMessages,
158
) as OvertimeOptionType[];
159
160
interface DetailsFormValues {
161
  title: string;
162
  termLength: number | "";
163
  classification: number | "";
164
  level: number | "";
165
  educationRequirements: string;
166
  securityLevel: number | "";
167
  language: number | "";
168
  city: string;
169
  province: number | "";
170
  remoteWork: RemoteWorkType;
171
  telework: TeleworkOptionType;
172
  flexHours: FlexHourOptionType;
173
  travel: TravelOptionType;
174
  overtime: OvertimeOptionType;
175
}
176
177
const classificationCode = (classification: number | string): string =>
178
  getKeyByValue(ClassificationId, classification);
179
180
const isClassificationSet = (values: DetailsFormValues): boolean => {
181
  return values.classification !== "" && values.level !== "";
182
};
183
184
const getEducationMsgForClassification = (
185
  classification: number | string,
186
  intl: IntlShape,
187
): string => {
188
  return hasKey(educationMessages, classificationCode(classification))
189
    ? intl.formatMessage(educationMessages[classificationCode(classification)])
190
    : "he";
191
};
192
193
const jobToValues = (
194
  job: Job | null,
195
  locale: "en" | "fr",
196
  intl: IntlShape,
197
): DetailsFormValues => {
198
  const values: DetailsFormValues = job
199
    ? {
200
        title: localizeField(locale, job, "title") || "", // TODO: use utility method
201
        termLength: job.term_qty || "",
202
        classification: job.classification_id || "",
203
        level: job.classification_level || "",
204
        educationRequirements: localizeField(locale, job, "education") || "",
205
        securityLevel: job.security_clearance_id || "",
206
        language: job.language_requirement_id || "",
207
        city: localizeField(locale, job, "city") || "",
208
        province: job.province_id || "",
209
        remoteWork: job.remote_work_allowed
210
          ? "remoteWorkCanada"
211
          : "remoteWorkNone",
212
        // frequency ids range from 1-5
213
        telework: job.telework_allowed_frequency_id
214
          ? teleworkFrequencies[job.telework_allowed_frequency_id - 1]
215
          : "teleworkFrequently",
216
        flexHours: job.flexible_hours_frequency_id
217
          ? flexHourFrequencies[job.flexible_hours_frequency_id - 1]
218
          : "flexHoursFrequently",
219
        travel: job.travel_requirement_id
220
          ? travelRequirements[job.travel_requirement_id - 1]
221
          : "travelFrequently",
222
        overtime: job.overtime_requirement_id
223
          ? overtimeRequirements[job.overtime_requirement_id - 1]
224
          : "overtimeFrequently",
225
      }
226
    : {
227
        title: "",
228
        termLength: "",
229
        classification: "",
230
        level: "",
231
        educationRequirements: "",
232
        securityLevel: "",
233
        language: "",
234
        city: "",
235
        province: "",
236
        remoteWork: "remoteWorkCanada",
237
        telework: "teleworkFrequently",
238
        flexHours: "flexHoursFrequently",
239
        travel: "travelFrequently",
240
        overtime: "overtimeFrequently",
241
      };
242
  // If the job has the standard education requirements saved, no need to fill the custom textbox
243
  if (
244
    values.classification &&
245
    values.educationRequirements ===
246
      getEducationMsgForClassification(values.classification, intl)
247
  ) {
248
    return {
249
      ...values,
250
      educationRequirements: "",
251
    };
252
  }
253
  return values;
254
};
255
256
const updateJobWithValues = (
257
  initialJob: Job,
258
  locale: "en" | "fr",
259
  {
260
    title,
261
    termLength,
262
    classification,
263
    level,
264
    educationRequirements,
265
    securityLevel,
266
    language,
267
    city,
268
    province,
269
    remoteWork,
270
    telework,
271
    flexHours,
272
    travel,
273
    overtime,
274
  }: DetailsFormValues,
275
): Job => ({
276
  ...initialJob,
277
  term_qty: termLength || null,
278
  classification_id: classification || null,
279
  classification_level: level || null,
280
  security_clearance_id: securityLevel || null,
281
  language_requirement_id: language || null,
282
  province_id: province || null,
283
  remote_work_allowed: remoteWork !== "remoteWorkNone",
284
  telework_allowed_frequency_id: teleworkFrequencies.indexOf(telework) + 1,
285
  flexible_hours_frequency_id: flexHourFrequencies.indexOf(flexHours) + 1,
286
  travel_requirement_id: travelRequirements.indexOf(travel) + 1,
287
  overtime_requirement_id: overtimeRequirements.indexOf(overtime) + 1,
288
  title: {
289
    ...initialJob.title,
290
    [locale]: title,
291
  },
292
  city: {
293
    ...initialJob.city,
294
    [locale]: city,
295
  },
296
  education: {
297
    ...initialJob.education,
298
    [locale]: educationRequirements,
299
  },
300
});
301
302
export const JobDetails: React.FunctionComponent<JobDetailsProps> = ({
303
  job,
304
  handleSubmit,
305
  handleReturn,
306
  handleModalCancel,
307
  handleModalConfirm,
308
  jobIsComplete,
309
  handleSkipToReview,
310
}: JobDetailsProps): React.ReactElement => {
311
  const intl = useIntl();
312
  const locale = getLocale(intl.locale);
313
  const [isModalVisible, setIsModalVisible] = useState(false);
314
  const modalParentRef = useRef<HTMLDivElement>(null);
315
  if (locale !== "en" && locale !== "fr") {
316
    throw Error("Unexpected intl.locale"); // TODO: Deal with this more elegantly.
317
  }
318
  const initialValues: DetailsFormValues = jobToValues(
319
    job || null,
320
    locale,
321
    intl,
322
  );
323
324
  const remoteWorkPossibleValues: RemoteWorkType[] = [
325
    "remoteWorkNone",
326
    "remoteWorkCanada",
327
    "remoteWorkWorld",
328
  ];
329
330
  const jobSchema = Yup.object().shape({
331
    title: Yup.string()
332
      .min(2, intl.formatMessage(validationMessages.tooShort))
333
      .required(intl.formatMessage(validationMessages.required)),
334
    termLength: Yup.number()
335
      .min(1, intl.formatMessage(validationMessages.tooShort))
336
      .max(36, intl.formatMessage(validationMessages.tooLong))
337
      .required(intl.formatMessage(validationMessages.required)),
338
    classification: Yup.number()
339
      .oneOf(
340
        Object.values(ClassificationId),
341
        intl.formatMessage(validationMessages.invalidSelection),
342
      )
343
      .required(intl.formatMessage(validationMessages.required)),
344
    level: Yup.number()
345
      .min(1, intl.formatMessage(validationMessages.invalidSelection))
346
      .max(9, intl.formatMessage(validationMessages.invalidSelection))
347
      .required(intl.formatMessage(validationMessages.required)),
348
    educationRequirements: Yup.string(),
349
    securityLevel: Yup.number()
350
      .oneOf(
351
        Object.values(SecurityClearanceId),
352
        intl.formatMessage(validationMessages.invalidSelection),
353
      )
354
      .required(intl.formatMessage(validationMessages.required)),
355
    language: Yup.number()
356
      .oneOf(
357
        Object.values(LanguageRequirementId),
358
        intl.formatMessage(validationMessages.invalidSelection),
359
      )
360
      .required(intl.formatMessage(validationMessages.required)),
361
    city: Yup.string()
362
      .min(3, intl.formatMessage(validationMessages.tooShort))
363
      .max(50, intl.formatMessage(validationMessages.tooLong))
364
      .required(intl.formatMessage(validationMessages.required)),
365
    province: Yup.number()
366
      .oneOf(
367
        Object.values(ProvinceId),
368
        intl.formatMessage(validationMessages.invalidSelection),
369
      )
370
      .required(intl.formatMessage(validationMessages.required)),
371
    remoteWork: Yup.mixed()
372
      .oneOf(
373
        remoteWorkPossibleValues,
374
        intl.formatMessage(validationMessages.invalidSelection),
375
      )
376
      .required(intl.formatMessage(validationMessages.required)),
377
    telework: Yup.mixed()
378
      .oneOf(
379
        teleworkFrequencies,
380
        intl.formatMessage(validationMessages.invalidSelection),
381
      )
382
      .required(intl.formatMessage(validationMessages.required)),
383
    flexHours: Yup.mixed()
384
      .oneOf(
385
        flexHourFrequencies,
386
        intl.formatMessage(validationMessages.invalidSelection),
387
      )
388
      .required(intl.formatMessage(validationMessages.required)),
389
    travel: Yup.mixed()
390
      .oneOf(
391
        travelRequirements,
392
        intl.formatMessage(validationMessages.invalidSelection),
393
      )
394
      .required(intl.formatMessage(validationMessages.required)),
395
    overtime: Yup.mixed()
396
      .oneOf(
397
        overtimeRequirements,
398
        intl.formatMessage(validationMessages.invalidSelection),
399
      )
400
      .required(intl.formatMessage(validationMessages.required)),
401
  });
402
403
  const handleEducationRequirements = (values: DetailsFormValues): string => {
404
    return values.educationRequirements.length > 0
405
      ? values.educationRequirements
406
      : getEducationMsgForClassification(values.classification, intl);
407
  };
408
409
  const updateValuesAndReturn = (values: DetailsFormValues): void => {
410
    nprogress.start();
411
    // The following only triggers after validations pass
412
    const educationRequirements = handleEducationRequirements(values);
413
    const modifiedValues: DetailsFormValues = {
414
      ...values,
415
      educationRequirements,
416
    };
417
    handleSubmit(
418
      updateJobWithValues(job || emptyJob(), locale, modifiedValues),
419
    ).then((isSuccessful: boolean): void => {
420
      if (isSuccessful) {
421
        nprogress.done();
422
        handleReturn();
423
      }
424
    });
425
  };
426
427
  return (
428
    <section>
429
      <div
430
        data-c-container="form"
431
        data-c-padding="top(triple) bottom(triple)"
432
        ref={modalParentRef}
433
      >
434
        <h3
435
          data-c-font-size="h3"
436
          data-c-font-weight="bold"
437
          data-c-margin="bottom(double)"
438
        >
439
          <FormattedMessage
440
            id="jobBuilder.details.heading"
441
            defaultMessage="Job Details"
442
            description="Job Details page heading"
443
          />
444
        </h3>
445
        <Formik
446
          enableReinitialize
447
          initialValues={initialValues}
448
          validationSchema={jobSchema}
449
          onSubmit={(values, actions): void => {
450
            // The following only triggers after validations pass
451
            const educationRequirements: string = handleEducationRequirements(
452
              values,
453
            );
454
            const detailsFormValues: DetailsFormValues = {
455
              ...values,
456
              educationRequirements,
457
            };
458
459
            nprogress.start();
460
            handleSubmit(
461
              updateJobWithValues(job || emptyJob(), locale, detailsFormValues),
462
            )
463
              .then((isSuccessful: boolean): void => {
464
                if (isSuccessful) {
465
                  nprogress.done();
466
                  setIsModalVisible(true);
467
                }
468
              })
469
              .finally((): void => {
470
                actions.setSubmitting(false); // Required by Formik to finish the submission cycle
471
              });
472
          }}
473
        >
474
          {({ errors, touched, isSubmitting, values }): React.ReactElement => (
475
            <section>
476
              <Form id="job-information" data-c-grid="gutter">
477
                <FastField
478
                  id="builder02JobTitle"
479
                  type="text"
480
                  name="title"
481
                  component={TextInput}
482
                  required
483
                  grid="tl(1of2)"
484
                  label={intl.formatMessage(formMessages.titleLabel)}
485
                  placeholder={intl.formatMessage(
486
                    formMessages.titlePlaceholder,
487
                  )}
488
                />
489
                <FastField
490
                  id="builder02TermLength"
491
                  type="number"
492
                  name="termLength"
493
                  component={NumberInput}
494
                  min={1}
495
                  max={36}
496
                  required
497
                  grid="tl(1of2)"
498
                  label={intl.formatMessage(formMessages.termLengthLabel)}
499
                  placeholder={intl.formatMessage(
500
                    formMessages.termLengthPlaceholder,
501
                  )}
502
                />
503
                <FastField
504
                  id="builder02Classification"
505
                  name="classification"
506
                  label={intl.formatMessage(formMessages.classificationLabel)}
507
                  grid="tl(1of2)"
508
                  component={SelectInput}
509
                  required
510
                  nullSelection={intl.formatMessage(
511
                    formMessages.classificationNullSelection,
512
                  )}
513
                  options={Object.values(ClassificationId).map((id: number): {
514
                    value: number;
515
                    label: string;
516
                  } => ({
517
                    value: id,
518
                    label: intl.formatMessage(classificationCodeOption(id)),
519
                  }))}
520
                />
521
                <FastField
522
                  name="level"
523
                  id="builder02Level"
524
                  component={SelectInput}
525
                  required
526
                  label={intl.formatMessage(formMessages.levelLabel)}
527
                  grid="tl(1of2)"
528
                  nullSelection={intl.formatMessage(
529
                    formMessages.levelNullSelection,
530
                  )}
531
                  options={[
532
                    { value: 1, label: "1" },
533
                    { value: 2, label: "2" },
534
                    { value: 3, label: "3" },
535
                    { value: 4, label: "4" },
536
                    { value: 5, label: "5" },
537
                    { value: 6, label: "6" },
538
                    { value: 7, label: "7" },
539
                    { value: 8, label: "8" },
540
                    { value: 9, label: "9" },
541
                  ]}
542
                />
543
                <div data-c-grid-item="base(1of1)">
544
                  {!isClassificationSet(values) ? (
545
                    <p
546
                      data-c-font-weight="bold"
547
                      data-c-margin="bottom(normal)"
548
                      data-c-colour="grey"
549
                      data-c-border="all(thin, solid, grey)"
550
                      data-c-background="white(100)"
551
                      data-c-padding="all(normal)"
552
                      data-c-alignment="base(center)"
553
                    >
554
                      <FormattedMessage
555
                        id="jobBuilder.details.SelectClassAndLvlMessage"
556
                        defaultMessage="Please select a classification and level before preparing the education requirements."
557
                        description="Message displayed after classification and level select boxes."
558
                      />
559
                    </p>
560
                  ) : (
561
                    <>
562
                      <p
563
                        data-c-font-weight="bold"
564
                        data-c-margin="bottom(normal)"
565
                      >
566
                        <FormattedMessage
567
                          id="jobBuilder.details.educationRequirementHeader"
568
                          defaultMessage="Based on the classification level you selected, this standard paragraph will appear on the job poster."
569
                          description="Header message displayed for the Education requirement section."
570
                        />
571
                      </p>
572
                      <div>
573
                        <ContextBlockItem
574
                          wrapperMargin="bottom(normal)"
575
                          subtext={getEducationMsgForClassification(
576
                            values.classification,
577
                            intl,
578
                          )}
579
                        />
580
                      </div>
581
582
                      <div className="job-builder-education-customization active">
583
                        <p data-c-margin="bottom(normal)">
584
                          <FormattedMessage
585
                            id="jobBuilder.details.educationRequirementCopyAndPaste"
586
                            defaultMessage="If you want to customize this paragraph, copy and paste it into the textbox below."
587
                            description="Footer message displayed for the Education requirement section."
588
                          />
589
                        </p>
590
                        <p
591
                          data-c-font-weight="bold"
592
                          data-c-margin="bottom(normal)"
593
                        >
594
                          <FormattedMessage
595
                            id="jobBuilder.details.educationRequirementReviewChanges"
596
                            defaultMessage="Your HR advisor will review your changes."
597
                            description="Footer message displayed for the Education requirement section."
598
                          />
599
                        </p>
600
                        <div
601
                          data-c-alignment="base(centre)"
602
                          data-c-margin="top(normal) bottom(half)"
603
                        >
604
                          <CopyToClipboardButton
605
                            actionText={
606
                              <FormattedMessage
607
                                id="button.copyToClipboard"
608
                                defaultMessage="Copy to Clipboard"
609
                                description="Button to copy text to clipboard."
610
                              />
611
                            }
612
                            postActionText={
613
                              <FormattedMessage
614
                                id="button.copied"
615
                                defaultMessage="Copied!"
616
                                description="Confirmation for Button to copy text to clipboard."
617
                              />
618
                            }
619
                            textToCopy={getEducationMsgForClassification(
620
                              values.classification,
621
                              intl,
622
                            )}
623
                          />
624
                        </div>
625
                        <Field
626
                          type="textarea"
627
                          id="education_requirements"
628
                          name="educationRequirements"
629
                          label={intl.formatMessage(
630
                            formMessages.educationRequirementsLabel,
631
                          )}
632
                          placeholder={intl.formatMessage(
633
                            formMessages.educationRequirementPlaceholder,
634
                          )}
635
                          component={TextAreaInput}
636
                          grid="base(1of1)"
637
                        />
638
                      </div>
639
                    </>
640
                  )}
641
                </div>
642
                <FastField
643
                  name="securityLevel"
644
                  id="builder02SecurityLevel"
645
                  component={SelectInput}
646
                  required
647
                  grid="tl(1of2)"
648
                  label={intl.formatMessage(formMessages.securityLevelLabel)}
649
                  nullSelection={intl.formatMessage(
650
                    formMessages.securityLevelNullSelection,
651
                  )}
652
                  options={Object.values(SecurityClearanceId).map(
653
                    (id: number): { value: number; label: string } => ({
654
                      value: id,
655
                      label: intl.formatMessage(securityClearance(id)),
656
                    }),
657
                  )}
658
                />
659
                <FastField
660
                  name="language"
661
                  id="builder02Language"
662
                  component={SelectInput}
663
                  required
664
                  grid="tl(1of2)"
665
                  label={intl.formatMessage(formMessages.languageLabel)}
666
                  nullSelection={intl.formatMessage(
667
                    formMessages.languageNullSelection,
668
                  )}
669
                  options={Object.values(LanguageRequirementId).map(
670
                    (id: number): { value: number; label: string } => ({
671
                      value: id,
672
                      label: intl.formatMessage(languageRequirement(id)),
673
                    }),
674
                  )}
675
                />
676
                <FastField
677
                  name="city"
678
                  type="text"
679
                  component={TextInput}
680
                  required
681
                  grid="tl(1of2)"
682
                  id="builder02City"
683
                  label={intl.formatMessage(formMessages.cityLabel)}
684
                  placeholder={intl.formatMessage(formMessages.cityPlaceholder)}
685
                />
686
                <FastField
687
                  name="province"
688
                  id="builder02Province"
689
                  component={SelectInput}
690
                  required
691
                  grid="tl(1of2)"
692
                  label={intl.formatMessage(formMessages.provinceLabel)}
693
                  nullSelection={intl.formatMessage(
694
                    formMessages.provinceNullSelection,
695
                  )}
696
                  options={Object.values(ProvinceId).map((id: number): {
697
                    value: number;
698
                    label: string;
699
                  } => ({
700
                    value: id,
701
                    label: intl.formatMessage(provinceName(id)),
702
                  }))}
703
                />
704
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
705
                  <FormattedMessage
706
                    id="jobBuilder.details.remoteWorkGroupHeader"
707
                    defaultMessage="Is remote work allowed?"
708
                    description="Header message displayed on the remote work group input."
709
                  />
710
                </p>
711
                <p data-c-margin="bottom(normal)">
712
                  <FormattedMessage
713
                    id="jobBuilder.details.remoteWorkGroupBody"
714
                    defaultMessage="Want the best talent in Canada? You increase your chances when you allow those in other parts of Canada to apply. Regional diversity also adds perspective to your team culture. Make sure to discuss this in advance with your HR Advisor."
715
                    description="Body message displayed on the remote work group input."
716
                  />
717
                </p>
718
                <RadioGroup
719
                  id="remoteWork"
720
                  label={intl.formatMessage(formMessages.remoteWorkGroupLabel)}
721
                  required
722
                  grid="base(1of1)"
723
                  error={errors.remoteWork}
724
                  touched={touched.remoteWork}
725
                  value={values.remoteWork}
726
                >
727
                  {Object.keys(remoteWorkMessages).map(
728
                    (key): React.ReactElement => {
729
                      return (
730
                        <FastField
731
                          key={key}
732
                          name="remoteWork"
733
                          component={RadioInput}
734
                          id={key}
735
                          label={intl.formatMessage(remoteWorkMessages[key])}
736
                        />
737
                      );
738
                    },
739
                  )}
740
                </RadioGroup>
741
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
742
                  <FormattedMessage
743
                    id="jobBuilder.details.teleworkGroupHeader"
744
                    defaultMessage="How often is telework allowed?"
745
                    description="Header message displayed on the telework group input."
746
                  />
747
                </p>
748
                <p data-c-margin="bottom(normal)">
749
                  <FormattedMessage
750
                    id="jobBuilder.details.teleworkGroupBody"
751
                    defaultMessage="Demonstrate that you trust your employees and you have a positive workplace culture. Allow telework as an option."
752
                    description="Body message displayed on the telework group input."
753
                  />
754
                </p>
755
                <RadioGroup
756
                  id="telework"
757
                  label={intl.formatMessage(formMessages.teleworkGroupLabel)}
758
                  required
759
                  grid="base(1of1)"
760
                  error={errors.telework}
761
                  touched={touched.telework}
762
                  value={values.telework}
763
                >
764
                  {Object.keys(teleworkMessages).map(
765
                    (key): React.ReactElement => {
766
                      return (
767
                        <FastField
768
                          key={key}
769
                          name="telework"
770
                          component={RadioInput}
771
                          id={key}
772
                          label={intl.formatMessage(teleworkMessages[key])}
773
                        />
774
                      );
775
                    },
776
                  )}
777
                </RadioGroup>
778
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
779
                  <FormattedMessage
780
                    id="jobBuilder.details.flexHoursGroupHeader"
781
                    defaultMessage="How often are flexible hours allowed?"
782
                    description="Header message displayed on the flex hours group input."
783
                  />
784
                </p>
785
                <p data-c-margin="bottom(normal)">
786
                  <FormattedMessage
787
                    id="jobBuilder.details.flexHoursGroupBody"
788
                    defaultMessage={`Want to support a more gender inclusive workplace?
789
                          Studies show allowing flex hours is a great way to improve opportunities for women and parents.`}
790
                    description="Body message displayed on the flex hours group input."
791
                  />
792
                </p>
793
                <RadioGroup
794
                  id="flexHours"
795
                  required
796
                  grid="base(1of1)"
797
                  label={intl.formatMessage(formMessages.flexHoursGroupLabel)}
798
                  error={errors.flexHours}
799
                  touched={touched.flexHours}
800
                  value={values.flexHours}
801
                >
802
                  {Object.keys(flexHourMessages).map(
803
                    (key): React.ReactElement => {
804
                      return (
805
                        <FastField
806
                          key={key}
807
                          name="flexHours"
808
                          component={RadioInput}
809
                          id={key}
810
                          label={intl.formatMessage(flexHourMessages[key])}
811
                        />
812
                      );
813
                    },
814
                  )}
815
                </RadioGroup>
816
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
817
                  <FormattedMessage
818
                    id="jobBuilder.details.travelGroupHeader"
819
                    defaultMessage="Is travel required?"
820
                    description="Header message displayed on the travel group input."
821
                  />
822
                </p>
823
                <RadioGroup
824
                  id="travel"
825
                  required
826
                  grid="base(1of1)"
827
                  label={intl.formatMessage(formMessages.travelGroupLabel)}
828
                  error={errors.travel}
829
                  touched={touched.travel}
830
                  value={values.travel}
831
                >
832
                  {Object.keys(travelMessages).map(
833
                    (key): React.ReactElement => {
834
                      return (
835
                        <FastField
836
                          key={key}
837
                          name="travel"
838
                          component={RadioInput}
839
                          id={key}
840
                          label={intl.formatMessage(travelMessages[key])}
841
                        />
842
                      );
843
                    },
844
                  )}
845
                </RadioGroup>
846
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
847
                  <FormattedMessage
848
                    id="jobBuilder.details.overtimeGroupHeader"
849
                    defaultMessage="Is overtime required?"
850
                    description="Header message displayed on the overtime group input."
851
                  />
852
                </p>
853
                <RadioGroup
854
                  id="overtime"
855
                  required
856
                  grid="base(1of1)"
857
                  label={intl.formatMessage(formMessages.overtimeGroupLabel)}
858
                  error={errors.overtime}
859
                  touched={touched.overtime}
860
                  value={values.overtime}
861
                >
862
                  {Object.keys(overtimeMessages).map(
863
                    (key): React.ReactElement => {
864
                      return (
865
                        <FastField
866
                          key={key}
867
                          name="overtime"
868
                          component={RadioInput}
869
                          id={key}
870
                          label={intl.formatMessage(overtimeMessages[key])}
871
                        />
872
                      );
873
                    },
874
                  )}
875
                </RadioGroup>
876
                <div data-c-grid="gutter" data-c-grid-item="base(1of1)">
877
                  <div data-c-grid-item="base(1of1)">
878
                    <hr data-c-margin="top(normal) bottom(normal)" />
879
                  </div>
880
                  <div
881
                    data-c-alignment="base(centre) tp(left)"
882
                    data-c-grid-item="tp(1of2)"
883
                  >
884
                    <button
885
                      data-c-button="outline(c2)"
886
                      data-c-radius="rounded"
887
                      type="button"
888
                      disabled={isSubmitting}
889
                      onClick={(): void => {
890
                        updateValuesAndReturn(values);
891
                      }}
892
                    >
893
                      <FormattedMessage
894
                        id="jobBuilder.details.returnButtonLabel"
895
                        defaultMessage="Save & Return to Intro"
896
                        description="The text displayed on the Save & Return button of the Job Details form."
897
                      />
898
                    </button>
899
                  </div>
900
                  <div
901
                    data-c-alignment="base(centre) tp(right)"
902
                    data-c-grid-item="tp(1of2)"
903
                  >
904
                    <button
905
                      data-c-button="solid(c1)"
906
                      data-c-radius="rounded"
907
                      type="submit"
908
                      disabled={isSubmitting}
909
                    >
910
                      <FormattedMessage
911
                        id="jobBuilder.details.submitButtonLabel"
912
                        defaultMessage="Save & Preview"
913
                        description="The text displayed on the submit button for the Job Details form."
914
                      />
915
                    </button>
916
                  </div>
917
                </div>
918
              </Form>
919
              <Modal
920
                id="job-details-preview"
921
                parentElement={modalParentRef.current}
922
                visible={isModalVisible}
923
                onModalConfirm={(): void => {
924
                  handleModalConfirm();
925
                  setIsModalVisible(false);
926
                }}
927
                onModalCancel={(): void => {
928
                  handleModalCancel();
929
                  setIsModalVisible(false);
930
                }}
931
                onModalMiddle={(): void => {
932
                  handleSkipToReview().finally((): void => {
933
                    setIsModalVisible(false);
934
                  });
935
                }}
936
              >
937
                <Modal.Header>
938
                  <div
939
                    data-c-background="c1(100)"
940
                    data-c-border="bottom(thin, solid, black)"
941
                    data-c-padding="normal"
942
                  >
943
                    <h5
944
                      data-c-colour="white"
945
                      data-c-font-size="h4"
946
                      id="job-details-preview-title"
947
                    >
948
                      <FormattedMessage
949
                        id="jobBuilder.details.modalHeader"
950
                        defaultMessage="You're off to a great start!"
951
                        description="The text displayed in the header of the Job Details modal."
952
                      />
953
                    </h5>
954
                  </div>
955
                </Modal.Header>
956
                <Modal.Body>
957
                  <div
958
                    data-c-border="bottom(thin, solid, black)"
959
                    data-c-padding="normal"
960
                    id="job-details-preview-description"
961
                  >
962
                    <p>
963
                      <FormattedMessage
964
                        id="jobBuilder.details.modalBody"
965
                        defaultMessage="Here's a preview of the Job Information you just entered. Feel free to go back and edit things or move to the next step if you're happy with it."
966
                        description="The text displayed in the body of the Job Details modal."
967
                      />
968
                    </p>
969
                  </div>
970
                  <div
971
                    data-c-background="grey(20)"
972
                    data-c-border="bottom(thin, solid, black)"
973
                    data-c-padding="normal"
974
                  >
975
                    {/* TODO: Pull in the signed-in Manager's department */}
976
                    <JobPreview
977
                      title={values.title}
978
                      department="Department"
979
                      remoteWork={intl.formatMessage(
980
                        remoteWorkMessages[values.remoteWork],
981
                      )}
982
                      language={
983
                        typeof values.language === "string"
984
                          ? ""
985
                          : intl.formatMessage(
986
                              languageRequirement(Number(values.language)),
987
                            )
988
                      }
989
                      city={values.city}
990
                      province={
991
                        typeof values.province === "string"
992
                          ? ""
993
                          : intl.formatMessage(
994
                              provinceName(Number(values.province)),
995
                            )
996
                      }
997
                      education={
998
                        values.educationRequirements.length > 0
999
                          ? values.educationRequirements
1000
                          : getEducationMsgForClassification(
1001
                              values.classification,
1002
                              intl,
1003
                            )
1004
                      }
1005
                      termLength={
1006
                        typeof values.termLength === "string"
1007
                          ? null
1008
                          : Number(values.termLength)
1009
                      }
1010
                      telework={intl.formatMessage(
1011
                        teleworkMessages[values.telework],
1012
                      )}
1013
                      flexHours={intl.formatMessage(
1014
                        flexHourMessages[values.flexHours],
1015
                      )}
1016
                      securityLevel={
1017
                        typeof values.securityLevel === "string"
1018
                          ? ""
1019
                          : intl.formatMessage(
1020
                              securityClearance(Number(values.securityLevel)),
1021
                            )
1022
                      }
1023
                      classification={getKeyByValue(
1024
                        ClassificationId,
1025
                        values.classification,
1026
                      )}
1027
                      level={String(values.level)}
1028
                      travel={intl.formatMessage(travelMessages[values.travel])}
1029
                      overtime={intl.formatMessage(
1030
                        overtimeMessages[values.overtime],
1031
                      )}
1032
                    />
1033
                  </div>
1034
                </Modal.Body>
1035
                <Modal.Footer>
1036
                  <Modal.FooterCancelBtn>
1037
                    <FormattedMessage
1038
                      id="jobBuilder.details.modalCancelLabel"
1039
                      defaultMessage="Go Back"
1040
                      description="The text displayed on the cancel button of the Job Details modal."
1041
                    />
1042
                  </Modal.FooterCancelBtn>
1043
                  {jobIsComplete && (
1044
                    <Modal.FooterMiddleBtn>
1045
                      <FormattedMessage
1046
                        id="jobBuilder.details.modalMiddleLabel"
1047
                        defaultMessage="Skip to Review"
1048
                        description="The text displayed on the 'Skip to Review' button of the Job Details modal."
1049
                      />
1050
                    </Modal.FooterMiddleBtn>
1051
                  )}
1052
                  <Modal.FooterConfirmBtn>
1053
                    <FormattedMessage
1054
                      id="jobBuilder.details.modalConfirmLabel"
1055
                      defaultMessage="Next Step"
1056
                      description="The text displayed on the confirm button of the Job Details modal."
1057
                    />
1058
                  </Modal.FooterConfirmBtn>
1059
                </Modal.Footer>
1060
              </Modal>
1061
            </section>
1062
          )}
1063
        </Formik>
1064
      </div>
1065
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1066
    </section>
1067
  );
1068
};
1069
1070
interface JobDetailsContainerProps {
1071
  jobId: number | null;
1072
  handleModalCancel: () => void;
1073
  handleModalConfirm: () => void;
1074
}
1075
1076
const mapStateToProps = (
1077
  state: RootState,
1078
  ownProps: JobDetailsContainerProps,
1079
): {
1080
  job: Job | null;
1081
} => ({
1082
  job: ownProps.jobId ? selectJob(state, ownProps as { jobId: number }) : null,
1083
});
1084
1085
const mapDispatchToProps = (
1086
  dispatch: DispatchType,
1087
  ownProps: JobDetailsContainerProps,
1088
): {
1089
  handleSubmit: (newJob: Job) => Promise<boolean>;
1090
} => ({
1091
  handleSubmit: ownProps.jobId
1092
    ? async (newJob: Job): Promise<boolean> => {
1093
        const result = await dispatch(updateJob(newJob));
1094
        return !result.error;
1095
      }
1096
    : async (newJob: Job): Promise<boolean> => {
1097
        const result = await dispatch(createJob(newJob));
1098
        return !result.error;
1099
      },
1100
});
1101
1102
export const JobDetailsContainer = connect(
1103
  mapStateToProps,
1104
  mapDispatchToProps,
1105
)(JobDetails);
1106
1107
export default JobDetailsContainer;
1108