Passed
Push — dev ( 4ec5c1...56ac6b )
by Tristan
07:13 queued 03:21
created

resources/assets/js/components/Application/Experience/ExperiencePage.tsx   A

Complexity

Total Complexity 17
Complexity/F 0

Size

Lines of Code 256
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 211
c 0
b 0
f 0
dl 0
loc 256
rs 10
mnd 17
bc 17
fnc 0
bpm 0
cpm 0
noi 0
1
/* eslint-disable @typescript-eslint/camelcase */
2
/* eslint-disable camelcase */
3
import React from "react";
4
import { useIntl } from "react-intl";
5
import { useDispatch } from "react-redux";
6
import { getLocale, localizeField } from "../../../helpers/localize";
7
import { navigate } from "../../../helpers/router";
8
import {
9
  applicationSkillsIntro,
10
  applicationBasic,
11
  applicationIndex,
12
} from "../../../helpers/routes";
13
import ProgressBar, { stepNames } from "../ProgressBar/ProgressBar";
14
import makeProgressBarSteps from "../ProgressBar/progressHelpers";
15
import { ExperienceStep, ExperienceSubmitData } from "./Experience";
16
import {
17
  Experience as ExperienceType,
18
  ExperienceSkill,
19
} from "../../../models/types";
20
import {
21
  createExperience,
22
  createExperienceSkill,
23
  updateExperience,
24
  deleteExperienceSkill,
25
  deleteExperience,
26
} from "../../../store/Experience/experienceActions";
27
import { getId } from "../../../helpers/queries";
28
import { DispatchType } from "../../../configureStore";
29
import { loadingMessages } from "../applicationMessages";
30
import {
31
  useExperienceSkills,
32
  useFetchAllApplicationData,
33
  useExperienceConstants,
34
  useApplication,
35
  useJob,
36
  useCriteria,
37
  useExperiences,
38
  useSkills,
39
  useJobApplicationSteps,
40
  useTouchApplicationStep,
41
} from "../../../hooks/applicationHooks";
42
43
interface ExperiencePageProps {
44
  applicationId: number;
45
}
46
47
export const ExperiencePage: React.FC<ExperiencePageProps> = ({
48
  applicationId,
49
}) => {
50
  const intl = useIntl();
51
  const locale = getLocale(intl.locale);
52
  const dispatch = useDispatch<DispatchType>();
53
54
  // Fetch all un-loaded data that may be required for the Application.
55
  const {
56
    experiencesLoaded,
57
    experienceConstantsLoaded,
58
    skillsLoaded,
59
  } = useFetchAllApplicationData(applicationId, dispatch);
60
61
  const application = useApplication(applicationId);
62
  const jobId = application?.job_poster_id;
63
  const job = useJob(jobId);
64
  const criteria = useCriteria(jobId);
65
  const experiences = useExperiences(applicationId, application);
66
  const experienceSkills = useExperienceSkills(applicationId, application);
67
  const skills = useSkills();
68
  const {
69
    awardRecipientTypes,
70
    awardRecognitionTypes,
71
    educationTypes,
72
    educationStatuses,
73
  } = useExperienceConstants();
74
  const steps = useJobApplicationSteps();
75
76
  const stepsAreUpdating = useTouchApplicationStep(
77
    applicationId,
78
    "experience",
79
    dispatch,
80
  );
81
82
  const showLoadingState =
83
    application === null ||
84
    job === null ||
85
    !experiencesLoaded ||
86
    !skillsLoaded ||
87
    !experienceConstantsLoaded;
88
89
  const handleSubmit = async (data: ExperienceSubmitData): Promise<void> => {
90
    // extract the Experience object from the data.
91
    let experience: ExperienceType | null = null;
92
    if ("experienceWork" in data) {
93
      experience = data.experienceWork;
94
    } else if ("experienceAward" in data) {
95
      experience = data.experienceAward;
96
    } else if ("experienceCommunity" in data) {
97
      experience = data.experienceCommunity;
98
    } else if ("experienceEducation" in data) {
99
      experience = data.experienceEducation;
100
    } else if ("experiencePersonal" in data) {
101
      experience = data.experiencePersonal;
102
    }
103
    if (experience === null) {
104
      return;
105
    }
106
107
    const newLinkedSkills = [
108
      ...data.savedRequiredSkills,
109
      ...data.savedOptionalSkills,
110
    ];
111
112
    const newExpSkill = (
113
      skillId: number,
114
      exp: ExperienceType,
115
    ): ExperienceSkill => ({
116
      id: 0,
117
      skill_id: skillId,
118
      experience_id: exp.id,
119
      experience_type: exp.type,
120
      justification: "",
121
      created_at: new Date(),
122
      updated_at: new Date(),
123
    });
124
125
    if (experience.id === 0) {
126
      const applicantId = application?.applicant_id;
127
      if (applicantId === undefined) {
128
        // This should never happen. By the time the Submit handler is called, application must be loaded.
129
        throw new Error(
130
          "Submitting new Experience before Application has loaded.",
131
        );
132
      }
133
      // If the experience is brand new, it (and related experience skills) must be created on server.
134
      const result = await dispatch(createExperience(experience, applicantId));
135
      if (!result.error) {
136
        const newExperience = (await result.payload).experience;
137
        const saveRequests = newLinkedSkills.map((skill) => {
138
          const expSkill = newExpSkill(skill.id, newExperience);
139
          return dispatch(createExperienceSkill(expSkill));
140
        });
141
        await Promise.allSettled(saveRequests);
142
      }
143
    } else {
144
      // If the experience already exists it can simply be updated.
145
      const updateExpRequest = dispatch(updateExperience(experience));
146
      // Determine which skills were already linked to the experience
147
      const prevExpSkills = experienceSkills.filter(
148
        (expSkill) =>
149
          expSkill.experience_id === experience?.id &&
150
          expSkill.experience_type === experience?.type &&
151
          criteria.find(
152
            (criterion) => criterion.skill_id === expSkill.skill_id,
153
          ),
154
      );
155
      const prevSkillIds = prevExpSkills.map((expSkill) => expSkill.skill_id);
156
      const newSkillIds = newLinkedSkills.map(getId);
157
158
      // Delete skills that were removed.
159
      const deleteRequests = prevExpSkills
160
        .filter((expSkill) => !newSkillIds.includes(expSkill.skill_id))
161
        .map((expSkill) =>
162
          dispatch(
163
            deleteExperienceSkill(
164
              expSkill.id,
165
              expSkill.experience_id,
166
              expSkill.experience_type,
167
            ),
168
          ),
169
        );
170
      // Created new Experience Skills for skills which don't exist yet.
171
      const createRequests = newSkillIds
172
        .filter((newSkillId) => !prevSkillIds.includes(newSkillId))
173
        .map((newSkillId) =>
174
          experience
175
            ? dispatch(
176
                createExperienceSkill(newExpSkill(newSkillId, experience)),
177
              )
178
            : Promise.reject(),
179
        );
180
      await Promise.allSettled([
181
        updateExpRequest,
182
        ...deleteRequests,
183
        ...createRequests,
184
      ]);
185
    }
186
  };
187
  const handleDelete = async (
188
    id: number,
189
    type: ExperienceType["type"],
190
  ): Promise<void> => {
191
    // Deleting an Experience automatically handles deleting associated ExperienceSkills.
192
    await dispatch(deleteExperience(id, type));
193
  };
194
195
  const handleContinue = async (): Promise<void> => {
196
    navigate(applicationSkillsIntro(locale, applicationId));
197
  };
198
  const handleReturn = (): void => {
199
    navigate(applicationBasic(locale, applicationId));
200
  };
201
  const handleQuit = (): void => {
202
    window.location.href = applicationIndex(locale);
203
  };
204
  const closeDate = job?.close_date_time ?? null;
205
  return (
206
    <>
207
      {application !== null && (
208
        <ProgressBar
209
          closeDateTime={closeDate}
210
          currentTitle={intl.formatMessage(stepNames.step02)}
211
          steps={makeProgressBarSteps(
212
            applicationId,
213
            steps,
214
            intl,
215
            "experience",
216
            stepsAreUpdating,
217
          )}
218
        />
219
      )}
220
      {showLoadingState && (
221
        <h2
222
          data-c-heading="h2"
223
          data-c-align="center"
224
          data-c-padding="top(2) bottom(2)"
225
        >
226
          {intl.formatMessage(loadingMessages.loading)}
227
        </h2>
228
      )}
229
      {/* Note: if showLoadingState is false, job must not be null, but TypeScript can't seem to infer that. */}
230
      {!showLoadingState && job !== null && (
231
        <ExperienceStep
232
          experiences={experiences}
233
          educationStatuses={educationStatuses}
234
          educationTypes={educationTypes}
235
          experienceSkills={experienceSkills}
236
          criteria={criteria}
237
          skills={skills}
238
          jobId={job.id}
239
          jobClassificationId={job.classification_id}
240
          jobEducationRequirements={localizeField(locale, job, "education")}
241
          recipientTypes={awardRecipientTypes}
242
          recognitionTypes={awardRecognitionTypes}
243
          handleSubmitExperience={handleSubmit}
244
          handleDeleteExperience={handleDelete}
245
          handleContinue={handleContinue}
246
          handleReturn={handleReturn}
247
          handleQuit={handleQuit}
248
        />
249
      )}
250
      <div id="modal-root" data-clone />
251
    </>
252
  );
253
};
254
255
export default ExperiencePage;
256