Passed
Push — dev ( faa4e4...4e7cee )
by
unknown
04:17
created

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

Complexity

Total Complexity 17
Complexity/F 0

Size

Lines of Code 253
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 208
mnd 17
bc 17
fnc 0
dl 0
loc 253
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 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
      );
152
      const prevSkillIds = prevExpSkills.map((expSkill) => expSkill.skill_id);
153
      const newSkillIds = newLinkedSkills.map(getId);
154
155
      // Delete skills that were removed.
156
      const deleteRequests = prevExpSkills
157
        .filter((expSkill) => !newSkillIds.includes(expSkill.skill_id))
158
        .map((expSkill) =>
159
          dispatch(
160
            deleteExperienceSkill(
161
              expSkill.id,
162
              expSkill.experience_id,
163
              expSkill.experience_type,
164
            ),
165
          ),
166
        );
167
      // Created new Experience Skills for skills which don't exist yet.
168
      const createRequests = newSkillIds
169
        .filter((newSkillId) => !prevSkillIds.includes(newSkillId))
170
        .map((newSkillId) =>
171
          experience
172
            ? dispatch(
173
                createExperienceSkill(newExpSkill(newSkillId, experience)),
174
              )
175
            : Promise.reject(),
176
        );
177
      await Promise.allSettled([
178
        updateExpRequest,
179
        ...deleteRequests,
180
        ...createRequests,
181
      ]);
182
    }
183
  };
184
  const handleDelete = async (
185
    id: number,
186
    type: ExperienceType["type"],
187
  ): Promise<void> => {
188
    // Deleting an Experience automatically handles deleting associated ExperienceSkills.
189
    await dispatch(deleteExperience(id, type));
190
  };
191
192
  const handleContinue = async (): Promise<void> => {
193
    navigate(applicationSkillsIntro(locale, applicationId));
194
  };
195
  const handleReturn = (): void => {
196
    navigate(applicationBasic(locale, applicationId));
197
  };
198
  const handleQuit = (): void => {
199
    window.location.href = applicationIndex(locale);
200
  };
201
  const closeDate = job?.close_date_time ?? null;
202
  return (
203
    <>
204
      {application !== null && (
205
        <ProgressBar
206
          closeDateTime={closeDate}
207
          currentTitle={intl.formatMessage(stepNames.step02)}
208
          steps={makeProgressBarSteps(
209
            applicationId,
210
            steps,
211
            intl,
212
            "experience",
213
            stepsAreUpdating,
214
          )}
215
        />
216
      )}
217
      {showLoadingState && (
218
        <h2
219
          data-c-heading="h2"
220
          data-c-align="center"
221
          data-c-padding="top(2) bottom(2)"
222
        >
223
          {intl.formatMessage(loadingMessages.loading)}
224
        </h2>
225
      )}
226
      {/* Note: if showLoadingState is false, job must not be null, but TypeScript can't seem to infer that. */}
227
      {!showLoadingState && job !== null && (
228
        <ExperienceStep
229
          experiences={experiences}
230
          educationStatuses={educationStatuses}
231
          educationTypes={educationTypes}
232
          experienceSkills={experienceSkills}
233
          criteria={criteria}
234
          skills={skills}
235
          jobId={job.id}
236
          jobClassificationId={job.classification_id}
237
          jobEducationRequirements={localizeField(locale, job, "education")}
238
          recipientTypes={awardRecipientTypes}
239
          recognitionTypes={awardRecognitionTypes}
240
          handleSubmitExperience={handleSubmit}
241
          handleDeleteExperience={handleDelete}
242
          handleContinue={handleContinue}
243
          handleReturn={handleReturn}
244
          handleQuit={handleQuit}
245
        />
246
      )}
247
      <div id="modal-root" data-clone />
248
    </>
249
  );
250
};
251
252
export default ExperiencePage;
253