Passed
Push — feature/micro-ref-email ( 7f63c8...cb4bd3 )
by Tristan
03:45
created

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

Complexity

Total Complexity 60
Complexity/F 0

Size

Lines of Code 996
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 60
eloc 797
mnd 60
bc 60
fnc 0
dl 0
loc 996
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 3.403

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}>`.trim();
198
  const renderAddresses = (adrs: EmailAddress[]): string =>
199
    adrs.map(renderAddress).join(", ");
200
  return (
201
    <>
202
      <div data-c-dialog-overlay={visible ? "active" : ""} />
203
      <Modal
204
        id={id}
205
        parentElement={parent}
206
        visible={visible}
207
        onModalConfirm={onConfirm}
208
        onModalCancel={onCancel}
209
      >
210
        <Modal.Header>
211
          <div
212
            data-c-background="c1(100)"
213
            data-c-border="bottom(thin, solid, black)"
214
            data-c-padding="normal"
215
          >
216
            <h5 data-c-colour="white" data-c-font-size="h4">
217
              <FormattedMessage
218
                id="referenceEmailModal.title"
219
                defaultMessage="Email for Reference Check"
220
                description="Text displayed on the title of the MicroReference Email modal."
221
              />
222
            </h5>
223
          </div>
224
        </Modal.Header>
225
        <Modal.Body>
226
          <div data-c-border="bottom(thin, solid, black)">
227
            <div
228
              data-c-border="bottom(thin, solid, black)"
229
              data-c-padding="normal"
230
            >
231
              <p>
232
                <span>
233
                  <strong>
234
                    <FormattedMessage
235
                      id="referenceEmailModal.toLabel"
236
                      defaultMessage="To:"
237
                    />
238
                  </strong>
239
                </span>
240
                {` `}
241
                <span>{renderAddresses(email?.to ?? [])}</span>
242
              </p>
243
              <p>
244
                <span>
245
                  <strong>
246
                    <FormattedMessage
247
                      id="referenceEmailModal.fromLabel"
248
                      defaultMessage="From:"
249
                    />
250
                  </strong>
251
                </span>
252
                {` `}
253
                <span>{renderAddresses(email?.from ?? [])}</span>
254
              </p>
255
              <p>
256
                <span>
257
                  <strong>
258
                    <FormattedMessage
259
                      id="referenceEmailModal.ccLabel"
260
                      defaultMessage="CC:"
261
                    />
262
                  </strong>
263
                </span>
264
                {` `}
265
                <span>{renderAddresses(email?.cc ?? [])}</span>
266
              </p>
267
              <p>
268
                <span>
269
                  <strong>
270
                    <FormattedMessage
271
                      id="referenceEmailModal.bccLabel"
272
                      defaultMessage="BCC:"
273
                    />
274
                  </strong>
275
                </span>
276
                {` `}
277
                <span>{renderAddresses(email?.bcc ?? [])}</span>
278
              </p>
279
              <p>
280
                <span>
281
                  <strong>
282
                    <FormattedMessage
283
                      id="referenceEmailModal.subjectLabel"
284
                      defaultMessage="Subject:"
285
                    />
286
                  </strong>
287
                </span>
288
                {` `}
289
                <span>{email?.subject}</span>
290
              </p>
291
            </div>
292
            <div data-c-background="grey(20)" data-c-padding="normal">
293
              <div
294
                data-c-background="white(100)"
295
                data-c-padding="normal"
296
                data-c-radius="rounded"
297
              >
298
                {email ? (
299
                  <ReactMarkdown source={email.body} />
300
                ) : (
301
                  <p>
302
                    <FormattedMessage
303
                      id="referenceEmailModal.nullState"
304
                      defaultMessage="Loading reference email..."
305
                    />
306
                  </p>
307
                )}
308
              </div>
309
            </div>
310
          </div>
311
        </Modal.Body>
312
        <Modal.Footer>
313
          <Modal.FooterCancelBtn>
314
            <FormattedMessage
315
              id="referenceEmailModal.cancel"
316
              defaultMessage="Cancel"
317
            />
318
          </Modal.FooterCancelBtn>
319
          <Modal.FooterConfirmBtn>
320
            <FormattedMessage
321
              id="referenceEmailModal.confirm"
322
              defaultMessage="Send Email"
323
            />
324
          </Modal.FooterConfirmBtn>
325
        </Modal.Footer>
326
      </Modal>
327
    </>
328
  );
329
};
330
331
interface FormValues {
332
  reviewStatus: ReviewStatusId | ResponseReviewStatusId | null;
333
  department: number | null;
334
  notes: string;
335
}
336
337
interface ApplicationRowProps {
338
  application: Application;
339
  departmentEditable: boolean;
340
  departments: Department[];
341
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
342
  portal: Portal;
343
  directorReferenceEmail: Email | null;
344
  secondaryReferenceEmail: Email | null;
345
  requestReferenceEmails: (applicationId: number) => void;
346
  sendReferenceEmail: (
347
    applicationId: number,
348
    referenceType: "director" | "secondary",
349
  ) => Promise<void>;
350
}
351
352
const ApplicationRow: React.FC<ApplicationRowProps> = ({
353
  application,
354
  departmentEditable,
355
  departments,
356
  handleUpdateReview,
357
  portal,
358
  directorReferenceEmail,
359
  secondaryReferenceEmail,
360
  requestReferenceEmails,
361
  sendReferenceEmail,
362
}): React.ReactElement => {
363
  const intl = useIntl();
364
  const locale = getLocale(intl.locale);
365
366
  const applicantUrlMap: { [key in typeof portal]: string } = {
367
    hr: routes.hrApplicantShow(intl.locale, application.id),
368
    manager: routes.managerApplicantShow(intl.locale, application.id),
369
  };
370
  const applicationUrlMap: { [key in typeof portal]: string } = {
371
    hr: routes.hrApplicationShow(intl.locale, application.id),
372
    manager: routes.managerApplicationShow(intl.locale, application.id),
373
  };
374
  const applicantUrl = applicantUrlMap[portal];
375
  const applicationUrl = applicationUrlMap[portal];
376
377
  const reviewOptions = Object.values(ResponseReviewStatuses).map((status): {
378
    value: number;
379
    label: string;
380
  } => ({
381
    value: status.id,
382
    label: intl.formatMessage(status.name),
383
  }));
384
385
  const departmentOptions = departments.map((department) => ({
386
    value: department.id,
387
    label: localizeFieldNonNull(locale, department, "name"),
388
  }));
389
390
  let rowIcon: React.ReactElement;
391
392
  switch (application.application_review?.review_status_id) {
393
    case 4:
394
    case 5:
395
    case 7:
396
      rowIcon = (
397
        <StatusIcon status={IconStatus.READY} color="go" small={false} />
398
      );
399
      break;
400
    case 6:
401
      rowIcon = (
402
        <StatusIcon status={IconStatus.ASSESSMENT} color="slow" small={false} />
403
      );
404
      break;
405
    default:
406
      rowIcon = (
407
        <StatusIcon status={IconStatus.RECEIVED} color="c1" small={false} />
408
      );
409
  }
410
411
  const emptyReview: ApplicationReview = {
412
    id: 0,
413
    job_application_id: application.id,
414
    review_status_id: null,
415
    department_id: null,
416
    notes: null,
417
    created_at: new Date(),
418
    updated_at: new Date(),
419
    department: undefined,
420
    review_status: undefined,
421
    director_email_sent: false,
422
    reference_email_sent: false,
423
  };
424
425
  const initialValues: FormValues = {
426
    reviewStatus: application.application_review?.review_status_id || null,
427
    department: application.application_review?.department_id || null,
428
    notes: application.application_review?.notes || "",
429
  };
430
431
  const updateApplicationReview = (
432
    oldReview: ApplicationReview,
433
    values: FormValues,
434
  ): ApplicationReview => {
435
    const applicationReview: ApplicationReview = {
436
      ...oldReview,
437
      review_status_id: values.reviewStatus
438
        ? Number(values.reviewStatus)
439
        : null,
440
      department_id: values.department ? Number(values.department) : null,
441
      notes: values.notes || null,
442
    };
443
    return applicationReview;
444
  };
445
446
  const handleNotesButtonClick = (
447
    notes: string,
448
    updateField: (
449
      field: string,
450
      value: any,
451
      shouldValidate?: boolean | undefined,
452
    ) => void,
453
  ): void => {
454
    Swal.fire({
455
      title: intl.formatMessage(displayMessages.notes),
456
      icon: "info",
457
      input: "textarea",
458
      showCancelButton: true,
459
      confirmButtonColor: "#0A6CBC",
460
      cancelButtonColor: "#F94D4D",
461
      cancelButtonText: intl.formatMessage(displayMessages.cancel),
462
      confirmButtonText: intl.formatMessage(displayMessages.save),
463
      inputValue: notes,
464
    }).then((result) => {
465
      if (result && result.value !== undefined) {
466
        const value = result.value ? result.value : "";
467
        updateField("notes", value);
468
      }
469
    });
470
  };
471
472
  // MicroReferences
473
  // A single modal component is used for both emails, since only one will appear at a time.
474
  const [showingEmail, setShowingEmail] = useState<"director" | "secondary">(
475
    "director",
476
  );
477
  // But each email needs its own 'sending' flag, because its possible have both sending at the same time.
478
  const [sendingDirectorEmail, setSendingDirectorEmail] = useState(false);
479
  const [sendingSecondaryEmail, setSendingSecondaryEmail] = useState(false);
480
  const emailOptions = {
481
    director: directorReferenceEmail,
482
    secondary: secondaryReferenceEmail,
483
  };
484
  const [emailModalVisible, setEmailModalVisible] = useState(false);
485
  const showDirectorEmail = (): void => {
486
    requestReferenceEmails(application.id);
487
    setShowingEmail("director");
488
    setEmailModalVisible(true);
489
  };
490
  const showSecondaryEmail = (): void => {
491
    requestReferenceEmails(application.id);
492
    setShowingEmail("secondary");
493
    setEmailModalVisible(true);
494
  };
495
  const hideEmail = (): void => {
496
    setEmailModalVisible(false);
497
  };
498
  const onConfirm = async (): Promise<void> => {
499
    hideEmail();
500
    const setSendingEmail = {
501
      director: setSendingDirectorEmail,
502
      secondary: setSendingSecondaryEmail,
503
    }[showingEmail];
504
    setSendingEmail(true);
505
    await sendReferenceEmail(application.id, showingEmail);
506
    setSendingEmail(false);
507
  };
508
  const modalParentRef = useRef<HTMLDivElement>(null);
509
510
  return (
511
    <div className="applicant" ref={modalParentRef}>
512
      <Formik
513
        initialValues={initialValues}
514
        onSubmit={(values, { setSubmitting, resetForm }): void => {
515
          const review = updateApplicationReview(
516
            application.application_review || emptyReview,
517
            values,
518
          );
519
          const performUpdate = (): void => {
520
            handleUpdateReview(review)
521
              .then(() => {
522
                setSubmitting(false);
523
                resetForm();
524
              })
525
              .catch(() => {
526
                setSubmitting(false);
527
              });
528
          };
529
          // Changing an application's status to Allocated
530
          if (
531
            Number(values.reviewStatus) ===
532
              Number(ResponseReviewStatusId.Allocated) &&
533
            values.reviewStatus !== initialValues.reviewStatus
534
          ) {
535
            Swal.fire({
536
              title: intl.formatMessage(displayMessages.confirmAction),
537
              text: intl.formatMessage(displayMessages.setAllocated),
538
              icon: "warning",
539
              showCancelButton: true,
540
              confirmButtonColor: "#0A6CBC",
541
              cancelButtonColor: "#F94D4D",
542
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
543
              confirmButtonText: intl.formatMessage(displayMessages.yes),
544
            }).then((result) => {
545
              if (result.value === undefined) {
546
                setSubmitting(false);
547
              } else {
548
                performUpdate();
549
              }
550
            });
551
            // Changing an Application's status to Unavailable
552
          } else if (
553
            Number(values.reviewStatus) ===
554
              Number(ResponseReviewStatusId.NotAvailable) &&
555
            values.reviewStatus !== initialValues.reviewStatus
556
          ) {
557
            Swal.fire({
558
              title: intl.formatMessage(displayMessages.confirmAction),
559
              text: intl.formatMessage(displayMessages.setUnavailable),
560
              icon: "warning",
561
              showCancelButton: true,
562
              confirmButtonColor: "#0A6CBC",
563
              cancelButtonColor: "#F94D4D",
564
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
565
              confirmButtonText: intl.formatMessage(displayMessages.yes),
566
            }).then((result) => {
567
              if (result.value === undefined) {
568
                setSubmitting(false);
569
              } else {
570
                performUpdate();
571
              }
572
            });
573
            // Everything else
574
          } else {
575
            performUpdate();
576
          }
577
        }}
578
      >
579
        {({
580
          values,
581
          dirty,
582
          isSubmitting,
583
          setFieldValue,
584
        }): React.ReactElement => (
585
          <Form data-c-grid="gutter(all, 1) middle">
586
            <AlertWhenUnsaved />
587
            <div data-c-grid-item="base(1of4)">
588
              <div>
589
                {rowIcon}
590
                <div>
591
                  <p data-c-font-weight="bold" data-c-font-size="h4">
592
                    {application.applicant.user.full_name}
593
                  </p>
594
                  <p data-c-margin="bottom(.5)">
595
                    <a
596
                      href={`mailto:${application.applicant.user.email}`}
597
                      title=""
598
                    >
599
                      {application.applicant.user.email}
600
                    </a>
601
                  </p>
602
                  <p data-c-font-size="small">
603
                    <a href={applicationUrl} title="" data-c-margin="right(.5)">
604
                      {intl.formatMessage(displayMessages.viewApplication)}
605
                    </a>
606
                    <a href={applicantUrl} title="">
607
                      {intl.formatMessage(displayMessages.viewProfile)}
608
                    </a>
609
                  </p>
610
                </div>
611
              </div>
612
            </div>
613
            <FastField
614
              id={`review-status-select-${application.applicant_id}`}
615
              name="reviewStatus"
616
              label={intl.formatMessage(displayMessages.selectStatusLabel)}
617
              grid="base(1of4)"
618
              component={SelectInput}
619
              nullSelection={intl.formatMessage(
620
                displayMessages.selectStatusDefault,
621
              )}
622
              options={reviewOptions}
623
            />
624
            {departmentEditable && (
625
              <FastField
626
                id={`department-allocation-select-${application.applicant_id}`}
627
                name="department"
628
                label={intl.formatMessage(
629
                  displayMessages.selectDepartmentLabel,
630
                )}
631
                grid="base(1of4)"
632
                component={SelectInput}
633
                nullSelection={intl.formatMessage(
634
                  displayMessages.selectDepartmentDefault,
635
                )}
636
                options={departmentOptions}
637
              />
638
            )}
639
            <div
640
              data-c-grid-item={`base(${departmentEditable ? 1 : 2}of4)`}
641
              data-c-align="base(right)"
642
            >
643
              <button
644
                data-c-button="outline(c1)"
645
                type="button"
646
                data-c-radius="rounded"
647
                onClick={(): void =>
648
                  handleNotesButtonClick(values.notes, setFieldValue)
649
                }
650
              >
651
                <i className="fas fa-plus" />
652
                <span>{intl.formatMessage(displayMessages.notes)}</span>
653
              </button>
654
              <button
655
                data-c-button="solid(c1)"
656
                type="submit"
657
                data-c-radius="rounded"
658
                disabled={isSubmitting}
659
              >
660
                <span>
661
                  {dirty
662
                    ? intl.formatMessage(displayMessages.save)
663
                    : intl.formatMessage(displayMessages.saved)}
664
                </span>
665
              </button>
666
            </div>
667
          </Form>
668
        )}
669
      </Formik>
670
      <div>
671
        <span data-c-margin="right(normal)">
672
          <i
673
            className="fa fa-check-circle"
674
            data-c-color="go"
675
            data-c-visibility={
676
              application.application_review?.director_email_sent
677
                ? "visible"
678
                : "invisible"
679
            }
680
            data-c-font-size="small"
681
            data-c-margin="right(.5)"
682
          />
683
          <button
684
            data-c-button="outline(black)"
685
            data-c-radius="rounded"
686
            type="button"
687
            data-c-font-size="small"
688
            onClick={showDirectorEmail}
689
            disabled={sendingDirectorEmail}
690
          >
691
            {sendingDirectorEmail ? (
692
              <FormattedMessage
693
                id="responseScreening.applicant.directorEmailSending"
694
                defaultMessage="Sending email..."
695
              />
696
            ) : (
697
              <FormattedMessage
698
                id="responseScreening.applicant.directorEmailButton"
699
                defaultMessage="Show director email."
700
              />
701
            )}
702
          </button>
703
        </span>
704
        <span data-c-margin="right(normal)">
705
          <i
706
            className="fa fa-check-circle"
707
            data-c-color="go"
708
            data-c-visibility={
709
              application.application_review?.reference_email_sent
710
                ? "visible"
711
                : "invisible"
712
            }
713
            data-c-font-size="small"
714
            data-c-margin="right(.5)"
715
          />
716
          <button
717
            data-c-button="outline(black)"
718
            data-c-radius="rounded"
719
            type="button"
720
            data-c-font-size="small"
721
            onClick={showSecondaryEmail}
722
            disabled={sendingSecondaryEmail}
723
          >
724
            {sendingSecondaryEmail ? (
725
              <FormattedMessage
726
                id="responseScreening.applicant.secondaryEmailSending"
727
                defaultMessage="Sending email..."
728
              />
729
            ) : (
730
              <FormattedMessage
731
                id="responseScreening.applicant.secondaryEmailButton"
732
                defaultMessage="Show reference email."
733
              />
734
            )}
735
          </button>
736
        </span>
737
      </div>
738
      <ReferenceEmailModal
739
        id={`referenceEmailModal_application${application.id}`}
740
        parent={modalParentRef.current}
741
        visible={emailModalVisible}
742
        email={emailOptions[showingEmail]}
743
        onConfirm={onConfirm}
744
        onCancel={hideEmail}
745
      />
746
    </div>
747
  );
748
};
749
750
const applicationSort = (locale: Locales) => {
751
  return (first: Application, second: Application): number => {
752
    // Applications without a review status should appear first
753
    if (
754
      empty(first.application_review?.review_status_id) &&
755
      notEmpty(second.application_review?.review_status_id)
756
    ) {
757
      return -1;
758
    }
759
    if (
760
      notEmpty(first.application_review?.review_status_id) &&
761
      empty(second.application_review?.review_status_id)
762
    ) {
763
      return 1;
764
    }
765
    // Applications with a review status should be grouped by status
766
    if (
767
      notEmpty(first.application_review) &&
768
      notEmpty(second.application_review)
769
    ) {
770
      if (
771
        first.application_review.review_status_id &&
772
        second.application_review.review_status_id
773
      ) {
774
        if (
775
          first.application_review.review_status_id <
776
          second.application_review.review_status_id
777
        ) {
778
          return -1;
779
        }
780
        if (
781
          first.application_review.review_status_id >
782
          second.application_review.review_status_id
783
        ) {
784
          return 1;
785
        }
786
      }
787
      // Applications without a Department should appear first
788
      if (
789
        empty(first.application_review.department) &&
790
        notEmpty(second.application_review.department)
791
      ) {
792
        return -1;
793
      }
794
      if (
795
        notEmpty(first.application_review.department) &&
796
        empty(second.application_review.department)
797
      ) {
798
        return 1;
799
      }
800
      // Applications with a Department set should be grouped by Department
801
      if (
802
        first.application_review.department &&
803
        second.application_review.department
804
      ) {
805
        const firstDepartmentName = localizeFieldNonNull(
806
          locale,
807
          first.application_review.department,
808
          "name",
809
        ).toUpperCase();
810
        const secondDepartmentName = localizeFieldNonNull(
811
          locale,
812
          second.application_review.department,
813
          "name",
814
        ).toUpperCase();
815
        if (firstDepartmentName < secondDepartmentName) {
816
          return -1;
817
        }
818
        if (firstDepartmentName > secondDepartmentName) {
819
          return 1;
820
        }
821
      }
822
    }
823
    return first.applicant.user.full_name.localeCompare(
824
      second.applicant.user.full_name,
825
    );
826
  };
827
};
828
829
interface ApplicantBucketProps {
830
  applications: Application[];
831
  bucket: string;
832
  departments: Department[];
833
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
834
  portal: Portal;
835
  referenceEmails: {
836
    director: {
837
      byApplicationId: {
838
        [applicationId: number]: Email;
839
      };
840
    };
841
    secondary: {
842
      byApplicationId: {
843
        [applicationId: number]: Email;
844
      };
845
    };
846
  };
847
  requestReferenceEmails: (applicationId: number) => void;
848
  sendReferenceEmail: (
849
    applicationId: number,
850
    referenceType: "director" | "secondary",
851
  ) => Promise<void>;
852
}
853
854
const ApplicantBucket: React.FC<ApplicantBucketProps> = ({
855
  applications,
856
  bucket,
857
  departments,
858
  handleUpdateReview,
859
  portal,
860
  referenceEmails,
861
  requestReferenceEmails,
862
  sendReferenceEmail,
863
}): React.ReactElement => {
864
  const intl = useIntl();
865
  const locale = getLocale(intl.locale);
866
867
  const [isExpanded, setIsExpanded] = useState(false);
868
  const {
869
    title: bucketTitle,
870
    description: bucketDescription,
871
  }: ValuesOf<typeof ResponseScreeningBuckets> = ResponseScreeningBuckets[
872
    bucket
873
  ];
874
875
  const handleExpandClick = (): void => {
876
    setIsExpanded(!isExpanded);
877
  };
878
879
  return (
880
    <div
881
      data-c-accordion=""
882
      data-c-background="white(100)"
883
      data-c-margin="top(.5)"
884
      data-c-card=""
885
      className={isExpanded ? "active" : ""}
886
    >
887
      <button
888
        aria-expanded={isExpanded}
889
        data-c-accordion-trigger
890
        tabIndex={0}
891
        type="button"
892
        onClick={handleExpandClick}
893
      >
894
        <div data-c-padding="top(normal) right bottom(normal) left(normal)">
895
          <p data-c-font-weight="bold" data-c-font-size="h3">
896
            {intl.formatMessage(bucketTitle)} ({applications.length})
897
          </p>
898
          <p data-c-margin="top(quarter)" data-c-colour="gray">
899
            {bucket === ResponseBuckets.Consideration
900
              ? intl.formatMessage(
901
                  ResponseScreeningBuckets.consideration.description,
902
                  {
903
                    iconAssessment: (
904
                      <StatusIcon
905
                        status={IconStatus.ASSESSMENT}
906
                        color="slow"
907
                        small
908
                      />
909
                    ),
910
                    iconReady: (
911
                      <StatusIcon status={IconStatus.READY} color="go" small />
912
                    ),
913
                    iconReceived: (
914
                      <StatusIcon
915
                        status={IconStatus.RECEIVED}
916
                        color="c1"
917
                        small
918
                      />
919
                    ),
920
                  },
921
                )
922
              : intl.formatMessage(bucketDescription)}
923
          </p>
924
        </div>
925
        <span data-c-visibility="invisible">
926
          {intl.formatMessage(displayMessages.clickView)}
927
        </span>
928
        {isExpanded ? (
929
          <i
930
            aria-hidden="true"
931
            className="fas fa-minus"
932
            data-c-accordion-remove=""
933
            data-c-colour="black"
934
          />
935
        ) : (
936
          <i
937
            aria-hidden="true"
938
            className="fas fa-plus"
939
            data-c-accordion-add=""
940
            data-c-colour="black"
941
          />
942
        )}
943
      </button>
944
      <div
945
        aria-hidden={!isExpanded}
946
        data-c-accordion-content=""
947
        data-c-padding="right(normal) left(normal)"
948
      >
949
        <div data-c-padding="bottom(normal)">
950
          {isExpanded &&
951
            applications.length > 0 &&
952
            applications
953
              .sort(applicationSort(locale))
954
              .map((application) => (
955
                <ApplicationRow
956
                  key={application.id}
957
                  application={application}
958
                  departmentEditable={bucket === ResponseBuckets.Allocated}
959
                  departments={departments}
960
                  handleUpdateReview={handleUpdateReview}
961
                  portal={portal}
962
                  directorReferenceEmail={
963
                    referenceEmails.director.byApplicationId[application.id] ??
964
                    null
965
                  }
966
                  secondaryReferenceEmail={
967
                    referenceEmails.secondary.byApplicationId[application.id] ??
968
                    null
969
                  }
970
                  requestReferenceEmails={requestReferenceEmails}
971
                  sendReferenceEmail={sendReferenceEmail}
972
                />
973
              ))}
974
          {isExpanded && applications.length === 0 && (
975
            <div data-c-padding="bottom(normal)">
976
              <div
977
                data-c-border="all(thin, solid, gray)"
978
                data-c-background="gray(10)"
979
                data-c-padding="all(1)"
980
                data-c-radius="rounded"
981
                data-c-align="base(center)"
982
              >
983
                <p data-c-color="gray">
984
                  {intl.formatMessage(displayMessages.noApplicants)}
985
                </p>
986
              </div>
987
            </div>
988
          )}
989
        </div>
990
      </div>
991
    </div>
992
  );
993
};
994
995
export default ApplicantBucket;
996