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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1131
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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