Passed
Push — task/ci-test-actions ( 622973...36c5a0 )
by Grant
05:05
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1114
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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