Passed
Push — dev ( 5b89c4...f3697e )
by
unknown
05:14
created

resources/assets/js/components/Application/ExperienceModals/EducationExperienceModal.tsx   A

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 500
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 411
c 0
b 0
f 0
dl 0
loc 500
rs 10
mnd 11
bc 11
fnc 0
bpm 0
cpm 0
noi 0
1
import React, { FunctionComponent } from "react";
2
import { FastField, Field, Formik, Form } from "formik";
3
import { defineMessages, useIntl, IntlShape } from "react-intl";
4
import * as Yup from "yup";
5
import {
6
  EducationFormValues,
7
  EducationSubform,
8
  validationShape as educationValidationShape,
9
} from "./EducationSubform";
10
import TextInput from "../../Form/TextInput";
11
import CheckboxInput from "../../Form/CheckboxInput";
12
import { validationMessages } from "../../Form/Messages";
13
import SkillSubform, {
14
  SkillFormValues,
15
  validationShape as skillValidationShape,
16
} from "./SkillSubform";
17
import {
18
  Skill,
19
  ExperienceEducation,
20
  EducationType,
21
  EducationStatus,
22
} from "../../../models/types";
23
import {
24
  ExperienceModalHeader,
25
  ExperienceDetailsIntro,
26
  ExperienceModalFooter,
27
  ExperienceSubmitData,
28
} from "./ExperienceModalCommon";
29
import Modal from "../../Modal";
30
import DateInput from "../../Form/DateInput";
31
import {
32
  Locales,
33
  localizeFieldNonNull,
34
  getLocale,
35
  matchValueToModel,
36
} from "../../../helpers/localize";
37
import { notEmpty } from "../../../helpers/queries";
38
import SelectInput from "../../Form/SelectInput";
39
import { newDateString } from "../../../helpers/dates";
40
41
export type FormEducationType = Pick<EducationType, "id" | "name">;
42
43
export type FormEducationStatus = Pick<EducationStatus, "id" | "name">;
44
45
interface EducationExperienceModalProps {
46
  modalId: string;
47
  experienceEducation: ExperienceEducation | null;
48
  educationTypes: FormEducationType[];
49
  educationStatuses: FormEducationStatus[];
50
  jobId: number;
51
  classificationEducationRequirements: string | null;
52
  jobEducationRequirements: string | null;
53
  requiredSkills: Skill[];
54
  savedRequiredSkills: Skill[];
55
  optionalSkills: Skill[];
56
  savedOptionalSkills: Skill[];
57
  experienceableId: number;
58
  experienceableType: ExperienceEducation["experienceable_type"];
59
  parentElement: Element | null;
60
  visible: boolean;
61
  onModalCancel: () => void;
62
  onModalConfirm: (
63
    data: ExperienceSubmitData<ExperienceEducation>,
64
  ) => Promise<void>;
65
}
66
67
export const messages = defineMessages({
68
  modalTitle: {
69
    id: "application.educationExperienceModal.modalTitle",
70
    defaultMessage: "Add Education",
71
  },
72
  modalDescription: {
73
    id: "application.educationExperienceModal.modalDescription",
74
    defaultMessage:
75
      'Got creds? Share your degree, certificates, online courses, a trade apprenticeship, licences or alternative credentials. If you’ve learned something from a recognized educational provider, include your experiences here.  (Learned something from your community or on your own? Share this as a "Community Experience" or "Personal Experience".)',
76
  },
77
  educationTypeLabel: {
78
    id: "application.educationExperienceModal.educationTypeLabel",
79
    defaultMessage: "Type of Education",
80
  },
81
  educationTypeDefault: {
82
    id: "application.educationExperienceModal.educationTypeDefault",
83
    defaultMessage: "Select an education type...",
84
    description: "Default selection in the Education Type dropdown menu.",
85
  },
86
  areaStudyLabel: {
87
    id: "application.educationExperienceModal.areaStudyLabel",
88
    defaultMessage: "Area of Study",
89
  },
90
  areaStudyPlaceholder: {
91
    id: "application.educationExperienceModal.areaStudyPlaceholder",
92
    defaultMessage: "e.g. Organic Chemistry",
93
  },
94
  institutionLabel: {
95
    id: "application.educationExperienceModal.institutionLabel",
96
    defaultMessage: "Institution",
97
  },
98
  institutionPlaceholder: {
99
    id: "application.educationExperienceModal.institutionPlaceholder",
100
    defaultMessage: "e.g. Bishop's University",
101
  },
102
  completionLabel: {
103
    id: "application.educationExperienceModal.completionLabel",
104
    defaultMessage: "Completion Status",
105
  },
106
  completionDefault: {
107
    id: "application.educationExperienceModal.completionDefault",
108
    defaultMessage: "Select a completion status...",
109
  },
110
  thesisLabel: {
111
    id: "application.educationExperienceModal.thesisLabel",
112
    defaultMessage: "Thesis Title (Optional)",
113
  },
114
  thesisPlaceholder: {
115
    id: "application.educationExperienceModal.thesisPlaceholder",
116
    defaultMessage: "e.g. How bats navigate between each other during flight",
117
  },
118
  blockcertLabel: {
119
    id: "application.educationExperienceModal.blockcertLabel",
120
    defaultMessage: "Blockcert Link (Optional)",
121
  },
122
  blockcertInlineLabel: {
123
    id: "application.educationExperienceModal.blockcertInlineLabel",
124
    defaultMessage:
125
      "Yes, I have a Blockcert and can provide it on request. (Optional)",
126
  },
127
  startDateLabel: {
128
    id: "application.educationExperienceModal.startDateLabel",
129
    defaultMessage: "Select a Start Date",
130
  },
131
  datePlaceholder: {
132
    id: "application.educationExperienceModal.datePlaceholder",
133
    defaultMessage: "yyyy-mm-dd",
134
  },
135
  isActiveLabel: {
136
    id: "application.educationExperienceModal.isActiveLabel",
137
    defaultMessage: "This experience is still ongoing, or...",
138
    description: "Label for checkbox that indicates work is still ongoing.",
139
  },
140
  endDateLabel: {
141
    id: "application.educationExperienceModal.endDateLabel",
142
    defaultMessage: "Select an End Date",
143
  },
144
});
145
146
export const EducationDetailsSubform: FunctionComponent<{
147
  educationTypes: FormEducationType[];
148
  educationStatuses: FormEducationStatus[];
149
}> = ({ educationTypes, educationStatuses }) => {
150
  const intl = useIntl();
151
  const locale = getLocale(intl.locale);
152
  return (
153
    <div data-c-container="medium">
154
      <div data-c-grid="gutter(all, 1) middle">
155
        <FastField
156
          id="education-educationTypeId"
157
          name="educationTypeId"
158
          label={intl.formatMessage(messages.educationTypeLabel)}
159
          grid="tl(1of2)"
160
          component={SelectInput}
161
          required
162
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
163
          options={educationTypes.map((type) => ({
164
            value: type.id,
165
            label: localizeFieldNonNull(locale, type, "name"),
166
          }))}
167
        />
168
        <FastField
169
          id="education-areaOfStudy"
170
          type="text"
171
          name="areaOfStudy"
172
          component={TextInput}
173
          required
174
          grid="tl(1of2)"
175
          label={intl.formatMessage(messages.areaStudyLabel)}
176
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
177
        />
178
        <FastField
179
          id="education-institution"
180
          type="text"
181
          name="institution"
182
          component={TextInput}
183
          required
184
          grid="tl(1of2)"
185
          label={intl.formatMessage(messages.institutionLabel)}
186
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
187
        />
188
        <FastField
189
          id="education-educationStatusId"
190
          name="educationStatusId"
191
          label={intl.formatMessage(messages.completionLabel)}
192
          grid="tl(1of2)"
193
          component={SelectInput}
194
          required
195
          nullSelection={intl.formatMessage(messages.completionDefault)}
196
          options={educationStatuses.map((status) => ({
197
            value: status.id,
198
            label: localizeFieldNonNull(locale, status, "name"),
199
          }))}
200
        />
201
        <FastField
202
          id="education-startDate"
203
          name="startDate"
204
          component={DateInput}
205
          required
206
          grid="base(1of1)"
207
          label={intl.formatMessage(messages.startDateLabel)}
208
          placeholder={intl.formatMessage(messages.datePlaceholder)}
209
        />
210
        <Field
211
          id="education-isActive"
212
          name="isActive"
213
          component={CheckboxInput}
214
          grid="tl(1of2)"
215
          label={intl.formatMessage(messages.isActiveLabel)}
216
        />
217
        <Field
218
          id="education-endDate"
219
          name="endDate"
220
          component={DateInput}
221
          grid="base(1of2)"
222
          label={intl.formatMessage(messages.endDateLabel)}
223
          placeholder={intl.formatMessage(messages.datePlaceholder)}
224
        />
225
        <FastField
226
          id="education-thesisTitle"
227
          type="text"
228
          name="thesisTitle"
229
          component={TextInput}
230
          grid="base(1of1)"
231
          label={intl.formatMessage(messages.thesisLabel)}
232
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
233
        />
234
        <div data-c-grid-item="base(1of1)">
235
          <FastField
236
            id="education-hasBlockcert"
237
            name="hasBlockcert"
238
            component={CheckboxInput}
239
            grid="base(1of1)"
240
            label={intl.formatMessage(messages.blockcertInlineLabel)}
241
            checkboxBorder
242
            borderLabel={intl.formatMessage(messages.blockcertLabel)}
243
          />
244
        </div>
245
      </div>
246
    </div>
247
  );
248
};
249
250
export interface EducationDetailsFormValues {
251
  educationTypeId: number | "";
252
  areaOfStudy: string;
253
  institution: string;
254
  educationStatusId: number | "";
255
  thesisTitle: string;
256
  startDate: string;
257
  isActive: boolean;
258
  endDate: string;
259
  hasBlockcert: boolean;
260
}
261
262
export const experienceToDetails = (
263
  experience: ExperienceEducation,
264
  creatingNew: boolean,
265
): EducationDetailsFormValues => {
266
  return {
267
    educationTypeId: creatingNew ? "" : experience.education_type_id,
268
    areaOfStudy: experience.area_of_study,
269
    institution: experience.institution,
270
    educationStatusId: creatingNew ? "" : experience.education_status_id,
271
    thesisTitle: experience.thesis_title ?? "",
272
    hasBlockcert: experience.has_blockcert,
273
    startDate: experience.start_date,
274
    isActive: experience.is_active,
275
    endDate: experience.end_date ? experience.end_date : "",
276
  };
277
};
278
279
export const detailsToExperience = (
280
  formValues: EducationDetailsFormValues,
281
  originalExperience: ExperienceEducation,
282
): ExperienceEducation => {
283
  return {
284
    ...originalExperience,
285
    education_type_id: formValues.educationTypeId
286
      ? Number(formValues.educationTypeId)
287
      : 1,
288
    area_of_study: formValues.areaOfStudy,
289
    institution: formValues.institution,
290
    education_status_id: formValues.educationStatusId
291
      ? Number(formValues.educationStatusId)
292
      : 1,
293
    thesis_title: formValues.thesisTitle ? formValues.thesisTitle : "",
294
    has_blockcert: formValues.hasBlockcert,
295
    start_date: formValues.startDate,
296
    is_active: formValues.isActive,
297
    end_date: formValues.endDate ? formValues.endDate : null,
298
  };
299
};
300
301
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
302
export const educationExperienceValidationShape = (intl: IntlShape) => {
303
  const requiredMsg = intl.formatMessage(validationMessages.required);
304
  const conditionalRequiredMsg = intl.formatMessage(
305
    validationMessages.endDateRequiredIfNotOngoing,
306
  );
307
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
308
  const afterStartDateMsg = intl.formatMessage(
309
    validationMessages.endDateAfterStart,
310
  );
311
  return {
312
    educationTypeId: Yup.number().required(requiredMsg),
313
    areaOfStudy: Yup.string().required(requiredMsg),
314
    institution: Yup.string().required(requiredMsg),
315
    educationStatusId: Yup.number().required(requiredMsg),
316
    thesisTitle: Yup.string(),
317
    hasBlockcert: Yup.boolean(),
318
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
319
    isActive: Yup.boolean(),
320
    endDate: Yup.date().when("isActive", {
321
      is: false,
322
      then: Yup.date()
323
        .required(conditionalRequiredMsg)
324
        .min(Yup.ref("startDate"), afterStartDateMsg),
325
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
326
    }),
327
  };
328
};
329
330
export const newExperienceEducation = (
331
  experienceableId: number,
332
  experienceableType: ExperienceEducation["experienceable_type"],
333
): ExperienceEducation => ({
334
  id: 0,
335
  education_type_id: 0,
336
  education_type: { en: "", fr: "" },
337
  area_of_study: "",
338
  institution: "",
339
  education_status_id: 0,
340
  education_status: { en: "", fr: "" },
341
  thesis_title: "",
342
  has_blockcert: false,
343
  is_active: false,
344
  start_date: newDateString(),
345
  end_date: null,
346
  is_education_requirement: false,
347
  experienceable_id: experienceableId,
348
  experienceable_type: experienceableType,
349
  type: "experience_education",
350
});
351
352
type EducationExperienceFormValues = SkillFormValues &
353
  EducationFormValues &
354
  EducationDetailsFormValues;
355
356
const dataToFormValues = (
357
  data: ExperienceSubmitData<ExperienceEducation>,
358
  locale: Locales,
359
  creatingNew: boolean,
360
): EducationExperienceFormValues => {
361
  const { experience, savedRequiredSkills, savedOptionalSkills } = data;
362
  const skillToName = (skill: Skill): string =>
363
    localizeFieldNonNull(locale, skill, "name");
364
  return {
365
    requiredSkills: savedRequiredSkills.map(skillToName),
366
    optionalSkills: savedOptionalSkills.map(skillToName),
367
    useAsEducationRequirement: experience.is_education_requirement,
368
    ...experienceToDetails(data.experience, creatingNew),
369
  };
370
};
371
372
const formValuesToData = (
373
  formValues: EducationExperienceFormValues,
374
  originalExperience: ExperienceEducation,
375
  locale: Locales,
376
  skills: Skill[],
377
): ExperienceSubmitData<ExperienceEducation> => {
378
  const nameToSkill = (name: string): Skill | null =>
379
    matchValueToModel(locale, "name", name, skills);
380
  return {
381
    experience: {
382
      ...detailsToExperience(formValues, originalExperience),
383
      is_education_requirement: formValues.useAsEducationRequirement,
384
    },
385
    savedRequiredSkills: formValues.requiredSkills
386
      .map(nameToSkill)
387
      .filter(notEmpty),
388
    savedOptionalSkills: formValues.optionalSkills
389
      .map(nameToSkill)
390
      .filter(notEmpty),
391
  };
392
};
393
394
export const EducationExperienceModal: React.FC<EducationExperienceModalProps> = ({
395
  modalId,
396
  experienceEducation,
397
  educationTypes,
398
  educationStatuses,
399
  jobId,
400
  classificationEducationRequirements,
401
  jobEducationRequirements,
402
  requiredSkills,
403
  savedRequiredSkills,
404
  optionalSkills,
405
  savedOptionalSkills,
406
  experienceableId,
407
  experienceableType,
408
  parentElement,
409
  visible,
410
  onModalCancel,
411
  onModalConfirm,
412
}) => {
413
  const intl = useIntl();
414
  const locale = getLocale(intl.locale);
415
416
  const originalExperience =
417
    experienceEducation ??
418
    newExperienceEducation(experienceableId, experienceableType);
419
420
  const skillToName = (skill: Skill): string =>
421
    localizeFieldNonNull(locale, skill, "name");
422
423
  const initialFormValues = dataToFormValues(
424
    {
425
      experience: originalExperience,
426
      savedRequiredSkills,
427
      savedOptionalSkills,
428
    },
429
    locale,
430
    experienceEducation === null,
431
  );
432
433
  const validationSchema = Yup.object().shape({
434
    ...skillValidationShape,
435
    ...educationValidationShape,
436
    ...educationExperienceValidationShape(intl),
437
  });
438
439
  return (
440
    <Modal
441
      id={modalId}
442
      parentElement={parentElement}
443
      visible={visible}
444
      onModalCancel={onModalCancel}
445
      onModalConfirm={onModalCancel}
446
      className="application-experience-dialog"
447
    >
448
      <ExperienceModalHeader
449
        title={intl.formatMessage(messages.modalTitle)}
450
        iconClass="fa-book"
451
      />
452
      <Formik
453
        enableReinitialize
454
        initialValues={initialFormValues}
455
        onSubmit={async (values, actions): Promise<void> => {
456
          await onModalConfirm(
457
            formValuesToData(values, originalExperience, locale, [
458
              ...requiredSkills,
459
              ...optionalSkills,
460
            ]),
461
          );
462
          actions.setSubmitting(false);
463
          actions.resetForm();
464
        }}
465
        validationSchema={validationSchema}
466
      >
467
        {(formikProps): React.ReactElement => (
468
          <Form>
469
            <Modal.Body>
470
              <ExperienceDetailsIntro
471
                description={intl.formatMessage(messages.modalDescription)}
472
              />
473
              <EducationDetailsSubform
474
                educationTypes={educationTypes}
475
                educationStatuses={educationStatuses}
476
              />
477
              <SkillSubform
478
                keyPrefix="education"
479
                jobId={jobId}
480
                jobRequiredSkills={requiredSkills.map(skillToName)}
481
                jobOptionalSkills={optionalSkills.map(skillToName)}
482
              />
483
              <EducationSubform
484
                keyPrefix="education"
485
                classificationEducationRequirements={
486
                  classificationEducationRequirements
487
                }
488
                jobEducationRequirements={jobEducationRequirements}
489
              />
490
            </Modal.Body>
491
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
492
          </Form>
493
        )}
494
      </Formik>
495
    </Modal>
496
  );
497
};
498
499
export default EducationExperienceModal;
500