Passed
Push — task/refactor-breadcrumbs ( 91a962...bee80d )
by Yonathan
04:22
created

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

Complexity

Total Complexity 63
Complexity/F 0

Size

Lines of Code 1055
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 63
eloc 842
dl 0
loc 1055
rs 3.118
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(
377
      intl.locale,
378
      application.id,
379
      application.job_poster_id,
380
    ),
381
    manager: routes.managerApplicantShow(
382
      intl.locale,
383
      application.id,
384
      application.job_poster_id,
385
    ),
386
  };
387
  const applicationUrlMap: { [key in typeof portal]: string } = {
388
    hr: routes.hrApplicationShow(
389
      intl.locale,
390
      application.id,
391
      application.job_poster_id,
392
    ),
393
    manager: routes.managerApplicationShow(
394
      intl.locale,
395
      application.id,
396
      application.job_poster_id,
397
    ),
398
  };
399
  const applicantUrl = applicantUrlMap[portal];
400
  const applicationUrl = applicationUrlMap[portal];
401
402
  const reviewOptions = Object.values(ResponseReviewStatuses).map((status): {
403
    value: number;
404
    label: string;
405
  } => ({
406
    value: status.id,
407
    label: intl.formatMessage(status.name),
408
  }));
409
410
  const departmentOptions = departments.map((department) => ({
411
    value: department.id,
412
    label: localizeFieldNonNull(locale, department, "name"),
413
  }));
414
415
  let rowIcon: React.ReactElement;
416
417
  switch (application.application_review?.review_status_id) {
418
    case 4:
419
    case 5:
420
    case 7:
421
      rowIcon = (
422
        <StatusIcon status={IconStatus.READY} color="go" small={false} />
423
      );
424
      break;
425
    case 6:
426
      rowIcon = (
427
        <StatusIcon status={IconStatus.ASSESSMENT} color="slow" small={false} />
428
      );
429
      break;
430
    default:
431
      rowIcon = (
432
        <StatusIcon status={IconStatus.RECEIVED} color="c1" small={false} />
433
      );
434
  }
435
436
  const emptyReview: ApplicationReview = {
437
    id: 0,
438
    job_application_id: application.id,
439
    review_status_id: null,
440
    department_id: null,
441
    notes: null,
442
    created_at: new Date(),
443
    updated_at: new Date(),
444
    department: undefined,
445
    review_status: undefined,
446
    director_email_sent: false,
447
    reference_email_sent: false,
448
  };
449
450
  const initialValues: FormValues = {
451
    reviewStatus: application.application_review?.review_status_id || null,
452
    department: application.application_review?.department_id || null,
453
    notes: application.application_review?.notes || "",
454
  };
455
456
  const updateApplicationReview = (
457
    oldReview: ApplicationReview,
458
    values: FormValues,
459
  ): ApplicationReview => {
460
    const applicationReview: ApplicationReview = {
461
      ...oldReview,
462
      review_status_id: values.reviewStatus
463
        ? Number(values.reviewStatus)
464
        : null,
465
      department_id: values.department ? Number(values.department) : null,
466
      notes: values.notes || null,
467
    };
468
    return applicationReview;
469
  };
470
471
  const handleNotesButtonClick = (
472
    notes: string,
473
    updateField: (
474
      field: string,
475
      value: any,
476
      shouldValidate?: boolean | undefined,
477
    ) => void,
478
  ): void => {
479
    Swal.fire({
480
      title: intl.formatMessage(displayMessages.notes),
481
      icon: "info",
482
      input: "textarea",
483
      showCancelButton: true,
484
      confirmButtonColor: "#0A6CBC",
485
      cancelButtonColor: "#F94D4D",
486
      cancelButtonText: intl.formatMessage(displayMessages.cancel),
487
      confirmButtonText: intl.formatMessage(displayMessages.save),
488
      inputValue: notes,
489
    }).then((result) => {
490
      if (result && result.value !== undefined) {
491
        const value = result.value ? result.value : "";
492
        updateField("notes", value);
493
      }
494
    });
495
  };
496
497
  // MicroReferences
498
  const bucketsWithReferences: any = [
499
    ResponseReviewStatusId.ReadyForReference,
500
    ResponseReviewStatusId.ReadyToAllocate,
501
    ResponseReviewStatusId.Allocated,
502
  ];
503
  const showReferences: boolean =
504
    notEmpty(application.application_review?.review_status_id) &&
505
    bucketsWithReferences.includes(
506
      application.application_review?.review_status_id,
507
    );
508
  // A single modal component is used for both emails, since only one will appear at a time.
509
  const [showingEmail, setShowingEmail] = useState<"director" | "secondary">(
510
    "director",
511
  );
512
  // But each email needs its own 'sending' flag, because its possible have both sending at the same time.
513
  const [sendingDirectorEmail, setSendingDirectorEmail] = useState(false);
514
  const [sendingSecondaryEmail, setSendingSecondaryEmail] = useState(false);
515
  const emailOptions = {
516
    director: directorReferenceEmail,
517
    secondary: secondaryReferenceEmail,
518
  };
519
  const [emailModalVisible, setEmailModalVisible] = useState(false);
520
  const showDirectorEmail = (): void => {
521
    requestReferenceEmails(application.id);
522
    setShowingEmail("director");
523
    setEmailModalVisible(true);
524
  };
525
  const showSecondaryEmail = (): void => {
526
    requestReferenceEmails(application.id);
527
    setShowingEmail("secondary");
528
    setEmailModalVisible(true);
529
  };
530
  const hideEmail = (): void => {
531
    setEmailModalVisible(false);
532
  };
533
  const onConfirm = async (): Promise<void> => {
534
    hideEmail();
535
    const setSendingEmail = {
536
      director: setSendingDirectorEmail,
537
      secondary: setSendingSecondaryEmail,
538
    }[showingEmail];
539
    setSendingEmail(true);
540
    await sendReferenceEmail(application.id, showingEmail);
541
    setSendingEmail(false);
542
  };
543
  const modalParentRef = useRef<HTMLDivElement>(null);
544
545
  return (
546
    <div className="applicant" ref={modalParentRef}>
547
      <Formik
548
        initialValues={initialValues}
549
        onSubmit={(values, { setSubmitting, resetForm }): void => {
550
          const review = updateApplicationReview(
551
            application.application_review || emptyReview,
552
            values,
553
          );
554
          const performUpdate = (): void => {
555
            handleUpdateReview(review)
556
              .then(() => {
557
                setSubmitting(false);
558
                resetForm();
559
              })
560
              .catch(() => {
561
                setSubmitting(false);
562
              });
563
          };
564
          // Changing an application's status to Allocated
565
          if (
566
            Number(values.reviewStatus) ===
567
              Number(ResponseReviewStatusId.Allocated) &&
568
            values.reviewStatus !== initialValues.reviewStatus
569
          ) {
570
            Swal.fire({
571
              title: intl.formatMessage(displayMessages.confirmAction),
572
              text: intl.formatMessage(displayMessages.setAllocated),
573
              icon: "warning",
574
              showCancelButton: true,
575
              confirmButtonColor: "#0A6CBC",
576
              cancelButtonColor: "#F94D4D",
577
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
578
              confirmButtonText: intl.formatMessage(displayMessages.yes),
579
            }).then((result) => {
580
              if (result.value === undefined) {
581
                setSubmitting(false);
582
              } else {
583
                performUpdate();
584
              }
585
            });
586
            // Changing an Application's status to Unavailable
587
          } else if (
588
            Number(values.reviewStatus) ===
589
              Number(ResponseReviewStatusId.NotAvailable) &&
590
            values.reviewStatus !== initialValues.reviewStatus
591
          ) {
592
            Swal.fire({
593
              title: intl.formatMessage(displayMessages.confirmAction),
594
              text: intl.formatMessage(displayMessages.setUnavailable),
595
              icon: "warning",
596
              showCancelButton: true,
597
              confirmButtonColor: "#0A6CBC",
598
              cancelButtonColor: "#F94D4D",
599
              cancelButtonText: intl.formatMessage(displayMessages.cancel),
600
              confirmButtonText: intl.formatMessage(displayMessages.yes),
601
            }).then((result) => {
602
              if (result.value === undefined) {
603
                setSubmitting(false);
604
              } else {
605
                performUpdate();
606
              }
607
            });
608
            // Everything else
609
          } else {
610
            performUpdate();
611
          }
612
        }}
613
      >
614
        {({
615
          values,
616
          dirty,
617
          isSubmitting,
618
          setFieldValue,
619
        }): React.ReactElement => (
620
          <Form data-c-grid="gutter(all, 1) middle">
621
            <AlertWhenUnsaved />
622
            <div data-c-grid-item="base(1of4)">
623
              <div>
624
                {rowIcon}
625
                <div>
626
                  <p data-c-font-weight="bold" data-c-font-size="h4">
627
                    {application.applicant.user.full_name}
628
                  </p>
629
                  <p data-c-margin="bottom(.5)">
630
                    <a
631
                      href={`mailto:${application.applicant.user.email}`}
632
                      title=""
633
                    >
634
                      {application.applicant.user.email}
635
                    </a>
636
                  </p>
637
                  <div
638
                    data-c-font-size="small"
639
                    data-c-grid="gutter(all, 1) middle"
640
                  >
641
                    <a
642
                      href={applicationUrl}
643
                      title=""
644
                      data-c-grid-item="base(1of2)"
645
                    >
646
                      {intl.formatMessage(displayMessages.viewApplication)}
647
                    </a>
648
                    <a
649
                      href={applicantUrl}
650
                      title=""
651
                      data-c-grid-item="base(1of2)"
652
                    >
653
                      {intl.formatMessage(displayMessages.viewProfile)}
654
                    </a>
655
                    {showReferences && (
656
                      <>
657
                        <div
658
                          className="email-reference-wrapper"
659
                          data-c-grid-item="base(1of2)"
660
                        >
661
                          <i
662
                            className="fa fa-check-circle"
663
                            data-c-color="go"
664
                            data-c-visibility={
665
                              application.application_review
666
                                ?.director_email_sent
667
                                ? "visible"
668
                                : "invisible"
669
                            }
670
                            data-c-font-size="small"
671
                            data-c-margin="right(.5)"
672
                          />
673
                          <button
674
                            data-c-button="reset"
675
                            data-c-font-style="underline"
676
                            data-c-font-size="small"
677
                            type="button"
678
                            onClick={showDirectorEmail}
679
                            disabled={sendingDirectorEmail}
680
                          >
681
                            {sendingDirectorEmail ? (
682
                              <FormattedMessage
683
                                id="responseScreening.applicant.directorEmailSending"
684
                                defaultMessage="Sending email..."
685
                              />
686
                            ) : (
687
                              <FormattedMessage
688
                                id="responseScreening.applicant.directorEmailButton"
689
                                defaultMessage="Director email."
690
                              />
691
                            )}
692
                          </button>
693
                        </div>
694
                        <div
695
                          data-c-grid-item="base(1of2)"
696
                          className="email-reference-wrapper"
697
                        >
698
                          <i
699
                            className="fa fa-check-circle"
700
                            data-c-color="go"
701
                            data-c-visibility={
702
                              application.application_review
703
                                ?.reference_email_sent
704
                                ? "visible"
705
                                : "invisible"
706
                            }
707
                            data-c-font-size="small"
708
                            data-c-margin="right(.5)"
709
                          />
710
                          <button
711
                            data-c-button="reset"
712
                            data-c-font-style="underline"
713
                            data-c-font-size="small"
714
                            type="button"
715
                            onClick={showSecondaryEmail}
716
                            disabled={sendingSecondaryEmail}
717
                          >
718
                            {sendingSecondaryEmail ? (
719
                              <FormattedMessage
720
                                id="responseScreening.applicant.secondaryEmailSending"
721
                                defaultMessage="Sending email..."
722
                              />
723
                            ) : (
724
                              <FormattedMessage
725
                                id="responseScreening.applicant.secondaryEmailButton"
726
                                defaultMessage="Reference email."
727
                              />
728
                            )}
729
                          </button>
730
                        </div>
731
                      </>
732
                    )}
733
                  </div>
734
                </div>
735
              </div>
736
            </div>
737
            <FastField
738
              id={`review-status-select-${application.applicant_id}`}
739
              name="reviewStatus"
740
              label={intl.formatMessage(displayMessages.selectStatusLabel)}
741
              grid="base(1of4)"
742
              component={SelectInput}
743
              nullSelection={intl.formatMessage(
744
                displayMessages.selectStatusDefault,
745
              )}
746
              options={reviewOptions}
747
            />
748
            {departmentEditable && (
749
              <FastField
750
                id={`department-allocation-select-${application.applicant_id}`}
751
                name="department"
752
                label={intl.formatMessage(
753
                  displayMessages.selectDepartmentLabel,
754
                )}
755
                grid="base(1of4)"
756
                component={SelectInput}
757
                nullSelection={intl.formatMessage(
758
                  displayMessages.selectDepartmentDefault,
759
                )}
760
                options={departmentOptions}
761
              />
762
            )}
763
            <div
764
              data-c-grid-item={`base(${departmentEditable ? 1 : 2}of4)`}
765
              data-c-align="base(right)"
766
            >
767
              <button
768
                data-c-button="outline(c1)"
769
                type="button"
770
                data-c-radius="rounded"
771
                data-c-margin="right(1)"
772
                onClick={(): void =>
773
                  handleNotesButtonClick(values.notes, setFieldValue)
774
                }
775
              >
776
                <i className="fas fa-plus" />
777
                <span>{intl.formatMessage(displayMessages.notes)}</span>
778
              </button>
779
              <button
780
                data-c-button="solid(c1)"
781
                type="submit"
782
                data-c-radius="rounded"
783
                disabled={isSubmitting}
784
              >
785
                <span>
786
                  {dirty
787
                    ? intl.formatMessage(displayMessages.save)
788
                    : intl.formatMessage(displayMessages.saved)}
789
                </span>
790
              </button>
791
            </div>
792
          </Form>
793
        )}
794
      </Formik>
795
      {showReferences && (
796
        <ReferenceEmailModal
797
          id={`referenceEmailModal_application${application.id}`}
798
          parent={modalParentRef.current}
799
          visible={emailModalVisible}
800
          email={emailOptions[showingEmail]}
801
          onConfirm={onConfirm}
802
          onCancel={hideEmail}
803
        />
804
      )}
805
    </div>
806
  );
807
};
808
809
const applicationSort = (locale: Locales) => {
810
  return (first: Application, second: Application): number => {
811
    // Applications without a review status should appear first
812
    if (
813
      empty(first.application_review?.review_status_id) &&
814
      notEmpty(second.application_review?.review_status_id)
815
    ) {
816
      return -1;
817
    }
818
    if (
819
      notEmpty(first.application_review?.review_status_id) &&
820
      empty(second.application_review?.review_status_id)
821
    ) {
822
      return 1;
823
    }
824
    // Applications with a review status should be grouped by status
825
    if (
826
      notEmpty(first.application_review) &&
827
      notEmpty(second.application_review)
828
    ) {
829
      if (
830
        first.application_review.review_status_id &&
831
        second.application_review.review_status_id
832
      ) {
833
        if (
834
          first.application_review.review_status_id <
835
          second.application_review.review_status_id
836
        ) {
837
          return -1;
838
        }
839
        if (
840
          first.application_review.review_status_id >
841
          second.application_review.review_status_id
842
        ) {
843
          return 1;
844
        }
845
      }
846
      // Applications without a Department should appear first
847
      if (
848
        empty(first.application_review.department) &&
849
        notEmpty(second.application_review.department)
850
      ) {
851
        return -1;
852
      }
853
      if (
854
        notEmpty(first.application_review.department) &&
855
        empty(second.application_review.department)
856
      ) {
857
        return 1;
858
      }
859
      // Applications with a Department set should be grouped by Department
860
      if (
861
        first.application_review.department &&
862
        second.application_review.department
863
      ) {
864
        const firstDepartmentName = localizeFieldNonNull(
865
          locale,
866
          first.application_review.department,
867
          "name",
868
        ).toUpperCase();
869
        const secondDepartmentName = localizeFieldNonNull(
870
          locale,
871
          second.application_review.department,
872
          "name",
873
        ).toUpperCase();
874
        if (firstDepartmentName < secondDepartmentName) {
875
          return -1;
876
        }
877
        if (firstDepartmentName > secondDepartmentName) {
878
          return 1;
879
        }
880
      }
881
    }
882
    return first.applicant.user.full_name.localeCompare(
883
      second.applicant.user.full_name,
884
    );
885
  };
886
};
887
888
interface ApplicantBucketProps {
889
  applications: Application[];
890
  bucket: string;
891
  departments: Department[];
892
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
893
  portal: Portal;
894
  referenceEmails: {
895
    director: {
896
      byApplicationId: {
897
        [applicationId: number]: Email;
898
      };
899
    };
900
    secondary: {
901
      byApplicationId: {
902
        [applicationId: number]: Email;
903
      };
904
    };
905
  };
906
  requestReferenceEmails: (applicationId: number) => void;
907
  sendReferenceEmail: (
908
    applicationId: number,
909
    referenceType: "director" | "secondary",
910
  ) => Promise<void>;
911
}
912
913
const ApplicantBucket: React.FC<ApplicantBucketProps> = ({
914
  applications,
915
  bucket,
916
  departments,
917
  handleUpdateReview,
918
  portal,
919
  referenceEmails,
920
  requestReferenceEmails,
921
  sendReferenceEmail,
922
}): React.ReactElement => {
923
  const intl = useIntl();
924
  const locale = getLocale(intl.locale);
925
926
  const [isExpanded, setIsExpanded] = useState(false);
927
  const {
928
    title: bucketTitle,
929
    description: bucketDescription,
930
  }: ValuesOf<typeof ResponseScreeningBuckets> = ResponseScreeningBuckets[
931
    bucket
932
  ];
933
934
  const handleExpandClick = (): void => {
935
    setIsExpanded(!isExpanded);
936
  };
937
938
  return (
939
    <div
940
      data-c-accordion=""
941
      data-c-background="white(100)"
942
      data-c-margin="top(.5)"
943
      data-c-card=""
944
      className={isExpanded ? "active" : ""}
945
    >
946
      <button
947
        aria-expanded={isExpanded}
948
        data-c-accordion-trigger
949
        tabIndex={0}
950
        type="button"
951
        onClick={handleExpandClick}
952
      >
953
        <div data-c-padding="top(normal) right bottom(normal) left(normal)">
954
          <p data-c-font-weight="bold" data-c-font-size="h3">
955
            {intl.formatMessage(bucketTitle)} ({applications.length})
956
          </p>
957
          <p data-c-margin="top(quarter)" data-c-colour="gray">
958
            {bucket === ResponseBuckets.Consideration
959
              ? intl.formatMessage(
960
                  ResponseScreeningBuckets.consideration.description,
961
                  {
962
                    iconAssessment: (
963
                      <StatusIcon
964
                        status={IconStatus.ASSESSMENT}
965
                        color="slow"
966
                        small
967
                      />
968
                    ),
969
                    iconReady: (
970
                      <StatusIcon status={IconStatus.READY} color="go" small />
971
                    ),
972
                    iconReceived: (
973
                      <StatusIcon
974
                        status={IconStatus.RECEIVED}
975
                        color="c1"
976
                        small
977
                      />
978
                    ),
979
                  },
980
                )
981
              : intl.formatMessage(bucketDescription)}
982
          </p>
983
        </div>
984
        <span data-c-visibility="invisible">
985
          {intl.formatMessage(displayMessages.clickView)}
986
        </span>
987
        {isExpanded ? (
988
          <i
989
            aria-hidden="true"
990
            className="fas fa-minus"
991
            data-c-accordion-remove=""
992
            data-c-colour="black"
993
          />
994
        ) : (
995
          <i
996
            aria-hidden="true"
997
            className="fas fa-plus"
998
            data-c-accordion-add=""
999
            data-c-colour="black"
1000
          />
1001
        )}
1002
      </button>
1003
      <div
1004
        aria-hidden={!isExpanded}
1005
        data-c-accordion-content=""
1006
        data-c-padding="right(normal) left(normal)"
1007
      >
1008
        <div data-c-padding="bottom(normal)">
1009
          {isExpanded &&
1010
            applications.length > 0 &&
1011
            applications
1012
              .sort(applicationSort(locale))
1013
              .map((application) => (
1014
                <ApplicationRow
1015
                  key={application.id}
1016
                  application={application}
1017
                  departmentEditable={bucket === ResponseBuckets.Allocated}
1018
                  departments={departments}
1019
                  handleUpdateReview={handleUpdateReview}
1020
                  portal={portal}
1021
                  directorReferenceEmail={
1022
                    referenceEmails.director.byApplicationId[application.id] ??
1023
                    null
1024
                  }
1025
                  secondaryReferenceEmail={
1026
                    referenceEmails.secondary.byApplicationId[application.id] ??
1027
                    null
1028
                  }
1029
                  requestReferenceEmails={requestReferenceEmails}
1030
                  sendReferenceEmail={sendReferenceEmail}
1031
                />
1032
              ))}
1033
          {isExpanded && applications.length === 0 && (
1034
            <div data-c-padding="bottom(normal)">
1035
              <div
1036
                data-c-border="all(thin, solid, gray)"
1037
                data-c-background="gray(10)"
1038
                data-c-padding="all(1)"
1039
                data-c-radius="rounded"
1040
                data-c-align="base(center)"
1041
              >
1042
                <p data-c-color="gray">
1043
                  {intl.formatMessage(displayMessages.noApplicants)}
1044
                </p>
1045
              </div>
1046
            </div>
1047
          )}
1048
        </div>
1049
      </div>
1050
    </div>
1051
  );
1052
};
1053
1054
export default ApplicantBucket;
1055