Passed
Push — dev ( b67929...4d1389 )
by
unknown
14:02 queued 08:38
created

resources/assets/js/components/ApplicantProfile/Experience/ProfileExperiencePage.tsx   A

Complexity

Total Complexity 12
Complexity/F 0

Size

Lines of Code 268
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 12
eloc 211
mnd 12
bc 12
fnc 0
dl 0
loc 268
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { FunctionComponent, useEffect, useState } from "react";
2
import ReactDOM from "react-dom";
3
import { useDispatch, useSelector } from "react-redux";
4
import { useIntl } from "react-intl";
5
import RootContainer from "../../RootContainer";
6
import ProfileExperience from "./ProfileExperience";
7
import {
8
  useFetchExperienceConstants,
9
  useFetchSkills,
10
} from "../../../hooks/applicationHooks";
11
import { RootState } from "../../../store/store";
12
import {
13
  getExperienceByApplicant,
14
  getExperienceSkillsByApplicant,
15
  getUpdatingByApplicant,
16
} from "../../../store/Experience/experienceSelector";
17
import { Experience, ExperienceSkill } from "../../../models/types";
18
import {
19
  batchCreateExperienceSkills,
20
  batchDeleteExperienceSkills,
21
  batchUpdateExperienceSkills,
22
  createExperience,
23
  deleteExperience,
24
  fetchExperienceByApplicant,
25
  updateExperience,
26
} from "../../../store/Experience/experienceActions";
27
import { loadingMessages } from "../../Application/applicationMessages";
28
import { ExperienceSubmitData } from "./ProfileExperienceCommon";
29
import { useApplicantSkillIds } from "../../../hooks/apiResourceHooks";
30
import { find, where } from "../../../helpers/queries";
31
import { deepEquals } from "../../../helpers/deepEquals";
32
33
const ProfileExperiencePage: FunctionComponent<{ applicantId: number }> = ({
34
  applicantId,
35
}) => {
36
  const intl = useIntl();
37
  const dispatch = useDispatch();
38
39
  const skills = useFetchSkills(dispatch);
40
  const applicantSkillIdsResource = useApplicantSkillIds(applicantId);
41
42
  const experiencesByType = useSelector((state: RootState) =>
43
    getExperienceByApplicant(state, { applicantId }),
44
  );
45
  const experiences: Experience[] = [
46
    ...experiencesByType.award,
47
    ...experiencesByType.community,
48
    ...experiencesByType.education,
49
    ...experiencesByType.personal,
50
    ...experiencesByType.work,
51
  ];
52
53
  // Fetch Experiences.
54
  const experiencesUpdating = useSelector((state: RootState) =>
55
    getUpdatingByApplicant(state, { applicantId }),
56
  );
57
  const [experiencesFetched, setExperiencesFetched] = useState(false);
58
  useEffect(() => {
59
    // Only load experiences if they have never been fetched by this component (!experiencesFetched),
60
    //  have never been fetched by another component (length === 0),
61
    //  and are not currently being fetched (!experiencesUpdating).
62
    if (
63
      !experiencesFetched &&
64
      !experiencesUpdating &&
65
      experiences.length === 0
66
    ) {
67
      setExperiencesFetched(true);
68
      dispatch(fetchExperienceByApplicant(applicantId));
69
    }
70
  }, [
71
    applicantId,
72
    dispatch,
73
    experiences.length,
74
    experiencesFetched,
75
    experiencesUpdating,
76
  ]);
77
  const experienceSkills: ExperienceSkill[] = useSelector((state: RootState) =>
78
    getExperienceSkillsByApplicant(state, { applicantId }),
79
  );
80
  const {
81
    awardRecipientTypes,
82
    awardRecognitionTypes,
83
    educationTypes,
84
    educationStatuses,
85
  } = useFetchExperienceConstants(dispatch);
86
87
  const userSkills =
88
    applicantSkillIdsResource.status !== "rejected"
89
      ? skills.filter((skill) =>
90
          applicantSkillIdsResource.value.skill_ids.includes(skill.id),
91
        )
92
      : [];
93
94
  const newExpSkill = (
95
    exp: Experience,
96
    skillId: number,
97
    justification: string,
98
  ): ExperienceSkill => ({
99
    id: 0,
100
    skill_id: skillId,
101
    experience_id: exp.id,
102
    experience_type: exp.type,
103
    justification,
104
    created_at: new Date(),
105
    updated_at: new Date(),
106
  });
107
108
  const handleCreateExperience = async (
109
    data: ExperienceSubmitData<Experience>,
110
  ): Promise<void> => {
111
    // If the experience is brand new, it (and related experience skills) must be created on server.
112
    const result = await dispatch(
113
      createExperience(data.experience, applicantId),
114
    );
115
    if (!result.error) {
116
      const createdExperience = (await result.payload).experience;
117
      const expSkills: ExperienceSkill[] = data.savedSkills.map((skill) => {
118
        return newExpSkill(
119
          createdExperience,
120
          skill.skillId,
121
          skill.justification,
122
        );
123
      });
124
      if (expSkills.length > 0) {
125
        dispatch(batchCreateExperienceSkills(expSkills));
126
      }
127
    }
128
  };
129
  const handleUpdateExperience = async ({
130
    experience,
131
    savedSkills,
132
  }: ExperienceSubmitData<Experience>): Promise<void> => {
133
    const allRequests: Promise<any>[] = [];
134
    // Determine if the experience has changed and needs to be updated.
135
    const prevExperience = find(
136
      where(experiences, "type", experience.type),
137
      experience.id,
138
    );
139
    if (!deepEquals(experience, prevExperience)) {
140
      const updateExpRequest = dispatch(updateExperience(experience));
141
      allRequests.push(updateExpRequest);
142
    }
143
144
    // Determine which skills were already linked to the experience
145
    const prevExpSkills = experienceSkills.filter(
146
      (expSkill) =>
147
        expSkill.experience_id === experience.id &&
148
        expSkill.experience_type === experience.type,
149
    );
150
    const prevSkillIds = prevExpSkills.map((expSkill) => expSkill.skill_id);
151
    const newSkillIds = savedSkills.map((x) => x.skillId);
152
153
    // Delete skills that were removed.
154
    const expSkillsToDelete = prevExpSkills.filter(
155
      (expSkill) => !newSkillIds.includes(expSkill.skill_id),
156
    );
157
    if (expSkillsToDelete.length > 0) {
158
      const batchDeleteExpSkillsRequest = dispatch(
159
        batchDeleteExperienceSkills(expSkillsToDelete),
160
      );
161
      allRequests.push(batchDeleteExpSkillsRequest);
162
    }
163
164
    // Created new Experience Skills for skills which don't exist yet.
165
    const createdSkills = savedSkills
166
      .filter((savedSkill) => !prevSkillIds.includes(savedSkill.skillId))
167
      .map((savedSkill) =>
168
        newExpSkill(experience, savedSkill.skillId, savedSkill.justification),
169
      );
170
    if (createdSkills.length > 0) {
171
      const batchCreateExpSkillsRequest = dispatch(
172
        batchCreateExperienceSkills(createdSkills),
173
      );
174
      allRequests.push(batchCreateExpSkillsRequest);
175
    }
176
177
    // Update Experience Skills which already existed but for which justifications have changed.
178
    const updatedSkills = prevExpSkills
179
      .filter((expSkill) => newSkillIds.includes(expSkill.skill_id))
180
      .reduce(
181
        (modifiedSkills: ExperienceSkill[], expSkill: ExperienceSkill) => {
182
          const matchingSavedSkill = savedSkills.find(
183
            (x) => x.skillId === expSkill.skill_id,
184
          );
185
          if (
186
            matchingSavedSkill !== undefined &&
187
            expSkill.justification !== matchingSavedSkill.justification
188
          ) {
189
            modifiedSkills.push({
190
              ...expSkill,
191
              justification: matchingSavedSkill.justification,
192
            });
193
          }
194
          return modifiedSkills;
195
        },
196
        [],
197
      );
198
    if (updatedSkills.length > 0) {
199
      const batchUpdateExpSkillsRequest = dispatch(
200
        batchUpdateExperienceSkills(updatedSkills),
201
      );
202
      allRequests.push(batchUpdateExpSkillsRequest);
203
    }
204
205
    await Promise.allSettled(allRequests);
206
  };
207
  const handleDeleteExperience = async (
208
    id: number,
209
    type: Experience["type"],
210
  ): Promise<void> => {
211
    return dispatch(deleteExperience(id, type));
212
  };
213
214
  const experienceConstantsLoaded =
215
    awardRecipientTypes.length > 0 &&
216
    awardRecognitionTypes.length > 0 &&
217
    educationTypes.length > 0 &&
218
    educationStatuses.length > 0;
219
  const experiencesLoaded = experiencesFetched && !experiencesUpdating;
220
  const showComponent =
221
    experienceConstantsLoaded &&
222
    skills.length > 0 &&
223
    applicantSkillIdsResource.status !== "pending" &&
224
    experiencesLoaded;
225
226
  return (
227
    <>
228
      {showComponent ? (
229
        <ProfileExperience
230
          applicantId={applicantId}
231
          experiences={experiences}
232
          experienceSkills={experienceSkills}
233
          userSkills={userSkills}
234
          educationStatuses={educationStatuses}
235
          educationTypes={educationTypes}
236
          handleCreateExperience={handleCreateExperience}
237
          handleUpdateExperience={handleUpdateExperience}
238
          handleDeleteExperience={handleDeleteExperience}
239
          recipientTypes={awardRecipientTypes}
240
          recognitionTypes={awardRecognitionTypes}
241
        />
242
      ) : (
243
        <h2
244
          data-c-heading="h2"
245
          data-c-align="center"
246
          data-c-padding="top(2) bottom(2)"
247
        >
248
          {intl.formatMessage(loadingMessages.loading)}
249
        </h2>
250
      )}
251
      <div id="modal-root" data-clone />
252
    </>
253
  );
254
};
255
256
if (document.getElementById("profile-experience")) {
257
  const root = document.getElementById("profile-experience");
258
  if (root && "applicantId" in root.dataset) {
259
    const applicantId = Number(root.dataset.applicantId as string);
260
    ReactDOM.render(
261
      <RootContainer>
262
        <ProfileExperiencePage applicantId={applicantId} />
263
      </RootContainer>,
264
      root,
265
    );
266
  }
267
}
268