Passed
Push — task/application-profile-react... ( 95a834...f06e77 )
by Yonathan
08:39
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 265
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 208
mnd 11
bc 11
fnc 0
dl 0
loc 265
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 = skills.filter((skill) =>
88
    applicantSkillIdsResource.value.skill_ids.includes(skill.id),
89
  );
90
91
  const newExpSkill = (
92
    exp: Experience,
93
    skillId: number,
94
    justification: string,
95
  ): ExperienceSkill => ({
96
    id: 0,
97
    skill_id: skillId,
98
    experience_id: exp.id,
99
    experience_type: exp.type,
100
    justification,
101
    created_at: new Date(),
102
    updated_at: new Date(),
103
  });
104
105
  const handleCreateExperience = async (
106
    data: ExperienceSubmitData<Experience>,
107
  ): Promise<void> => {
108
    // If the experience is brand new, it (and related experience skills) must be created on server.
109
    const result = await dispatch(
110
      createExperience(data.experience, applicantId),
111
    );
112
    if (!result.error) {
113
      const createdExperience = (await result.payload).experience;
114
      const expSkills: ExperienceSkill[] = data.savedSkills.map((skill) => {
115
        return newExpSkill(
116
          createdExperience,
117
          skill.skillId,
118
          skill.justification,
119
        );
120
      });
121
      if (expSkills.length > 0) {
122
        dispatch(batchCreateExperienceSkills(expSkills));
123
      }
124
    }
125
  };
126
  const handleUpdateExperience = async ({
127
    experience,
128
    savedSkills,
129
  }: ExperienceSubmitData<Experience>): Promise<void> => {
130
    const allRequests: Promise<any>[] = [];
131
    // Determine if the experience has changed and needs to be updated.
132
    const prevExperience = find(
133
      where(experiences, "type", experience.type),
134
      experience.id,
135
    );
136
    if (!deepEquals(experience, prevExperience)) {
137
      const updateExpRequest = dispatch(updateExperience(experience));
138
      allRequests.push(updateExpRequest);
139
    }
140
141
    // Determine which skills were already linked to the experience
142
    const prevExpSkills = experienceSkills.filter(
143
      (expSkill) =>
144
        expSkill.experience_id === experience.id &&
145
        expSkill.experience_type === experience.type,
146
    );
147
    const prevSkillIds = prevExpSkills.map((expSkill) => expSkill.skill_id);
148
    const newSkillIds = savedSkills.map((x) => x.skillId);
149
150
    // Delete skills that were removed.
151
    const expSkillsToDelete = prevExpSkills.filter(
152
      (expSkill) => !newSkillIds.includes(expSkill.skill_id),
153
    );
154
    if (expSkillsToDelete.length > 0) {
155
      const batchDeleteExpSkillsRequest = dispatch(
156
        batchDeleteExperienceSkills(expSkillsToDelete),
157
      );
158
      allRequests.push(batchDeleteExpSkillsRequest);
159
    }
160
161
    // Created new Experience Skills for skills which don't exist yet.
162
    const createdSkills = savedSkills
163
      .filter((savedSkill) => !prevSkillIds.includes(savedSkill.skillId))
164
      .map((savedSkill) =>
165
        newExpSkill(experience, savedSkill.skillId, savedSkill.justification),
166
      );
167
    if (createdSkills.length > 0) {
168
      const batchCreateExpSkillsRequest = dispatch(
169
        batchCreateExperienceSkills(createdSkills),
170
      );
171
      allRequests.push(batchCreateExpSkillsRequest);
172
    }
173
174
    // Update Experience Skills which already existed but for which justifications have changed.
175
    const updatedSkills = prevExpSkills
176
      .filter((expSkill) => newSkillIds.includes(expSkill.skill_id))
177
      .reduce(
178
        (modifiedSkills: ExperienceSkill[], expSkill: ExperienceSkill) => {
179
          const matchingSavedSkill = savedSkills.find(
180
            (x) => x.skillId === expSkill.skill_id,
181
          );
182
          if (
183
            matchingSavedSkill !== undefined &&
184
            expSkill.justification !== matchingSavedSkill.justification
185
          ) {
186
            modifiedSkills.push({
187
              ...expSkill,
188
              justification: matchingSavedSkill.justification,
189
            });
190
          }
191
          return modifiedSkills;
192
        },
193
        [],
194
      );
195
    if (updatedSkills.length > 0) {
196
      const batchUpdateExpSkillsRequest = dispatch(
197
        batchUpdateExperienceSkills(updatedSkills),
198
      );
199
      allRequests.push(batchUpdateExpSkillsRequest);
200
    }
201
202
    await Promise.allSettled(allRequests);
203
  };
204
  const handleDeleteExperience = async (
205
    id: number,
206
    type: Experience["type"],
207
  ): Promise<void> => {
208
    return dispatch(deleteExperience(id, type));
209
  };
210
211
  const experienceConstantsLoaded =
212
    awardRecipientTypes.length > 0 &&
213
    awardRecognitionTypes.length > 0 &&
214
    educationTypes.length > 0 &&
215
    educationStatuses.length > 0;
216
  const experiencesLoaded = experiencesFetched && !experiencesUpdating;
217
  const showComponent =
218
    experienceConstantsLoaded &&
219
    skills.length > 0 &&
220
    applicantSkillIdsResource.status === "fulfilled" &&
221
    experiencesLoaded;
222
223
  return (
224
    <>
225
      {showComponent ? (
226
        <ProfileExperience
227
          applicantId={applicantId}
228
          experiences={experiences}
229
          experienceSkills={experienceSkills}
230
          userSkills={userSkills}
231
          educationStatuses={educationStatuses}
232
          educationTypes={educationTypes}
233
          handleCreateExperience={handleCreateExperience}
234
          handleUpdateExperience={handleUpdateExperience}
235
          handleDeleteExperience={handleDeleteExperience}
236
          recipientTypes={awardRecipientTypes}
237
          recognitionTypes={awardRecognitionTypes}
238
        />
239
      ) : (
240
        <h2
241
          data-c-heading="h2"
242
          data-c-align="center"
243
          data-c-padding="top(2) bottom(2)"
244
        >
245
          {intl.formatMessage(loadingMessages.loading)}
246
        </h2>
247
      )}
248
      <div id="modal-root" data-clone />
249
    </>
250
  );
251
};
252
253
if (document.getElementById("profile-experience")) {
254
  const root = document.getElementById("profile-experience");
255
  if (root && "applicantId" in root.dataset) {
256
    const applicantId = Number(root.dataset.applicantId as string);
257
    ReactDOM.render(
258
      <RootContainer>
259
        <ProfileExperiencePage applicantId={applicantId} />
260
      </RootContainer>,
261
      root,
262
    );
263
  }
264
}
265