Passed
Push — task/applicant-profile-api ( 747d8b...3edd34 )
by Yonathan
09:18 queued 01:23
created

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

Complexity

Total Complexity 33
Complexity/F 0

Size

Lines of Code 1898
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 33
eloc 1459
mnd 33
bc 33
fnc 0
dl 0
loc 1898
rs 8.56
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { useState, useRef, useReducer } from "react";
2
import { FormattedMessage, defineMessages, useIntl } from "react-intl";
3
import nprogress from "nprogress";
4
import { Job, Skill, Criteria, JobPosterKeyTask } from "../../../models/types";
5
import Modal from "../../Modal";
6
import CriteriaForm from "./CriteriaForm";
7
import { mapToObject, getId, hasKey, notEmpty } from "../../../helpers/queries";
8
import {
9
  CriteriaTypeId,
10
  getKeyByValue,
11
  ClassificationId,
12
} from "../../../models/lookupConstants";
13
import Select, { SelectOption } from "../../Select";
14
import { getSkillLevelName } from "../../../models/jobUtil";
15
import Criterion from "../Criterion";
16
import {
17
  localizeField,
18
  getLocale,
19
  localizeFieldNonNull,
20
} from "../../../helpers/localize";
21
import { imageUrl } from "../../../helpers/routes";
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
                  data-c-display="inline-block"
521
                  data-c-alignment="center"
522
                >
523
                  {intl.formatMessage(getSkillLevelName(criterion, skill))}
524
                </span>
525
              </div>
526
            </div>
527
          </div>
528
          <div data-c-grid-item="base(3of10)">
529
            <div data-c-grid="gutter">
530
              <div
531
                data-c-grid-item="base(1of1) tl(1of2)"
532
                data-c-align="base(centre)"
533
              >
534
                <button
535
                  type="button"
536
                  data-c-colour="c1"
537
                  onClick={(): void => setCriteriaBeingEdited(criterion)}
538
                >
539
                  <i className="fas fa-edit" />
540
                </button>
541
              </div>
542
              <div
543
                data-c-grid-item="base(1of1) tl(1of2)"
544
                data-c-align="base(centre)"
545
              >
546
                <button
547
                  type="button"
548
                  data-c-colour="c1"
549
                  data-c-hover-colour="stop"
550
                  onClick={(): void =>
551
                    criteriaDispatch({
552
                      type: "remove",
553
                      payload: criterion,
554
                    })
555
                  }
556
                >
557
                  <i className="fas fa-trash" />
558
                </button>
559
              </div>
560
            </div>
561
          </div>
562
        </div>
563
      </li>
564
    );
565
  };
566
567
  const renderSkillButton = (skill: Skill): React.ReactElement => {
568
    const alreadySelected = skillAlreadySelected(jobCriteria, skill);
569
    // Open the Add Skill modal, or remove it if its already added
570
    const handleClick = (): void =>
571
      alreadySelected
572
        ? criteriaDispatch({
573
            type: "removeSkill",
574
            payload: { skillId: skill.id },
575
          })
576
        : setSkillBeingAdded(skill);
577
    return (
578
      <li key={skill.id}>
579
        <button
580
          className={`jpb-skill-trigger ${alreadySelected ? "active" : ""}`}
581
          data-c-button="outline(c1)"
582
          data-c-radius="rounded"
583
          data-c-padding="all(half)"
584
          type="button"
585
          onClick={handleClick}
586
        >
587
          <span data-c-padding="right(half)">
588
            <i className="fas fa-plus-circle" />
589
            <i className="fas fa-minus-circle" />
590
          </span>
591
          {localizeField(locale, skill, "name")}
592
        </button>
593
      </li>
594
    );
595
  };
596
597
  const submitButton = (
598
    <button
599
      data-c-button="solid(c2)"
600
      data-c-radius="rounded"
601
      type="button"
602
      disabled={isSaving}
603
      onClick={(): void => saveAndPreview()}
604
    >
605
      <FormattedMessage
606
        id="jobBuilder.skills.button.previewSkills"
607
        defaultMessage="Save &amp; Preview Skills"
608
        description="Label of Button"
609
      />
610
    </button>
611
  );
612
  const smileyArrowBadImg = imageUrl("icon-smiley-arrow-bad.svg");
613
  const smileyArrowMediumImg = imageUrl("icon-smiley-arrow-medium.svg");
614
  const smileyArrowGoodImg = imageUrl("icon-smiley-arrow-good.svg");
615
  const smileyBadImg = imageUrl("icon-smiley-bad.svg");
616
  const smileyBadGreyImg = imageUrl("icon-smiley-bad-grey.svg");
617
  const smileyMediumImg = imageUrl("icon-smiley-medium.svg");
618
  const smileyMediumGreyImg = imageUrl("icon-smiley-medium-grey.svg");
619
  const smileyGoodImg = imageUrl("icon-smiley-good.svg");
620
  const smileyGoodGreyImg = imageUrl("icon-smiley-good-grey.svg");
621
622
  return (
623
    <>
624
      <div
625
        data-c-container="form"
626
        data-c-padding="top(triple) bottom(triple)"
627
        ref={modalParentRef}
628
      >
629
        <h3
630
          data-c-font-size="h3"
631
          data-c-font-weight="bold"
632
          data-c-margin="bottom(double)"
633
        >
634
          <FormattedMessage
635
            id="jobBuilder.skills.title"
636
            defaultMessage="Skills"
637
            description="section title"
638
          />
639
        </h3>
640
        <p data-c-margin="bottom(triple)">
641
          <FormattedMessage
642
            id="jobBuilder.skills.description"
643
            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."
644
            description="section description under title"
645
          />
646
        </p>
647
        <div
648
          data-c-margin="bottom(triple)"
649
          data-c-align="base(centre) tl(left)"
650
        >
651
          {/* 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. */}
652
          <button
653
            data-c-button="solid(c2)"
654
            data-c-radius="rounded"
655
            type="button"
656
            disabled={tasksModalVisible}
657
            onClick={(): void => setTasksModalVisible(true)}
658
          >
659
            <FormattedMessage
660
              id="jobBuilder.skills.button.keyTasks"
661
              defaultMessage="View Key Tasks"
662
              description="Button label"
663
            />
664
          </button>
665
        </div>
666
667
        {/* Total Skills List */}
668
        <h4
669
          data-c-colour="c2"
670
          data-c-font-size="h4"
671
          data-c-margin="bottom(normal)"
672
        >
673
          <FormattedMessage
674
            id="jobBuilder.skills.listTitle"
675
            defaultMessage="Your Skills List"
676
            description="List section title"
677
          />
678
        </h4>
679
        <div data-c-grid="gutter top">
680
          <div data-c-grid-item="base(1of1) tl(1of2)">
681
            <div
682
              data-c-border="all(thin, solid, black)"
683
              data-c-radius="rounded"
684
              data-c-padding="normal"
685
            >
686
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
687
                <FormattedMessage
688
                  id="jobBuilder.skills.statusSmiley.essentialTitle"
689
                  defaultMessage="Number of Essential Skills"
690
                  description="Title of skill status tracker"
691
                />
692
              </p>
693
              {/* TODO: SmileyStatusIndicator can be extracted as its own component, since its already repeated within this page. */}
694
              {/* 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. */}
695
              <div
696
                data-c-grid="gutter"
697
                data-c-align="centre"
698
                data-c-padding="top(normal)"
699
              >
700
                <div
701
                  className={`jpb-skill-measure-item bad ${
702
                    countInRange(0, 1, essentialCount) ? "active" : ""
703
                  }`}
704
                  data-c-grid-item="base(1of5)"
705
                >
706
                  {/* 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). */}
707
                  <div>
708
                    <img
709
                      src={smileyArrowBadImg}
710
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
711
                    />
712
                    <span
713
                      data-c-font-weight="bold"
714
                      data-c-colour="white"
715
                      data-c-font-size="small"
716
                    >
717
                      {essentialCount}
718
                    </span>
719
                  </div>
720
                  <img
721
                    src={smileyBadImg}
722
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
723
                  />
724
                  <img
725
                    src={smileyBadGreyImg}
726
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
727
                  />
728
                  <p data-c-font-size="small" data-c-font-weight="bold">
729
                    <FormattedMessage
730
                      id="jobBuilder.skills.statusSmiley.essential.tooFew"
731
                      defaultMessage="Too Few"
732
                      description="Description of quantity of skills"
733
                    />
734
                  </p>
735
                  <p data-c-font-size="small">0 - 1</p>
736
                </div>
737
                <div
738
                  className={`jpb-skill-measure-item medium ${
739
                    countInRange(2, 3, essentialCount) ? "active" : ""
740
                  }`}
741
                  data-c-grid-item="base(1of5)"
742
                >
743
                  <div>
744
                    <img
745
                      src={smileyArrowMediumImg}
746
                      alt={intl.formatMessage(altMessages.neutralArrow)}
747
                    />
748
                    <span
749
                      data-c-font-weight="bold"
750
                      data-c-colour="white"
751
                      data-c-font-size="small"
752
                    >
753
                      {essentialCount}
754
                    </span>
755
                  </div>
756
                  <img
757
                    src={smileyMediumImg}
758
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
759
                  />
760
                  <img
761
                    src={smileyMediumGreyImg}
762
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
763
                  />
764
                  <p data-c-font-size="small" data-c-font-weight="bold">
765
                    <FormattedMessage
766
                      id="jobBuilder.skills.statusSmiley.essential.almost"
767
                      defaultMessage="Almost"
768
                      description="Description of quantity of skills"
769
                    />
770
                  </p>
771
                  <p data-c-font-size="small">2 - 3</p>
772
                </div>
773
                <div
774
                  className={`jpb-skill-measure-item good ${
775
                    countInRange(4, 6, essentialCount) ? "active" : ""
776
                  }`}
777
                  data-c-grid-item="base(1of5)"
778
                >
779
                  <div>
780
                    <img
781
                      src={smileyArrowGoodImg}
782
                      alt={intl.formatMessage(altMessages.happyArrow)}
783
                    />
784
                    <span
785
                      data-c-font-weight="bold"
786
                      data-c-colour="white"
787
                      data-c-font-size="small"
788
                    >
789
                      {essentialCount}
790
                    </span>
791
                  </div>
792
                  <img
793
                    src={smileyGoodImg}
794
                    alt={intl.formatMessage(altMessages.happySmiley)}
795
                  />
796
                  <img
797
                    src={smileyGoodGreyImg}
798
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
799
                  />
800
                  <p data-c-font-size="small" data-c-font-weight="bold">
801
                    <FormattedMessage
802
                      id="jobBuilder.skills.statusSmiley.essential.awesome"
803
                      defaultMessage="Awesome"
804
                      description="Description of quantity of skills"
805
                    />
806
                  </p>
807
                  <p data-c-font-size="small">4 - 6</p>
808
                </div>
809
                <div
810
                  className={`jpb-skill-measure-item medium ${
811
                    countInRange(7, 8, essentialCount) ? "active" : ""
812
                  }`}
813
                  data-c-grid-item="base(1of5)"
814
                >
815
                  <div>
816
                    <img
817
                      src={smileyArrowMediumImg}
818
                      alt={intl.formatMessage(altMessages.neutralArrow)}
819
                    />
820
                    <span
821
                      data-c-font-weight="bold"
822
                      data-c-colour="white"
823
                      data-c-font-size="small"
824
                    >
825
                      {essentialCount}
826
                    </span>
827
                  </div>
828
                  <img
829
                    src={smileyMediumImg}
830
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
831
                  />
832
                  <img
833
                    src={smileyMediumGreyImg}
834
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
835
                  />
836
                  <p data-c-font-size="small" data-c-font-weight="bold">
837
                    <FormattedMessage
838
                      id="jobBuilder.skills.statusSmiley.essential.acceptable"
839
                      defaultMessage="Acceptable"
840
                      description="Description of quantity of skills"
841
                    />
842
                  </p>
843
                  <p data-c-font-size="small">7 - 8</p>
844
                </div>
845
                <div
846
                  className={`jpb-skill-measure-item bad ${
847
                    essentialCount >= 9 ? "active" : ""
848
                  }`}
849
                  data-c-grid-item="base(1of5)"
850
                >
851
                  <div>
852
                    {/* TODO: Alt Text Translations */}
853
                    <img
854
                      src={smileyArrowBadImg}
855
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
856
                    />
857
                    <span
858
                      data-c-font-weight="bold"
859
                      data-c-colour="white"
860
                      data-c-font-size="small"
861
                    >
862
                      {essentialCount}
863
                    </span>
864
                  </div>
865
                  <img
866
                    src={smileyBadImg}
867
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
868
                  />
869
                  <img
870
                    src={smileyBadGreyImg}
871
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
872
                  />
873
                  <p data-c-font-size="small" data-c-font-weight="bold">
874
                    <FormattedMessage
875
                      id="jobBuilder.skills.statusSmiley.essential.tooMany"
876
                      defaultMessage="Too Many"
877
                      description="Description of quantity of skills"
878
                    />
879
                  </p>
880
                  <p data-c-font-size="small">9 +</p>
881
                </div>
882
              </div>
883
            </div>
884
          </div>
885
          <div data-c-grid-item="base(1of1) tl(1of2)">
886
            <div
887
              data-c-border="all(thin, solid, black)"
888
              data-c-radius="rounded"
889
              data-c-padding="normal"
890
            >
891
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
892
                <FormattedMessage
893
                  id="jobBuilder.skills.statusSmiley.title"
894
                  defaultMessage="Total Number of Skills"
895
                  description="Title of skill quantity evaluator"
896
                />
897
              </p>
898
              {/* This is the second smiley indicator, used for total skills. Note the difference in the scale from the first. */}
899
              <div
900
                data-c-grid="gutter"
901
                data-c-align="centre"
902
                data-c-padding="top(normal)"
903
              >
904
                <div
905
                  className={`jpb-skill-measure-item bad ${
906
                    countInRange(0, 3, skillCount) ? "active" : ""
907
                  }`}
908
                  data-c-grid-item="base(1of5)"
909
                >
910
                  <div>
911
                    <img
912
                      src={smileyArrowBadImg}
913
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
914
                    />
915
                    <span
916
                      data-c-font-weight="bold"
917
                      data-c-colour="white"
918
                      data-c-font-size="small"
919
                    >
920
                      {skillCount}
921
                    </span>
922
                  </div>
923
                  <img
924
                    src={smileyBadImg}
925
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
926
                  />
927
                  <img
928
                    src={smileyBadGreyImg}
929
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
930
                  />
931
                  <p data-c-font-size="small" data-c-font-weight="bold">
932
                    <FormattedMessage
933
                      id="jobBuilder.skills.statusSmiley.tooFew"
934
                      defaultMessage="Too Few"
935
                      description="Description of quantity of skills"
936
                    />
937
                  </p>
938
                  <p data-c-font-size="small">0 - 3</p>
939
                </div>
940
                <div
941
                  className={`jpb-skill-measure-item medium ${
942
                    countInRange(4, 6, skillCount) ? "active" : ""
943
                  }`}
944
                  data-c-grid-item="base(1of5)"
945
                >
946
                  <div>
947
                    <img
948
                      src={smileyArrowMediumImg}
949
                      alt={intl.formatMessage(altMessages.neutralArrow)}
950
                    />
951
                    <span
952
                      data-c-font-weight="bold"
953
                      data-c-colour="white"
954
                      data-c-font-size="small"
955
                    >
956
                      {skillCount}
957
                    </span>
958
                  </div>
959
                  <img
960
                    src={smileyMediumImg}
961
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
962
                  />
963
                  <img
964
                    src={smileyMediumGreyImg}
965
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
966
                  />
967
                  <p data-c-font-size="small" data-c-font-weight="bold">
968
                    <FormattedMessage
969
                      id="jobBuilder.skills.statusSmiley.almost"
970
                      defaultMessage="Almost"
971
                      description="Description of quantity of skills"
972
                    />
973
                  </p>
974
                  <p data-c-font-size="small">4 - 6</p>
975
                </div>
976
                <div
977
                  className={`jpb-skill-measure-item good ${
978
                    countInRange(7, 8, skillCount) ? "active" : ""
979
                  }`}
980
                  data-c-grid-item="base(1of5)"
981
                >
982
                  <div>
983
                    <img
984
                      src={smileyArrowGoodImg}
985
                      alt={intl.formatMessage(altMessages.happyArrow)}
986
                    />
987
                    <span
988
                      data-c-font-weight="bold"
989
                      data-c-colour="white"
990
                      data-c-font-size="small"
991
                    >
992
                      {skillCount}
993
                    </span>
994
                  </div>
995
                  <img
996
                    src={smileyGoodImg}
997
                    alt={intl.formatMessage(altMessages.happySmiley)}
998
                  />
999
                  <img
1000
                    src={smileyGoodGreyImg}
1001
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
1002
                  />
1003
                  <p data-c-font-size="small" data-c-font-weight="bold">
1004
                    <FormattedMessage
1005
                      id="jobBuilder.skills.statusSmiley.awesome"
1006
                      defaultMessage="Awesome"
1007
                      description="Description of quantity of skills"
1008
                    />
1009
                  </p>
1010
                  <p data-c-font-size="small">7 - 8</p>
1011
                </div>
1012
                <div
1013
                  className={`jpb-skill-measure-item medium  ${
1014
                    countInRange(9, 10, skillCount) ? "active" : ""
1015
                  }`}
1016
                  data-c-grid-item="base(1of5)"
1017
                >
1018
                  <div>
1019
                    <img
1020
                      src={smileyArrowMediumImg}
1021
                      alt={intl.formatMessage(altMessages.neutralArrow)}
1022
                    />
1023
                    <span
1024
                      data-c-font-weight="bold"
1025
                      data-c-colour="white"
1026
                      data-c-font-size="small"
1027
                    >
1028
                      {skillCount}
1029
                    </span>
1030
                  </div>
1031
                  <img
1032
                    src={smileyMediumImg}
1033
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
1034
                  />
1035
                  <img
1036
                    src={smileyMediumGreyImg}
1037
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
1038
                  />
1039
                  <p data-c-font-size="small" data-c-font-weight="bold">
1040
                    <FormattedMessage
1041
                      id="jobBuilder.skills.statusSmiley.acceptable"
1042
                      defaultMessage="Acceptable"
1043
                      description="Description of quantity of skills"
1044
                    />
1045
                  </p>
1046
                  <p data-c-font-size="small">9 - 10</p>
1047
                </div>
1048
                <div
1049
                  className={`jpb-skill-measure-item bad ${
1050
                    skillCount >= 11 ? "active" : ""
1051
                  }`}
1052
                  data-c-grid-item="base(1of5)"
1053
                >
1054
                  <div>
1055
                    <img
1056
                      src={smileyArrowBadImg}
1057
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
1058
                    />
1059
                    <span
1060
                      data-c-font-weight="bold"
1061
                      data-c-colour="white"
1062
                      data-c-font-size="small"
1063
                    >
1064
                      {skillCount}
1065
                    </span>
1066
                  </div>
1067
                  <img
1068
                    src={smileyBadImg}
1069
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
1070
                  />
1071
                  <img
1072
                    src={smileyBadGreyImg}
1073
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
1074
                  />
1075
                  <p data-c-font-size="small" data-c-font-weight="bold">
1076
                    <FormattedMessage
1077
                      id="jobBuilder.skills.statusSmiley.tooMany"
1078
                      defaultMessage="Too Many"
1079
                      description="Description of quantity of skills"
1080
                    />
1081
                  </p>
1082
                  <p data-c-font-size="small">11 +</p>
1083
                </div>
1084
              </div>
1085
            </div>
1086
          </div>
1087
        </div>
1088
        {/* 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. */}
1089
        <div
1090
          data-c-background="grey(10)"
1091
          data-c-radius="rounded"
1092
          data-c-padding="all(normal)"
1093
          data-c-margin="top(normal) bottom(normal)"
1094
        >
1095
          <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
1096
            <FormattedMessage
1097
              id="jobBuilder.skills.title.essentialSkills"
1098
              defaultMessage="Essential Skills"
1099
              description="Title of essential skills list"
1100
            />
1101
          </p>
1102
          {/* 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. */}
1103
          {/* Null state. */}
1104
          {essentialCount === 0 && renderNullCriteriaRow()}
1105
          <ol className="jpb-skill-list" data-tc-up-down-list>
1106
            {/* 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. */}
1107
            {essentialCriteria.map(renderCriteriaRow)}
1108
          </ol>
1109
          {/* 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). */}
1110
          <p
1111
            data-c-font-weight="bold"
1112
            data-c-margin="top(normal) bottom(normal)"
1113
          >
1114
            <FormattedMessage
1115
              id="jobBuilder.skills.title.assetSkills"
1116
              defaultMessage="Asset Skills"
1117
              description="Title of asset skills list"
1118
            />
1119
          </p>
1120
          {/* Asset null state goes here. */}
1121
          {assetCount === 0 && renderNullCriteriaRow()}
1122
          <ol className="jpb-skill-list" data-tc-up-down-list>
1123
            {assetCriteria.map(renderCriteriaRow)}
1124
          </ol>
1125
        </div>
1126
        <div
1127
          data-c-margin="bottom(triple)"
1128
          data-c-align="base(centre) tl(right)"
1129
        >
1130
          {/* 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. */}
1131
          {submitButton}
1132
        </div>
1133
        {/* 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. */}
1134
        <h4
1135
          data-c-colour="c2"
1136
          data-c-font-size="h4"
1137
          data-c-margin="bottom(normal)"
1138
        >
1139
          <FormattedMessage
1140
            id="jobBuilder.skills.title.skillSelection"
1141
            defaultMessage="Skill Selection"
1142
            description="Title of skill selection section"
1143
          />
1144
        </h4>
1145
        {/* Occupational Skills */}
1146
        {/* You can modify colour/icon using the category classes here again (occupational, cultural, future) on the "jpb-skill-category" element. */}
1147
        <div
1148
          id="jpb-occupational-skills"
1149
          className="jpb-skill-category occupational"
1150
          data-c-margin="bottom(normal)"
1151
          data-c-padding="normal"
1152
          data-c-radius="rounded"
1153
          data-c-background="grey(10)"
1154
        >
1155
          <div data-c-grid="gutter top">
1156
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1157
              <h5
1158
                className="jpb-skill-section-title"
1159
                data-c-font-size="h4"
1160
                data-c-margin="bottom(normal)"
1161
              >
1162
                {/* These icons will change automatically based on the class specified above. */}
1163
                <span
1164
                  data-c-font-size="small"
1165
                  data-c-margin="right(half)"
1166
                  data-c-padding="tb(quarter) rl(half)"
1167
                  data-c-radius="rounded"
1168
                  data-c-colour="white"
1169
                >
1170
                  <i className="fas fa-briefcase" />
1171
                  <i className="fas fa-coffee" />
1172
                  <i className="fas fa-certificate" />
1173
                  <i className="fas fa-book" />
1174
                </span>
1175
                {/* Category Title */}
1176
                <FormattedMessage
1177
                  id="jobBuilder.skills.title.occupationalSkills"
1178
                  defaultMessage="Occupational Competencies"
1179
                  description="Title of skills category"
1180
                />
1181
              </h5>
1182
              {/* Category description - basically this outlines what the category means. */}
1183
              {/* <p>
1184
                // TODO: Add this message back in once we have copy.
1185
                <FormattedMessage
1186
                  id="jobBuilder.skills.description.occupationalSkills"
1187
                  defaultMessage=""
1188
                  description="Description of a category of skills"
1189
                />
1190
              </p> */}
1191
            </div>
1192
            <div
1193
              data-c-grid-item="tp(1of3) ds(1of4)"
1194
              data-c-align="base(centre) tp(right)"
1195
            >
1196
              {/* 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. */}
1197
              <div
1198
                className={`jpb-skill-target ${
1199
                  countInRange(
1200
                    minOccupational,
1201
                    maxOccupational,
1202
                    occupationalCriteria.length,
1203
                  )
1204
                    ? "complete"
1205
                    : ""
1206
                }`}
1207
              >
1208
                <i data-c-colour="stop" className="fas fa-bullseye" />
1209
                <i data-c-colour="go" className="fas fa-check" />
1210
                <span>
1211
                  <FormattedMessage
1212
                    id="jobBuilder.skills.range.occupationalSkills"
1213
                    defaultMessage="Aim for {minOccupational} - {maxOccupational} skills."
1214
                    description="Ranage recommendation for occupational competencies in job poster"
1215
                    values={{ minOccupational, maxOccupational }}
1216
                  />
1217
                </span>
1218
              </div>
1219
            </div>
1220
            {/* 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. */}
1221
            {(job.classification_id === undefined ||
1222
              job.classification_id === null) && (
1223
              <p data-c-font-weight="bold" data-c-grid-item="base(1of1)">
1224
                <FormattedMessage
1225
                  id="jobBuilder.skills.nullText.occupationalSkills"
1226
                  defaultMessage="You must return to Step 1 and choose a Classification."
1227
                  description="Placeholder text for occupational competencies list."
1228
                />
1229
              </p>
1230
            )}
1231
1232
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1233
              {occupationalSkills.map(renderSkillButton)}
1234
            </ul>
1235
          </div>
1236
        </div>
1237
        {/* Cultural Skills */}
1238
        {/* This section is here so that you can see the categorical division of culture skills. */}
1239
        <div
1240
          className="jpb-skill-category cultural"
1241
          data-c-margin="bottom(normal)"
1242
          data-c-padding="normal"
1243
          data-c-radius="rounded"
1244
          data-c-background="grey(10)"
1245
        >
1246
          <div data-c-grid="gutter top">
1247
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1248
              <h5
1249
                className="jpb-skill-section-title"
1250
                data-c-font-size="h4"
1251
                data-c-margin="bottom(normal)"
1252
              >
1253
                <span
1254
                  data-c-font-size="small"
1255
                  data-c-margin="right(half)"
1256
                  data-c-padding="tb(quarter) rl(half)"
1257
                  data-c-radius="rounded"
1258
                  data-c-colour="white"
1259
                >
1260
                  <i className="fas fa-briefcase" />
1261
                  <i className="fas fa-coffee" />
1262
                  <i className="fas fa-certificate" />
1263
                  <i className="fas fa-book" />
1264
                </span>
1265
                <FormattedMessage
1266
                  id="jobBuilder.skills.title.culturalSkills"
1267
                  defaultMessage="Behavioural Competencies"
1268
                  description="Title of skills category"
1269
                />
1270
              </h5>
1271
              {/* <p>
1272
              // TODO: Add this message back in once we have copy.
1273
                <FormattedMessage
1274
                  id="jobBuilder.skills.description.culturalSkills"
1275
                  defaultMessage=""
1276
                  description="Description of a category of skills"
1277
                />
1278
              </p> */}
1279
            </div>
1280
            <div
1281
              data-c-grid-item="tp(1of3) ds(1of4)"
1282
              data-c-align="base(centre) tp(right)"
1283
            >
1284
              <div
1285
                className={`jpb-skill-target ${
1286
                  countInRange(minCulture, maxCulture, cultureCriteria.length)
1287
                    ? "complete"
1288
                    : ""
1289
                }`}
1290
              >
1291
                <i data-c-colour="stop" className="fas fa-bullseye" />
1292
                <i data-c-colour="go" className="fas fa-check" />
1293
                <span>
1294
                  <FormattedMessage
1295
                    id="jobBuilder.skills.range.culturalSkills"
1296
                    defaultMessage="Aim for {minCulture} - {maxCulture} skills."
1297
                    description="Range recommendation for behavioural competencies in job poster"
1298
                    values={{ minCulture, maxCulture }}
1299
                  />
1300
                </span>
1301
              </div>
1302
            </div>
1303
            {/** Culture skills are intended to be split into two lists, Recommended and Remaining. Until the recommendation logic is nailed down, its just one. */}
1304
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1305
              {cultureSkills.map(renderSkillButton)}
1306
            </ul>
1307
            {/* 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. */}
1308
            {/* <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1309
              // Note that this "p" tag has a different margin value than the one in the "ul" below.
1310
              <p
1311
                data-c-font-weight="bold"
1312
                data-c-margin="top(half) bottom(normal)"
1313
              >
1314
                Recommended Skills:
1315
              </p>
1316
              // This is where the skill recommendations from Work Environment go.
1317
              <li>
1318
                <button
1319
                  className="jpb-skill-trigger"
1320
                  data-c-button="outline(c1)"
1321
                  data-c-radius="rounded"
1322
                >
1323
                  <i className="fas fa-plus-circle" />
1324
                  <i className="fas fa-minus-circle" />
1325
                  Skill Name
1326
                </button>
1327
              </li>
1328
            </ul>
1329
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1330
              <p
1331
                data-c-font-weight="bold"
1332
                data-c-margin="top(normal) bottom(normal)"
1333
              >
1334
                Remaining Skills:
1335
              </p>
1336
              // This is where the remaining culture skills go. Please make sure that the skills in the recommendation list above do not appear here.
1337
              <li>
1338
                <button
1339
                  className="jpb-skill-trigger"
1340
                  data-c-button="outline(c1)"
1341
                  data-c-radius="rounded"
1342
                >
1343
                  <i className="fas fa-plus-circle" />
1344
                  <i className="fas fa-minus-circle" />
1345
                  Skill Name
1346
                </button>
1347
              </li>
1348
            </ul> */}
1349
          </div>
1350
        </div>
1351
        {/* Future Skills */}
1352
        {/* This section is just here so you can see what it looks like with the future class. */}
1353
        <div
1354
          className="jpb-skill-category future"
1355
          data-c-margin="bottom(normal)"
1356
          data-c-padding="normal"
1357
          data-c-radius="rounded"
1358
          data-c-background="grey(10)"
1359
        >
1360
          <div data-c-grid="gutter top">
1361
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1362
              <h5
1363
                className="jpb-skill-section-title"
1364
                data-c-font-size="h4"
1365
                data-c-margin="bottom(normal)"
1366
              >
1367
                <span
1368
                  data-c-font-size="small"
1369
                  data-c-margin="right(half)"
1370
                  data-c-padding="tb(quarter) rl(half)"
1371
                  data-c-radius="rounded"
1372
                  data-c-colour="white"
1373
                >
1374
                  <i className="fas fa-briefcase" />
1375
                  <i className="fas fa-coffee" />
1376
                  <i className="fas fa-certificate" />
1377
                  <i className="fas fa-book" />
1378
                </span>
1379
                <FormattedMessage
1380
                  id="jobBuilder.skills.title.futureSkills"
1381
                  defaultMessage="Public Service Competencies"
1382
                  description="Title of skills category"
1383
                />
1384
              </h5>
1385
              {/* <p>
1386
              // TODO: Add this message back in once we have copy.
1387
                <FormattedMessage
1388
                  id="jobBuilder.skills.description.futureSkills"
1389
                  defaultMessage=""
1390
                  description="Description of a category of skills"
1391
                />
1392
              </p> */}
1393
            </div>
1394
            <div
1395
              data-c-grid-item="tp(1of3) ds(1of4)"
1396
              data-c-align="base(centre) tp(right)"
1397
            >
1398
              <div
1399
                className={`jpb-skill-target ${
1400
                  countInRange(minFuture, maxFuture, futureCriteria.length)
1401
                    ? "complete"
1402
                    : ""
1403
                }`}
1404
              >
1405
                <i data-c-colour="stop" className="fas fa-bullseye" />
1406
                <i data-c-colour="go" className="fas fa-check" />
1407
                <span>
1408
                  <FormattedMessage
1409
                    id="jobBuilder.skills.range.futureSkills"
1410
                    defaultMessage="Aim for {minFuture} - {maxFuture} skills."
1411
                    description="Ranage recommendation for public service competencies in job poster"
1412
                    values={{ minFuture, maxFuture }}
1413
                  />
1414
                </span>
1415
              </div>
1416
            </div>
1417
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1418
              {futureSkills.map(renderSkillButton)}
1419
            </ul>
1420
          </div>
1421
        </div>
1422
        {/* 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. */}
1423
        {/* "Custom" Skills */}
1424
        <h5 data-c-font-weight="bold" data-c-margin="top(double) bottom(half)">
1425
          <FormattedMessage
1426
            id="jobBuilder.skills.title.missingSkill"
1427
            defaultMessage="Can't find the skill you need?"
1428
            description="Title of instructions for missing skill"
1429
          />
1430
        </h5>
1431
        <p data-c-margin="bottom(normal)">
1432
          {/* TODO: Refactor for react-intl version 3, using new rich text xml tag syntax eg.
1433
          <FormattedMessage
1434
            id="jobBuilder.skills.instructions.missingSkills"
1435
            defaultMessage="Building a skills list is a huge endeavour, and it's not
1436
  surprising that Talent Cloud's list doesn't have the skill
1437
  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
1438
  kick-off the discussion."
1439
            values={{
1440
              link: msg => (
1441
                <a href="mailto:[email protected]">
1442
                  {msg}
1443
                </a>
1444
              ),
1445
            }}
1446
          /> */}
1447
          <FormattedMessage
1448
            id="jobBuilder.skills.instructions.missingSkills"
1449
            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."
1450
            values={{
1451
              link: (
1452
                <a href="mailto:[email protected]">
1453
                  {intl.formatMessage(messages.emailUs)}
1454
                </a>
1455
              ),
1456
            }}
1457
          />
1458
        </p>
1459
        <div
1460
          className="jpb-skill-category optional"
1461
          data-c-margin="bottom(normal)"
1462
          data-c-padding="normal"
1463
          data-c-radius="rounded"
1464
          data-c-background="grey(10)"
1465
        >
1466
          <div data-c-grid="gutter top">
1467
            <div data-c-grid-item="base(1of1)">
1468
              {/** TODO: Fix the layout of the skill cloud */}
1469
              <h5 className="jpb-skill-section-title" data-c-font-size="h4">
1470
                <span
1471
                  data-c-font-size="small"
1472
                  data-c-margin="right(half)"
1473
                  data-c-padding="tb(quarter) rl(half)"
1474
                  data-c-radius="rounded"
1475
                  data-c-colour="white"
1476
                >
1477
                  <i className="fas fa-briefcase" />
1478
                  <i className="fas fa-coffee" />
1479
                  <i className="fas fa-certificate" />
1480
                  <i className="fas fa-book" />
1481
                </span>
1482
                <FormattedMessage
1483
                  id="jobBuilder.skills.title.otherSkills"
1484
                  defaultMessage="Other Skills"
1485
                  description="Title of other skills section"
1486
                />
1487
              </h5>
1488
            </div>
1489
            <div data-c-grid-item="base(1of1)">
1490
              <Select
1491
                id="jpb-all-skills-select"
1492
                name="jpbAllSkillsSelect"
1493
                label={intl.formatMessage(messages.selectSkillLabel)}
1494
                selected={null}
1495
                nullSelection={intl.formatMessage(messages.selectSkillNull)}
1496
                options={unselectedOtherSkills.map(
1497
                  (skill): SelectOption => ({
1498
                    value: skill.id,
1499
                    label: localizeFieldNonNull(locale, skill, "name"),
1500
                  }),
1501
                )}
1502
                onChange={(event): void => {
1503
                  const skillId = Number(event.target.value);
1504
                  if (hasKey(skillsById, skillId)) {
1505
                    const skill = skillsById[skillId];
1506
                    setSkillBeingAdded(skill);
1507
                  }
1508
                }}
1509
              />
1510
            </div>
1511
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1512
              {/** TODO: Get this null state text hiding/showing. */}
1513
              {selectedOtherSkills.length === 0 && (
1514
                <p>
1515
                  <FormattedMessage
1516
                    id="jobBuilder.skills.placeholder.otherSkills"
1517
                    defaultMessage="There are no extra skills added."
1518
                    description="Placeholder when there are no other skills"
1519
                  />
1520
                </p>
1521
              )}
1522
              {selectedOtherSkills.map(renderSkillButton)}
1523
            </ul>
1524
          </div>
1525
        </div>
1526
        <div data-c-grid="gutter">
1527
          <div data-c-grid-item="base(1of1)">
1528
            <hr data-c-margin="top(normal) bottom(normal)" />
1529
          </div>
1530
          <div
1531
            data-c-alignment="base(centre) tp(left)"
1532
            data-c-grid-item="tp(1of2)"
1533
          >
1534
            <button
1535
              data-c-button="outline(c2)"
1536
              data-c-radius="rounded"
1537
              type="button"
1538
              disabled={isSaving}
1539
              onClick={(): void => saveAndReturn()}
1540
            >
1541
              <FormattedMessage
1542
                id="jobBuilder.skills.button.returnToTasks"
1543
                defaultMessage="Save &amp; Return to Tasks"
1544
                description="Button Label"
1545
              />
1546
            </button>
1547
          </div>
1548
          <div
1549
            data-c-alignment="base(centre) tp(right)"
1550
            data-c-grid-item="tp(1of2)"
1551
          >
1552
            {/* Modal trigger, same as last step. */}
1553
            {submitButton}
1554
1555
            <div
1556
              role="alert"
1557
              data-c-alert="error"
1558
              data-c-radius="rounded"
1559
              data-c-margin="top(normal)"
1560
              data-c-padding="all(half)"
1561
              data-c-visibility={
1562
                essentialCount === 0 && submitTouched ? "visible" : "invisible"
1563
              }
1564
              style={{
1565
                display: `inline-block`,
1566
              }}
1567
            >
1568
              <a
1569
                href="#jpb-occupational-skills"
1570
                tabIndex={0}
1571
                ref={errorMessage}
1572
              >
1573
                <FormattedMessage
1574
                  id="jobBuilder.skills.essentialSkillRequiredError"
1575
                  defaultMessage="At least one 'Essential Skill' is required."
1576
                  description="Label of Button"
1577
                />
1578
              </a>
1579
            </div>
1580
          </div>
1581
        </div>
1582
      </div>
1583
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1584
      {/** This modal simply displays key tasks. */}
1585
      <Modal
1586
        id={tasksModalId}
1587
        parentElement={modalParentRef.current}
1588
        visible={tasksModalVisible}
1589
        onModalCancel={(): void => setTasksModalVisible(false)}
1590
        onModalConfirm={(): void => setTasksModalVisible(false)}
1591
      >
1592
        <Modal.Header>
1593
          <div
1594
            data-c-background="c1(100)"
1595
            data-c-border="bottom(thin, solid, black)"
1596
            data-c-padding="normal"
1597
          >
1598
            <h5
1599
              data-c-colour="white"
1600
              data-c-font-size="h4"
1601
              id={`${tasksModalId}-title`}
1602
            >
1603
              <FormattedMessage
1604
                id="jobBuilder.skills.title.keyTasks"
1605
                defaultMessage="Key Tasks"
1606
                description="Title of Key Tasks Section"
1607
              />
1608
            </h5>
1609
          </div>
1610
        </Modal.Header>
1611
        <Modal.Body>
1612
          <div data-c-border="bottom(thin, solid, black)">
1613
            <div
1614
              data-c-border="bottom(thin, solid, black)"
1615
              data-c-padding="normal"
1616
              id={`${tasksModalId}-description`}
1617
            >
1618
              <ul>
1619
                {keyTasks.map(
1620
                  (task): React.ReactElement => (
1621
                    <li key={task.id}>
1622
                      {localizeField(locale, task, "description")}
1623
                    </li>
1624
                  ),
1625
                )}
1626
              </ul>
1627
            </div>
1628
          </div>
1629
        </Modal.Body>
1630
        <Modal.Footer>
1631
          <Modal.FooterCancelBtn>
1632
            <FormattedMessage
1633
              id="jobBuilder.skills.tasksModalCancelLabel"
1634
              defaultMessage="Back to Skills"
1635
              description="The text displayed on the cancel button of the Key Tasks modal on the Job Builder Skills step."
1636
            />
1637
          </Modal.FooterCancelBtn>
1638
        </Modal.Footer>
1639
      </Modal>
1640
      {/** This modal is for adding brand new skills */}
1641
      <Modal
1642
        id={addModalId}
1643
        parentElement={modalParentRef.current}
1644
        visible={skillBeingAdded !== null}
1645
        onModalCancel={(): void => {
1646
          setSkillBeingAdded(null);
1647
        }}
1648
        onModalConfirm={(): void => {
1649
          setSkillBeingAdded(null);
1650
        }}
1651
      >
1652
        <Modal.Header>
1653
          <div
1654
            data-c-background="c1(100)"
1655
            data-c-border="bottom(thin, solid, black)"
1656
            data-c-padding="normal"
1657
          >
1658
            <h5
1659
              data-c-colour="white"
1660
              data-c-font-size="h4"
1661
              id={`${addModalId}-title`}
1662
            >
1663
              <FormattedMessage
1664
                id="jobBuilder.skills.title.addASkill"
1665
                defaultMessage="Add a skill"
1666
                description="Title of Add a skill Section"
1667
              />
1668
            </h5>
1669
          </div>
1670
        </Modal.Header>
1671
        <Modal.Body>
1672
          {skillBeingAdded !== null && (
1673
            <CriteriaForm
1674
              jobPosterId={job.id}
1675
              skill={skillBeingAdded}
1676
              handleCancel={(): void => {
1677
                setSkillBeingAdded(null);
1678
              }}
1679
              handleSubmit={(criteria: Criteria): void => {
1680
                criteriaDispatch({ type: "add", payload: criteria });
1681
                setSkillBeingAdded(null);
1682
              }}
1683
            />
1684
          )}
1685
        </Modal.Body>
1686
      </Modal>
1687
      {/** This modal is for editing already added skills */}
1688
      <Modal
1689
        id={editModalId}
1690
        parentElement={modalParentRef.current}
1691
        visible={criteriaBeingEdited !== null}
1692
        onModalCancel={(): void => {
1693
          setCriteriaBeingEdited(null);
1694
        }}
1695
        onModalConfirm={(): void => {
1696
          setCriteriaBeingEdited(null);
1697
        }}
1698
      >
1699
        <Modal.Header>
1700
          <div
1701
            data-c-background="c1(100)"
1702
            data-c-border="bottom(thin, solid, black)"
1703
            data-c-padding="normal"
1704
          >
1705
            <h5
1706
              data-c-colour="white"
1707
              data-c-font-size="h4"
1708
              id={`${editModalId}-title`}
1709
            >
1710
              <FormattedMessage
1711
                id="jobBuilder.skills.title.editSkill"
1712
                defaultMessage="Edit skill"
1713
                description="Title of Edit skill Modal"
1714
              />
1715
            </h5>
1716
          </div>
1717
        </Modal.Header>
1718
        <Modal.Body>
1719
          {criteriaBeingEdited !== null &&
1720
            getSkillOfCriteria(criteriaBeingEdited) !== null && (
1721
              <CriteriaForm
1722
                jobPosterId={job.id}
1723
                criteria={criteriaBeingEdited}
1724
                skill={getSkillOfCriteria(criteriaBeingEdited) as Skill} // The cast is okay here (but still not ideal) because of the !== null check a few lines up
1725
                handleCancel={(): void => {
1726
                  setCriteriaBeingEdited(null);
1727
                }}
1728
                handleSubmit={(criteria: Criteria): void => {
1729
                  criteriaDispatch({ type: "edit", payload: criteria });
1730
                  setCriteriaBeingEdited(null);
1731
                }}
1732
              />
1733
            )}
1734
        </Modal.Body>
1735
      </Modal>
1736
      {/** This modal is the preview */}
1737
      <Modal
1738
        id={previewModalId}
1739
        parentElement={modalParentRef.current}
1740
        visible={isPreviewVisible}
1741
        onModalCancel={(): void => setIsPreviewVisible(false)}
1742
        onModalConfirm={(): void => handleContinue()}
1743
        onModalMiddle={(): void => {
1744
          handleSkipToReview();
1745
        }}
1746
      >
1747
        <Modal.Header>
1748
          <div
1749
            data-c-background="c1(100)"
1750
            data-c-border="bottom(thin, solid, black)"
1751
            data-c-padding="normal"
1752
          >
1753
            <h5
1754
              data-c-colour="white"
1755
              data-c-font-size="h4"
1756
              id={`${previewModalId}-title`}
1757
            >
1758
              <FormattedMessage
1759
                id="jobBuilder.skills.title.keepItUp"
1760
                defaultMessage="Keep it up!"
1761
                description="Title of Keep it up! Modal"
1762
              />
1763
            </h5>
1764
          </div>
1765
        </Modal.Header>
1766
        <Modal.Body>
1767
          <div data-c-border="bottom(thin, solid, black)">
1768
            <div
1769
              data-c-border="bottom(thin, solid, black)"
1770
              data-c-padding="normal"
1771
              id={`${previewModalId}-description`}
1772
            >
1773
              <p>
1774
                <FormattedMessage
1775
                  id="jobBuilder.skills.description.keepItUp"
1776
                  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."
1777
                  description="Body text of Keep it up! Modal"
1778
                />
1779
              </p>
1780
            </div>
1781
1782
            <div data-c-background="grey(20)" data-c-padding="normal">
1783
              <div
1784
                className="manager-job-card"
1785
                data-c-background="white(100)"
1786
                data-c-padding="normal"
1787
                data-c-radius="rounded"
1788
              >
1789
                <h4
1790
                  data-c-border="bottom(thin, solid, black)"
1791
                  data-c-font-size="h4"
1792
                  data-c-font-weight="600"
1793
                  data-c-margin="bottom(normal)"
1794
                  data-c-padding="bottom(normal)"
1795
                >
1796
                  <FormattedMessage
1797
                    id="jobBuilder.skills.title.needsToHave"
1798
                    defaultMessage="Skills the Employee Needs to Have"
1799
                    description="Section Header in Modal"
1800
                  />
1801
                </h4>
1802
                {essentialCriteria.length === 0 ? (
1803
                  <p>
1804
                    <FormattedMessage
1805
                      id="jobBuilder.skills.nullState"
1806
                      defaultMessage="You haven't added any skills yet."
1807
                      description="The text displayed in the skills modal when you haven't added any skills."
1808
                    />
1809
                  </p>
1810
                ) : (
1811
                  essentialCriteria.map(
1812
                    (criterion): React.ReactElement | null => {
1813
                      const skill = getSkillOfCriteria(criterion);
1814
                      if (skill === null) {
1815
                        return null;
1816
                      }
1817
                      return (
1818
                        <Criterion
1819
                          criterion={criterion}
1820
                          skill={skill}
1821
                          key={skill.id}
1822
                        />
1823
                      );
1824
                    },
1825
                  )
1826
                )}
1827
                <h4
1828
                  data-c-border="bottom(thin, solid, black)"
1829
                  data-c-font-size="h4"
1830
                  data-c-font-weight="600"
1831
                  data-c-margin="top(double) bottom(normal)"
1832
                  data-c-padding="bottom(normal)"
1833
                >
1834
                  <FormattedMessage
1835
                    id="jobBuilder.skills.title.niceToHave"
1836
                    defaultMessage="Skills That Would Be Nice For the Employee to Have"
1837
                    description="Section Header in Modal"
1838
                  />
1839
                </h4>
1840
                {assetCriteria.length === 0 ? (
1841
                  <p>
1842
                    <FormattedMessage
1843
                      id="jobBuilder.skills.nullState"
1844
                      defaultMessage="You haven't added any skills yet."
1845
                      description="The text displayed in the skills modal when you haven't added any skills."
1846
                    />
1847
                  </p>
1848
                ) : (
1849
                  assetCriteria.map((criterion): React.ReactElement | null => {
1850
                    const skill = getSkillOfCriteria(criterion);
1851
                    if (skill === null) {
1852
                      return null;
1853
                    }
1854
                    return (
1855
                      <Criterion
1856
                        criterion={criterion}
1857
                        skill={skill}
1858
                        key={skill.id}
1859
                      />
1860
                    );
1861
                  })
1862
                )}
1863
              </div>
1864
            </div>
1865
          </div>
1866
        </Modal.Body>
1867
        <Modal.Footer>
1868
          <Modal.FooterCancelBtn>
1869
            <FormattedMessage
1870
              id="jobBuilder.skills.previewModalCancelLabel"
1871
              defaultMessage="Go Back"
1872
              description="The text displayed on the cancel button of the Job Builder Skills Preview modal."
1873
            />
1874
          </Modal.FooterCancelBtn>
1875
          {jobIsComplete && (
1876
            <Modal.FooterMiddleBtn>
1877
              <FormattedMessage
1878
                id="jobBuilder.skills.previewModalMiddleLabel"
1879
                defaultMessage="Skip to Review"
1880
                description="The text displayed on the 'Skip to Review' button of the Job Builder Skills Preview modal."
1881
              />
1882
            </Modal.FooterMiddleBtn>
1883
          )}
1884
          <Modal.FooterConfirmBtn>
1885
            <FormattedMessage
1886
              id="jobBuilder.skills.previewModalConfirmLabel"
1887
              defaultMessage="Next Step"
1888
              description="The text displayed on the confirm button of the Job Builder Skills Preview modal."
1889
            />
1890
          </Modal.FooterConfirmBtn>
1891
        </Modal.Footer>
1892
      </Modal>
1893
    </>
1894
  );
1895
};
1896
1897
export default JobSkills;
1898