Passed
Push — dev ( e37f55...168365 )
by Tristan
05:02 queued 11s
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 521
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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