Passed
Push — dev ( d0d2b5...21fa71 )
by Yonathan
08:43 queued 10s
created

resources/assets/js/components/StrategicTalentResponse/ResponseScreening/ApplicantBucket.tsx   F

Complexity

Total Complexity 63
Complexity/F 0

Size

Lines of Code 1039
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 63
eloc 830
dl 0
loc 1039
rs 3.13
c 0
b 0
f 0
mnd 63
bc 63
fnc 0
bpm 0
cpm 0
noi 0

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/components/StrategicTalentResponse/ResponseScreening/ApplicantBucket.tsx often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/* eslint camelcase: "off", @typescript-eslint/camelcase: "off" */
2
import React, { useState, useEffect, useRef } from "react";
3
import { defineMessages, useIntl, FormattedMessage } from "react-intl";
4
import { FastField, Formik, Form, useFormikContext } from "formik";
5
import Swal from "sweetalert2";
6
import ReactMarkdown from "react-markdown";
7
import SelectInput from "../../Form/SelectInput";
8
import {
9
  Application,
10
  Department,
11
  ApplicationReview,
12
  Email,
13
  EmailAddress,
14
} from "../../../models/types";
15
import { Portal, ValuesOf } from "../../../models/app";
16
import * as routes from "../../../helpers/routes";
17
import {
18
  ResponseScreeningBuckets,
19
  ResponseReviewStatuses,
20
} from "../../../models/localizedConstants";
21
import {
22
  ResponseScreeningBuckets as ResponseBuckets,
23
  ResponseReviewStatusId,
24
  ReviewStatusId,
25
} from "../../../models/lookupConstants";
26
import {
27
  localizeFieldNonNull,
28
  getLocale,
29
  Locales,
30
} from "../../../helpers/localize";
31
import Modal from "../../Modal";
32
import { notEmpty, empty } from "../../../helpers/queries";
33
34
const displayMessages = defineMessages({
35
  viewApplication: {
36
    id: "responseScreening.bucket.viewApplicationLabel",
37
    defaultMessage: "View Application",
38
    description: "Label for 'View Application' link.",
39
  },
40
  viewProfile: {
41
    id: "responseScreening.bucket.viewProfileLabel",
42
    defaultMessage: "View Profile",
43
    description: "Label for 'View Profile' link.",
44
  },
45
  notes: {
46
    id: "responseScreening.bucket.notesLabel",
47
    defaultMessage: "Add/Edit Notes",
48
    description: "Label for 'Add/Edit Notes' button.",
49
  },
50
  save: {
51
    id: "responseScreening.bucket.saveLabel",
52
    defaultMessage: "Save Changes",
53
    description: "Label for 'Save Changes' button when changes are unsaved.",
54
  },
55
  saving: {
56
    id: "responseScreening.bucket.savingLabel",
57
    defaultMessage: "Saving...",
58
    description:
59
      "Label for 'Save Changes' button when form submission is in progress.",
60
  },
61
  saved: {
62
    id: "responseScreening.bucket.savedLabel",
63
    defaultMessage: "Saved",
64
    description:
65
      "Label for 'Save Changes' button when form submission succeeded.",
66
  },
67
  cancel: {
68
    id: "responseScreening.bucket.cancelLabel",
69
    defaultMessage: "Cancel",
70
    description: "Label for 'Cancel' button.",
71
  },
72
  selectStatusDefault: {
73
    id: "responseScreening.bucket.selectStatusDefault",
74
    defaultMessage: "Select a status...",
75
    description: "Default option text for the Status dropdown.",
76
  },
77
  selectStatusLabel: {
78
    id: "responseScreening.bucket.selectStatusLabel",
79
    defaultMessage: "Review Status",
80
    description: "Label for the Status dropdown.",
81
  },
82
  selectDepartmentDefault: {
83
    id: "responseScreening.bucket.selectDepartmentDefault",
84
    defaultMessage: "Select a department...",
85
    description: "Default option text for the Department dropdown.",
86
  },
87
  selectDepartmentLabel: {
88
    id: "responseScreening.bucket.selectDepartmentLabel",
89
    defaultMessage: "Department Allocation",
90
    description: "Label for the Department dropdown.",
91
  },
92
  clickView: {
93
    id: "responseScreening.bucket.accessibleViewLabel",
94
    defaultMessage: "Click to view...",
95
    description: "Accessible text for screen reading accordion elements.",
96
  },
97
  noApplicants: {
98
    id: "responseScreening.bucket.noApplicants",
99
    defaultMessage: "There are currently no applicants in this bucket.",
100
    description: "Fallback label for a bucket with no applicants.",
101
  },
102
  setAllocated: {
103
    id: "responseScreening.bucket.confirmSetAllocated",
104
    defaultMessage:
105
      "Setting this candidate to allocated will mark them as currently unavailable for all other streams. Are you sure you want to continue?",
106
    description:
107
      "Confirmation text when attempting to set a candidate as Allocated.",
108
  },
109
  setUnavailable: {
110
    id: "responseScreening.bucket.confirmSetUnavailable",
111
    defaultMessage:
112
      "Setting this candidate to not available will mark them as currently unavailable for all other streams. Are you sure you want to continue?",
113
    description:
114
      "Confirmation text when attempting to set a candidate as Not Available.",
115
  },
116
  confirmAction: {
117
    id: "responseScreening.bucket.confirmAction",
118
    defaultMessage: "Please confirm this action",
119
    description:
120
      "Title for the confirmation dialog when making changes with side effects.",
121
  },
122
  yes: {
123
    id: "responseScreening.bucket.yes",
124
    defaultMessage: "Yes",
125
    description: "Confirmation button text for dialog",
126
  },
127
});
128
129
enum IconStatus {
130
  ASSESSMENT = "question",
131
  READY = "check",
132
  RECEIVED = "exclamation",
133
}
134
135
interface StatusIconProps {
136
  status: IconStatus;
137
  color: string;
138
  small: boolean;
139
}
140
141
const StatusIcon: React.FC<StatusIconProps> = ({
142
  status,
143
  color,
144
  small,
145
}): React.ReactElement => {
146
  return (
147
    <i
148
      className={`fas fa-${status}-circle`}
149
      data-c-color={color}
150
      data-c-font-size={small ? "small" : ""}
151
    />
152
  );
153
};
154
155
// Kinda weird "empty" component that hooks into Formik's
156
// context, listens to the 'dirty' prop, and registers
157
// a beforeunload listener to fire if a user attempts to
158
// leave with unsaved work.
159
// https://github.com/jaredpalmer/formik/issues/1657#issuecomment-509388871
160
const AlertWhenUnsaved = (): React.ReactElement => {
161
  const { dirty } = useFormikContext();
162
  const handleUnload = (event: BeforeUnloadEvent): void => {
163
    event.preventDefault();
164
    event.returnValue = "Are you sure you want to leave with unsaved changes?";
165
  };
166
167
  useEffect(() => {
168
    if (dirty) {
169
      window.addEventListener("beforeunload", handleUnload);
170
    }
171
    return (): void => {
172
      window.removeEventListener("beforeunload", handleUnload);
173
    };
174
  }, [dirty]);
175
176
  return <></>;
177
};
178
179
interface ReferenceEmailModalProps {
180
  id: string;
181
  parent: Element | null;
182
  email: Email | null;
183
  visible: boolean;
184
  onConfirm: () => void;
185
  onCancel: () => void;
186
}
187
188
const ReferenceEmailModal: React.FC<ReferenceEmailModalProps> = ({
189
  id,
190
  parent,
191
  email,
192
  visible,
193
  onConfirm,
194
  onCancel,
195
}): React.ReactElement => {
196
  const renderAddress = (adr: EmailAddress): string =>
197
    `${adr.name} <${adr.address}>`;
198
  const renderAddresses = (adrs: EmailAddress[]): string =>
199
    adrs.map(renderAddress).join(", ");
200
  const noDeliveryAdr = email == null || email.to.length == 0;
201
  return (
202
    <>
203
      <div data-c-dialog-overlay={visible ? "active" : ""} />
204
      <Modal
205
        id={id}
206
        parentElement={parent}
207
        visible={visible}
208
        onModalConfirm={onConfirm}
209
        onModalCancel={onCancel}
210
      >
211
        <Modal.Header>
212
          <div
213
            data-c-background="c1(100)"
214
            data-c-border="bottom(thin, solid, black)"
215
            data-c-padding="normal"
216
          >
217
            <h5 data-c-colour="white" data-c-font-size="h4">
218
              <FormattedMessage
219
                id="referenceEmailModal.title"
220
                defaultMessage="Email for Reference Check"
221
                description="Text displayed on the title of the MicroReference Email modal."
222
              />
223
            </h5>
224
          </div>
225
        </Modal.Header>
226
        <Modal.Body>
227
          <div data-c-border="bottom(thin, solid, black)">
228
            <div
229
              data-c-border="bottom(thin, solid, black)"
230
              data-c-padding="normal"
231
            >
232
              <p>
233
                <span>
234
                  <strong>
235
                    <FormattedMessage
236
                      id="referenceEmailModal.toLabel"
237
                      defaultMessage="To:"
238
                    />
239
                  </strong>
240
                </span>
241
                {` `}
242
                <span>{renderAddresses(email?.to ?? [])}</span>
243
              </p>
244
              <p>
245
                <span>
246
                  <strong>
247
                    <FormattedMessage
248
                      id="referenceEmailModal.fromLabel"
249
                      defaultMessage="From:"
250
                    />
251
                  </strong>
252
                </span>
253
                {` `}
254
                <span>{renderAddresses(email?.from ?? [])}</span>
255
              </p>
256
              <p>
257
                <span>
258
                  <strong>
259
                    <FormattedMessage
260
                      id="referenceEmailModal.ccLabel"
261
                      defaultMessage="CC:"
262
                    />
263
                  </strong>
264
                </span>
265
                {` `}
266
                <span>{renderAddresses(email?.cc ?? [])}</span>
267
              </p>
268
              <p>
269
                <span>
270
                  <strong>
271
                    <FormattedMessage
272
                      id="referenceEmailModal.bccLabel"
273
                      defaultMessage="BCC:"
274
                    />
275
                  </strong>
276
                </span>
277
                {` `}
278
                <span>{renderAddresses(email?.bcc ?? [])}</span>
279
              </p>
280
              <p>
281
                <span>
282
                  <strong>
283
                    <FormattedMessage
284
                      id="referenceEmailModal.subjectLabel"
285
                      defaultMessage="Subject:"
286
                    />
287
                  </strong>
288
                </span>
289
                {` `}
290
                <span>{email?.subject}</span>
291
              </p>
292
            </div>
293
            <div data-c-background="grey(20)" data-c-padding="normal">
294
              <div
295
                data-c-background="white(100)"
296
                data-c-padding="normal"
297
                data-c-radius="rounded"
298
                className="spaced-paragraphs"
299
              >
300
                {email ? (
301
                  <ReactMarkdown source={email.body} />
302
                ) : (
303
                  <p>
304
                    <FormattedMessage
305
                      id="referenceEmailModal.nullState"
306
                      defaultMessage="Loading reference email..."
307
                    />
308
                  </p>
309
                )}
310
              </div>
311
            </div>
312
          </div>
313
        </Modal.Body>
314
        <Modal.Footer>
315
          <Modal.FooterCancelBtn>
316
            <FormattedMessage
317
              id="referenceEmailModal.cancel"
318
              defaultMessage="Cancel"
319
            />
320
          </Modal.FooterCancelBtn>
321
          <Modal.FooterConfirmBtn disabled={noDeliveryAdr}>
322
            {noDeliveryAdr ? (
323
              <FormattedMessage
324
                id="referenceEmailModal.noDeliveryAddress"
325
                defaultMessage="No Delivery Address"
326
              />
327
            ) : (
328
              <FormattedMessage
329
                id="referenceEmailModal.confirm"
330
                defaultMessage="Send Email"
331
              />
332
            )}
333
          </Modal.FooterConfirmBtn>
334
        </Modal.Footer>
335
      </Modal>
336
    </>
337
  );
338
};
339
340
interface FormValues {
341
  reviewStatus: ReviewStatusId | ResponseReviewStatusId | null;
342
  department: number | null;
343
  notes: string;
344
}
345
346
interface ApplicationRowProps {
347
  application: Application;
348
  departmentEditable: boolean;
349
  departments: Department[];
350
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
351
  portal: Portal;
352
  directorReferenceEmail: Email | null;
353
  secondaryReferenceEmail: Email | null;
354
  requestReferenceEmails: (applicationId: number) => void;
355
  sendReferenceEmail: (
356
    applicationId: number,
357
    referenceType: "director" | "secondary",
358
  ) => Promise<void>;
359
}
360
361
const ApplicationRow: React.FC<ApplicationRowProps> = ({
362
  application,
363
  departmentEditable,
364
  departments,
365
  handleUpdateReview,
366
  portal,
367
  directorReferenceEmail,
368
  secondaryReferenceEmail,
369
  requestReferenceEmails,
370
  sendReferenceEmail,
371
}): React.ReactElement => {
372
  const intl = useIntl();
373
  const locale = getLocale(intl.locale);
374
375
  const applicantUrlMap: { [key in typeof portal]: string } = {
376
    hr: routes.hrApplicantShow(intl.locale, application.id),
377
    manager: routes.managerApplicantShow(intl.locale, application.id),
378
  };
379
  const applicationUrlMap: { [key in typeof portal]: string } = {
380
    hr: routes.hrApplicationShow(intl.locale, application.id),
381
    manager: routes.managerApplicationShow(intl.locale, application.id),
382
  };
383
  const applicantUrl = applicantUrlMap[portal];
384
  const applicationUrl = applicationUrlMap[portal];
385
386
  const reviewOptions = Object.values(ResponseReviewStatuses).map((status): {
387
    value: number;
388
    label: string;
389
  } => ({
390
    value: status.id,
391
    label: intl.formatMessage(status.name),
392
  }));
393
394
  const departmentOptions = departments.map((department) => ({
395
    value: department.id,
396
    label: localizeFieldNonNull(locale, department, "name"),
397
  }));
398
399
  let rowIcon: React.ReactElement;
400
401
  switch (application.application_review?.review_status_id) {
402
    case 4:
403
    case 5:
404
    case 7:
405
      rowIcon = (
406
        <StatusIcon status={IconStatus.READY} color="go" small={false} />
407
      );
408
      break;
409
    case 6:
410
      rowIcon = (
411
        <StatusIcon status={IconStatus.ASSESSMENT} color="slow" small={false} />
412
      );
413
      break;
414
    default:
415
      rowIcon = (
416
        <StatusIcon status={IconStatus.RECEIVED} color="c1" small={false} />
417
      );
418
  }
419
420
  const emptyReview: ApplicationReview = {
421
    id: 0,
422
    job_application_id: application.id,
423
    review_status_id: null,
424
    department_id: null,
425
    notes: null,
426
    created_at: new Date(),
427
    updated_at: new Date(),
428
    department: undefined,
429
    review_status: undefined,
430
    director_email_sent: false,
431
    reference_email_sent: false,
432
  };
433
434
  const initialValues: FormValues = {
435
    reviewStatus: application.application_review?.review_status_id || null,
436
    department: application.application_review?.department_id || null,
437
    notes: application.application_review?.notes || "",
438
  };
439
440
  const updateApplicationReview = (
441
    oldReview: ApplicationReview,
442
    values: FormValues,
443
  ): ApplicationReview => {
444
    const applicationReview: ApplicationReview = {
445
      ...oldReview,
446
      review_status_id: values.reviewStatus
447
        ? Number(values.reviewStatus)
448
        : null,
449
      department_id: values.department ? Number(values.department) : null,
450
      notes: values.notes || null,
451
    };
452
    return applicationReview;
453
  };
454
455
  const handleNotesButtonClick = (
456
    notes: string,
457
    updateField: (
458
      field: string,
459
      value: any,
460
      shouldValidate?: boolean | undefined,
461
    ) => void,
462
  ): void => {
463
    Swal.fire({
464
      title: intl.formatMessage(displayMessages.notes),
465
      icon: "info",
466
      input: "textarea",
467
      showCancelButton: true,
468
      confirmButtonColor: "#0A6CBC",
469
      cancelButtonColor: "#F94D4D",
470
      cancelButtonText: intl.formatMessage(displayMessages.cancel),
471
      confirmButtonText: intl.formatMessage(displayMessages.save),
472
      inputValue: notes,
473
    }).then((result) => {
474
      if (result && result.value !== undefined) {
475
        const value = result.value ? result.value : "";
476
        updateField("notes", value);
477
      }
478
    });
479
  };
480
481
  // MicroReferences
482
  const bucketsWithReferences: any = [
483
    ResponseReviewStatusId.ReadyForReference,
484
    ResponseReviewStatusId.ReadyToAllocate,
485
    ResponseReviewStatusId.Allocated,
486
  ];
487
  const showReferences: boolean =
488
    notEmpty(application.application_review?.review_status_id) &&
489
    bucketsWithReferences.includes(
490
      application.application_review?.review_status_id,
491
    );
492
  // A single modal component is used for both emails, since only one will appear at a time.
493
  const [showingEmail, setShowingEmail] = useState<"director" | "secondary">(
494
    "director",
495
  );
496
  // But each email needs its own 'sending' flag, because its possible have both sending at the same time.
497
  const [sendingDirectorEmail, setSendingDirectorEmail] = useState(false);
498
  const [sendingSecondaryEmail, setSendingSecondaryEmail] = useState(false);
499
  const emailOptions = {
500
    director: directorReferenceEmail,
501
    secondary: secondaryReferenceEmail,
502
  };
503
  const [emailModalVisible, setEmailModalVisible] = useState(false);
504
  const showDirectorEmail = (): void => {
505
    requestReferenceEmails(application.id);
506
    setShowingEmail("director");
507
    setEmailModalVisible(true);
508
  };
509
  const showSecondaryEmail = (): void => {
510
    requestReferenceEmails(application.id);
511
    setShowingEmail("secondary");
512
    setEmailModalVisible(true);
513
  };
514
  const hideEmail = (): void => {
515
    setEmailModalVisible(false);
516
  };
517
  const onConfirm = async (): Promise<void> => {
518
    hideEmail();
519
    const setSendingEmail = {
520
      director: setSendingDirectorEmail,
521
      secondary: setSendingSecondaryEmail,
522
    }[showingEmail];
523
    setSendingEmail(true);
524
    await sendReferenceEmail(application.id, showingEmail);
525
    setSendingEmail(false);
526
  };
527
  const modalParentRef = useRef<HTMLDivElement>(null);
528
529
  return (
530
    <div className="applicant" ref={modalParentRef}>
531
      <Formik
532
        initialValues={initialValues}
533
        onSubmit={(values, { setSubmitting, resetForm }): void => {
534
          const review = updateApplicationReview(
535
            application.application_review || emptyReview,
536
            values,
537
          );
538
          const performUpdate = (): void => {
539
            handleUpdateReview(review)
540
              .then(() => {
541
                setSubmitting(false);
542
                resetForm();
543
              })
544
              .catch(() => {
545
                setSubmitting(false);
546
              });
547
          };
548
          // Changing an application's status to Allocated
549
          if (
550
            Number(values.reviewStatus) ===
551
              Number(ResponseReviewStatusId.Allocated) &&
552
            values.reviewStatus !== initialValues.reviewStatus
553
          ) {
554
            Swal.fire({
555
              title: intl.formatMessage(displayMessages.confirmAction),
556
              text: intl.formatMessage(displayMessages.setAllocated),
557
              icon: "warning",
558
              showCancelButton: true,
559
              confirmButtonColor: "#0A6CBC",
560
              cancelButtonColor: "#F94D4D",
561
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
562
              confirmButtonText: intl.formatMessage(displayMessages.yes),
563
            }).then((result) => {
564
              if (result.value === undefined) {
565
                setSubmitting(false);
566
              } else {
567
                performUpdate();
568
              }
569
            });
570
            // Changing an Application's status to Unavailable
571
          } else if (
572
            Number(values.reviewStatus) ===
573
              Number(ResponseReviewStatusId.NotAvailable) &&
574
            values.reviewStatus !== initialValues.reviewStatus
575
          ) {
576
            Swal.fire({
577
              title: intl.formatMessage(displayMessages.confirmAction),
578
              text: intl.formatMessage(displayMessages.setUnavailable),
579
              icon: "warning",
580
              showCancelButton: true,
581
              confirmButtonColor: "#0A6CBC",
582
              cancelButtonColor: "#F94D4D",
583
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
584
              confirmButtonText: intl.formatMessage(displayMessages.yes),
585
            }).then((result) => {
586
              if (result.value === undefined) {
587
                setSubmitting(false);
588
              } else {
589
                performUpdate();
590
              }
591
            });
592
            // Everything else
593
          } else {
594
            performUpdate();
595
          }
596
        }}
597
      >
598
        {({
599
          values,
600
          dirty,
601
          isSubmitting,
602
          setFieldValue,
603
        }): React.ReactElement => (
604
          <Form data-c-grid="gutter(all, 1) middle">
605
            <AlertWhenUnsaved />
606
            <div data-c-grid-item="base(1of4)">
607
              <div>
608
                {rowIcon}
609
                <div>
610
                  <p data-c-font-weight="bold" data-c-font-size="h4">
611
                    {application.applicant.user.full_name}
612
                  </p>
613
                  <p data-c-margin="bottom(.5)">
614
                    <a
615
                      href={`mailto:${application.applicant.user.email}`}
616
                      title=""
617
                    >
618
                      {application.applicant.user.email}
619
                    </a>
620
                  </p>
621
                  <div
622
                    data-c-font-size="small"
623
                    data-c-grid="gutter(all, 1) middle"
624
                  >
625
                    <a
626
                      href={applicationUrl}
627
                      title=""
628
                      data-c-grid-item="base(1of2)"
629
                    >
630
                      {intl.formatMessage(displayMessages.viewApplication)}
631
                    </a>
632
                    <a
633
                      href={applicantUrl}
634
                      title=""
635
                      data-c-grid-item="base(1of2)"
636
                    >
637
                      {intl.formatMessage(displayMessages.viewProfile)}
638
                    </a>
639
                    {showReferences && (
640
                      <>
641
                        <div
642
                          className="email-reference-wrapper"
643
                          data-c-grid-item="base(1of2)"
644
                        >
645
                          <i
646
                            className="fa fa-check-circle"
647
                            data-c-color="go"
648
                            data-c-visibility={
649
                              application.application_review
650
                                ?.director_email_sent
651
                                ? "visible"
652
                                : "invisible"
653
                            }
654
                            data-c-font-size="small"
655
                            data-c-margin="right(.5)"
656
                          />
657
                          <button
658
                            data-c-button="reset"
659
                            data-c-font-style="underline"
660
                            data-c-font-size="small"
661
                            type="button"
662
                            onClick={showDirectorEmail}
663
                            disabled={sendingDirectorEmail}
664
                          >
665
                            {sendingDirectorEmail ? (
666
                              <FormattedMessage
667
                                id="responseScreening.applicant.directorEmailSending"
668
                                defaultMessage="Sending email..."
669
                              />
670
                            ) : (
671
                              <FormattedMessage
672
                                id="responseScreening.applicant.directorEmailButton"
673
                                defaultMessage="Director email."
674
                              />
675
                            )}
676
                          </button>
677
                        </div>
678
                        <div
679
                          data-c-grid-item="base(1of2)"
680
                          className="email-reference-wrapper"
681
                        >
682
                          <i
683
                            className="fa fa-check-circle"
684
                            data-c-color="go"
685
                            data-c-visibility={
686
                              application.application_review
687
                                ?.reference_email_sent
688
                                ? "visible"
689
                                : "invisible"
690
                            }
691
                            data-c-font-size="small"
692
                            data-c-margin="right(.5)"
693
                          />
694
                          <button
695
                            data-c-button="reset"
696
                            data-c-font-style="underline"
697
                            data-c-font-size="small"
698
                            type="button"
699
                            onClick={showSecondaryEmail}
700
                            disabled={sendingSecondaryEmail}
701
                          >
702
                            {sendingSecondaryEmail ? (
703
                              <FormattedMessage
704
                                id="responseScreening.applicant.secondaryEmailSending"
705
                                defaultMessage="Sending email..."
706
                              />
707
                            ) : (
708
                              <FormattedMessage
709
                                id="responseScreening.applicant.secondaryEmailButton"
710
                                defaultMessage="Reference email."
711
                              />
712
                            )}
713
                          </button>
714
                        </div>
715
                      </>
716
                    )}
717
                  </div>
718
                </div>
719
              </div>
720
            </div>
721
            <FastField
722
              id={`review-status-select-${application.applicant_id}`}
723
              name="reviewStatus"
724
              label={intl.formatMessage(displayMessages.selectStatusLabel)}
725
              grid="base(1of4)"
726
              component={SelectInput}
727
              nullSelection={intl.formatMessage(
728
                displayMessages.selectStatusDefault,
729
              )}
730
              options={reviewOptions}
731
            />
732
            {departmentEditable && (
733
              <FastField
734
                id={`department-allocation-select-${application.applicant_id}`}
735
                name="department"
736
                label={intl.formatMessage(
737
                  displayMessages.selectDepartmentLabel,
738
                )}
739
                grid="base(1of4)"
740
                component={SelectInput}
741
                nullSelection={intl.formatMessage(
742
                  displayMessages.selectDepartmentDefault,
743
                )}
744
                options={departmentOptions}
745
              />
746
            )}
747
            <div
748
              data-c-grid-item={`base(${departmentEditable ? 1 : 2}of4)`}
749
              data-c-align="base(right)"
750
            >
751
              <button
752
                data-c-button="outline(c1)"
753
                type="button"
754
                data-c-radius="rounded"
755
                data-c-margin="right(1)"
756
                onClick={(): void =>
757
                  handleNotesButtonClick(values.notes, setFieldValue)
758
                }
759
              >
760
                <i className="fas fa-plus" />
761
                <span>{intl.formatMessage(displayMessages.notes)}</span>
762
              </button>
763
              <button
764
                data-c-button="solid(c1)"
765
                type="submit"
766
                data-c-radius="rounded"
767
                disabled={isSubmitting}
768
              >
769
                <span>
770
                  {dirty
771
                    ? intl.formatMessage(displayMessages.save)
772
                    : intl.formatMessage(displayMessages.saved)}
773
                </span>
774
              </button>
775
            </div>
776
          </Form>
777
        )}
778
      </Formik>
779
      {showReferences && (
780
        <ReferenceEmailModal
781
          id={`referenceEmailModal_application${application.id}`}
782
          parent={modalParentRef.current}
783
          visible={emailModalVisible}
784
          email={emailOptions[showingEmail]}
785
          onConfirm={onConfirm}
786
          onCancel={hideEmail}
787
        />
788
      )}
789
    </div>
790
  );
791
};
792
793
const applicationSort = (locale: Locales) => {
794
  return (first: Application, second: Application): number => {
795
    // Applications without a review status should appear first
796
    if (
797
      empty(first.application_review?.review_status_id) &&
798
      notEmpty(second.application_review?.review_status_id)
799
    ) {
800
      return -1;
801
    }
802
    if (
803
      notEmpty(first.application_review?.review_status_id) &&
804
      empty(second.application_review?.review_status_id)
805
    ) {
806
      return 1;
807
    }
808
    // Applications with a review status should be grouped by status
809
    if (
810
      notEmpty(first.application_review) &&
811
      notEmpty(second.application_review)
812
    ) {
813
      if (
814
        first.application_review.review_status_id &&
815
        second.application_review.review_status_id
816
      ) {
817
        if (
818
          first.application_review.review_status_id <
819
          second.application_review.review_status_id
820
        ) {
821
          return -1;
822
        }
823
        if (
824
          first.application_review.review_status_id >
825
          second.application_review.review_status_id
826
        ) {
827
          return 1;
828
        }
829
      }
830
      // Applications without a Department should appear first
831
      if (
832
        empty(first.application_review.department) &&
833
        notEmpty(second.application_review.department)
834
      ) {
835
        return -1;
836
      }
837
      if (
838
        notEmpty(first.application_review.department) &&
839
        empty(second.application_review.department)
840
      ) {
841
        return 1;
842
      }
843
      // Applications with a Department set should be grouped by Department
844
      if (
845
        first.application_review.department &&
846
        second.application_review.department
847
      ) {
848
        const firstDepartmentName = localizeFieldNonNull(
849
          locale,
850
          first.application_review.department,
851
          "name",
852
        ).toUpperCase();
853
        const secondDepartmentName = localizeFieldNonNull(
854
          locale,
855
          second.application_review.department,
856
          "name",
857
        ).toUpperCase();
858
        if (firstDepartmentName < secondDepartmentName) {
859
          return -1;
860
        }
861
        if (firstDepartmentName > secondDepartmentName) {
862
          return 1;
863
        }
864
      }
865
    }
866
    return first.applicant.user.full_name.localeCompare(
867
      second.applicant.user.full_name,
868
    );
869
  };
870
};
871
872
interface ApplicantBucketProps {
873
  applications: Application[];
874
  bucket: string;
875
  departments: Department[];
876
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
877
  portal: Portal;
878
  referenceEmails: {
879
    director: {
880
      byApplicationId: {
881
        [applicationId: number]: Email;
882
      };
883
    };
884
    secondary: {
885
      byApplicationId: {
886
        [applicationId: number]: Email;
887
      };
888
    };
889
  };
890
  requestReferenceEmails: (applicationId: number) => void;
891
  sendReferenceEmail: (
892
    applicationId: number,
893
    referenceType: "director" | "secondary",
894
  ) => Promise<void>;
895
}
896
897
const ApplicantBucket: React.FC<ApplicantBucketProps> = ({
898
  applications,
899
  bucket,
900
  departments,
901
  handleUpdateReview,
902
  portal,
903
  referenceEmails,
904
  requestReferenceEmails,
905
  sendReferenceEmail,
906
}): React.ReactElement => {
907
  const intl = useIntl();
908
  const locale = getLocale(intl.locale);
909
910
  const [isExpanded, setIsExpanded] = useState(false);
911
  const {
912
    title: bucketTitle,
913
    description: bucketDescription,
914
  }: ValuesOf<typeof ResponseScreeningBuckets> = ResponseScreeningBuckets[
915
    bucket
916
  ];
917
918
  const handleExpandClick = (): void => {
919
    setIsExpanded(!isExpanded);
920
  };
921
922
  return (
923
    <div
924
      data-c-accordion=""
925
      data-c-background="white(100)"
926
      data-c-margin="top(.5)"
927
      data-c-card=""
928
      className={isExpanded ? "active" : ""}
929
    >
930
      <button
931
        aria-expanded={isExpanded}
932
        data-c-accordion-trigger
933
        tabIndex={0}
934
        type="button"
935
        onClick={handleExpandClick}
936
      >
937
        <div data-c-padding="top(normal) right bottom(normal) left(normal)">
938
          <p data-c-font-weight="bold" data-c-font-size="h3">
939
            {intl.formatMessage(bucketTitle)} ({applications.length})
940
          </p>
941
          <p data-c-margin="top(quarter)" data-c-colour="gray">
942
            {bucket === ResponseBuckets.Consideration
943
              ? intl.formatMessage(
944
                  ResponseScreeningBuckets.consideration.description,
945
                  {
946
                    iconAssessment: (
947
                      <StatusIcon
948
                        status={IconStatus.ASSESSMENT}
949
                        color="slow"
950
                        small
951
                      />
952
                    ),
953
                    iconReady: (
954
                      <StatusIcon status={IconStatus.READY} color="go" small />
955
                    ),
956
                    iconReceived: (
957
                      <StatusIcon
958
                        status={IconStatus.RECEIVED}
959
                        color="c1"
960
                        small
961
                      />
962
                    ),
963
                  },
964
                )
965
              : intl.formatMessage(bucketDescription)}
966
          </p>
967
        </div>
968
        <span data-c-visibility="invisible">
969
          {intl.formatMessage(displayMessages.clickView)}
970
        </span>
971
        {isExpanded ? (
972
          <i
973
            aria-hidden="true"
974
            className="fas fa-minus"
975
            data-c-accordion-remove=""
976
            data-c-colour="black"
977
          />
978
        ) : (
979
          <i
980
            aria-hidden="true"
981
            className="fas fa-plus"
982
            data-c-accordion-add=""
983
            data-c-colour="black"
984
          />
985
        )}
986
      </button>
987
      <div
988
        aria-hidden={!isExpanded}
989
        data-c-accordion-content=""
990
        data-c-padding="right(normal) left(normal)"
991
      >
992
        <div data-c-padding="bottom(normal)">
993
          {isExpanded &&
994
            applications.length > 0 &&
995
            applications
996
              .sort(applicationSort(locale))
997
              .map((application) => (
998
                <ApplicationRow
999
                  key={application.id}
1000
                  application={application}
1001
                  departmentEditable={bucket === ResponseBuckets.Allocated}
1002
                  departments={departments}
1003
                  handleUpdateReview={handleUpdateReview}
1004
                  portal={portal}
1005
                  directorReferenceEmail={
1006
                    referenceEmails.director.byApplicationId[application.id] ??
1007
                    null
1008
                  }
1009
                  secondaryReferenceEmail={
1010
                    referenceEmails.secondary.byApplicationId[application.id] ??
1011
                    null
1012
                  }
1013
                  requestReferenceEmails={requestReferenceEmails}
1014
                  sendReferenceEmail={sendReferenceEmail}
1015
                />
1016
              ))}
1017
          {isExpanded && applications.length === 0 && (
1018
            <div data-c-padding="bottom(normal)">
1019
              <div
1020
                data-c-border="all(thin, solid, gray)"
1021
                data-c-background="gray(10)"
1022
                data-c-padding="all(1)"
1023
                data-c-radius="rounded"
1024
                data-c-align="base(center)"
1025
              >
1026
                <p data-c-color="gray">
1027
                  {intl.formatMessage(displayMessages.noApplicants)}
1028
                </p>
1029
              </div>
1030
            </div>
1031
          )}
1032
        </div>
1033
      </div>
1034
    </div>
1035
  );
1036
};
1037
1038
export default ApplicantBucket;
1039