Passed
Push — feature/timeline ( d18b1c...ddfcdc )
by Yonathan
06:15 queued 11s
created

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

Complexity

Total Complexity 33
Complexity/F 0

Size

Lines of Code 1886
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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