Passed
Push — master ( 795d23...149f73 )
by Grant
06:52 queued 12s
created

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

Complexity

Total Complexity 63
Complexity/F 0

Size

Lines of Code 1068
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 63
eloc 853
mnd 63
bc 63
fnc 0
dl 0
loc 1068
rs 3.107
bpm 0
cpm 0
noi 0
c 0
b 0
f 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, { SweetAlertResult } 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: SweetAlertResult) => {
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: SweetAlertResult) => {
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: SweetAlertResult) => {
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
623
              data-c-grid-item="base(1of1) tl(1of4)"
624
              data-c-align="base(center) tl(left)"
625
            >
626
              <div className="applicant-info-wrapper">
627
                <p data-c-font-weight="bold" data-c-font-size="h4">
628
                  {rowIcon}
629
                  <span data-c-padding="left(.5)">
630
                    {application.applicant.user.full_name}
631
                  </span>
632
                </p>
633
                <p data-c-margin="bottom(.5)">
634
                  <a
635
                    className="applicant-info-email"
636
                    href={`mailto:${application.applicant.user.email}`}
637
                    title=""
638
                  >
639
                    {application.applicant.user.email}
640
                  </a>
641
                </p>
642
                <div
643
                  data-c-font-size="small"
644
                  data-c-grid="gutter(all, 1) middle"
645
                >
646
                  <span
647
                    data-c-grid-item="base(1of2)"
648
                    data-c-align="base(center) tl(left)"
649
                  >
650
                    <a href={applicationUrl} title="">
651
                      {intl.formatMessage(displayMessages.viewApplication)}
652
                    </a>
653
                  </span>
654
                  <span
655
                    data-c-grid-item="base(1of2)"
656
                    data-c-align="base(center) tl(left)"
657
                  >
658
                    <a href={applicantUrl} title="">
659
                      {intl.formatMessage(displayMessages.viewProfile)}
660
                    </a>
661
                  </span>
662
                  {showReferences && (
663
                    <>
664
                      <div data-c-grid-item="base(1of2)">
665
                        <button
666
                          className="email-reference-button"
667
                          data-c-button="reset"
668
                          data-c-font-style="underline"
669
                          data-c-font-size="small"
670
                          type="button"
671
                          onClick={showDirectorEmail}
672
                          disabled={sendingDirectorEmail}
673
                        >
674
                          <i
675
                            className="fa fa-check-circle"
676
                            data-c-color="go"
677
                            data-c-visibility={
678
                              application.application_review
679
                                ?.director_email_sent
680
                                ? "visible"
681
                                : "invisible"
682
                            }
683
                            data-c-font-size="small"
684
                            data-c-margin="right(.5)"
685
                          />
686
                          {sendingDirectorEmail ? (
687
                            <FormattedMessage
688
                              id="responseScreening.applicant.directorEmailSending"
689
                              defaultMessage="Sending email..."
690
                            />
691
                          ) : (
692
                            <FormattedMessage
693
                              id="responseScreening.applicant.directorEmailButton"
694
                              defaultMessage="Director email."
695
                            />
696
                          )}
697
                        </button>
698
                      </div>
699
                      <div
700
                        className="applicant-buttons"
701
                        data-c-grid-item="base(1of2)"
702
                      >
703
                        <button
704
                          className="email-reference-button"
705
                          data-c-button="reset"
706
                          data-c-font-style="underline"
707
                          data-c-font-size="small"
708
                          type="button"
709
                          onClick={showSecondaryEmail}
710
                          disabled={sendingSecondaryEmail}
711
                        >
712
                          <i
713
                            className="fa fa-check-circle"
714
                            data-c-color="go"
715
                            data-c-visibility={
716
                              application.application_review
717
                                ?.reference_email_sent
718
                                ? "visible"
719
                                : "invisible"
720
                            }
721
                            data-c-font-size="small"
722
                            data-c-margin="right(.5)"
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="Reference email."
733
                            />
734
                          )}
735
                        </button>
736
                      </div>
737
                    </>
738
                  )}
739
                </div>
740
              </div>
741
            </div>
742
            <div
743
              className="review-status-wrapper"
744
              data-c-grid-item="base(1of1) tp(2of4) tl(1of4)"
745
            >
746
              <FastField
747
                id={`review-status-select-${application.applicant_id}`}
748
                name="reviewStatus"
749
                label={intl.formatMessage(displayMessages.selectStatusLabel)}
750
                component={SelectInput}
751
                nullSelection={intl.formatMessage(
752
                  displayMessages.selectStatusDefault,
753
                )}
754
                options={reviewOptions}
755
              />
756
            </div>
757
            {departmentEditable && (
758
              <div data-c-grid-item="base(1of1) tp(2of4) tl(1of4)">
759
                <FastField
760
                  id={`department-allocation-select-${application.applicant_id}`}
761
                  name="department"
762
                  label={intl.formatMessage(
763
                    displayMessages.selectDepartmentLabel,
764
                  )}
765
                  component={SelectInput}
766
                  nullSelection={intl.formatMessage(
767
                    displayMessages.selectDepartmentDefault,
768
                  )}
769
                  options={departmentOptions}
770
                />
771
              </div>
772
            )}
773
            <div
774
              className="applicant-buttons-wrapper"
775
              data-c-grid-item={`base(1of1) tl(${
776
                departmentEditable ? 1 : 2
777
              }of4)`}
778
              data-c-align="base(center) tp(center) tl(right)"
779
            >
780
              <button
781
                data-c-button="outline(c1)"
782
                type="button"
783
                data-c-radius="rounded"
784
                onClick={(): void =>
785
                  handleNotesButtonClick(values.notes, setFieldValue)
786
                }
787
              >
788
                <i className="fas fa-plus" />
789
                <span>{intl.formatMessage(displayMessages.notes)}</span>
790
              </button>
791
              <button
792
                data-c-button="solid(c1)"
793
                type="submit"
794
                data-c-radius="rounded"
795
                data-c-margin="left(.5)"
796
                disabled={isSubmitting}
797
              >
798
                <span>
799
                  {dirty
800
                    ? intl.formatMessage(displayMessages.save)
801
                    : intl.formatMessage(displayMessages.saved)}
802
                </span>
803
              </button>
804
            </div>
805
          </Form>
806
        )}
807
      </Formik>
808
      {showReferences && (
809
        <ReferenceEmailModal
810
          id={`referenceEmailModal_application${application.id}`}
811
          parent={modalParentRef.current}
812
          visible={emailModalVisible}
813
          email={emailOptions[showingEmail]}
814
          onConfirm={onConfirm}
815
          onCancel={hideEmail}
816
        />
817
      )}
818
    </div>
819
  );
820
};
821
822
const applicationSort = (locale: Locales) => {
823
  return (first: Application, second: Application): number => {
824
    // Applications without a review status should appear first
825
    if (
826
      empty(first.application_review?.review_status_id) &&
827
      notEmpty(second.application_review?.review_status_id)
828
    ) {
829
      return -1;
830
    }
831
    if (
832
      notEmpty(first.application_review?.review_status_id) &&
833
      empty(second.application_review?.review_status_id)
834
    ) {
835
      return 1;
836
    }
837
    // Applications with a review status should be grouped by status
838
    if (
839
      notEmpty(first.application_review) &&
840
      notEmpty(second.application_review)
841
    ) {
842
      if (
843
        first.application_review.review_status_id &&
844
        second.application_review.review_status_id
845
      ) {
846
        if (
847
          first.application_review.review_status_id <
848
          second.application_review.review_status_id
849
        ) {
850
          return -1;
851
        }
852
        if (
853
          first.application_review.review_status_id >
854
          second.application_review.review_status_id
855
        ) {
856
          return 1;
857
        }
858
      }
859
      // Applications without a Department should appear first
860
      if (
861
        empty(first.application_review.department) &&
862
        notEmpty(second.application_review.department)
863
      ) {
864
        return -1;
865
      }
866
      if (
867
        notEmpty(first.application_review.department) &&
868
        empty(second.application_review.department)
869
      ) {
870
        return 1;
871
      }
872
      // Applications with a Department set should be grouped by Department
873
      if (
874
        first.application_review.department &&
875
        second.application_review.department
876
      ) {
877
        const firstDepartmentName = localizeFieldNonNull(
878
          locale,
879
          first.application_review.department,
880
          "name",
881
        ).toUpperCase();
882
        const secondDepartmentName = localizeFieldNonNull(
883
          locale,
884
          second.application_review.department,
885
          "name",
886
        ).toUpperCase();
887
        if (firstDepartmentName < secondDepartmentName) {
888
          return -1;
889
        }
890
        if (firstDepartmentName > secondDepartmentName) {
891
          return 1;
892
        }
893
      }
894
    }
895
    return first.applicant.user.full_name.localeCompare(
896
      second.applicant.user.full_name,
897
    );
898
  };
899
};
900
901
interface ApplicantBucketProps {
902
  applications: Application[];
903
  bucket: string;
904
  departments: Department[];
905
  handleUpdateReview: (review: ApplicationReview) => Promise<ApplicationReview>;
906
  portal: Portal;
907
  referenceEmails: {
908
    director: {
909
      byApplicationId: {
910
        [applicationId: number]: Email;
911
      };
912
    };
913
    secondary: {
914
      byApplicationId: {
915
        [applicationId: number]: Email;
916
      };
917
    };
918
  };
919
  requestReferenceEmails: (applicationId: number) => void;
920
  sendReferenceEmail: (
921
    applicationId: number,
922
    referenceType: "director" | "secondary",
923
  ) => Promise<void>;
924
}
925
926
const ApplicantBucket: React.FC<ApplicantBucketProps> = ({
927
  applications,
928
  bucket,
929
  departments,
930
  handleUpdateReview,
931
  portal,
932
  referenceEmails,
933
  requestReferenceEmails,
934
  sendReferenceEmail,
935
}): React.ReactElement => {
936
  const intl = useIntl();
937
  const locale = getLocale(intl.locale);
938
939
  const [isExpanded, setIsExpanded] = useState(false);
940
  const {
941
    title: bucketTitle,
942
    description: bucketDescription,
943
  }: ValuesOf<typeof ResponseScreeningBuckets> = ResponseScreeningBuckets[
944
    bucket
945
  ];
946
947
  const handleExpandClick = (): void => {
948
    setIsExpanded(!isExpanded);
949
  };
950
951
  return (
952
    <div
953
      data-c-accordion=""
954
      data-c-background="white(100)"
955
      data-c-margin="top(.5)"
956
      data-c-card=""
957
      className={isExpanded ? "active" : ""}
958
    >
959
      <button
960
        aria-expanded={isExpanded}
961
        data-c-accordion-trigger
962
        tabIndex={0}
963
        type="button"
964
        onClick={handleExpandClick}
965
      >
966
        <div data-c-padding="top(normal) right bottom(normal) left(normal)">
967
          <p data-c-font-weight="bold" data-c-font-size="h3">
968
            {intl.formatMessage(bucketTitle)} ({applications.length})
969
          </p>
970
          <p data-c-margin="top(quarter)" data-c-colour="gray">
971
            {bucket === ResponseBuckets.Consideration
972
              ? intl.formatMessage(
973
                  ResponseScreeningBuckets.consideration.description,
974
                  {
975
                    iconAssessment: (
976
                      <StatusIcon
977
                        status={IconStatus.ASSESSMENT}
978
                        color="slow"
979
                        small
980
                      />
981
                    ),
982
                    iconReady: (
983
                      <StatusIcon status={IconStatus.READY} color="go" small />
984
                    ),
985
                    iconReceived: (
986
                      <StatusIcon
987
                        status={IconStatus.RECEIVED}
988
                        color="c1"
989
                        small
990
                      />
991
                    ),
992
                  },
993
                )
994
              : intl.formatMessage(bucketDescription)}
995
          </p>
996
        </div>
997
        <span data-c-visibility="invisible">
998
          {intl.formatMessage(displayMessages.clickView)}
999
        </span>
1000
        {isExpanded ? (
1001
          <i
1002
            aria-hidden="true"
1003
            className="fas fa-minus"
1004
            data-c-accordion-remove=""
1005
            data-c-colour="black"
1006
          />
1007
        ) : (
1008
          <i
1009
            aria-hidden="true"
1010
            className="fas fa-plus"
1011
            data-c-accordion-add=""
1012
            data-c-colour="black"
1013
          />
1014
        )}
1015
      </button>
1016
      <div
1017
        aria-hidden={!isExpanded}
1018
        data-c-accordion-content=""
1019
        data-c-padding="right(normal) left(normal)"
1020
      >
1021
        <div data-c-padding="bottom(normal)">
1022
          {isExpanded &&
1023
            applications.length > 0 &&
1024
            applications
1025
              .sort(applicationSort(locale))
1026
              .map((application) => (
1027
                <ApplicationRow
1028
                  key={application.id}
1029
                  application={application}
1030
                  departmentEditable={bucket === ResponseBuckets.Allocated}
1031
                  departments={departments}
1032
                  handleUpdateReview={handleUpdateReview}
1033
                  portal={portal}
1034
                  directorReferenceEmail={
1035
                    referenceEmails.director.byApplicationId[application.id] ??
1036
                    null
1037
                  }
1038
                  secondaryReferenceEmail={
1039
                    referenceEmails.secondary.byApplicationId[application.id] ??
1040
                    null
1041
                  }
1042
                  requestReferenceEmails={requestReferenceEmails}
1043
                  sendReferenceEmail={sendReferenceEmail}
1044
                />
1045
              ))}
1046
          {isExpanded && applications.length === 0 && (
1047
            <div data-c-padding="bottom(normal)">
1048
              <div
1049
                data-c-border="all(thin, solid, gray)"
1050
                data-c-background="gray(10)"
1051
                data-c-padding="all(1)"
1052
                data-c-radius="rounded"
1053
                data-c-align="base(center)"
1054
              >
1055
                <p data-c-color="gray">
1056
                  {intl.formatMessage(displayMessages.noApplicants)}
1057
                </p>
1058
              </div>
1059
            </div>
1060
          )}
1061
        </div>
1062
      </div>
1063
    </div>
1064
  );
1065
};
1066
1067
export default ApplicantBucket;
1068