Passed
Push — feature/azure-webapp-pipeline-... ( 9e9b64...fdb227 )
by Grant
07:49 queued 11s
created

resources/assets/js/components/ApplicantProfile/BasicInfo/ProfileBasicInfo.tsx   A

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 511
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 418
mnd 21
bc 21
fnc 0
dl 0
loc 511
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
/* eslint-disable camelcase */
2
import React from "react";
3
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
4
import * as Yup from "yup";
5
import { yupResolver } from "@hookform/resolvers/yup";
6
import { useForm, useFieldArray } from "react-hook-form";
7
import {
8
  ApplicantClassification,
9
  ApplicantProfile,
10
  Classification,
11
} from "../../../models/types";
12
import {
13
  citizenshipDeclaration as citizenshipDeclarationMessages,
14
  veteranStatus as veteranStatusMessages,
15
} from "../../Application/BasicInfo/basicInfoMessages";
16
17
import {
18
  CitizenshipId,
19
  VeteranId,
20
  GCEmployeeStatus,
21
} from "../../../models/lookupConstants";
22
import Form from "../../H2Components/Form";
23
import { validationMessages } from "../../Form/Messages";
24
import Select from "../../H2Components/Select";
25
import { gcEmployeeStatus as gcEmployeeStatusMessages } from "../../../models/localizedConstants";
26
import { getLocale, localizeFieldNonNull } from "../../../helpers/localize";
27
import { accountSettings } from "../../../helpers/routes";
28
29
const messages = defineMessages({
30
  heading: {
31
    id: "applicantProfile.basicInfo.heading",
32
    defaultMessage: "My Basic Information",
33
    description:
34
      "Heading for the My Basic Information section of the Applicant Profile.",
35
  },
36
  name: {
37
    id: "applicantProfile.basicInfo.name",
38
    defaultMessage: "Name",
39
  },
40
  personalEmail: {
41
    id: "applicantProfile.basicInfo.personalEmail",
42
    defaultMessage: "Personal Email",
43
  },
44
  toChangeGoTo: {
45
    id: "applicantProfile.basicInfo.toChangeGoTo",
46
    defaultMessage: "To change these go to",
47
  },
48
  accountSettings: {
49
    id: "applicantProfile.basicInfo.accountSettings",
50
    defaultMessage: "Account Settings",
51
  },
52
  govtJobInfoHeading: {
53
    id: "applicantProfile.basicInfo.govtJobInfo",
54
    defaultMessage: "Government Job Information",
55
    description:
56
      "Heading for the Government Job Information section of the Applicant Profile.",
57
  },
58
  citizenStatusLabel: {
59
    id: "applicantProfile.basicInfo.citizenStatusLabel",
60
    defaultMessage: "Citizenship Status:",
61
    description:
62
      "Label for the My Basic Information section of the Applicant Profile for the citizenship status input.",
63
  },
64
  veteranStatusLabel: {
65
    id: "applicantProfile.basicInfo.veteranStatusLabel",
66
    defaultMessage:
67
      "Are you a veteran or a member of the Canadian Armed forces?",
68
    description:
69
      "Label for the My Basic Information section of the Applicant Profile for the veteran status input.",
70
  },
71
  gcEmployeeStatusLabel: {
72
    id: "applicantProfile.basicInfo.gcEmployeeStatusLabel",
73
    defaultMessage: "Currently an employee of the Government of Canada",
74
    description:
75
      "Label for the Government Job Information section of the Applicant Profile for the employee status input.",
76
  },
77
  currentClassificationAndLevel: {
78
    id: "applicantProfile.basicInfo.currentClassificationAndLevel",
79
    defaultMessage: "Current classification and level:",
80
    description:
81
      "Label for the Government Job Information section of the Applicant Profile for the current classification input.",
82
  },
83
  classificationLabel: {
84
    id: "applicantProfile.basicInfo.classificationLabel",
85
    defaultMessage: "Classification:",
86
    description:
87
      "Label for the Government Job Information section of the Applicant Profile for the classification input.",
88
  },
89
  levelLabel: {
90
    id: "applicantProfile.basicInfo.levelLabel",
91
    defaultMessage: "Level:",
92
    description:
93
      "Label for the Government Job Information section of the Applicant Profile for the current classification input.",
94
  },
95
  addPreviousGcClassification: {
96
    id: "applicantProfile.basicInfo.addPreviousGcClassification",
97
    defaultMessage: "Add previous Government classifications:",
98
    description:
99
      "Heading for the Government Job Information section of the Applicant Profile for adding previous classification.",
100
  },
101
  addClassificationLabel: {
102
    id: "applicantProfile.basicInfo.addClassificationLabel",
103
    defaultMessage: "Add Previous Classification",
104
    description:
105
      "Label for the Government Job Information section of the Applicant Profile to add a previous classification.",
106
  },
107
  removeClassificationLabel: {
108
    id: "applicantProfile.basicInfo.removeClassification",
109
    defaultMessage: "Remove",
110
    description:
111
      "Label for the Government Job Information section of the Applicant Profile to remove a previous classification.",
112
  },
113
  submitFormLabel: {
114
    id: "applicantProfile.basicInfo.submitFormLabel",
115
    defaultMessage: "Submit",
116
    description:
117
      "Label for the Government Job Information section of the Applicant Profile to submit the form.",
118
  },
119
});
120
export interface ProfileBasicInfoProps {
121
  applicantId: number;
122
  applicantClassifications: ApplicantClassification[];
123
  citizenshipDeclaration: number | null;
124
  classifications: Classification[];
125
  email: string;
126
  gcEmployeeStatus: number | null;
127
  name: string;
128
  veteranStatus: number | null;
129
  handleUpdateProfile: (data: ApplicantProfile) => Promise<void>;
130
}
131
132
export const ProfileBasicInfo: React.FC<ProfileBasicInfoProps> = ({
133
  applicantId,
134
  applicantClassifications,
135
  citizenshipDeclaration,
136
  classifications,
137
  email,
138
  gcEmployeeStatus,
139
  name,
140
  veteranStatus,
141
  handleUpdateProfile,
142
}) => {
143
  const intl = useIntl();
144
  const locale = getLocale(intl.locale);
145
146
  const currentClassification =
147
    applicantClassifications.length > 0 ? applicantClassifications[0] : null; // This explicitly leaves it null if applicantClassifications is an empty array.
148
  const previousClassifications = applicantClassifications.slice(1);
149
150
  // new applicant classification object used when appending a new field in the fieldArray.
151
  const emptyApplicantClassification = (): any => {
152
    return {
153
      id: 0,
154
      applicant_id: applicantId,
155
      classification_id: "",
156
      level: "",
157
      order: -1,
158
    };
159
  };
160
161
  const defaultValues = {
162
    citizenshipDeclaration: citizenshipDeclaration || "",
163
    veteranStatus: veteranStatus || "",
164
    gcEmployeeStatus: gcEmployeeStatus || "",
165
    currentClassification:
166
      currentClassification ?? emptyApplicantClassification(),
167
    previousClassifications,
168
  };
169
170
  const validationSchema = Yup.object().shape({
171
    citizenshipDeclaration: Yup.number()
172
      .typeError(intl.formatMessage(validationMessages.required))
173
      .required(intl.formatMessage(validationMessages.required)),
174
    veteranStatus: Yup.number()
175
      .typeError(intl.formatMessage(validationMessages.required))
176
      .required(intl.formatMessage(validationMessages.required)),
177
    gcEmployeeStatus: Yup.number()
178
      .typeError(intl.formatMessage(validationMessages.required))
179
      .required(intl.formatMessage(validationMessages.required)),
180
    currentClassification: Yup.object().when("gcEmployeeStatus", {
181
      is: GCEmployeeStatus.current,
182
      then: Yup.object().shape({
183
        classification_id: Yup.number()
184
          .typeError(intl.formatMessage(validationMessages.required))
185
          .required(intl.formatMessage(validationMessages.required)),
186
        level: Yup.number()
187
          .typeError(intl.formatMessage(validationMessages.required))
188
          .required(intl.formatMessage(validationMessages.required)),
189
      }),
190
    }),
191
    previousClassifications: Yup.array().of(
192
      Yup.object().shape({
193
        classification_id: Yup.number()
194
          .typeError(intl.formatMessage(validationMessages.required))
195
          .required(intl.formatMessage(validationMessages.required)),
196
        level: Yup.number()
197
          .typeError(intl.formatMessage(validationMessages.required))
198
          .required(intl.formatMessage(validationMessages.required)),
199
      }),
200
    ),
201
  });
202
203
  interface BasicInfoFormValues {
204
    citizenshipDeclaration: number | string;
205
    veteranStatus: number | string;
206
    gcEmployeeStatus: number | string;
207
    currentClassification: ApplicantClassification;
208
    previousClassifications: ApplicantClassification[] | undefined;
209
  }
210
211
  // Main hook to create a new form. Comes with many optional arguments.
212
  // https://react-hook-form.com/api#useForm
213
  const {
214
    formState: { errors },
215
    register,
216
    handleSubmit,
217
    control,
218
    watch,
219
  } = useForm<BasicInfoFormValues>({
220
    mode: "onBlur",
221
    defaultValues,
222
    resolver: yupResolver(validationSchema),
223
  });
224
225
  const gcEmployeeStatusState = watch("gcEmployeeStatus");
226
227
  // Custom hook for working with uncontrolled field arrays (dynamic inputs).
228
  // https://react-hook-form.com/api#useFieldArray
229
  const { fields, append, remove } = useFieldArray({
230
    control,
231
    name: "previousClassifications",
232
    keyName: "key",
233
  });
234
235
  const NUM_OF_CLASSIFICATION_LEVELS = 9; // Since we do not fetch classification levels from an api the possible levels are 1-9.
236
  /**
237
   * This returns a list of classification level Option component (found in the H2 Select component).
238
   * @returns List of classification level option elements.
239
   */
240
  const classificationLevels = (): React.ReactElement[] => {
241
    const levels: number[] = [];
242
    for (let i = 1; i <= NUM_OF_CLASSIFICATION_LEVELS; i += 1) {
243
      levels.push(i);
244
    }
245
246
    return levels.map((level) => (
247
      <Select.Option key={level} value={level}>
248
        {level}
249
      </Select.Option>
250
    ));
251
  };
252
253
  const formToValuesData = (values: BasicInfoFormValues): ApplicantProfile => {
254
    const applicant_classifications =
255
      values.currentClassification &&
256
      values.gcEmployeeStatus === GCEmployeeStatus.current
257
        ? [
258
            values.currentClassification,
259
            ...(values.previousClassifications ?? []),
260
          ]
261
        : values.previousClassifications ?? [];
262
    return {
263
      citizenship_declaration_id: Number(values.citizenshipDeclaration),
264
      veteran_status_id: Number(values.veteranStatus),
265
      gc_employee_status: Number(values.gcEmployeeStatus),
266
      applicant_classifications,
267
    };
268
  };
269
270
  return (
271
    <>
272
      <h2 data-h2-font-size="b(h3)" data-h2-margin="b(bottom, 1)">
273
        {intl.formatMessage(messages.heading)}
274
      </h2>
275
      <p data-h2-margin="b(bottom, 1)">
276
        <FormattedMessage
277
          id="profile.experience.preamble"
278
          defaultMessage="This profile is also shared when you submit a job application."
279
          description="First section of text on the 'My Basic Information' of the Application Timeline."
280
        />
281
      </p>
282
283
      <p>
284
        {intl.formatMessage(messages.name)}:{" "}
285
        <span data-h2-font-color="b(theme-1)" data-h2-font-weight="b(700)">
286
          {name}
287
        </span>
288
      </p>
289
      <p>
290
        {intl.formatMessage(messages.personalEmail)}:{" "}
291
        <span data-h2-font-color="b(theme-1)" data-h2-font-weight="b(700)">
292
          {email}
293
        </span>
294
      </p>
295
      <p data-h2-margin="b(bottom, 1)">
296
        {intl.formatMessage(messages.toChangeGoTo)}:{" "}
297
        <a data-h2-font-color="b(theme-1)" href={accountSettings(locale)}>
298
          {intl.formatMessage(messages.accountSettings)}
299
        </a>
300
      </p>
301
302
      <Form
303
        onSubmit={handleSubmit((values, e) => {
304
          e?.preventDefault();
305
          const updateApplicantProfileWithValues = formToValuesData(values);
306
          handleUpdateProfile(updateApplicantProfileWithValues);
307
        })}
308
        data-h2-container="b(left, small)"
309
      >
310
        <Select
311
          name="citizenshipDeclaration"
312
          required
313
          register={register}
314
          label={intl.formatMessage(messages.citizenStatusLabel)}
315
          errorMessage={errors.citizenshipDeclaration?.message}
316
          data-h2-padding="b(right, 5)"
317
        >
318
          {Object.values(CitizenshipId).map(
319
            (id: number): React.ReactElement => (
320
              <Select.Option key={id} value={id}>
321
                {intl.formatMessage(citizenshipDeclarationMessages(id))}
322
              </Select.Option>
323
            ),
324
          )}
325
        </Select>
326
        <Select
327
          name="veteranStatus"
328
          required
329
          register={register}
330
          label={intl.formatMessage(messages.veteranStatusLabel)}
331
          errorMessage={errors.veteranStatus?.message}
332
          data-h2-padding="b(right, 5)"
333
          data-h2-margin="b(bottom, 2)"
334
        >
335
          {Object.values(VeteranId).map(
336
            (id: number): React.ReactElement => (
337
              <Select.Option key={id} value={id}>
338
                {intl.formatMessage(veteranStatusMessages(id))}
339
              </Select.Option>
340
            ),
341
          )}
342
        </Select>
343
        <h2 data-h2-font-size="b(h3)" data-h2-margin="b(bottom, 1)">
344
          {intl.formatMessage(messages.govtJobInfoHeading)}
345
        </h2>
346
        <Select
347
          name="gcEmployeeStatus"
348
          required
349
          register={register}
350
          label={intl.formatMessage(messages.gcEmployeeStatusLabel)}
351
          errorMessage={errors.gcEmployeeStatus?.message}
352
          data-h2-padding="b(right, 5) b(bottom, 1)"
353
        >
354
          {Object.values(GCEmployeeStatus).map((id) => (
355
            <Select.Option key={id} value={id}>
356
              {intl.formatMessage(gcEmployeeStatusMessages(id))}
357
            </Select.Option>
358
          ))}
359
        </Select>
360
        {/* If the user is currently a member of the GOC then allow setting a current classification */}
361
        {gcEmployeeStatusState &&
362
          Number(gcEmployeeStatusState) === GCEmployeeStatus.current && (
363
            <div data-h2-grid="b(top, expanded, flush, 1)">
364
              <p data-h2-grid-item="b(1of1)">
365
                {intl.formatMessage(messages.currentClassificationAndLevel)}
366
              </p>
367
              <Select
368
                name="currentClassification.classification_id"
369
                required
370
                register={register}
371
                label={intl.formatMessage(messages.classificationLabel)}
372
                errorMessage={
373
                  errors.currentClassification &&
374
                  errors.currentClassification.classification_id?.message
375
                }
376
                data-h2-grid-item="b(1of2)"
377
                data-h2-padding="b(right, 5)"
378
              >
379
                {classifications.map((classification) => (
380
                  <Select.Option
381
                    key={classification.key}
382
                    value={classification.id}
383
                  >
384
                    {localizeFieldNonNull(locale, classification, "name")}
385
                  </Select.Option>
386
                ))}
387
              </Select>
388
              <Select
389
                name="currentClassification.level"
390
                required
391
                register={register}
392
                label={intl.formatMessage(messages.levelLabel)}
393
                errorMessage={
394
                  errors.currentClassification &&
395
                  errors.currentClassification.level?.message
396
                }
397
                data-h2-grid-item="b(1of2)"
398
                data-h2-padding="b(right, 5)"
399
              >
400
                {classificationLevels()}
401
              </Select>
402
            </div>
403
          )}
404
        {/* If the user is currently OR a previous member of the GOC then allow creating previous classifications */}
405
        {gcEmployeeStatusState &&
406
          Number(gcEmployeeStatusState) !== GCEmployeeStatus.no && (
407
            <>
408
              <p data-h2-margin="b(bottom, 1)">
409
                {intl.formatMessage(messages.addPreviousGcClassification)}
410
              </p>
411
              <ul>
412
                {fields.map((previousClassification: any, index) => {
413
                  return (
414
                    <li
415
                      data-h2-grid="b(middle, expanded, padded, 1)"
416
                      key={previousClassification.key}
417
                    >
418
                      <Select
419
                        name={
420
                          `previousClassifications.${index}.classification_id` as const
421
                        }
422
                        defaultValue={`${
423
                          previousClassification.classification_id !== -1
424
                            ? previousClassification.classification_id
425
                            : ""
426
                        }`}
427
                        required
428
                        register={register}
429
                        label={intl.formatMessage(messages.classificationLabel)}
430
                        errorMessage={
431
                          errors.previousClassifications &&
432
                          errors.previousClassifications[index]
433
                            ?.classification_id?.message
434
                        }
435
                        data-h2-grid-item="b(5of12)"
436
                      >
437
                        {classifications.map((classification) => (
438
                          <Select.Option
439
                            key={classification.key}
440
                            value={classification.id}
441
                          >
442
                            {localizeFieldNonNull(
443
                              locale,
444
                              classification,
445
                              "name",
446
                            )}
447
                          </Select.Option>
448
                        ))}
449
                      </Select>
450
                      <Select
451
                        name={`previousClassifications.${index}.level` as const}
452
                        defaultValue={`${
453
                          previousClassification.level !== -1
454
                            ? previousClassification.level
455
                            : ""
456
                        }`}
457
                        required
458
                        register={register}
459
                        label={intl.formatMessage(messages.levelLabel)}
460
                        errorMessage={
461
                          errors.previousClassifications &&
462
                          errors.previousClassifications[index]?.level?.message
463
                        }
464
                        data-h2-grid-item="b(5of12)"
465
                      >
466
                        {classificationLevels()}
467
                      </Select>
468
                      <button
469
                        data-h2-button=""
470
                        data-h2-grid-item="b(1of12)"
471
                        data-h2-font-style="b(underline)"
472
                        data-h2-font-weight="b(700)"
473
                        type="button"
474
                        onClick={() => remove(index)}
475
                      >
476
                        {intl.formatMessage(messages.removeClassificationLabel)}
477
                      </button>
478
                    </li>
479
                  );
480
                })}
481
              </ul>
482
              <button
483
                data-h2-button=""
484
                data-h2-font-style="b(underline)"
485
                data-h2-font-weight="b(700)"
486
                data-h2-margin="b(left, 2)"
487
                type="button"
488
                onClick={() => append(emptyApplicantClassification())}
489
              >
490
                <i data-h2-padding="b(right, .25)" className="fas fa-plus" />
491
                {intl.formatMessage(messages.addClassificationLabel)}
492
              </button>
493
            </>
494
          )}
495
        <div>
496
          <button
497
            data-h2-display="b(block)"
498
            data-h2-button="theme-1, round, medium, solid"
499
            data-h2-margin="b(top, 2)"
500
            type="submit"
501
          >
502
            {intl.formatMessage(messages.submitFormLabel)}
503
          </button>
504
        </div>
505
      </Form>
506
    </>
507
  );
508
};
509
510
export default ProfileBasicInfo;
511