Passed
Push — dev ( 1ed2ed...40ef1d )
by Yonathan
04:47 queued 10s
created

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

Complexity

Total Complexity 16
Complexity/F 0

Size

Lines of Code 260
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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