Passed
Push — dev ( 8ad92c...d02306 )
by
unknown
04:51 queued 11s
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1118
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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