Passed
Push — feature/micro-ref-email ( a82660...7f63c8 )
by Tristan
04:37
created

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

Complexity

Total Complexity 50
Complexity/F 0

Size

Lines of Code 917
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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