Passed
Push — feature/application-fit-functi... ( 2528fc )
by Yonathan
09:03
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1116
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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