Passed
Push — feature/improve-applicant-list ( 17becb )
by Chris
06:05
created

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

Complexity

Total Complexity 12
Complexity/F 0

Size

Lines of Code 396
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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