Passed
Push — task/applicant-profile-api ( 747d8b...3edd34 )
by Yonathan
09:18 queued 01:23
created

resources/assets/js/components/ApplicationReview/ApplicationRow.tsx   A

Complexity

Total Complexity 13
Complexity/F 0

Size

Lines of Code 414
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 343
c 0
b 0
f 0
dl 0
loc 414
rs 10
mnd 13
bc 13
fnc 0
bpm 0
cpm 0
noi 0
1
/* eslint-disable camelcase, @typescript-eslint/camelcase */
2
import React from "react";
3
import { useIntl, defineMessages } from "react-intl";
4
import { Formik, Form, FastField } from "formik";
5
import Swal, { SweetAlertResult } from "sweetalert2";
6
import classNames from "classnames";
7
import { Application, ApplicationReview } from "../../models/types";
8
import { Portal } from "../../models/app";
9
import { getLocale } from "../../helpers/localize";
10
import {
11
  getApplicantUrl,
12
  getApplicationUrl,
13
  imageUrl,
14
} from "../../helpers/routes";
15
import { ReviewStatusId } from "../../models/lookupConstants";
16
import { ReviewStatuses } from "../../models/localizedConstants";
17
import AlertWhenUnsaved from "../Form/AlertWhenUnsaved";
18
import SelectInput from "../Form/SelectInput";
19
20
const messages = defineMessages({
21
  priorityLogo: {
22
    id: "application.review.priorityStatus.priorityLogoTitle",
23
    defaultMessage: "Talent cloud priority logo",
24
    description: "Title for Priority Logo Img",
25
  },
26
  priorityStatus: {
27
    id: "application.review.priorityStatus.priority",
28
    defaultMessage: "Priority",
29
    description: "Priority",
30
  },
31
  veteranLogo: {
32
    id: "application.review.veteranStatus.veteranLogoAlt",
33
    defaultMessage: "Talent cloud veteran logo",
34
    description: "Alt Text for Veteran Logo Img",
35
  },
36
  veteranStatus: {
37
    id: "application.review.veteranStatus.veteran",
38
    defaultMessage: "Veteran",
39
    description: "Veteran",
40
  },
41
  emailCandidate: {
42
    id: "application.review.emailCandidateLinkTitle",
43
    defaultMessage: "Email this candidate.",
44
    description: "Title, hover text, for email link.",
45
  },
46
  viewApplicationText: {
47
    id: "application.review.viewApplication",
48
    defaultMessage: "View Application",
49
    description: "Button text View Application",
50
  },
51
  viewApplicationTitle: {
52
    id: "application.review.viewApplicationLinkTitle",
53
    defaultMessage: "View this applicant's application.",
54
    description: "Title, hover text, for View Application Link",
55
  },
56
  viewProfileText: {
57
    id: "application.review.viewProfile",
58
    defaultMessage: "View Profile",
59
    description: "Button text View Profile",
60
  },
61
  viewProfileTitle: {
62
    id: "application.review.viewProfileLinkTitle",
63
    defaultMessage: "View this applicant's profile.",
64
    description: "Title, hover text, for View Profile Link",
65
  },
66
  decision: {
67
    id: "application.review.decision",
68
    defaultMessage: "Decision",
69
    description: "Decision dropdown label",
70
  },
71
  notReviewed: {
72
    id: "application.review.reviewStatus.notReviewed",
73
    defaultMessage: "Not Reviewed",
74
    description: "Decision dropdown label",
75
  },
76
  saving: {
77
    id: "application.review.button.saving",
78
    defaultMessage: "Saving...",
79
    description: "Dynamic Save button label",
80
  },
81
  save: {
82
    id: "application.review.button.save",
83
    defaultMessage: "Save",
84
    description: "Dynamic Save button label",
85
  },
86
  saved: {
87
    id: "application.review.button.saved",
88
    defaultMessage: "Saved",
89
    description: "Dynamic Save button label",
90
  },
91
  addNote: {
92
    id: "application.review.addNote",
93
    defaultMessage: "+ Add a Note",
94
    description: "Dynamic Note button label",
95
  },
96
  editNote: {
97
    id: "application.review.editNote",
98
    defaultMessage: "Edit Note",
99
    description: "Dynamic Note button label",
100
  },
101
  screenedOut: {
102
    id: "application.review.reviewStatus.screenedOut",
103
    defaultMessage: "Screened Out",
104
    description: "Dynamic Note button label",
105
  },
106
  stillThinking: {
107
    id: "application.review.reviewStatus.stillThinking",
108
    defaultMessage: "Still Thinking",
109
    description: "Dynamic Note button label",
110
  },
111
  stillIn: {
112
    id: "application.review.reviewStatus.stillIn",
113
    defaultMessage: "Still In",
114
    description: "Dynamic Note button label",
115
  },
116
  cancelButton: {
117
    id: "application.review.button.cancel",
118
    defaultMessage: "Cancel",
119
    description: "Cancel button label",
120
  },
121
  confirmButton: {
122
    id: "application.review.button.confirm",
123
    defaultMessage: "Confirm",
124
    description: "Confirm button for modal dialogue boxes",
125
  },
126
  screenOutConfirm: {
127
    id: "application.review.screenOutConfirm",
128
    defaultMessage: "Screen out the candidate?",
129
    description: "Are you sure you want to screen out the candidate warning",
130
  },
131
  screenInConfirm: {
132
    id: "application.review.screenInConfirm",
133
    defaultMessage: "Screen the candidate back in?",
134
    description: "Are you sure you want to screen in the candidate warning",
135
  },
136
  viewProfile: {
137
    id: "application.review.viewProfile",
138
    defaultMessage: "View Profile",
139
    description: "Button text View Profile",
140
  },
141
});
142
143
interface FormValues {
144
  reviewStatus: ReviewStatusId | null;
145
  notes: string;
146
}
147
148
interface ApplicationRowProps {
149
  application: Application;
150
  handleUpdateReview: (review: ApplicationReview) => Promise<void>;
151
  portal: Portal;
152
}
153
154
const ApplicationRow: React.FC<ApplicationRowProps> = ({
155
  application,
156
  handleUpdateReview,
157
  portal,
158
}) => {
159
  const intl = useIntl();
160
  const locale = getLocale(intl.locale);
161
162
  const applicantUrl = getApplicantUrl(
163
    locale,
164
    portal,
165
    application.applicant_id,
166
    application.job_poster_id,
167
  );
168
  const applicationUrl = getApplicationUrl(
169
    locale,
170
    portal,
171
    application.id,
172
    application.job_poster_id,
173
  );
174
175
  const reviewOptions = [
176
    {
177
      value: ReviewStatuses.screened_out.id,
178
      label: intl.formatMessage(ReviewStatuses.screened_out.name),
179
    },
180
    {
181
      value: ReviewStatuses.still_thinking.id,
182
      label: intl.formatMessage(ReviewStatuses.still_thinking.name),
183
    },
184
    {
185
      value: ReviewStatuses.still_in.id,
186
      label: intl.formatMessage(ReviewStatuses.still_in.name),
187
    },
188
  ];
189
190
  const statusIconClass = classNames("fas", {
191
    "fa-ban":
192
      application.application_review?.review_status_id ===
193
      ReviewStatusId.ScreenedOut,
194
    "fa-question-circle":
195
      application.application_review?.review_status_id ===
196
      ReviewStatusId.StillThinking,
197
    "fa-check-circle":
198
      application.application_review?.review_status_id ===
199
      ReviewStatusId.StillIn,
200
    "fa-exclamation-circle":
201
      application.application_review === undefined ||
202
      application.application_review?.review_status_id === null,
203
  });
204
205
  const noteButtonText =
206
    application.application_review && application.application_review.notes
207
      ? intl.formatMessage(messages.editNote)
208
      : intl.formatMessage(messages.addNote);
209
210
  const emptyReview: ApplicationReview = {
211
    id: 0,
212
    job_application_id: application.id,
213
    review_status_id: null,
214
    notes: null,
215
    created_at: new Date(),
216
    updated_at: new Date(),
217
    review_status: undefined,
218
    department_id: null,
219
    department: undefined,
220
    director_email_sent: false,
221
    reference_email_sent: false,
222
  };
223
224
  const validReviewStatus = (id: number | undefined | null): number | null => {
225
    if (id !== undefined && id !== null && id in ReviewStatusId) {
226
      return id;
227
    }
228
    return null;
229
  };
230
231
  const applicationReviewStatus = validReviewStatus(
232
    application.application_review?.review_status_id,
233
  );
234
235
  const initialValues: FormValues = {
236
    reviewStatus: applicationReviewStatus,
237
    notes: application.application_review?.notes || "",
238
  };
239
240
  const updateApplicationReview = (
241
    oldReview: ApplicationReview,
242
    values: FormValues,
243
  ): ApplicationReview => {
244
    const applicationReview: ApplicationReview = {
245
      ...oldReview,
246
      review_status_id: values.reviewStatus
247
        ? Number(values.reviewStatus)
248
        : null,
249
      notes: values.notes || null,
250
    };
251
    return applicationReview;
252
  };
253
254
  const handleNotesButtonClick = (
255
    notes: string,
256
    updateField: (
257
      field: string,
258
      value: any,
259
      shouldValidate?: boolean | undefined,
260
    ) => void,
261
  ): void => {
262
    Swal.fire({
263
      title: intl.formatMessage(messages.editNote),
264
      icon: "info",
265
      input: "textarea",
266
      showCancelButton: true,
267
      confirmButtonColor: "#0A6CBC",
268
      cancelButtonColor: "#F94D4D",
269
      cancelButtonText: intl.formatMessage(messages.cancelButton),
270
      confirmButtonText: intl.formatMessage(messages.save),
271
      inputValue: notes,
272
    }).then((result: SweetAlertResult) => {
273
      if (result && result.value !== undefined) {
274
        const value = result.value ? result.value : "";
275
        updateField("notes", value);
276
      }
277
    });
278
  };
279
280
  return (
281
    <Formik
282
      initialValues={initialValues}
283
      enableReinitialize
284
      onSubmit={(values, { setSubmitting, resetForm }): void => {
285
        const review = updateApplicationReview(
286
          application.application_review || emptyReview,
287
          values,
288
        );
289
        handleUpdateReview(review)
290
          .then(() => {
291
            setSubmitting(false);
292
            resetForm();
293
          })
294
          .catch(() => {
295
            setSubmitting(false);
296
          });
297
      }}
298
    >
299
      {({ values, dirty, isSubmitting, setFieldValue }): React.ReactElement => {
300
        const buttonDisabled = !dirty || isSubmitting;
301
302
        return (
303
          <Form className="applicant-summary">
304
            <AlertWhenUnsaved />
305
            <div className="flex-grid middle gutter">
306
              <div className="box lg-1of11 applicant-status">
307
                <i className={statusIconClass} />
308
              </div>
309
310
              <div className="box lg-2of11 applicant-information">
311
                <span
312
                  className="name"
313
                  data-name={`${application.applicant.user.first_name} ${application.applicant.user.last_name}`}
314
                >
315
                  {application.applicant.user.first_name}{" "}
316
                  {application.applicant.user.last_name}
317
                </span>
318
                <a
319
                  href={`mailto: ${application.applicant.user.email}`}
320
                  title={intl.formatMessage(messages.emailCandidate)}
321
                  data-email={`${application.applicant.user.email}`}
322
                  className="email"
323
                >
324
                  {application.applicant.user.email}
325
                </a>
326
                {/* This span only shown for priority applicants */}
327
                {application.applicant.user.is_priority && (
328
                  <span className="priority-status">
329
                    <i
330
                      aria-hidden="true"
331
                      className="fab fa-product-hunt"
332
                      title={intl.formatMessage(messages.priorityLogo)}
333
                    />
334
                    {intl.formatMessage(messages.priorityStatus)}
335
                  </span>
336
                )}
337
                {/* This span only shown for veterans */}
338
                {(application.veteran_status.name === "current" ||
339
                  application.veteran_status.name === "past") && (
340
                  <span className="veteran-status">
341
                    <img
342
                      alt={intl.formatMessage(messages.veteranLogo)}
343
                      src={imageUrl("icon_veteran.svg")}
344
                    />{" "}
345
                    {intl.formatMessage(messages.veteranStatus)}
346
                  </span>
347
                )}
348
              </div>
349
350
              <div className="box lg-2of11 applicant-links">
351
                <a
352
                  title={intl.formatMessage(messages.viewApplicationTitle)}
353
                  href={applicationUrl}
354
                >
355
                  <i className="fas fa-file-alt" />
356
                  {intl.formatMessage(messages.viewApplicationText)}
357
                </a>
358
                <a
359
                  title={intl.formatMessage(messages.viewProfileTitle)}
360
                  href={applicantUrl}
361
                >
362
                  <i className="fas fa-user" />
363
                  {intl.formatMessage(messages.viewProfile)}
364
                </a>
365
              </div>
366
367
              <div className="box lg-2of11 applicant-decision">
368
                <FastField
369
                  id={`review-status-${application.id}`}
370
                  name="reviewStatus"
371
                  label={intl.formatMessage(messages.decision)}
372
                  component={SelectInput}
373
                  nullSelection={intl.formatMessage(messages.notReviewed)}
374
                  options={reviewOptions}
375
                />
376
              </div>
377
378
              <div className="box lg-2of11 applicant-notes">
379
                <button
380
                  className="button--outline"
381
                  type="button"
382
                  onClick={(): void =>
383
                    handleNotesButtonClick(values.notes, setFieldValue)
384
                  }
385
                >
386
                  {noteButtonText}
387
                </button>
388
              </div>
389
390
              <div className="box lg-2of11 applicant-save-button">
391
                <button
392
                  className={`button--blue light-bg${
393
                    buttonDisabled ? " disabled" : ""
394
                  }`}
395
                  type="submit"
396
                  disabled={buttonDisabled}
397
                >
398
                  <span>
399
                    {dirty
400
                      ? intl.formatMessage(messages.save)
401
                      : intl.formatMessage(messages.saved)}
402
                  </span>
403
                </button>
404
              </div>
405
            </div>
406
          </Form>
407
        );
408
      }}
409
    </Formik>
410
  );
411
};
412
413
export default ApplicationRow;
414