Passed
Push — task/application-touch-step-st... ( c95732...291e16 )
by Tristan
04:39
created

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

Complexity

Total Complexity 61
Complexity/F 0

Size

Lines of Code 1006
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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