Passed
Push — dev ( 83fcf8...bf065e )
by Grant
04:35 queued 12s
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1117
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 901
c 0
b 0
f 0
dl 0
loc 1117
rs 9.699
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 { Classification, Job } from "../../../models/types";
22
import { DispatchType } from "../../../configureStore";
23
import { updateJob, createJob } from "../../../store/Job/jobActions";
24
import { validationMessages } from "../../Form/Messages";
25
import RadioInput from "../../Form/RadioInput";
26
import {
27
  LanguageRequirementId,
28
  SecurityClearanceId,
29
  ProvinceId,
30
  FrequencyId,
31
  TravelRequirementId,
32
  OvertimeRequirementId,
33
  getKeyByValue,
34
} from "../../../models/lookupConstants";
35
import { emptyJob } from "../../../models/jobUtil";
36
import {
37
  securityClearance,
38
  languageRequirement,
39
  provinceName,
40
  frequencyName,
41
  travelRequirementDescription,
42
  overtimeRequirementDescription,
43
} from "../../../models/localizedConstants";
44
import ContextBlockItem from "../../ContextBlock/ContextBlockItem";
45
import CopyToClipboardButton from "../../CopyToClipboardButton";
46
import TextAreaInput from "../../Form/TextAreaInput";
47
import {
48
  formMessages,
49
  educationMessages,
50
  buttonMessages,
51
} from "./JobDetailsMessages";
52
import { hasKey } from "../../../helpers/queries";
53
import { localizeField, getLocale } from "../../../helpers/localize";
54
import textToParagraphs from "../../../helpers/textToParagraphs";
55
import {
56
  classificationsExtractKeyValueJsonArray,
57
  classificationsExtractKeyValueJson,
58
  getClassificationKey } from "../../../store/Classification/classificationSelector";
59
60
interface JobDetailsProps {
61
  // Optional Job to prepopulate form values from.
62
  job: Job | null;
63
  classifications : Classification[];
64
  // Function to run after successful form validation.
65
  // It must return true if the submission was successful, false otherwise.
66
  handleSubmit: (values: Job) => Promise<boolean>;
67
  // The function to run when user clicks Prev Page
68
  handleReturn: () => void;
69
  // Function to run when modal cancel is clicked.
70
  handleModalCancel: () => void;
71
  // Function to run when modal confirm is clicked.
72
  handleModalConfirm: () => void;
73
  jobIsComplete: boolean;
74
  handleSkipToReview: () => Promise<void>;
75
}
76
77
type RemoteWorkType = "remoteWorkNone" | "remoteWorkCanada" | "remoteWorkWorld";
78
79
const remoteWorkMessages = {
80
  remoteWorkWorld: formMessages.remoteWorkWorldLabel,
81
  remoteWorkCanada: formMessages.remoteWorkCanadaLabel,
82
  remoteWorkNone: formMessages.remoteWorkNoneLabel,
83
};
84
85
type TeleworkOptionType =
86
  | "teleworkNever"
87
  | "teleworkOccasionally"
88
  | "teleworkSometimes"
89
  | "teleworkFrequently"
90
  | "teleworkAlways";
91
92
const teleworkMessages: {
93
  [key in TeleworkOptionType]: MessageDescriptor;
94
} = {
95
  teleworkNever: frequencyName(FrequencyId.never),
96
  teleworkOccasionally: frequencyName(FrequencyId.rarely),
97
  teleworkSometimes: frequencyName(FrequencyId.sometimes),
98
  teleworkFrequently: frequencyName(FrequencyId.often),
99
  teleworkAlways: frequencyName(FrequencyId.always),
100
};
101
102
const teleworkFrequencies: TeleworkOptionType[] = Object.keys(
103
  teleworkMessages,
104
) as TeleworkOptionType[];
105
106
type FlexHourOptionType =
107
  | "flexHoursNever"
108
  | "flexHoursOccasionally"
109
  | "flexHoursSometimes"
110
  | "flexHoursFrequently"
111
  | "flexHoursAlways";
112
113
const flexHourMessages: {
114
  [key in FlexHourOptionType]: MessageDescriptor;
115
} = {
116
  flexHoursNever: frequencyName(FrequencyId.never),
117
  flexHoursOccasionally: frequencyName(FrequencyId.sometimes),
118
  flexHoursSometimes: frequencyName(FrequencyId.rarely),
119
  flexHoursFrequently: frequencyName(FrequencyId.often),
120
  flexHoursAlways: frequencyName(FrequencyId.always),
121
};
122
const flexHourFrequencies: FlexHourOptionType[] = Object.keys(
123
  flexHourMessages,
124
) as FlexHourOptionType[];
125
126
type TravelOptionType =
127
  | "travelFrequently"
128
  | "travelOpportunitiesAvailable"
129
  | "travelNoneRequired";
130
131
const travelMessages: {
132
  [key in TravelOptionType]: MessageDescriptor;
133
} = {
134
  travelFrequently: travelRequirementDescription(
135
    TravelRequirementId.frequently,
136
  ),
137
  travelOpportunitiesAvailable: travelRequirementDescription(
138
    TravelRequirementId.available,
139
  ),
140
  travelNoneRequired: travelRequirementDescription(TravelRequirementId.none),
141
};
142
const travelRequirements: TravelOptionType[] = Object.keys(
143
  travelMessages,
144
) as TravelOptionType[];
145
146
type OvertimeOptionType =
147
  | "overtimeFrequently"
148
  | "overtimeOpportunitiesAvailable"
149
  | "overtimeNoneRequired";
150
151
const overtimeMessages: {
152
  [key in OvertimeOptionType]: MessageDescriptor;
153
} = {
154
  overtimeFrequently: overtimeRequirementDescription(
155
    OvertimeRequirementId.frequently,
156
  ),
157
  overtimeOpportunitiesAvailable: overtimeRequirementDescription(
158
    OvertimeRequirementId.available,
159
  ),
160
  overtimeNoneRequired: overtimeRequirementDescription(
161
    OvertimeRequirementId.none,
162
  ),
163
};
164
const overtimeRequirements: OvertimeOptionType[] = Object.keys(
165
  overtimeMessages,
166
) as OvertimeOptionType[];
167
168
interface DetailsFormValues {
169
  title: string;
170
  termLength: number | "";
171
  classification: number | "";
172
  level: number | "";
173
  educationRequirements: string;
174
  securityLevel: number | "";
175
  language: number | "";
176
  city: string;
177
  province: number | "";
178
  remoteWork: RemoteWorkType;
179
  telework: TeleworkOptionType;
180
  flexHours: FlexHourOptionType;
181
  travel: TravelOptionType;
182
  overtime: OvertimeOptionType;
183
}
184
185
const isClassificationSet = (values: DetailsFormValues): boolean => {
186
  return values.classification !== "" && values.level !== "";
187
};
188
189
const getEducationMsgForClassification = (
190
  classifications : Classification[],
191
  classification: number | string,
192
  intl: IntlShape,
193
): string => {
194
  return hasKey(educationMessages, getClassificationKey(classifications, Number(classification)))
195
    ? intl.formatMessage(
196
        educationMessages[getClassificationKey(classifications, Number(classification))],
197
      )
198
    : intl.formatMessage(educationMessages.classificationNotFound);
199
};
200
201
const jobToValues = (
202
  job: Job | null,
203
  classifications : Classification[],
204
  locale: "en" | "fr",
205
  intl: IntlShape,
206
): DetailsFormValues => {
207
  const values: DetailsFormValues = job
208
    ? {
209
        title: localizeField(locale, job, "title") || "", // TODO: use utility method
210
        termLength: job.term_qty || "",
211
        classification: job.classification_id || "",
212
        level: job.classification_level || "",
213
        educationRequirements: localizeField(locale, job, "education") || "",
214
        securityLevel: job.security_clearance_id || "",
215
        language: job.language_requirement_id || "",
216
        city: localizeField(locale, job, "city") || "",
217
        province: job.province_id || "",
218
        remoteWork: job.remote_work_allowed
219
          ? "remoteWorkCanada"
220
          : "remoteWorkNone",
221
        // frequency ids range from 1-5
222
        telework: job.telework_allowed_frequency_id
223
          ? teleworkFrequencies[job.telework_allowed_frequency_id - 1]
224
          : "teleworkFrequently",
225
        flexHours: job.flexible_hours_frequency_id
226
          ? flexHourFrequencies[job.flexible_hours_frequency_id - 1]
227
          : "flexHoursFrequently",
228
        travel: job.travel_requirement_id
229
          ? travelRequirements[job.travel_requirement_id - 1]
230
          : "travelFrequently",
231
        overtime: job.overtime_requirement_id
232
          ? overtimeRequirements[job.overtime_requirement_id - 1]
233
          : "overtimeFrequently",
234
      }
235
    : {
236
        title: "",
237
        termLength: "",
238
        classification: "",
239
        level: "",
240
        educationRequirements: "",
241
        securityLevel: "",
242
        language: "",
243
        city: "",
244
        province: "",
245
        remoteWork: "remoteWorkCanada",
246
        telework: "teleworkFrequently",
247
        flexHours: "flexHoursFrequently",
248
        travel: "travelFrequently",
249
        overtime: "overtimeFrequently",
250
      };
251
  // If the job has the standard education requirements saved, no need to fill the custom textbox
252
  if (
253
    values.classification &&
254
    values.educationRequirements ===
255
      getEducationMsgForClassification(classifications, values.classification, intl)
256
  ) {
257
    return {
258
      ...values,
259
      educationRequirements: "",
260
    };
261
  }
262
  return values;
263
};
264
265
const updateJobWithValues = (
266
  initialJob: Job,
267
  locale: "en" | "fr",
268
  {
269
    title,
270
    termLength,
271
    classification,
272
    level,
273
    educationRequirements,
274
    securityLevel,
275
    language,
276
    city,
277
    province,
278
    remoteWork,
279
    telework,
280
    flexHours,
281
    travel,
282
    overtime,
283
  }: DetailsFormValues,
284
): Job => ({
285
  ...initialJob,
286
  term_qty: termLength || null,
287
  classification_id: classification || null,
288
  classification_level: level || null,
289
  security_clearance_id: securityLevel || null,
290
  language_requirement_id: language || null,
291
  province_id: province || null,
292
  remote_work_allowed: remoteWork !== "remoteWorkNone",
293
  telework_allowed_frequency_id: teleworkFrequencies.indexOf(telework) + 1,
294
  flexible_hours_frequency_id: flexHourFrequencies.indexOf(flexHours) + 1,
295
  travel_requirement_id: travelRequirements.indexOf(travel) + 1,
296
  overtime_requirement_id: overtimeRequirements.indexOf(overtime) + 1,
297
  title: {
298
    ...initialJob.title,
299
    [locale]: title,
300
  },
301
  city: {
302
    ...initialJob.city,
303
    [locale]: city,
304
  },
305
  education: {
306
    ...initialJob.education,
307
    [locale]: educationRequirements,
308
  },
309
});
310
311
export const JobDetails: React.FunctionComponent<JobDetailsProps> = ({
312
  job,
313
  classifications,
314
  handleSubmit,
315
  handleReturn,
316
  handleModalCancel,
317
  handleModalConfirm,
318
  jobIsComplete,
319
  handleSkipToReview,
320
}: JobDetailsProps): React.ReactElement => {
321
  const intl = useIntl();
322
  const locale = getLocale(intl.locale);
323
  const [isModalVisible, setIsModalVisible] = useState(false);
324
  const modalParentRef = useRef<HTMLDivElement>(null);
325
  if (locale !== "en" && locale !== "fr") {
326
    throw Error("Unexpected intl.locale"); // TODO: Deal with this more elegantly.
327
  }
328
329
  const initialValues: DetailsFormValues = jobToValues(
330
    job || null,
331
    classifications,
332
    locale,
333
    intl,
334
  );
335
336
  const remoteWorkPossibleValues: RemoteWorkType[] = [
337
    "remoteWorkNone",
338
    "remoteWorkCanada",
339
    "remoteWorkWorld",
340
  ];
341
342
  const jobSchema = Yup.object().shape({
343
    title: Yup.string()
344
      .min(2, intl.formatMessage(validationMessages.tooShort))
345
      .required(intl.formatMessage(validationMessages.required)),
346
    termLength: Yup.number()
347
      .min(1, intl.formatMessage(validationMessages.tooShort))
348
      .max(36, intl.formatMessage(validationMessages.tooLong))
349
      .required(intl.formatMessage(validationMessages.required)),
350
      classification: Yup.number()
351
      .oneOf(
352
        Object.values(classificationsExtractKeyValueJson(classifications)),
353
        intl.formatMessage(validationMessages.invalidSelection),
354
      )
355
      .required(intl.formatMessage(validationMessages.required)),
356
    level: Yup.number()
357
      .min(1, intl.formatMessage(validationMessages.invalidSelection))
358
      .max(9, intl.formatMessage(validationMessages.invalidSelection))
359
      .required(intl.formatMessage(validationMessages.required)),
360
    educationRequirements: Yup.string(),
361
    securityLevel: Yup.number()
362
      .oneOf(
363
        Object.values(SecurityClearanceId),
364
        intl.formatMessage(validationMessages.invalidSelection),
365
      )
366
      .required(intl.formatMessage(validationMessages.required)),
367
    language: Yup.number()
368
      .oneOf(
369
        Object.values(LanguageRequirementId),
370
        intl.formatMessage(validationMessages.invalidSelection),
371
      )
372
      .required(intl.formatMessage(validationMessages.required)),
373
    city: Yup.string()
374
      .min(3, intl.formatMessage(validationMessages.tooShort))
375
      .max(50, intl.formatMessage(validationMessages.tooLong))
376
      .required(intl.formatMessage(validationMessages.required)),
377
    province: Yup.number()
378
      .oneOf(
379
        Object.values(ProvinceId),
380
        intl.formatMessage(validationMessages.invalidSelection),
381
      )
382
      .required(intl.formatMessage(validationMessages.required)),
383
    remoteWork: Yup.mixed()
384
      .oneOf(
385
        remoteWorkPossibleValues,
386
        intl.formatMessage(validationMessages.invalidSelection),
387
      )
388
      .required(intl.formatMessage(validationMessages.required)),
389
    telework: Yup.mixed()
390
      .oneOf(
391
        teleworkFrequencies,
392
        intl.formatMessage(validationMessages.invalidSelection),
393
      )
394
      .required(intl.formatMessage(validationMessages.required)),
395
    flexHours: Yup.mixed()
396
      .oneOf(
397
        flexHourFrequencies,
398
        intl.formatMessage(validationMessages.invalidSelection),
399
      )
400
      .required(intl.formatMessage(validationMessages.required)),
401
    travel: Yup.mixed()
402
      .oneOf(
403
        travelRequirements,
404
        intl.formatMessage(validationMessages.invalidSelection),
405
      )
406
      .required(intl.formatMessage(validationMessages.required)),
407
    overtime: Yup.mixed()
408
      .oneOf(
409
        overtimeRequirements,
410
        intl.formatMessage(validationMessages.invalidSelection),
411
      )
412
      .required(intl.formatMessage(validationMessages.required)),
413
  });
414
415
  const handleEducationRequirements = (values: DetailsFormValues): string => {
416
    return values.educationRequirements.length > 0
417
      ? values.educationRequirements
418
      : getEducationMsgForClassification(classifications, values.classification, intl);
419
  };
420
421
  const updateValuesAndReturn = (values: DetailsFormValues): void => {
422
    nprogress.start();
423
    // The following only triggers after validations pass
424
    const educationRequirements = handleEducationRequirements(values);
425
    const modifiedValues: DetailsFormValues = {
426
      ...values,
427
      educationRequirements,
428
    };
429
    handleSubmit(
430
      updateJobWithValues(job || emptyJob(), locale, modifiedValues),
431
    ).then((isSuccessful: boolean): void => {
432
      if (isSuccessful) {
433
        nprogress.done();
434
        handleReturn();
435
      }
436
    });
437
  };
438
439
  return (
440
    <section>
441
      <div
442
        data-c-container="form"
443
        data-c-padding="top(triple) bottom(triple)"
444
        ref={modalParentRef}
445
      >
446
        <h3
447
          data-c-font-size="h3"
448
          data-c-font-weight="bold"
449
          data-c-margin="bottom(double)"
450
        >
451
          <FormattedMessage
452
            id="jobBuilder.details.heading"
453
            defaultMessage="Job Details"
454
            description="Job Details page heading"
455
          />
456
        </h3>
457
        <Formik
458
          enableReinitialize
459
          initialValues={initialValues}
460
          validationSchema={jobSchema}
461
          onSubmit={(values, actions): void => {
462
            // The following only triggers after validations pass
463
            const educationRequirements: string = handleEducationRequirements(
464
              values,
465
            );
466
            const detailsFormValues: DetailsFormValues = {
467
              ...values,
468
              educationRequirements,
469
            };
470
471
            nprogress.start();
472
            handleSubmit(
473
              updateJobWithValues(job || emptyJob(), locale, detailsFormValues),
474
            )
475
              .then((isSuccessful: boolean): void => {
476
                if (isSuccessful) {
477
                  nprogress.done();
478
                  setIsModalVisible(true);
479
                }
480
              })
481
              .finally((): void => {
482
                actions.setSubmitting(false); // Required by Formik to finish the submission cycle
483
              });
484
          }}
485
        >
486
          {({ errors, touched, isSubmitting, values }): React.ReactElement => (
487
            <section>
488
              <Form id="job-information" data-c-grid="gutter">
489
                <FastField
490
                  id="builder02JobTitle"
491
                  type="text"
492
                  name="title"
493
                  component={TextInput}
494
                  required
495
                  grid="tl(1of2)"
496
                  label={intl.formatMessage(formMessages.titleLabel)}
497
                  placeholder={intl.formatMessage(
498
                    formMessages.titlePlaceholder,
499
                  )}
500
                />
501
                <FastField
502
                  id="builder02TermLength"
503
                  type="number"
504
                  name="termLength"
505
                  component={NumberInput}
506
                  min={1}
507
                  max={36}
508
                  required
509
                  grid="tl(1of2)"
510
                  label={intl.formatMessage(formMessages.termLengthLabel)}
511
                  placeholder={intl.formatMessage(
512
                    formMessages.termLengthPlaceholder,
513
                  )}
514
                />
515
                <FastField
516
                  id="builder02Classification"
517
                  name="classification"
518
                  label={intl.formatMessage(formMessages.classificationLabel)}
519
                  grid="tl(1of2)"
520
                  component={SelectInput}
521
                  required
522
                  nullSelection={intl.formatMessage(
523
                    formMessages.classificationNullSelection,
524
                  )}
525
                  options={classificationsExtractKeyValueJsonArray(classifications)}
526
                />
527
                <FastField
528
                  name="level"
529
                  id="builder02Level"
530
                  component={SelectInput}
531
                  required
532
                  label={intl.formatMessage(formMessages.levelLabel)}
533
                  grid="tl(1of2)"
534
                  nullSelection={intl.formatMessage(
535
                    formMessages.levelNullSelection,
536
                  )}
537
                  options={[
538
                    { value: 1, label: "1" },
539
                    { value: 2, label: "2" },
540
                    { value: 3, label: "3" },
541
                    { value: 4, label: "4" },
542
                    { value: 5, label: "5" },
543
                    { value: 6, label: "6" },
544
                    { value: 7, label: "7" },
545
                    { value: 8, label: "8" },
546
                    { value: 9, label: "9" },
547
                  ]}
548
                />
549
                <div data-c-grid-item="base(1of1)">
550
                  {!isClassificationSet(values) ? (
551
                    <p
552
                      data-c-font-weight="bold"
553
                      data-c-margin="bottom(normal)"
554
                      data-c-colour="grey"
555
                      data-c-border="all(thin, solid, grey)"
556
                      data-c-background="white(100)"
557
                      data-c-padding="all(normal)"
558
                      data-c-alignment="base(center)"
559
                    >
560
                      <FormattedMessage
561
                        id="jobBuilder.details.SelectClassAndLvlMessage"
562
                        defaultMessage="Please select a classification and level before preparing the education requirements."
563
                        description="Message displayed after classification and level select boxes."
564
                      />
565
                    </p>
566
                  ) : (
567
                    <>
568
                      <p
569
                        data-c-font-weight="bold"
570
                        data-c-margin="bottom(normal)"
571
                      >
572
                        <FormattedMessage
573
                          id="jobBuilder.details.educationRequirementHeader"
574
                          defaultMessage="Based on the classification level you selected, this standard paragraph will appear on the job poster."
575
                          description="Header message displayed for the Education requirement section."
576
                        />
577
                      </p>
578
                      <div>
579
                        <ContextBlockItem
580
                          wrapperMargin="bottom(normal)"
581
                          subtext={textToParagraphs(
582
                            getEducationMsgForClassification(
583
                              //classificationsExtractKeyValue(classifications),
584
                              classifications,
585
                              values.classification,
586
                              intl,
587
                            ),
588
                            {},
589
                            {
590
                              0: { "data-c-font-weight": "bold" },
591
                              5: { "data-c-font-weight": "bold" },
592
                            },
593
                          )}
594
                        />
595
                      </div>
596
597
                      <div className="job-builder-education-customization active">
598
                        <p data-c-margin="bottom(normal)">
599
                          <FormattedMessage
600
                            id="jobBuilder.details.educationRequirementCopyAndPaste"
601
                            defaultMessage="If you want to customize this paragraph, copy and paste it into the textbox below."
602
                            description="Footer message displayed for the Education requirement section."
603
                          />
604
                        </p>
605
                        <p
606
                          data-c-font-weight="bold"
607
                          data-c-margin="bottom(normal)"
608
                        >
609
                          <FormattedMessage
610
                            id="jobBuilder.details.educationRequirementReviewChanges"
611
                            defaultMessage="Your HR advisor will review your changes."
612
                            description="Footer message displayed for the Education requirement section."
613
                          />
614
                        </p>
615
                        <div
616
                          data-c-alignment="base(centre)"
617
                          data-c-margin="top(normal) bottom(half)"
618
                        >
619
                          <CopyToClipboardButton
620
                            actionText={intl.formatMessage(
621
                              buttonMessages.buttonCopyToClipboard,
622
                            )}
623
                            postActionText={intl.formatMessage(
624
                              buttonMessages.buttonCopied,
625
                            )}
626
                            textToCopy={getEducationMsgForClassification(
627
                              classifications,
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
                              classifications,
1010
                              values.classification,
1011
                              intl,
1012
                            )
1013
                      }
1014
                      termLength={
1015
                        typeof values.termLength === "string"
1016
                          ? null
1017
                          : Number(values.termLength)
1018
                      }
1019
                      telework={intl.formatMessage(
1020
                        teleworkMessages[values.telework],
1021
                      )}
1022
                      flexHours={intl.formatMessage(
1023
                        flexHourMessages[values.flexHours],
1024
                      )}
1025
                      securityLevel={
1026
                        typeof values.securityLevel === "string"
1027
                          ? ""
1028
                          : intl.formatMessage(
1029
                              securityClearance(Number(values.securityLevel)),
1030
                            )
1031
                      }
1032
                      classification={getKeyByValue(
1033
                        classificationsExtractKeyValueJson(classifications),
1034
                        values.classification,
1035
                      )}
1036
                      level={String(values.level)}
1037
                      travel={intl.formatMessage(travelMessages[values.travel])}
1038
                      overtime={intl.formatMessage(
1039
                        overtimeMessages[values.overtime],
1040
                      )}
1041
                    />
1042
                  </div>
1043
                </Modal.Body>
1044
                <Modal.Footer>
1045
                  <Modal.FooterCancelBtn>
1046
                    <FormattedMessage
1047
                      id="jobBuilder.details.modalCancelLabel"
1048
                      defaultMessage="Go Back"
1049
                      description="The text displayed on the cancel button of the Job Details modal."
1050
                    />
1051
                  </Modal.FooterCancelBtn>
1052
                  {jobIsComplete && (
1053
                    <Modal.FooterMiddleBtn>
1054
                      <FormattedMessage
1055
                        id="jobBuilder.details.modalMiddleLabel"
1056
                        defaultMessage="Skip to Review"
1057
                        description="The text displayed on the 'Skip to Review' button of the Job Details modal."
1058
                      />
1059
                    </Modal.FooterMiddleBtn>
1060
                  )}
1061
                  <Modal.FooterConfirmBtn>
1062
                    <FormattedMessage
1063
                      id="jobBuilder.details.modalConfirmLabel"
1064
                      defaultMessage="Next Step"
1065
                      description="The text displayed on the confirm button of the Job Details modal."
1066
                    />
1067
                  </Modal.FooterConfirmBtn>
1068
                </Modal.Footer>
1069
              </Modal>
1070
            </section>
1071
          )}
1072
        </Formik>
1073
      </div>
1074
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1075
    </section>
1076
  );
1077
};
1078
1079
interface JobDetailsContainerProps {
1080
  jobId: number | null;
1081
  handleModalCancel: () => void;
1082
  handleModalConfirm: () => void;
1083
}
1084
1085
const mapStateToProps = (
1086
  state: RootState,
1087
  ownProps: JobDetailsContainerProps,
1088
): {
1089
  job: Job | null;
1090
} => ({
1091
  job: ownProps.jobId ? selectJob(state, ownProps as { jobId: number }) : null,
1092
});
1093
1094
const mapDispatchToProps = (
1095
  dispatch: DispatchType,
1096
  ownProps: JobDetailsContainerProps,
1097
): {
1098
  handleSubmit: (newJob: Job) => Promise<boolean>;
1099
} => ({
1100
  handleSubmit: ownProps.jobId
1101
    ? async (newJob: Job): Promise<boolean> => {
1102
        const result = await dispatch(updateJob(newJob));
1103
        return !result.error;
1104
      }
1105
    : async (newJob: Job): Promise<boolean> => {
1106
        const result = await dispatch(createJob(newJob));
1107
        return !result.error;
1108
      },
1109
});
1110
1111
export const JobDetailsContainer = connect(
1112
  mapStateToProps,
1113
  mapDispatchToProps,
1114
)(JobDetails);
1115
1116
export default JobDetailsContainer;
1117