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

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

Complexity

Total Complexity 33
Complexity/F 0

Size

Lines of Code 1897
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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