Passed
Push — feature/checkbox-group-field ( 8f8586...916999 )
by Grant
04:03
created

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

Complexity

Total Complexity 18
Complexity/F 4.5

Size

Lines of Code 429
Function Count 4

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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