resources/assets/js/components/JobBuilder/Skills/JobSkills.tsx   B
last analyzed

Complexity

Total Complexity 33
Complexity/F 0

Size

Lines of Code 1895
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 33
eloc 1456
mnd 33
bc 33
fnc 0
dl 0
loc 1895
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 8.56
1
import React, { useState, useRef, useReducer } from "react";
2
import { FormattedMessage, defineMessages, useIntl } from "react-intl";
3
import nprogress from "nprogress";
4
import { Job, Skill, Criteria, JobPosterKeyTask } from "../../../models/types";
5
import Modal from "../../Modal";
6
import CriteriaForm from "./CriteriaForm";
7
import { mapToObject, getId, hasKey, notEmpty } from "../../../helpers/queries";
8
import { CriteriaTypeId } from "../../../models/lookupConstants";
9
import Select, { SelectOption } from "../../Select";
10
import { getSkillLevelName } from "../../../models/jobUtil";
11
import Criterion from "../Criterion";
12
import {
13
  localizeField,
14
  getLocale,
15
  localizeFieldNonNull,
16
} from "../../../helpers/localize";
17
import { imageUrl } from "../../../helpers/routes";
18
19
interface JobSkillsProps {
20
  // The job being built
21
  job: Job;
22
  // This job's classification key (i.e. CS)
23
  classificationKey: string;
24
  // This job's key tasks
25
  keyTasks: JobPosterKeyTask[];
26
  // Criteria already part of the job
27
  initialCriteria: Criteria[];
28
  // The list of all possible skills
29
  skills: Skill[];
30
  // The function to run when user clicks Save. Must return the updated list of criteria if successful.
31
  handleSubmit: (criteria: Criteria[]) => Promise<Criteria[]>;
32
  // The function to run when user clicks Prev Pag
33
  handleReturn: () => void;
34
  // The function to run when user clicks Next Page
35
  handleContinue: () => void;
36
  /** Whether the entire job is complete and valid for submission. */
37
  jobIsComplete: boolean;
38
  /** Function that skips to final review. */
39
  handleSkipToReview: () => Promise<void>;
40
}
41
42
const messages = defineMessages({
43
  emailUs: {
44
    id: "jobBuilder.skills.emailLink",
45
    defaultMessage: "get in touch with us through email",
46
    description: "Text for an email link in a larger block of text",
47
  },
48
  selectSkillLabel: {
49
    id: "jobBuilder.skills.selectSkillLabel",
50
    defaultMessage: "Please select a skill from our list",
51
    description: "Label for skill selection dropdown menu",
52
  },
53
  selectSkillNull: {
54
    id: "jobBuilder.skills.selectSkillNull",
55
    defaultMessage: "Please select a skill",
56
    description: "Label for skill selection dropdown null/default state",
57
  },
58
});
59
60
const altMessages = defineMessages({
61
  unhappyArrow: {
62
    id: "jobBuilder.skills.alt.unhappyArrow",
63
    defaultMessage: "Arrow icon highlighting the unhappy smiley icon.",
64
    description: "Alternative text describing unhappy arrow image",
65
  },
66
  unhappySmiley: {
67
    id: "jobBuilder.skills.alt.unhappySmiley",
68
    defaultMessage: "Unhappy coloured smiley icon.",
69
    description: "Alternative text describing unhappy smiley image",
70
  },
71
  unhappyGraySmiley: {
72
    id: "jobBuilder.skills.alt.unhappyGraySmiley",
73
    defaultMessage: "Unhappy grayscale smiley icon.",
74
    description: "Alternative text describing unhappy grayscale smiley image",
75
  },
76
  neutralArrow: {
77
    id: "jobBuilder.skills.alt.neutralArrow",
78
    defaultMessage: "Arrow icon highlighting the neutral smiley icon.",
79
    description: "Alternative text describing neutral arrow image",
80
  },
81
  neutralSmiley: {
82
    id: "jobBuilder.skills.alt.neutralSmiley",
83
    defaultMessage: "neutral coloured smiley icon.",
84
    description: "Alternative text describing neutral smiley image",
85
  },
86
  neutralGraySmiley: {
87
    id: "jobBuilder.skills.alt.neutralGraySmiley",
88
    defaultMessage: "neutral grayscale smiley icon.",
89
    description: "Alternative text describing neutral grayscale smiley image",
90
  },
91
  happyArrow: {
92
    id: "jobBuilder.skills.alt.happyArrow",
93
    defaultMessage: "Arrow icon highlighting the happy smiley icon.",
94
    description: "Alternative text describing happy arrow image",
95
  },
96
  happySmiley: {
97
    id: "jobBuilder.skills.alt.happySmiley",
98
    defaultMessage: "happy coloured smiley icon.",
99
    description: "Alternative text describing happy smiley image",
100
  },
101
  happyGraySmiley: {
102
    id: "jobBuilder.skills.alt.happyGraySmiley",
103
    defaultMessage: "happy grayscale smiley icon.",
104
    description: "Alternative text describing happy grayscale smiley image",
105
  },
106
});
107
// "Arrow highlighting the neutral smiley icon."
108
// function arrayMove<T>(arr: T[], fromIndex: number, toIndex: number): T[] {
109
//   const arrCopy = [...arr];
110
//   const element = arrCopy[fromIndex];
111
//   arrCopy.splice(fromIndex, 1);
112
//   arrCopy.splice(toIndex, 0, element);
113
//   return arrCopy;
114
// }
115
116
// function moveUp<T>(arr: T[], fromIndex: number): T[] {
117
//   if (fromIndex <= 0) {
118
//     return arr;
119
//   }
120
//   return arrayMove(arr, fromIndex, fromIndex - 1);
121
// }
122
123
// function moveDown<T>(arr: T[], fromIndex: number): T[] {
124
//   if (fromIndex + 1 >= arr.length) {
125
//     return arr;
126
//   }
127
//   return arrayMove(arr, fromIndex, fromIndex + 1);
128
// }
129
130
type CriteriaAction =
131
  | {
132
      type: "add";
133
      payload: Criteria;
134
    }
135
  | {
136
      type: "edit";
137
      payload: Criteria;
138
    }
139
  | {
140
      type: "remove";
141
      payload: Criteria;
142
    }
143
  | {
144
      type: "removeSkill";
145
      payload: {
146
        skillId: number;
147
      };
148
    }
149
  | {
150
      type: "replace";
151
      payload: Criteria[];
152
    };
153
const criteriaReducer = (
154
  state: Criteria[],
155
  action: CriteriaAction,
156
): Criteria[] => {
157
  switch (action.type) {
158
    case "add":
159
      return [
160
        // When adding a criterion, make sure it isn't a duplicate
161
        // Always compare criteria using skill_id, to avoid duplicate skills. (And new criteria may not have unique ids yet.)
162
        ...state.filter(
163
          (criterion): boolean =>
164
            criterion.skill_id !== action.payload.skill_id,
165
        ),
166
        action.payload,
167
      ];
168
    case "edit":
169
      // Replace the edited criterion with the one from the action payload.
170
      // This is different from "add" because it keeps the same ordering.
171
      return state.map(
172
        (criterion): Criteria =>
173
          criterion.skill_id === action.payload.skill_id
174
            ? action.payload
175
            : criterion,
176
      );
177
    case "remove":
178
      return state.filter(
179
        (criterion): boolean => criterion.skill_id !== action.payload.skill_id,
180
      );
181
    case "removeSkill":
182
      return state.filter(
183
        (criterion): boolean => criterion.skill_id !== action.payload.skillId,
184
      );
185
    case "replace":
186
      // Totally replace the saved list of criteria with the received payload
187
      return action.payload;
188
189
    default:
190
      return state;
191
  }
192
};
193
194
export const skillAlreadySelected = (
195
  selectedCriteria: Criteria[],
196
  skill: Skill,
197
): boolean =>
198
  selectedCriteria.find(
199
    (criterion): boolean => criterion.skill_id === skill.id,
200
  ) !== undefined;
201
202
export const JobSkills: React.FunctionComponent<JobSkillsProps> = ({
203
  job,
204
  classificationKey,
205
  keyTasks,
206
  initialCriteria,
207
  skills,
208
  handleSubmit,
209
  handleReturn,
210
  handleContinue,
211
  jobIsComplete,
212
  handleSkipToReview,
213
}): React.ReactElement => {
214
  const intl = useIntl();
215
  const locale = getLocale(intl.locale);
216
217
  // The ideal number of skills for each category
218
  const minOccupational = 3;
219
  const maxOccupational = 5;
220
  const minCulture = 0;
221
  const maxCulture = 4;
222
  const minFuture = 0;
223
  const maxFuture = 2;
224
225
  // This is where the edited list of criteria is stored
226
  const [jobCriteria, criteriaDispatch] = useReducer(
227
    criteriaReducer,
228
    initialCriteria,
229
  );
230
  const skillCount: number = jobCriteria.length;
231
  const essentialCriteria: Criteria[] = jobCriteria.filter(
232
    (criteria): boolean =>
233
      criteria.criteria_type_id === CriteriaTypeId.Essential,
234
  );
235
  const essentialCount: number = essentialCriteria.length;
236
  const assetCriteria: Criteria[] = jobCriteria.filter(
237
    (criteria): boolean => criteria.criteria_type_id === CriteriaTypeId.Asset,
238
  );
239
  const assetCount: number = assetCriteria.length;
240
241
  // Set this to true to show the Key Tasks modal
242
  const [tasksModalVisible, setTasksModalVisible] = useState(false);
243
244
  // When skillBeingAdded is not null, the modal to add a new skill will appear.
245
  const [skillBeingAdded, setSkillBeingAdded] = useState<Skill | null>(null);
246
247
  // Set to true if submit button is touched
248
  const [submitTouched, setSubmitTouched] = useState(false);
249
250
  // When criteriaBeingEdited is not null, the modal for editing that criterion will appear.
251
  const [
252
    criteriaBeingEdited,
253
    setCriteriaBeingEdited,
254
  ] = useState<Criteria | null>(null);
255
256
  const [isPreviewVisible, setIsPreviewVisible] = useState(false);
257
258
  // This should be true if ANY modal is visible. The modal overlay uses this.
259
  const isModalVisible =
260
    tasksModalVisible ||
261
    skillBeingAdded !== null ||
262
    criteriaBeingEdited !== null ||
263
    isPreviewVisible;
264
  const modalParentRef = useRef<HTMLDivElement>(null);
265
266
  const tasksModalId = "job-builder-review-tasks";
267
  const addModalId = "job-builder-add-skill";
268
  const editModalId = "job-builder-edit-skill";
269
  const previewModalId = "job-builder-preview-skills";
270
271
  const countInRange = (min: number, max: number, count: number): boolean => {
272
    return count >= min && count <= max;
273
  };
274
275
  const sortAlphabetically = (a: Skill, b: Skill): number => {
276
    const skillA: string = localizeFieldNonNull(
277
      locale,
278
      a,
279
      "name",
280
    ).toUpperCase();
281
    const skillB: string = localizeFieldNonNull(
282
      locale,
283
      b,
284
      "name",
285
    ).toUpperCase();
286
287
    return skillA.localeCompare(skillB, locale, { sensitivity: "base" });
288
  };
289
290
  // Map the skills into a dictionary for quicker access
291
  const skillsById = mapToObject(skills, getId);
292
  const getSkillOfCriteria = (criterion: Criteria): Skill | null => {
293
    return hasKey(skillsById, criterion.skill_id)
294
      ? skillsById[criterion.skill_id]
295
      : null;
296
  };
297
298
  const getClassifications = (skill: Skill): string[] =>
299
    skill.classifications.map((classification): string => classification.key);
300
  const isOccupational = (skill: Skill): boolean =>
301
    job.classification_id !== null &&
302
    getClassifications(skill).includes(classificationKey);
303
  const occupationalSkills = skills
304
    .filter(isOccupational)
305
    .sort(sortAlphabetically);
306
  const occupationalCriteria = jobCriteria.filter((criterion): boolean => {
307
    const critSkill = getSkillOfCriteria(criterion);
308
    return critSkill !== null && isOccupational(critSkill);
309
  });
310
311
  const isCulture = (skill: Skill): boolean => skill.is_culture_skill;
312
  const cultureSkills = skills.filter(isCulture).sort(sortAlphabetically);
313
  const cultureCriteria = jobCriteria.filter((criterion): boolean => {
314
    const skill = getSkillOfCriteria(criterion);
315
    return skill !== null && isCulture(skill);
316
  });
317
  const isFuture = (skill: Skill): boolean => skill.is_future_skill;
318
  const futureSkills = skills.filter(isFuture).sort(sortAlphabetically);
319
  const futureCriteria = jobCriteria.filter((criterion): boolean => {
320
    const skill = getSkillOfCriteria(criterion);
321
    return skill !== null && isFuture(skill);
322
  });
323
324
  // Optional skills are those that don't fit into the other three categories
325
  const isOptional = (skill: Skill): boolean =>
326
    !isOccupational(skill) && !isCulture(skill) && !isFuture(skill);
327
  const otherSkills = skills.filter(isOptional).sort(sortAlphabetically);
328
  const selectedSkillIds = jobCriteria
329
    .map(getSkillOfCriteria)
330
    .filter(notEmpty)
331
    .map(getId);
332
  const selectedOtherSkills: Skill[] = otherSkills.filter((skill): boolean =>
333
    selectedSkillIds.includes(skill.id),
334
  );
335
  const unselectedOtherSkills: Skill[] = otherSkills.filter(
336
    (skill): boolean => !selectedSkillIds.includes(skill.id),
337
  );
338
339
  const [isSaving, setIsSaving] = useState(false);
340
341
  const errorMessage = useRef<HTMLAnchorElement>(null); // React.createRef<HTMLAnchorElement>();
342
  const focusOnError = (): void => {
343
    // eslint-disable-next-line no-unused-expressions
344
    errorMessage?.current?.focus();
345
  };
346
347
  const saveAndPreview = (): void => {
348
    setSubmitTouched(true);
349
    if (essentialCount > 0) {
350
      nprogress.start();
351
      setIsSaving(true);
352
      handleSubmit(jobCriteria)
353
        .then((criteria: Criteria[]): void => {
354
          criteriaDispatch({ type: "replace", payload: criteria });
355
          nprogress.done();
356
          setIsPreviewVisible(true);
357
          setIsSaving(false);
358
        })
359
        .catch((): void => {
360
          nprogress.done();
361
          setIsSaving(false);
362
        });
363
    } else {
364
      focusOnError();
365
    }
366
  };
367
  const saveAndReturn = (): void => {
368
    nprogress.start();
369
    setIsSaving(true);
370
    handleSubmit(jobCriteria)
371
      .then((criteria: Criteria[]): void => {
372
        criteriaDispatch({ type: "replace", payload: criteria });
373
        nprogress.done();
374
        handleReturn();
375
        setIsSaving(false);
376
      })
377
      .catch((): void => {
378
        nprogress.done();
379
        setIsSaving(false);
380
      });
381
  };
382
383
  const renderNullCriteriaRow = (): React.ReactElement => (
384
    <div className="jpb-skill-null" data-c-grid="gutter middle">
385
      {/** TODO: add these back in when implementing UP/DOWN buttons again */}
386
      {/* <div data-c-grid-item="base(2of10) tl(1of10)" data-c-align="base(centre)">
387
        <button type="button" data-tc-move-up-trigger>
388
          <i className="fas fa-angle-up" />
389
        </button>
390
        <button type="button" data-tc-move-down-trigger>
391
          <i className="fas fa-angle-down" />
392
        </button>
393
      </div> */}
394
      <div data-c-grid-item="base(6of10) tl(7of10)">
395
        <div data-c-grid="gutter">
396
          <div data-c-grid-item="base(1of1) tl(2of3)">
397
            {/* <span>0</span>
398
            <span
399
              data-c-background="grey(40)"
400
              data-c-font-size="small"
401
              data-c-margin="rl(half)"
402
              data-c-padding="tb(quarter) rl(half)"
403
              data-c-radius="rounded"
404
              data-c-colour="white"
405
            >
406
              <i className="fas fa-briefcase" />
407
            </span> */}
408
            <span>
409
              <FormattedMessage
410
                id="jobBuilder.skills.addSkillBelow"
411
                defaultMessage="Add skills below to proceed."
412
                description="Placeholder skill title / instructions to add skill"
413
              />
414
            </span>
415
          </div>
416
          <div data-c-grid-item="base(1of1) tl(1of3)">
417
            <span
418
              data-c-colour="white"
419
              data-c-background="grey(40)"
420
              data-c-padding="tb(quarter) rl(half)"
421
              data-c-radius="rounded"
422
              data-c-font-size="small"
423
            >
424
              <FormattedMessage
425
                id="jobBuilder.skills.skillLevel"
426
                defaultMessage="Skill Level"
427
                description="Placeholder label"
428
              />
429
            </span>
430
          </div>
431
        </div>
432
      </div>
433
      <div data-c-grid-item="base(3of10)">
434
        <div data-c-grid="gutter">
435
          <div
436
            data-c-grid-item="base(1of1) tl(1of2)"
437
            data-c-align="base(centre)"
438
          >
439
            <button type="button">
440
              <i className="fas fa-edit" />
441
            </button>
442
          </div>
443
          <div
444
            data-c-grid-item="base(1of1) tl(1of2)"
445
            data-c-align="base(centre)"
446
          >
447
            <button type="button">
448
              <i className="fas fa-trash" />
449
            </button>
450
          </div>
451
        </div>
452
      </div>
453
    </div>
454
  );
455
456
  const renderCriteriaRow = (
457
    criterion: Criteria,
458
    index: number,
459
  ): React.ReactElement | null => {
460
    const skill = getSkillOfCriteria(criterion);
461
    if (skill === null) {
462
      return null;
463
    }
464
    return (
465
      <li
466
        key={skill.id}
467
        className={`jpb-skill ${isOccupational(skill) ? "occupational" : ""} ${
468
          isCulture(skill) ? "cultural" : ""
469
        } ${isFuture(skill) ? "future" : ""} ${
470
          isOptional(skill) ? "optional" : ""
471
        }`}
472
        data-tc-up-down-item
473
      >
474
        <div data-c-grid="gutter middle">
475
          {/** Removing the up/down buttons for now */}
476
          {/** TODO: removing the buttons messes with the row height, get Josh's help to fix it */}
477
          {/* <div
478
            data-c-grid-item="base(2of10) tl(1of10)"
479
            data-c-align="base(centre)"
480
          >
481
            <button type="button" data-tc-move-up-trigger>
482
              <i className="fas fa-angle-up" />
483
            </button>
484
            <button type="button" data-tc-move-down-trigger>
485
              <i className="fas fa-angle-down" />
486
            </button>
487
          </div> */}
488
          <div data-c-grid-item="base(6of10) tl(7of10)">
489
            <div data-c-grid="gutter">
490
              <div data-c-grid-item="base(1of1) tl(2of3)">
491
                <span data-c-margin="right(normal)">{index + 1}.</span>
492
                {/* This icon will automatically update based on the class you've specified above, on the jpb-skill. */}
493
                {/* <span
494
                  className="jpb-skill-type"
495
                  data-c-font-size="small"
496
                  data-c-margin="rl(half)"
497
                  data-c-padding="tb(quarter) rl(half)"
498
                  data-c-radius="rounded"
499
                  data-c-colour="white"
500
                  title="This is an occupational skill."
501
                >
502
                  <i className="fas fa-briefcase" />
503
                  <i className="fas fa-coffee" />
504
                  <i className="fas fa-certificate" />
505
                  <i className="fas fa-book" />
506
                </span> */}
507
                {/* The skill name. */}
508
                <span>{localizeField(locale, skill, "name")}</span>
509
              </div>
510
              <div data-c-grid-item="base(1of1) tl(1of3)">
511
                <span
512
                  data-c-radius="pill"
513
                  data-c-padding="tb(quarter) rl(half)"
514
                  data-c-border="all(thin, solid, c1)"
515
                  data-c-colour="c1"
516
                  data-c-font-size="small"
517
                  data-c-display="inline-block"
518
                  data-c-alignment="center"
519
                >
520
                  {intl.formatMessage(getSkillLevelName(criterion, skill))}
521
                </span>
522
              </div>
523
            </div>
524
          </div>
525
          <div data-c-grid-item="base(3of10)">
526
            <div data-c-grid="gutter">
527
              <div
528
                data-c-grid-item="base(1of1) tl(1of2)"
529
                data-c-align="base(centre)"
530
              >
531
                <button
532
                  type="button"
533
                  data-c-colour="c1"
534
                  onClick={(): void => setCriteriaBeingEdited(criterion)}
535
                >
536
                  <i className="fas fa-edit" />
537
                </button>
538
              </div>
539
              <div
540
                data-c-grid-item="base(1of1) tl(1of2)"
541
                data-c-align="base(centre)"
542
              >
543
                <button
544
                  type="button"
545
                  data-c-colour="c1"
546
                  data-c-hover-colour="stop"
547
                  onClick={(): void =>
548
                    criteriaDispatch({
549
                      type: "remove",
550
                      payload: criterion,
551
                    })
552
                  }
553
                >
554
                  <i className="fas fa-trash" />
555
                </button>
556
              </div>
557
            </div>
558
          </div>
559
        </div>
560
      </li>
561
    );
562
  };
563
564
  const renderSkillButton = (skill: Skill): React.ReactElement => {
565
    const alreadySelected = skillAlreadySelected(jobCriteria, skill);
566
    // Open the Add Skill modal, or remove it if its already added
567
    const handleClick = (): void =>
568
      alreadySelected
569
        ? criteriaDispatch({
570
            type: "removeSkill",
571
            payload: { skillId: skill.id },
572
          })
573
        : setSkillBeingAdded(skill);
574
    return (
575
      <li key={skill.id}>
576
        <button
577
          className={`jpb-skill-trigger ${alreadySelected ? "active" : ""}`}
578
          data-c-button="outline(c1)"
579
          data-c-radius="rounded"
580
          data-c-padding="all(half)"
581
          type="button"
582
          onClick={handleClick}
583
        >
584
          <span data-c-padding="right(half)">
585
            <i className="fas fa-plus-circle" />
586
            <i className="fas fa-minus-circle" />
587
          </span>
588
          {localizeField(locale, skill, "name")}
589
        </button>
590
      </li>
591
    );
592
  };
593
594
  const submitButton = (
595
    <button
596
      data-c-button="solid(c2)"
597
      data-c-radius="rounded"
598
      type="button"
599
      disabled={isSaving}
600
      onClick={(): void => saveAndPreview()}
601
    >
602
      <FormattedMessage
603
        id="jobBuilder.skills.button.previewSkills"
604
        defaultMessage="Save &amp; Preview Skills"
605
        description="Label of Button"
606
      />
607
    </button>
608
  );
609
  const smileyArrowBadImg = imageUrl("icon-smiley-arrow-bad.svg");
610
  const smileyArrowMediumImg = imageUrl("icon-smiley-arrow-medium.svg");
611
  const smileyArrowGoodImg = imageUrl("icon-smiley-arrow-good.svg");
612
  const smileyBadImg = imageUrl("icon-smiley-bad.svg");
613
  const smileyBadGreyImg = imageUrl("icon-smiley-bad-grey.svg");
614
  const smileyMediumImg = imageUrl("icon-smiley-medium.svg");
615
  const smileyMediumGreyImg = imageUrl("icon-smiley-medium-grey.svg");
616
  const smileyGoodImg = imageUrl("icon-smiley-good.svg");
617
  const smileyGoodGreyImg = imageUrl("icon-smiley-good-grey.svg");
618
619
  return (
620
    <>
621
      <div
622
        data-c-container="form"
623
        data-c-padding="top(triple) bottom(triple)"
624
        ref={modalParentRef}
625
      >
626
        <h3
627
          data-c-font-size="h3"
628
          data-c-font-weight="bold"
629
          data-c-margin="bottom(double)"
630
        >
631
          <FormattedMessage
632
            id="jobBuilder.skills.title"
633
            defaultMessage="Skills"
634
            description="section title"
635
          />
636
        </h3>
637
        <p data-c-margin="bottom(triple)">
638
          <FormattedMessage
639
            id="jobBuilder.skills.description"
640
            defaultMessage="This is where you'll select the criteria that are required to do this job effectively. Below are two bars that indicate a measurement of your current skill selection."
641
            description="section description under title"
642
          />
643
        </p>
644
        <div
645
          data-c-margin="bottom(triple)"
646
          data-c-align="base(centre) tl(left)"
647
        >
648
          {/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */}
649
          <button
650
            data-c-button="solid(c2)"
651
            data-c-radius="rounded"
652
            type="button"
653
            disabled={tasksModalVisible}
654
            onClick={(): void => setTasksModalVisible(true)}
655
          >
656
            <FormattedMessage
657
              id="jobBuilder.skills.button.keyTasks"
658
              defaultMessage="View Key Tasks"
659
              description="Button label"
660
            />
661
          </button>
662
        </div>
663
664
        {/* Total Skills List */}
665
        <h4
666
          data-c-colour="c2"
667
          data-c-font-size="h4"
668
          data-c-margin="bottom(normal)"
669
        >
670
          <FormattedMessage
671
            id="jobBuilder.skills.listTitle"
672
            defaultMessage="Your Skills List"
673
            description="List section title"
674
          />
675
        </h4>
676
        <div data-c-grid="gutter top">
677
          <div data-c-grid-item="base(1of1) tl(1of2)">
678
            <div
679
              data-c-border="all(thin, solid, black)"
680
              data-c-radius="rounded"
681
              data-c-padding="normal"
682
            >
683
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
684
                <FormattedMessage
685
                  id="jobBuilder.skills.statusSmiley.essentialTitle"
686
                  defaultMessage="Number of Essential Skills"
687
                  description="Title of skill status tracker"
688
                />
689
              </p>
690
              {/* TODO: SmileyStatusIndicator can be extracted as its own component, since its already repeated within this page. */}
691
              {/* This is the new smiley status indicator component. It is reused twice on this page, once to indicate how many ESSENTIAL skills the user has selected, and a second time to indicate the TOTAL number of skills selected. The component functions the same way for both instances, but the ***scale is different***. There's a chance that the labels will be different too, so best to build it with that in mind. You can activate the appropriate smiley by assigning an "active" class to the relevant "jpb-skill-measure-item" element. See the UI in-browser for an example of what this looks like. */}
692
              <div
693
                data-c-grid="gutter"
694
                data-c-align="centre"
695
                data-c-padding="top(normal)"
696
              >
697
                <div
698
                  className={`jpb-skill-measure-item bad ${
699
                    countInRange(0, 1, essentialCount) ? "active" : ""
700
                  }`}
701
                  data-c-grid-item="base(1of5)"
702
                >
703
                  {/* This div appears in each step of the indicator, but we need the number inside the "span" to reflect the number of skills currently selected (within the context of the indicator, i.e. only show the number of essential skills selected in the essential indicator). */}
704
                  <div>
705
                    <img
706
                      src={smileyArrowBadImg}
707
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
708
                    />
709
                    <span
710
                      data-c-font-weight="bold"
711
                      data-c-colour="white"
712
                      data-c-font-size="small"
713
                    >
714
                      {essentialCount}
715
                    </span>
716
                  </div>
717
                  <img
718
                    src={smileyBadImg}
719
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
720
                  />
721
                  <img
722
                    src={smileyBadGreyImg}
723
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
724
                  />
725
                  <p data-c-font-size="small" data-c-font-weight="bold">
726
                    <FormattedMessage
727
                      id="jobBuilder.skills.statusSmiley.essential.tooFew"
728
                      defaultMessage="Too Few"
729
                      description="Description of quantity of skills"
730
                    />
731
                  </p>
732
                  <p data-c-font-size="small">0 - 1</p>
733
                </div>
734
                <div
735
                  className={`jpb-skill-measure-item medium ${
736
                    countInRange(2, 3, essentialCount) ? "active" : ""
737
                  }`}
738
                  data-c-grid-item="base(1of5)"
739
                >
740
                  <div>
741
                    <img
742
                      src={smileyArrowMediumImg}
743
                      alt={intl.formatMessage(altMessages.neutralArrow)}
744
                    />
745
                    <span
746
                      data-c-font-weight="bold"
747
                      data-c-colour="white"
748
                      data-c-font-size="small"
749
                    >
750
                      {essentialCount}
751
                    </span>
752
                  </div>
753
                  <img
754
                    src={smileyMediumImg}
755
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
756
                  />
757
                  <img
758
                    src={smileyMediumGreyImg}
759
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
760
                  />
761
                  <p data-c-font-size="small" data-c-font-weight="bold">
762
                    <FormattedMessage
763
                      id="jobBuilder.skills.statusSmiley.essential.almost"
764
                      defaultMessage="Almost"
765
                      description="Description of quantity of skills"
766
                    />
767
                  </p>
768
                  <p data-c-font-size="small">2 - 3</p>
769
                </div>
770
                <div
771
                  className={`jpb-skill-measure-item good ${
772
                    countInRange(4, 6, essentialCount) ? "active" : ""
773
                  }`}
774
                  data-c-grid-item="base(1of5)"
775
                >
776
                  <div>
777
                    <img
778
                      src={smileyArrowGoodImg}
779
                      alt={intl.formatMessage(altMessages.happyArrow)}
780
                    />
781
                    <span
782
                      data-c-font-weight="bold"
783
                      data-c-colour="white"
784
                      data-c-font-size="small"
785
                    >
786
                      {essentialCount}
787
                    </span>
788
                  </div>
789
                  <img
790
                    src={smileyGoodImg}
791
                    alt={intl.formatMessage(altMessages.happySmiley)}
792
                  />
793
                  <img
794
                    src={smileyGoodGreyImg}
795
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
796
                  />
797
                  <p data-c-font-size="small" data-c-font-weight="bold">
798
                    <FormattedMessage
799
                      id="jobBuilder.skills.statusSmiley.essential.awesome"
800
                      defaultMessage="Awesome"
801
                      description="Description of quantity of skills"
802
                    />
803
                  </p>
804
                  <p data-c-font-size="small">4 - 6</p>
805
                </div>
806
                <div
807
                  className={`jpb-skill-measure-item medium ${
808
                    countInRange(7, 8, essentialCount) ? "active" : ""
809
                  }`}
810
                  data-c-grid-item="base(1of5)"
811
                >
812
                  <div>
813
                    <img
814
                      src={smileyArrowMediumImg}
815
                      alt={intl.formatMessage(altMessages.neutralArrow)}
816
                    />
817
                    <span
818
                      data-c-font-weight="bold"
819
                      data-c-colour="white"
820
                      data-c-font-size="small"
821
                    >
822
                      {essentialCount}
823
                    </span>
824
                  </div>
825
                  <img
826
                    src={smileyMediumImg}
827
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
828
                  />
829
                  <img
830
                    src={smileyMediumGreyImg}
831
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
832
                  />
833
                  <p data-c-font-size="small" data-c-font-weight="bold">
834
                    <FormattedMessage
835
                      id="jobBuilder.skills.statusSmiley.essential.acceptable"
836
                      defaultMessage="Acceptable"
837
                      description="Description of quantity of skills"
838
                    />
839
                  </p>
840
                  <p data-c-font-size="small">7 - 8</p>
841
                </div>
842
                <div
843
                  className={`jpb-skill-measure-item bad ${
844
                    essentialCount >= 9 ? "active" : ""
845
                  }`}
846
                  data-c-grid-item="base(1of5)"
847
                >
848
                  <div>
849
                    {/* TODO: Alt Text Translations */}
850
                    <img
851
                      src={smileyArrowBadImg}
852
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
853
                    />
854
                    <span
855
                      data-c-font-weight="bold"
856
                      data-c-colour="white"
857
                      data-c-font-size="small"
858
                    >
859
                      {essentialCount}
860
                    </span>
861
                  </div>
862
                  <img
863
                    src={smileyBadImg}
864
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
865
                  />
866
                  <img
867
                    src={smileyBadGreyImg}
868
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
869
                  />
870
                  <p data-c-font-size="small" data-c-font-weight="bold">
871
                    <FormattedMessage
872
                      id="jobBuilder.skills.statusSmiley.essential.tooMany"
873
                      defaultMessage="Too Many"
874
                      description="Description of quantity of skills"
875
                    />
876
                  </p>
877
                  <p data-c-font-size="small">9 +</p>
878
                </div>
879
              </div>
880
            </div>
881
          </div>
882
          <div data-c-grid-item="base(1of1) tl(1of2)">
883
            <div
884
              data-c-border="all(thin, solid, black)"
885
              data-c-radius="rounded"
886
              data-c-padding="normal"
887
            >
888
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
889
                <FormattedMessage
890
                  id="jobBuilder.skills.statusSmiley.title"
891
                  defaultMessage="Total Number of Skills"
892
                  description="Title of skill quantity evaluator"
893
                />
894
              </p>
895
              {/* This is the second smiley indicator, used for total skills. Note the difference in the scale from the first. */}
896
              <div
897
                data-c-grid="gutter"
898
                data-c-align="centre"
899
                data-c-padding="top(normal)"
900
              >
901
                <div
902
                  className={`jpb-skill-measure-item bad ${
903
                    countInRange(0, 3, skillCount) ? "active" : ""
904
                  }`}
905
                  data-c-grid-item="base(1of5)"
906
                >
907
                  <div>
908
                    <img
909
                      src={smileyArrowBadImg}
910
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
911
                    />
912
                    <span
913
                      data-c-font-weight="bold"
914
                      data-c-colour="white"
915
                      data-c-font-size="small"
916
                    >
917
                      {skillCount}
918
                    </span>
919
                  </div>
920
                  <img
921
                    src={smileyBadImg}
922
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
923
                  />
924
                  <img
925
                    src={smileyBadGreyImg}
926
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
927
                  />
928
                  <p data-c-font-size="small" data-c-font-weight="bold">
929
                    <FormattedMessage
930
                      id="jobBuilder.skills.statusSmiley.tooFew"
931
                      defaultMessage="Too Few"
932
                      description="Description of quantity of skills"
933
                    />
934
                  </p>
935
                  <p data-c-font-size="small">0 - 3</p>
936
                </div>
937
                <div
938
                  className={`jpb-skill-measure-item medium ${
939
                    countInRange(4, 6, skillCount) ? "active" : ""
940
                  }`}
941
                  data-c-grid-item="base(1of5)"
942
                >
943
                  <div>
944
                    <img
945
                      src={smileyArrowMediumImg}
946
                      alt={intl.formatMessage(altMessages.neutralArrow)}
947
                    />
948
                    <span
949
                      data-c-font-weight="bold"
950
                      data-c-colour="white"
951
                      data-c-font-size="small"
952
                    >
953
                      {skillCount}
954
                    </span>
955
                  </div>
956
                  <img
957
                    src={smileyMediumImg}
958
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
959
                  />
960
                  <img
961
                    src={smileyMediumGreyImg}
962
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
963
                  />
964
                  <p data-c-font-size="small" data-c-font-weight="bold">
965
                    <FormattedMessage
966
                      id="jobBuilder.skills.statusSmiley.almost"
967
                      defaultMessage="Almost"
968
                      description="Description of quantity of skills"
969
                    />
970
                  </p>
971
                  <p data-c-font-size="small">4 - 6</p>
972
                </div>
973
                <div
974
                  className={`jpb-skill-measure-item good ${
975
                    countInRange(7, 8, skillCount) ? "active" : ""
976
                  }`}
977
                  data-c-grid-item="base(1of5)"
978
                >
979
                  <div>
980
                    <img
981
                      src={smileyArrowGoodImg}
982
                      alt={intl.formatMessage(altMessages.happyArrow)}
983
                    />
984
                    <span
985
                      data-c-font-weight="bold"
986
                      data-c-colour="white"
987
                      data-c-font-size="small"
988
                    >
989
                      {skillCount}
990
                    </span>
991
                  </div>
992
                  <img
993
                    src={smileyGoodImg}
994
                    alt={intl.formatMessage(altMessages.happySmiley)}
995
                  />
996
                  <img
997
                    src={smileyGoodGreyImg}
998
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
999
                  />
1000
                  <p data-c-font-size="small" data-c-font-weight="bold">
1001
                    <FormattedMessage
1002
                      id="jobBuilder.skills.statusSmiley.awesome"
1003
                      defaultMessage="Awesome"
1004
                      description="Description of quantity of skills"
1005
                    />
1006
                  </p>
1007
                  <p data-c-font-size="small">7 - 8</p>
1008
                </div>
1009
                <div
1010
                  className={`jpb-skill-measure-item medium  ${
1011
                    countInRange(9, 10, skillCount) ? "active" : ""
1012
                  }`}
1013
                  data-c-grid-item="base(1of5)"
1014
                >
1015
                  <div>
1016
                    <img
1017
                      src={smileyArrowMediumImg}
1018
                      alt={intl.formatMessage(altMessages.neutralArrow)}
1019
                    />
1020
                    <span
1021
                      data-c-font-weight="bold"
1022
                      data-c-colour="white"
1023
                      data-c-font-size="small"
1024
                    >
1025
                      {skillCount}
1026
                    </span>
1027
                  </div>
1028
                  <img
1029
                    src={smileyMediumImg}
1030
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
1031
                  />
1032
                  <img
1033
                    src={smileyMediumGreyImg}
1034
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
1035
                  />
1036
                  <p data-c-font-size="small" data-c-font-weight="bold">
1037
                    <FormattedMessage
1038
                      id="jobBuilder.skills.statusSmiley.acceptable"
1039
                      defaultMessage="Acceptable"
1040
                      description="Description of quantity of skills"
1041
                    />
1042
                  </p>
1043
                  <p data-c-font-size="small">9 - 10</p>
1044
                </div>
1045
                <div
1046
                  className={`jpb-skill-measure-item bad ${
1047
                    skillCount >= 11 ? "active" : ""
1048
                  }`}
1049
                  data-c-grid-item="base(1of5)"
1050
                >
1051
                  <div>
1052
                    <img
1053
                      src={smileyArrowBadImg}
1054
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
1055
                    />
1056
                    <span
1057
                      data-c-font-weight="bold"
1058
                      data-c-colour="white"
1059
                      data-c-font-size="small"
1060
                    >
1061
                      {skillCount}
1062
                    </span>
1063
                  </div>
1064
                  <img
1065
                    src={smileyBadImg}
1066
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
1067
                  />
1068
                  <img
1069
                    src={smileyBadGreyImg}
1070
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
1071
                  />
1072
                  <p data-c-font-size="small" data-c-font-weight="bold">
1073
                    <FormattedMessage
1074
                      id="jobBuilder.skills.statusSmiley.tooMany"
1075
                      defaultMessage="Too Many"
1076
                      description="Description of quantity of skills"
1077
                    />
1078
                  </p>
1079
                  <p data-c-font-size="small">11 +</p>
1080
                </div>
1081
              </div>
1082
            </div>
1083
          </div>
1084
        </div>
1085
        {/* This element is the skills list/management area for the user. From here they can see the skills they've added, modify the order, see the type (occupational [based on classification], cultural, future), see the level they've selected (only if the skill isn't an asset skill), edit the skill, and remove it. */}
1086
        <div
1087
          data-c-background="grey(10)"
1088
          data-c-radius="rounded"
1089
          data-c-padding="all(normal)"
1090
          data-c-margin="top(normal) bottom(normal)"
1091
        >
1092
          <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
1093
            <FormattedMessage
1094
              id="jobBuilder.skills.title.essentialSkills"
1095
              defaultMessage="Essential Skills"
1096
              description="Title of essential skills list"
1097
            />
1098
          </p>
1099
          {/* This is the null state to be used when the user lands on the page for the first time. Be sure to include it in the assets list too! Note that it exists outside the skill-list div to avoid it being confused with the list of skills. */}
1100
          {/* Null state. */}
1101
          {essentialCount === 0 && renderNullCriteriaRow()}
1102
          <ol className="jpb-skill-list" data-tc-up-down-list>
1103
            {/* This is an individual skill. I've handled the up/down script and the modal trigger, but I'll leave managing the value of the skill's list number, the modal contents,  and the deletion to you folks. I've also migrated the up/down script to a universal one. When it comes to the "jpb-skill", you'll need to add a class that specifies which TYPE of skill it is (occupational, cultural, future). This will handle interior colour/icon changes. */}
1104
            {essentialCriteria.map(renderCriteriaRow)}
1105
          </ol>
1106
          {/* Repeat what you have above for asset skills. The biggest thing to note here is that the level should be empty in this list, and when the user changes the level of an essential skill to asset, it should be moved down into this list (and vice versa). */}
1107
          <p
1108
            data-c-font-weight="bold"
1109
            data-c-margin="top(normal) bottom(normal)"
1110
          >
1111
            <FormattedMessage
1112
              id="jobBuilder.skills.title.assetSkills"
1113
              defaultMessage="Asset Skills"
1114
              description="Title of asset skills list"
1115
            />
1116
          </p>
1117
          {/* Asset null state goes here. */}
1118
          {assetCount === 0 && renderNullCriteriaRow()}
1119
          <ol className="jpb-skill-list" data-tc-up-down-list>
1120
            {assetCriteria.map(renderCriteriaRow)}
1121
          </ol>
1122
        </div>
1123
        <div
1124
          data-c-margin="bottom(triple)"
1125
          data-c-align="base(centre) tl(right)"
1126
        >
1127
          {/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */}
1128
          {submitButton}
1129
        </div>
1130
        {/* The 3 sections below are each functionally similar and can probably be united into one component. The biggest difference between the three is that "Cultural Skills" has a categorical breakdown between "Recommended Skills" and the rest of the category. These recommendations are based directly on the way the manager answered their work environment questions, but I'm not sure how the logic works, so you'll want to check in with Lauren/Jasmita on this. */}
1131
        <h4
1132
          data-c-colour="c2"
1133
          data-c-font-size="h4"
1134
          data-c-margin="bottom(normal)"
1135
        >
1136
          <FormattedMessage
1137
            id="jobBuilder.skills.title.skillSelection"
1138
            defaultMessage="Skill Selection"
1139
            description="Title of skill selection section"
1140
          />
1141
        </h4>
1142
        {/* Occupational Skills */}
1143
        {/* You can modify colour/icon using the category classes here again (occupational, cultural, future) on the "jpb-skill-category" element. */}
1144
        <div
1145
          id="jpb-occupational-skills"
1146
          className="jpb-skill-category occupational"
1147
          data-c-margin="bottom(normal)"
1148
          data-c-padding="normal"
1149
          data-c-radius="rounded"
1150
          data-c-background="grey(10)"
1151
        >
1152
          <div data-c-grid="gutter top">
1153
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1154
              <h5
1155
                className="jpb-skill-section-title"
1156
                data-c-font-size="h4"
1157
                data-c-margin="bottom(normal)"
1158
              >
1159
                {/* These icons will change automatically based on the class specified above. */}
1160
                <span
1161
                  data-c-font-size="small"
1162
                  data-c-margin="right(half)"
1163
                  data-c-padding="tb(quarter) rl(half)"
1164
                  data-c-radius="rounded"
1165
                  data-c-colour="white"
1166
                >
1167
                  <i className="fas fa-briefcase" />
1168
                  <i className="fas fa-coffee" />
1169
                  <i className="fas fa-certificate" />
1170
                  <i className="fas fa-book" />
1171
                </span>
1172
                {/* Category Title */}
1173
                <FormattedMessage
1174
                  id="jobBuilder.skills.title.occupationalSkills"
1175
                  defaultMessage="Occupational Competencies"
1176
                  description="Title of skills category"
1177
                />
1178
              </h5>
1179
              {/* Category description - basically this outlines what the category means. */}
1180
              {/* <p>
1181
                // TODO: Add this message back in once we have copy.
1182
                <FormattedMessage
1183
                  id="jobBuilder.skills.description.occupationalSkills"
1184
                  defaultMessage=""
1185
                  description="Description of a category of skills"
1186
                />
1187
              </p> */}
1188
            </div>
1189
            <div
1190
              data-c-grid-item="tp(1of3) ds(1of4)"
1191
              data-c-align="base(centre) tp(right)"
1192
            >
1193
              {/* This target value changes depending on the category (occupational has 3 - 4, cultural and future have fewer) - you can see these values in their respective sections below. You can also add a "complete" class to this "jpb-skill-target" element to change the target icon to a checkmark to indicate to the user that they're within the range. Note that the other two categories (cultural and future) start their ranges at 0, so the "complete" class should be on those sections by default. */}
1194
              <div
1195
                className={`jpb-skill-target ${
1196
                  countInRange(
1197
                    minOccupational,
1198
                    maxOccupational,
1199
                    occupationalCriteria.length,
1200
                  )
1201
                    ? "complete"
1202
                    : ""
1203
                }`}
1204
              >
1205
                <i data-c-colour="stop" className="fas fa-bullseye" />
1206
                <i data-c-colour="go" className="fas fa-check" />
1207
                <span>
1208
                  <FormattedMessage
1209
                    id="jobBuilder.skills.range.occupationalSkills"
1210
                    defaultMessage="Aim for {minOccupational} - {maxOccupational} skills."
1211
                    description="Ranage recommendation for occupational competencies in job poster"
1212
                    values={{ minOccupational, maxOccupational }}
1213
                  />
1214
                </span>
1215
              </div>
1216
            </div>
1217
            {/* This is the list of skills. Clicking a skill button should trigger the "Edit skill" modal so that the user can edit the definition/level before adding it. If they DO add it, you can assign an "active" class to the respective button so indicate that it's selected. This will change it's colour and icon automatically. This is also the area where "Culture Skills" is split into the two categories - see the Culture Skills section below for what that looks like. */}
1218
            {(job.classification_id === undefined ||
1219
              job.classification_id === null) && (
1220
              <p data-c-font-weight="bold" data-c-grid-item="base(1of1)">
1221
                <FormattedMessage
1222
                  id="jobBuilder.skills.nullText.occupationalSkills"
1223
                  defaultMessage="You must return to Step 1 and choose a Classification."
1224
                  description="Placeholder text for occupational competencies list."
1225
                />
1226
              </p>
1227
            )}
1228
1229
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1230
              {occupationalSkills.map(renderSkillButton)}
1231
            </ul>
1232
          </div>
1233
        </div>
1234
        {/* Cultural Skills */}
1235
        {/* This section is here so that you can see the categorical division of culture skills. */}
1236
        <div
1237
          className="jpb-skill-category cultural"
1238
          data-c-margin="bottom(normal)"
1239
          data-c-padding="normal"
1240
          data-c-radius="rounded"
1241
          data-c-background="grey(10)"
1242
        >
1243
          <div data-c-grid="gutter top">
1244
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1245
              <h5
1246
                className="jpb-skill-section-title"
1247
                data-c-font-size="h4"
1248
                data-c-margin="bottom(normal)"
1249
              >
1250
                <span
1251
                  data-c-font-size="small"
1252
                  data-c-margin="right(half)"
1253
                  data-c-padding="tb(quarter) rl(half)"
1254
                  data-c-radius="rounded"
1255
                  data-c-colour="white"
1256
                >
1257
                  <i className="fas fa-briefcase" />
1258
                  <i className="fas fa-coffee" />
1259
                  <i className="fas fa-certificate" />
1260
                  <i className="fas fa-book" />
1261
                </span>
1262
                <FormattedMessage
1263
                  id="jobBuilder.skills.title.culturalSkills"
1264
                  defaultMessage="Behavioural Competencies"
1265
                  description="Title of skills category"
1266
                />
1267
              </h5>
1268
              {/* <p>
1269
              // TODO: Add this message back in once we have copy.
1270
                <FormattedMessage
1271
                  id="jobBuilder.skills.description.culturalSkills"
1272
                  defaultMessage=""
1273
                  description="Description of a category of skills"
1274
                />
1275
              </p> */}
1276
            </div>
1277
            <div
1278
              data-c-grid-item="tp(1of3) ds(1of4)"
1279
              data-c-align="base(centre) tp(right)"
1280
            >
1281
              <div
1282
                className={`jpb-skill-target ${
1283
                  countInRange(minCulture, maxCulture, cultureCriteria.length)
1284
                    ? "complete"
1285
                    : ""
1286
                }`}
1287
              >
1288
                <i data-c-colour="stop" className="fas fa-bullseye" />
1289
                <i data-c-colour="go" className="fas fa-check" />
1290
                <span>
1291
                  <FormattedMessage
1292
                    id="jobBuilder.skills.range.culturalSkills"
1293
                    defaultMessage="Aim for {minCulture} - {maxCulture} skills."
1294
                    description="Range recommendation for behavioural competencies in job poster"
1295
                    values={{ minCulture, maxCulture }}
1296
                  />
1297
                </span>
1298
              </div>
1299
            </div>
1300
            {/** Culture skills are intended to be split into two lists, Recommended and Remaining. Until the recommendation logic is nailed down, its just one. */}
1301
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1302
              {cultureSkills.map(renderSkillButton)}
1303
            </ul>
1304
            {/* So here's where culture skills get broken into categories. In theory this logic will be used down the road to break occupational skills into occupations (e.g. CS - UX Designer), but for now this the only instance where it happens. */}
1305
            {/* <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1306
              // Note that this "p" tag has a different margin value than the one in the "ul" below.
1307
              <p
1308
                data-c-font-weight="bold"
1309
                data-c-margin="top(half) bottom(normal)"
1310
              >
1311
                Recommended Skills:
1312
              </p>
1313
              // This is where the skill recommendations from Work Environment go.
1314
              <li>
1315
                <button
1316
                  className="jpb-skill-trigger"
1317
                  data-c-button="outline(c1)"
1318
                  data-c-radius="rounded"
1319
                >
1320
                  <i className="fas fa-plus-circle" />
1321
                  <i className="fas fa-minus-circle" />
1322
                  Skill Name
1323
                </button>
1324
              </li>
1325
            </ul>
1326
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1327
              <p
1328
                data-c-font-weight="bold"
1329
                data-c-margin="top(normal) bottom(normal)"
1330
              >
1331
                Remaining Skills:
1332
              </p>
1333
              // This is where the remaining culture skills go. Please make sure that the skills in the recommendation list above do not appear here.
1334
              <li>
1335
                <button
1336
                  className="jpb-skill-trigger"
1337
                  data-c-button="outline(c1)"
1338
                  data-c-radius="rounded"
1339
                >
1340
                  <i className="fas fa-plus-circle" />
1341
                  <i className="fas fa-minus-circle" />
1342
                  Skill Name
1343
                </button>
1344
              </li>
1345
            </ul> */}
1346
          </div>
1347
        </div>
1348
        {/* Future Skills */}
1349
        {/* This section is just here so you can see what it looks like with the future class. */}
1350
        <div
1351
          className="jpb-skill-category future"
1352
          data-c-margin="bottom(normal)"
1353
          data-c-padding="normal"
1354
          data-c-radius="rounded"
1355
          data-c-background="grey(10)"
1356
        >
1357
          <div data-c-grid="gutter top">
1358
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1359
              <h5
1360
                className="jpb-skill-section-title"
1361
                data-c-font-size="h4"
1362
                data-c-margin="bottom(normal)"
1363
              >
1364
                <span
1365
                  data-c-font-size="small"
1366
                  data-c-margin="right(half)"
1367
                  data-c-padding="tb(quarter) rl(half)"
1368
                  data-c-radius="rounded"
1369
                  data-c-colour="white"
1370
                >
1371
                  <i className="fas fa-briefcase" />
1372
                  <i className="fas fa-coffee" />
1373
                  <i className="fas fa-certificate" />
1374
                  <i className="fas fa-book" />
1375
                </span>
1376
                <FormattedMessage
1377
                  id="jobBuilder.skills.title.futureSkills"
1378
                  defaultMessage="Public Service Competencies"
1379
                  description="Title of skills category"
1380
                />
1381
              </h5>
1382
              {/* <p>
1383
              // TODO: Add this message back in once we have copy.
1384
                <FormattedMessage
1385
                  id="jobBuilder.skills.description.futureSkills"
1386
                  defaultMessage=""
1387
                  description="Description of a category of skills"
1388
                />
1389
              </p> */}
1390
            </div>
1391
            <div
1392
              data-c-grid-item="tp(1of3) ds(1of4)"
1393
              data-c-align="base(centre) tp(right)"
1394
            >
1395
              <div
1396
                className={`jpb-skill-target ${
1397
                  countInRange(minFuture, maxFuture, futureCriteria.length)
1398
                    ? "complete"
1399
                    : ""
1400
                }`}
1401
              >
1402
                <i data-c-colour="stop" className="fas fa-bullseye" />
1403
                <i data-c-colour="go" className="fas fa-check" />
1404
                <span>
1405
                  <FormattedMessage
1406
                    id="jobBuilder.skills.range.futureSkills"
1407
                    defaultMessage="Aim for {minFuture} - {maxFuture} skills."
1408
                    description="Ranage recommendation for public service competencies in job poster"
1409
                    values={{ minFuture, maxFuture }}
1410
                  />
1411
                </span>
1412
              </div>
1413
            </div>
1414
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1415
              {futureSkills.map(renderSkillButton)}
1416
            </ul>
1417
          </div>
1418
        </div>
1419
        {/* This section is basically just text, but it prompts the manager to get in touch with us if they can't find the skill they're looking for. */}
1420
        {/* "Custom" Skills */}
1421
        <h5 data-c-font-weight="bold" data-c-margin="top(double) bottom(half)">
1422
          <FormattedMessage
1423
            id="jobBuilder.skills.title.missingSkill"
1424
            defaultMessage="Can't find the skill you need?"
1425
            description="Title of instructions for missing skill"
1426
          />
1427
        </h5>
1428
        <p data-c-margin="bottom(normal)">
1429
          {/* TODO: Refactor for react-intl version 3, using new rich text xml tag syntax eg.
1430
          <FormattedMessage
1431
            id="jobBuilder.skills.instructions.missingSkills"
1432
            defaultMessage="Building a skills list is a huge endeavour, and it's not
1433
  surprising that Talent Cloud's list doesn't have the skill
1434
  you're looking for. To help us expand our skill list, please <link>get in touch with us through email</link>. Provide the skill's name, as well as a short description to
1435
  kick-off the discussion."
1436
            values={{
1437
              link: msg => (
1438
                <a href="mailto:[email protected]">
1439
                  {msg}
1440
                </a>
1441
              ),
1442
            }}
1443
          /> */}
1444
          <FormattedMessage
1445
            id="jobBuilder.skills.instructions.missingSkills"
1446
            defaultMessage="Building a skills list is a huge endeavour, and it's not surprising that Talent Cloud's list doesn't have the skill you're looking for. To help us expand our skill list, please {link}. Provide the skill's name, as well as a short description to kick-off the discussion."
1447
            values={{
1448
              link: (
1449
                <a href="mailto:[email protected]">
1450
                  {intl.formatMessage(messages.emailUs)}
1451
                </a>
1452
              ),
1453
            }}
1454
          />
1455
        </p>
1456
        <div
1457
          className="jpb-skill-category optional"
1458
          data-c-margin="bottom(normal)"
1459
          data-c-padding="normal"
1460
          data-c-radius="rounded"
1461
          data-c-background="grey(10)"
1462
        >
1463
          <div data-c-grid="gutter top">
1464
            <div data-c-grid-item="base(1of1)">
1465
              {/** TODO: Fix the layout of the skill cloud */}
1466
              <h5 className="jpb-skill-section-title" data-c-font-size="h4">
1467
                <span
1468
                  data-c-font-size="small"
1469
                  data-c-margin="right(half)"
1470
                  data-c-padding="tb(quarter) rl(half)"
1471
                  data-c-radius="rounded"
1472
                  data-c-colour="white"
1473
                >
1474
                  <i className="fas fa-briefcase" />
1475
                  <i className="fas fa-coffee" />
1476
                  <i className="fas fa-certificate" />
1477
                  <i className="fas fa-book" />
1478
                </span>
1479
                <FormattedMessage
1480
                  id="jobBuilder.skills.title.otherSkills"
1481
                  defaultMessage="Other Skills"
1482
                  description="Title of other skills section"
1483
                />
1484
              </h5>
1485
            </div>
1486
            <div data-c-grid-item="base(1of1)">
1487
              <Select
1488
                id="jpb-all-skills-select"
1489
                name="jpbAllSkillsSelect"
1490
                label={intl.formatMessage(messages.selectSkillLabel)}
1491
                selected={null}
1492
                nullSelection={intl.formatMessage(messages.selectSkillNull)}
1493
                options={unselectedOtherSkills.map(
1494
                  (skill): SelectOption => ({
1495
                    value: skill.id,
1496
                    label: localizeFieldNonNull(locale, skill, "name"),
1497
                  }),
1498
                )}
1499
                onChange={(event): void => {
1500
                  const skillId = Number(event.target.value);
1501
                  if (hasKey(skillsById, skillId)) {
1502
                    const skill = skillsById[skillId];
1503
                    setSkillBeingAdded(skill);
1504
                  }
1505
                }}
1506
              />
1507
            </div>
1508
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1509
              {/** TODO: Get this null state text hiding/showing. */}
1510
              {selectedOtherSkills.length === 0 && (
1511
                <p>
1512
                  <FormattedMessage
1513
                    id="jobBuilder.skills.placeholder.otherSkills"
1514
                    defaultMessage="There are no extra skills added."
1515
                    description="Placeholder when there are no other skills"
1516
                  />
1517
                </p>
1518
              )}
1519
              {selectedOtherSkills.map(renderSkillButton)}
1520
            </ul>
1521
          </div>
1522
        </div>
1523
        <div data-c-grid="gutter">
1524
          <div data-c-grid-item="base(1of1)">
1525
            <hr data-c-margin="top(normal) bottom(normal)" />
1526
          </div>
1527
          <div
1528
            data-c-alignment="base(centre) tp(left)"
1529
            data-c-grid-item="tp(1of2)"
1530
          >
1531
            <button
1532
              data-c-button="outline(c2)"
1533
              data-c-radius="rounded"
1534
              type="button"
1535
              disabled={isSaving}
1536
              onClick={(): void => saveAndReturn()}
1537
            >
1538
              <FormattedMessage
1539
                id="jobBuilder.skills.button.returnToTasks"
1540
                defaultMessage="Save &amp; Return to Tasks"
1541
                description="Button Label"
1542
              />
1543
            </button>
1544
          </div>
1545
          <div
1546
            data-c-alignment="base(centre) tp(right)"
1547
            data-c-grid-item="tp(1of2)"
1548
          >
1549
            {/* Modal trigger, same as last step. */}
1550
            {submitButton}
1551
1552
            <div
1553
              role="alert"
1554
              data-c-alert="error"
1555
              data-c-radius="rounded"
1556
              data-c-margin="top(normal)"
1557
              data-c-padding="all(half)"
1558
              data-c-visibility={
1559
                essentialCount === 0 && submitTouched ? "visible" : "invisible"
1560
              }
1561
              style={{
1562
                display: `inline-block`,
1563
              }}
1564
            >
1565
              <a
1566
                href="#jpb-occupational-skills"
1567
                tabIndex={0}
1568
                ref={errorMessage}
1569
              >
1570
                <FormattedMessage
1571
                  id="jobBuilder.skills.essentialSkillRequiredError"
1572
                  defaultMessage="At least one 'Essential Skill' is required."
1573
                  description="Label of Button"
1574
                />
1575
              </a>
1576
            </div>
1577
          </div>
1578
        </div>
1579
      </div>
1580
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1581
      {/** This modal simply displays key tasks. */}
1582
      <Modal
1583
        id={tasksModalId}
1584
        parentElement={modalParentRef.current}
1585
        visible={tasksModalVisible}
1586
        onModalCancel={(): void => setTasksModalVisible(false)}
1587
        onModalConfirm={(): void => setTasksModalVisible(false)}
1588
      >
1589
        <Modal.Header>
1590
          <div
1591
            data-c-background="c1(100)"
1592
            data-c-border="bottom(thin, solid, black)"
1593
            data-c-padding="normal"
1594
          >
1595
            <h5
1596
              data-c-colour="white"
1597
              data-c-font-size="h4"
1598
              id={`${tasksModalId}-title`}
1599
            >
1600
              <FormattedMessage
1601
                id="jobBuilder.skills.title.keyTasks"
1602
                defaultMessage="Key Tasks"
1603
                description="Title of Key Tasks Section"
1604
              />
1605
            </h5>
1606
          </div>
1607
        </Modal.Header>
1608
        <Modal.Body>
1609
          <div data-c-border="bottom(thin, solid, black)">
1610
            <div
1611
              data-c-border="bottom(thin, solid, black)"
1612
              data-c-padding="normal"
1613
              id={`${tasksModalId}-description`}
1614
            >
1615
              <ul>
1616
                {keyTasks.map(
1617
                  (task): React.ReactElement => (
1618
                    <li key={task.id}>
1619
                      {localizeField(locale, task, "description")}
1620
                    </li>
1621
                  ),
1622
                )}
1623
              </ul>
1624
            </div>
1625
          </div>
1626
        </Modal.Body>
1627
        <Modal.Footer>
1628
          <Modal.FooterCancelBtn>
1629
            <FormattedMessage
1630
              id="jobBuilder.skills.tasksModalCancelLabel"
1631
              defaultMessage="Back to Skills"
1632
              description="The text displayed on the cancel button of the Key Tasks modal on the Job Builder Skills step."
1633
            />
1634
          </Modal.FooterCancelBtn>
1635
        </Modal.Footer>
1636
      </Modal>
1637
      {/** This modal is for adding brand new skills */}
1638
      <Modal
1639
        id={addModalId}
1640
        parentElement={modalParentRef.current}
1641
        visible={skillBeingAdded !== null}
1642
        onModalCancel={(): void => {
1643
          setSkillBeingAdded(null);
1644
        }}
1645
        onModalConfirm={(): void => {
1646
          setSkillBeingAdded(null);
1647
        }}
1648
      >
1649
        <Modal.Header>
1650
          <div
1651
            data-c-background="c1(100)"
1652
            data-c-border="bottom(thin, solid, black)"
1653
            data-c-padding="normal"
1654
          >
1655
            <h5
1656
              data-c-colour="white"
1657
              data-c-font-size="h4"
1658
              id={`${addModalId}-title`}
1659
            >
1660
              <FormattedMessage
1661
                id="jobBuilder.skills.title.addASkill"
1662
                defaultMessage="Add a skill"
1663
                description="Title of Add a skill Section"
1664
              />
1665
            </h5>
1666
          </div>
1667
        </Modal.Header>
1668
        <Modal.Body>
1669
          {skillBeingAdded !== null && (
1670
            <CriteriaForm
1671
              jobPosterId={job.id}
1672
              skill={skillBeingAdded}
1673
              handleCancel={(): void => {
1674
                setSkillBeingAdded(null);
1675
              }}
1676
              handleSubmit={(criteria: Criteria): void => {
1677
                criteriaDispatch({ type: "add", payload: criteria });
1678
                setSkillBeingAdded(null);
1679
              }}
1680
            />
1681
          )}
1682
        </Modal.Body>
1683
      </Modal>
1684
      {/** This modal is for editing already added skills */}
1685
      <Modal
1686
        id={editModalId}
1687
        parentElement={modalParentRef.current}
1688
        visible={criteriaBeingEdited !== null}
1689
        onModalCancel={(): void => {
1690
          setCriteriaBeingEdited(null);
1691
        }}
1692
        onModalConfirm={(): void => {
1693
          setCriteriaBeingEdited(null);
1694
        }}
1695
      >
1696
        <Modal.Header>
1697
          <div
1698
            data-c-background="c1(100)"
1699
            data-c-border="bottom(thin, solid, black)"
1700
            data-c-padding="normal"
1701
          >
1702
            <h5
1703
              data-c-colour="white"
1704
              data-c-font-size="h4"
1705
              id={`${editModalId}-title`}
1706
            >
1707
              <FormattedMessage
1708
                id="jobBuilder.skills.title.editSkill"
1709
                defaultMessage="Edit skill"
1710
                description="Title of Edit skill Modal"
1711
              />
1712
            </h5>
1713
          </div>
1714
        </Modal.Header>
1715
        <Modal.Body>
1716
          {criteriaBeingEdited !== null &&
1717
            getSkillOfCriteria(criteriaBeingEdited) !== null && (
1718
              <CriteriaForm
1719
                jobPosterId={job.id}
1720
                criteria={criteriaBeingEdited}
1721
                skill={getSkillOfCriteria(criteriaBeingEdited) as Skill} // The cast is okay here (but still not ideal) because of the !== null check a few lines up
1722
                handleCancel={(): void => {
1723
                  setCriteriaBeingEdited(null);
1724
                }}
1725
                handleSubmit={(criteria: Criteria): void => {
1726
                  criteriaDispatch({ type: "edit", payload: criteria });
1727
                  setCriteriaBeingEdited(null);
1728
                }}
1729
              />
1730
            )}
1731
        </Modal.Body>
1732
      </Modal>
1733
      {/** This modal is the preview */}
1734
      <Modal
1735
        id={previewModalId}
1736
        parentElement={modalParentRef.current}
1737
        visible={isPreviewVisible}
1738
        onModalCancel={(): void => setIsPreviewVisible(false)}
1739
        onModalConfirm={(): void => handleContinue()}
1740
        onModalMiddle={(): void => {
1741
          handleSkipToReview();
1742
        }}
1743
      >
1744
        <Modal.Header>
1745
          <div
1746
            data-c-background="c1(100)"
1747
            data-c-border="bottom(thin, solid, black)"
1748
            data-c-padding="normal"
1749
          >
1750
            <h5
1751
              data-c-colour="white"
1752
              data-c-font-size="h4"
1753
              id={`${previewModalId}-title`}
1754
            >
1755
              <FormattedMessage
1756
                id="jobBuilder.skills.title.keepItUp"
1757
                defaultMessage="Keep it up!"
1758
                description="Title of Keep it up! Modal"
1759
              />
1760
            </h5>
1761
          </div>
1762
        </Modal.Header>
1763
        <Modal.Body>
1764
          <div data-c-border="bottom(thin, solid, black)">
1765
            <div
1766
              data-c-border="bottom(thin, solid, black)"
1767
              data-c-padding="normal"
1768
              id={`${previewModalId}-description`}
1769
            >
1770
              <p>
1771
                <FormattedMessage
1772
                  id="jobBuilder.skills.description.keepItUp"
1773
                  defaultMessage="Here's a preview of the Skills you just entered. Feel free to go back and edit things or move to the next step if you're happy with it."
1774
                  description="Body text of Keep it up! Modal"
1775
                />
1776
              </p>
1777
            </div>
1778
1779
            <div data-c-background="grey(20)" data-c-padding="normal">
1780
              <div
1781
                className="manager-job-card"
1782
                data-c-background="white(100)"
1783
                data-c-padding="normal"
1784
                data-c-radius="rounded"
1785
              >
1786
                <h4
1787
                  data-c-border="bottom(thin, solid, black)"
1788
                  data-c-font-size="h4"
1789
                  data-c-font-weight="600"
1790
                  data-c-margin="bottom(normal)"
1791
                  data-c-padding="bottom(normal)"
1792
                >
1793
                  <FormattedMessage
1794
                    id="jobBuilder.skills.title.needsToHave"
1795
                    defaultMessage="Skills the Employee Needs to Have"
1796
                    description="Section Header in Modal"
1797
                  />
1798
                </h4>
1799
                {essentialCriteria.length === 0 ? (
1800
                  <p>
1801
                    <FormattedMessage
1802
                      id="jobBuilder.skills.nullState"
1803
                      defaultMessage="You haven't added any skills yet."
1804
                      description="The text displayed in the skills modal when you haven't added any skills."
1805
                    />
1806
                  </p>
1807
                ) : (
1808
                  essentialCriteria.map(
1809
                    (criterion): React.ReactElement | null => {
1810
                      const skill = getSkillOfCriteria(criterion);
1811
                      if (skill === null) {
1812
                        return null;
1813
                      }
1814
                      return (
1815
                        <Criterion
1816
                          criterion={criterion}
1817
                          skill={skill}
1818
                          key={skill.id}
1819
                        />
1820
                      );
1821
                    },
1822
                  )
1823
                )}
1824
                <h4
1825
                  data-c-border="bottom(thin, solid, black)"
1826
                  data-c-font-size="h4"
1827
                  data-c-font-weight="600"
1828
                  data-c-margin="top(double) bottom(normal)"
1829
                  data-c-padding="bottom(normal)"
1830
                >
1831
                  <FormattedMessage
1832
                    id="jobBuilder.skills.title.niceToHave"
1833
                    defaultMessage="Skills That Would Be Nice For the Employee to Have"
1834
                    description="Section Header in Modal"
1835
                  />
1836
                </h4>
1837
                {assetCriteria.length === 0 ? (
1838
                  <p>
1839
                    <FormattedMessage
1840
                      id="jobBuilder.skills.nullState"
1841
                      defaultMessage="You haven't added any skills yet."
1842
                      description="The text displayed in the skills modal when you haven't added any skills."
1843
                    />
1844
                  </p>
1845
                ) : (
1846
                  assetCriteria.map((criterion): React.ReactElement | null => {
1847
                    const skill = getSkillOfCriteria(criterion);
1848
                    if (skill === null) {
1849
                      return null;
1850
                    }
1851
                    return (
1852
                      <Criterion
1853
                        criterion={criterion}
1854
                        skill={skill}
1855
                        key={skill.id}
1856
                      />
1857
                    );
1858
                  })
1859
                )}
1860
              </div>
1861
            </div>
1862
          </div>
1863
        </Modal.Body>
1864
        <Modal.Footer>
1865
          <Modal.FooterCancelBtn>
1866
            <FormattedMessage
1867
              id="jobBuilder.skills.previewModalCancelLabel"
1868
              defaultMessage="Go Back"
1869
              description="The text displayed on the cancel button of the Job Builder Skills Preview modal."
1870
            />
1871
          </Modal.FooterCancelBtn>
1872
          {jobIsComplete && (
1873
            <Modal.FooterMiddleBtn>
1874
              <FormattedMessage
1875
                id="jobBuilder.skills.previewModalMiddleLabel"
1876
                defaultMessage="Skip to Review"
1877
                description="The text displayed on the 'Skip to Review' button of the Job Builder Skills Preview modal."
1878
              />
1879
            </Modal.FooterMiddleBtn>
1880
          )}
1881
          <Modal.FooterConfirmBtn>
1882
            <FormattedMessage
1883
              id="jobBuilder.skills.previewModalConfirmLabel"
1884
              defaultMessage="Next Step"
1885
              description="The text displayed on the confirm button of the Job Builder Skills Preview modal."
1886
            />
1887
          </Modal.FooterConfirmBtn>
1888
        </Modal.Footer>
1889
      </Modal>
1890
    </>
1891
  );
1892
};
1893
1894
export default JobSkills;
1895