@forwardimpact/pathway 0.25.15 → 0.25.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fit-pathway.js +62 -54
- package/package.json +1 -3
- package/src/commands/agent-io.js +120 -0
- package/src/commands/agent.js +266 -349
- package/src/commands/init.js +2 -2
- package/src/commands/job.js +237 -183
- package/src/components/comparison-radar.js +118 -103
- package/src/components/progression-table.js +244 -208
- package/src/formatters/interview/markdown.js +100 -88
- package/src/formatters/job/description.js +76 -75
- package/src/formatters/job/dom.js +113 -97
- package/src/formatters/level/dom.js +87 -102
- package/src/formatters/questions/markdown.js +37 -33
- package/src/formatters/questions/shared.js +142 -75
- package/src/formatters/skill/dom.js +102 -93
- package/src/lib/comparison-radar-chart.js +256 -0
- package/src/lib/radar-utils.js +199 -0
- package/src/lib/radar.js +25 -662
- package/src/pages/agent-builder-download.js +170 -0
- package/src/pages/agent-builder-preview.js +344 -0
- package/src/pages/agent-builder.js +6 -550
- package/src/pages/progress-comparison.js +110 -0
- package/src/pages/progress.js +11 -111
- package/src/pages/self-assessment-steps.js +494 -0
- package/src/pages/self-assessment.js +54 -504
package/src/pages/progress.js
CHANGED
|
@@ -7,11 +7,6 @@ import { render, div, h1, h2, p, a, label, section } from "../lib/render.js";
|
|
|
7
7
|
import { getState } from "../lib/state.js";
|
|
8
8
|
import { createBackLink } from "../components/nav.js";
|
|
9
9
|
import { createStatCard } from "../components/card.js";
|
|
10
|
-
import {
|
|
11
|
-
createComparisonSkillRadar,
|
|
12
|
-
createComparisonBehaviourRadar,
|
|
13
|
-
} from "../components/comparison-radar.js";
|
|
14
|
-
import { createProgressionTable } from "../components/progression-table.js";
|
|
15
10
|
import { renderError } from "../components/error-page.js";
|
|
16
11
|
import {
|
|
17
12
|
createSelectWithValue,
|
|
@@ -23,6 +18,7 @@ import {
|
|
|
23
18
|
getDefaultTargetLevel,
|
|
24
19
|
isValidCombination,
|
|
25
20
|
} from "../formatters/progress/shared.js";
|
|
21
|
+
import { buildComparisonResult } from "./progress-comparison.js";
|
|
26
22
|
|
|
27
23
|
/**
|
|
28
24
|
* Render career progress detail page
|
|
@@ -223,10 +219,8 @@ function createComparisonSelectorsSection({
|
|
|
223
219
|
* Update the comparison results based on current selections
|
|
224
220
|
*/
|
|
225
221
|
function updateComparison() {
|
|
226
|
-
// Clear previous results
|
|
227
222
|
comparisonResultsContainer.innerHTML = "";
|
|
228
223
|
|
|
229
|
-
// Track can be empty string for generalist, but discipline and level are required
|
|
230
224
|
if (!selectedDisciplineId || !selectedLevelId) {
|
|
231
225
|
comparisonResultsContainer.appendChild(
|
|
232
226
|
div(
|
|
@@ -244,16 +238,12 @@ function createComparisonSelectorsSection({
|
|
|
244
238
|
(d) => d.id === selectedDisciplineId,
|
|
245
239
|
);
|
|
246
240
|
const targetLevel = data.levels.find((g) => g.id === selectedLevelId);
|
|
247
|
-
// selectedTrackId can be empty string for generalist
|
|
248
241
|
const targetTrack = selectedTrackId
|
|
249
242
|
? data.tracks.find((t) => t.id === selectedTrackId)
|
|
250
243
|
: null;
|
|
251
244
|
|
|
252
|
-
if (!targetDiscipline || !targetLevel)
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
245
|
+
if (!targetDiscipline || !targetLevel) return;
|
|
255
246
|
|
|
256
|
-
// Check if comparing to same role
|
|
257
247
|
if (
|
|
258
248
|
targetDiscipline.id === discipline.id &&
|
|
259
249
|
targetLevel.id === currentLevel.id &&
|
|
@@ -271,7 +261,6 @@ function createComparisonSelectorsSection({
|
|
|
271
261
|
return;
|
|
272
262
|
}
|
|
273
263
|
|
|
274
|
-
// Use formatter shared module to analyze the progression
|
|
275
264
|
const progressionView = prepareCustomProgression({
|
|
276
265
|
discipline,
|
|
277
266
|
currentLevel,
|
|
@@ -293,102 +282,17 @@ function createComparisonSelectorsSection({
|
|
|
293
282
|
return;
|
|
294
283
|
}
|
|
295
284
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
summary.skillsGained > 0
|
|
306
|
-
? createStatCard({ value: summary.skillsGained, label: "New Skills" })
|
|
307
|
-
: null,
|
|
308
|
-
createStatCard({
|
|
309
|
-
value: summary.skillsUp,
|
|
310
|
-
label: "Skills to Grow",
|
|
311
|
-
}),
|
|
312
|
-
summary.skillsDown > 0
|
|
313
|
-
? createStatCard({
|
|
314
|
-
value: summary.skillsDown,
|
|
315
|
-
label: "Skills Decrease",
|
|
316
|
-
})
|
|
317
|
-
: null,
|
|
318
|
-
summary.skillsLost > 0
|
|
319
|
-
? createStatCard({
|
|
320
|
-
value: summary.skillsLost,
|
|
321
|
-
label: "Skills Removed",
|
|
322
|
-
})
|
|
323
|
-
: null,
|
|
324
|
-
createStatCard({
|
|
325
|
-
value: summary.behavioursUp,
|
|
326
|
-
label: "Behaviours to Mature",
|
|
327
|
-
}),
|
|
328
|
-
summary.behavioursDown > 0
|
|
329
|
-
? createStatCard({
|
|
330
|
-
value: summary.behavioursDown,
|
|
331
|
-
label: "Behaviours Decrease",
|
|
332
|
-
})
|
|
333
|
-
: null,
|
|
334
|
-
),
|
|
335
|
-
|
|
336
|
-
// Comparison radars
|
|
337
|
-
div(
|
|
338
|
-
{ className: "section auto-grid-lg" },
|
|
339
|
-
createComparisonSkillRadar(
|
|
340
|
-
currentJobView.skillMatrix,
|
|
341
|
-
target.skillMatrix,
|
|
342
|
-
{
|
|
343
|
-
title: "Skills Comparison",
|
|
344
|
-
currentLabel: `Current (${currentLevel.id})`,
|
|
345
|
-
targetLabel: `Target (${targetLevel.id})`,
|
|
346
|
-
size: 400,
|
|
347
|
-
capabilities: data.capabilities,
|
|
348
|
-
},
|
|
349
|
-
),
|
|
350
|
-
createComparisonBehaviourRadar(
|
|
351
|
-
currentJobView.behaviourProfile,
|
|
352
|
-
target.behaviourProfile,
|
|
353
|
-
{
|
|
354
|
-
title: "Behaviours Comparison",
|
|
355
|
-
currentLabel: `Current (${currentLevel.id})`,
|
|
356
|
-
targetLabel: `Target (${targetLevel.id})`,
|
|
357
|
-
size: 400,
|
|
358
|
-
},
|
|
359
|
-
),
|
|
360
|
-
),
|
|
361
|
-
|
|
362
|
-
// Skill changes section
|
|
363
|
-
section(
|
|
364
|
-
{ className: "section section-detail" },
|
|
365
|
-
h2({ className: "section-title" }, "Skill Changes"),
|
|
366
|
-
createProgressionTable(skillChanges, "skill"),
|
|
367
|
-
),
|
|
368
|
-
|
|
369
|
-
// Behaviour changes section
|
|
370
|
-
section(
|
|
371
|
-
{ className: "section section-detail" },
|
|
372
|
-
h2({ className: "section-title" }, "Behaviour Changes"),
|
|
373
|
-
createProgressionTable(behaviourChanges, "behaviour"),
|
|
374
|
-
),
|
|
375
|
-
|
|
376
|
-
// Link to target job
|
|
377
|
-
div(
|
|
378
|
-
{ className: "page-actions" },
|
|
379
|
-
a(
|
|
380
|
-
{
|
|
381
|
-
href: targetTrack
|
|
382
|
-
? `#/job/${targetDiscipline.id}/${targetLevel.id}/${targetTrack.id}`
|
|
383
|
-
: `#/job/${targetDiscipline.id}/${targetLevel.id}`,
|
|
384
|
-
className: "btn btn-secondary",
|
|
385
|
-
},
|
|
386
|
-
`View ${targetLevel.id}${targetTrack ? ` ${targetTrack.name}` : ""} Job Definition →`,
|
|
387
|
-
),
|
|
285
|
+
comparisonResultsContainer.appendChild(
|
|
286
|
+
buildComparisonResult(
|
|
287
|
+
progressionView,
|
|
288
|
+
currentJobView,
|
|
289
|
+
currentLevel,
|
|
290
|
+
targetLevel,
|
|
291
|
+
targetTrack,
|
|
292
|
+
targetDiscipline,
|
|
293
|
+
data,
|
|
388
294
|
),
|
|
389
295
|
);
|
|
390
|
-
|
|
391
|
-
comparisonResultsContainer.appendChild(result);
|
|
392
296
|
}
|
|
393
297
|
|
|
394
298
|
// Get initial available options
|
|
@@ -453,7 +357,6 @@ function createComparisonSelectorsSection({
|
|
|
453
357
|
trackSelectEl.appendChild(opt);
|
|
454
358
|
}
|
|
455
359
|
|
|
456
|
-
// Try to keep current selection if valid
|
|
457
360
|
const hasValidTrack = availableOptions.tracks.find(
|
|
458
361
|
(t) => t.id === selectedTrackId,
|
|
459
362
|
);
|
|
@@ -461,9 +364,6 @@ function createComparisonSelectorsSection({
|
|
|
461
364
|
selectedTrackId === "" && availableOptions.allowsTrackless;
|
|
462
365
|
if (hasValidTrack || isValidGeneralist) {
|
|
463
366
|
trackSelectEl.value = selectedTrackId;
|
|
464
|
-
} else if (availableOptions.allowsTrackless) {
|
|
465
|
-
selectedTrackId = "";
|
|
466
|
-
trackSelectEl.value = "";
|
|
467
367
|
} else {
|
|
468
368
|
selectedTrackId = "";
|
|
469
369
|
trackSelectEl.value = "";
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-assessment wizard step renderers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { div, h2, h3, h4, p, span, button, a } from "../lib/render.js";
|
|
6
|
+
import { createBadge } from "../components/card.js";
|
|
7
|
+
import { createDisciplineSelect } from "../lib/form-controls.js";
|
|
8
|
+
import {
|
|
9
|
+
SKILL_PROFICIENCY_ORDER,
|
|
10
|
+
BEHAVIOUR_MATURITY_ORDER,
|
|
11
|
+
getConceptEmoji,
|
|
12
|
+
} from "@forwardimpact/map/levels";
|
|
13
|
+
import { formatLevel } from "../lib/render.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a tip card
|
|
17
|
+
* @param {string} icon
|
|
18
|
+
* @param {string} title
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @returns {HTMLElement}
|
|
21
|
+
*/
|
|
22
|
+
function createTipCard(icon, title, text) {
|
|
23
|
+
return div(
|
|
24
|
+
{ className: "tip-card" },
|
|
25
|
+
span({ className: "tip-icon" }, icon),
|
|
26
|
+
h4({}, title),
|
|
27
|
+
p({}, text),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a level selection button
|
|
33
|
+
* @param {Object} item - Skill or behaviour
|
|
34
|
+
* @param {string} level - Level value
|
|
35
|
+
* @param {number} index - Level index
|
|
36
|
+
* @param {string} type - 'skill' or 'behaviour'
|
|
37
|
+
* @param {Object} assessmentState - Assessment state ref
|
|
38
|
+
* @param {Function} rerender - Function to trigger re-render
|
|
39
|
+
* @returns {HTMLElement}
|
|
40
|
+
*/
|
|
41
|
+
export function createLevelButton(
|
|
42
|
+
item,
|
|
43
|
+
level,
|
|
44
|
+
index,
|
|
45
|
+
type,
|
|
46
|
+
assessmentState,
|
|
47
|
+
rerender,
|
|
48
|
+
) {
|
|
49
|
+
const stateKey = type === "skill" ? "skills" : "behaviours";
|
|
50
|
+
const currentLevel = assessmentState[stateKey][item.id];
|
|
51
|
+
const isSelected = currentLevel === level;
|
|
52
|
+
const proficiencyDescriptions =
|
|
53
|
+
type === "skill" ? item.proficiencyDescriptions : item.maturityDescriptions;
|
|
54
|
+
const description = proficiencyDescriptions?.[level] || "";
|
|
55
|
+
|
|
56
|
+
return button(
|
|
57
|
+
{
|
|
58
|
+
className: `level-btn level-${index + 1} ${isSelected ? "selected" : ""}`,
|
|
59
|
+
title: `${formatLevel(level)}: ${description}`,
|
|
60
|
+
onClick: () => {
|
|
61
|
+
assessmentState[stateKey][item.id] = level;
|
|
62
|
+
rerender();
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
span({ className: "level-btn-number" }, String(index + 1)),
|
|
66
|
+
span({ className: "level-btn-name" }, formatLevel(level)),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render introduction step
|
|
72
|
+
* @param {Object} data - App data
|
|
73
|
+
* @param {Object} assessmentState - Assessment state
|
|
74
|
+
* @returns {HTMLElement}
|
|
75
|
+
*/
|
|
76
|
+
export function renderIntroStep(data, assessmentState) {
|
|
77
|
+
return div(
|
|
78
|
+
{ className: "assessment-step assessment-intro" },
|
|
79
|
+
div(
|
|
80
|
+
{ className: "intro-card" },
|
|
81
|
+
h2({}, "Welcome to the Self-Assessment"),
|
|
82
|
+
p(
|
|
83
|
+
{},
|
|
84
|
+
"This assessment helps you understand your current skill proficiencies and behaviours, " +
|
|
85
|
+
"then matches you with suitable roles in the organization.",
|
|
86
|
+
),
|
|
87
|
+
|
|
88
|
+
div(
|
|
89
|
+
{ className: "intro-info" },
|
|
90
|
+
div(
|
|
91
|
+
{ className: "info-item" },
|
|
92
|
+
span(
|
|
93
|
+
{ className: "info-icon" },
|
|
94
|
+
getConceptEmoji(data.framework, "skill"),
|
|
95
|
+
),
|
|
96
|
+
div(
|
|
97
|
+
{},
|
|
98
|
+
h4({}, `${data.skills.length} Skills`),
|
|
99
|
+
p({}, "Across " + data.capabilities.length + " capabilities"),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
div(
|
|
103
|
+
{ className: "info-item" },
|
|
104
|
+
span(
|
|
105
|
+
{ className: "info-icon" },
|
|
106
|
+
getConceptEmoji(data.framework, "behaviour"),
|
|
107
|
+
),
|
|
108
|
+
div(
|
|
109
|
+
{},
|
|
110
|
+
h4({}, `${data.behaviours.length} Behaviours`),
|
|
111
|
+
p({}, "Key mindsets and ways of working"),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
div(
|
|
115
|
+
{ className: "info-item" },
|
|
116
|
+
span({ className: "info-icon" }, "⏱️"),
|
|
117
|
+
div({}, h4({}, "10-15 Minutes"), p({}, "Complete at your own pace")),
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
div(
|
|
122
|
+
{ className: "discipline-filter" },
|
|
123
|
+
h3({}, "Optional: Focus on a Discipline"),
|
|
124
|
+
p(
|
|
125
|
+
{ className: "text-muted" },
|
|
126
|
+
"Select a discipline to highlight which skills are most relevant for that role. " +
|
|
127
|
+
"You can still assess all skills.",
|
|
128
|
+
),
|
|
129
|
+
createDisciplineSelect({
|
|
130
|
+
id: "discipline-filter-select",
|
|
131
|
+
disciplines: data.disciplines,
|
|
132
|
+
initialValue: assessmentState.discipline || "",
|
|
133
|
+
placeholder: "Select discipline",
|
|
134
|
+
onChange: (value) => {
|
|
135
|
+
assessmentState.discipline = value || null;
|
|
136
|
+
},
|
|
137
|
+
getDisplayName: (d) => d.specialization,
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
div(
|
|
142
|
+
{ className: "intro-tips" },
|
|
143
|
+
h3({}, "Tips for Accurate Self-Assessment"),
|
|
144
|
+
div(
|
|
145
|
+
{ className: "auto-grid-sm" },
|
|
146
|
+
createTipCard(
|
|
147
|
+
"🎯",
|
|
148
|
+
"Be Honest",
|
|
149
|
+
"Rate yourself where you genuinely are, not where you aspire to be.",
|
|
150
|
+
),
|
|
151
|
+
createTipCard(
|
|
152
|
+
"📚",
|
|
153
|
+
"Read Descriptions",
|
|
154
|
+
"Hover over levels to see detailed descriptions for each.",
|
|
155
|
+
),
|
|
156
|
+
createTipCard(
|
|
157
|
+
"⏭️",
|
|
158
|
+
"Skip if Unsure",
|
|
159
|
+
"You can leave items unrated and come back later.",
|
|
160
|
+
),
|
|
161
|
+
createTipCard(
|
|
162
|
+
"💾",
|
|
163
|
+
"Auto-Saved",
|
|
164
|
+
"Your progress is kept while you navigate between steps.",
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a skill assessment item
|
|
174
|
+
* @param {Object} skill - Skill data
|
|
175
|
+
* @param {string|null} relevance - Skill relevance for selected discipline
|
|
176
|
+
* @param {Object} assessmentState
|
|
177
|
+
* @param {Function} rerender
|
|
178
|
+
* @returns {HTMLElement}
|
|
179
|
+
*/
|
|
180
|
+
function createSkillAssessmentItem(
|
|
181
|
+
skill,
|
|
182
|
+
relevance,
|
|
183
|
+
assessmentState,
|
|
184
|
+
rerender,
|
|
185
|
+
) {
|
|
186
|
+
const currentLevel = assessmentState.skills[skill.id];
|
|
187
|
+
|
|
188
|
+
return div(
|
|
189
|
+
{
|
|
190
|
+
className: `assessment-item ${currentLevel ? "assessed" : ""} ${relevance ? `relevance-${relevance}` : ""}`,
|
|
191
|
+
},
|
|
192
|
+
div(
|
|
193
|
+
{ className: "assessment-item-header" },
|
|
194
|
+
div(
|
|
195
|
+
{ className: "assessment-item-title" },
|
|
196
|
+
a({ href: `#/skill/${skill.id}` }, skill.name),
|
|
197
|
+
relevance && createBadge(relevance, relevance),
|
|
198
|
+
),
|
|
199
|
+
currentLevel &&
|
|
200
|
+
span({ className: "current-level-badge" }, formatLevel(currentLevel)),
|
|
201
|
+
),
|
|
202
|
+
|
|
203
|
+
p({ className: "assessment-item-description" }, skill.description),
|
|
204
|
+
|
|
205
|
+
div(
|
|
206
|
+
{ className: "level-selector" },
|
|
207
|
+
...SKILL_PROFICIENCY_ORDER.map((level, index) =>
|
|
208
|
+
createLevelButton(
|
|
209
|
+
skill,
|
|
210
|
+
level,
|
|
211
|
+
index,
|
|
212
|
+
"skill",
|
|
213
|
+
assessmentState,
|
|
214
|
+
rerender,
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
button(
|
|
218
|
+
{
|
|
219
|
+
className: "level-clear-btn",
|
|
220
|
+
title: "Clear selection",
|
|
221
|
+
onClick: () => {
|
|
222
|
+
delete assessmentState.skills[skill.id];
|
|
223
|
+
rerender();
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
"✕",
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Render skills assessment step
|
|
234
|
+
* @param {Object} step - Step configuration with capability and items
|
|
235
|
+
* @param {Object} data - App data
|
|
236
|
+
* @param {Object} assessmentState
|
|
237
|
+
* @param {Function} rerender
|
|
238
|
+
* @param {Function} formatCapability
|
|
239
|
+
* @returns {HTMLElement}
|
|
240
|
+
*/
|
|
241
|
+
export function renderSkillsStep(
|
|
242
|
+
step,
|
|
243
|
+
data,
|
|
244
|
+
assessmentState,
|
|
245
|
+
rerender,
|
|
246
|
+
formatCapability,
|
|
247
|
+
) {
|
|
248
|
+
const { capability, items } = step;
|
|
249
|
+
const selectedDiscipline = assessmentState.discipline
|
|
250
|
+
? data.disciplines.find((d) => d.id === assessmentState.discipline)
|
|
251
|
+
: null;
|
|
252
|
+
|
|
253
|
+
const getSkillRelevance = (skill) => {
|
|
254
|
+
if (!selectedDiscipline) return null;
|
|
255
|
+
if (selectedDiscipline.coreSkills?.includes(skill.id)) return "primary";
|
|
256
|
+
if (selectedDiscipline.supportingSkills?.includes(skill.id))
|
|
257
|
+
return "secondary";
|
|
258
|
+
if (selectedDiscipline.broadSkills?.includes(skill.id)) return "broad";
|
|
259
|
+
return null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
263
|
+
const relevanceA = getSkillRelevance(a);
|
|
264
|
+
const relevanceB = getSkillRelevance(b);
|
|
265
|
+
const order = { primary: 0, secondary: 1, broad: 2 };
|
|
266
|
+
|
|
267
|
+
if (relevanceA && !relevanceB) return -1;
|
|
268
|
+
if (!relevanceA && relevanceB) return 1;
|
|
269
|
+
if (relevanceA && relevanceB) {
|
|
270
|
+
return (order[relevanceA] ?? 3) - (order[relevanceB] ?? 3);
|
|
271
|
+
}
|
|
272
|
+
return a.name.localeCompare(b.name);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const assessedCount = items.filter(
|
|
276
|
+
(item) => assessmentState.skills[item.id],
|
|
277
|
+
).length;
|
|
278
|
+
|
|
279
|
+
return div(
|
|
280
|
+
{ className: "assessment-step" },
|
|
281
|
+
div(
|
|
282
|
+
{ className: "step-header" },
|
|
283
|
+
h2(
|
|
284
|
+
{},
|
|
285
|
+
span({ className: "step-header-icon" }, step.icon),
|
|
286
|
+
` ${formatCapability(capability, data.capabilities)} Skills`,
|
|
287
|
+
),
|
|
288
|
+
span(
|
|
289
|
+
{ className: "step-progress" },
|
|
290
|
+
`${assessedCount}/${items.length} rated`,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
|
|
294
|
+
selectedDiscipline &&
|
|
295
|
+
div(
|
|
296
|
+
{ className: "discipline-context" },
|
|
297
|
+
span({}, `Showing relevance for: `),
|
|
298
|
+
span(
|
|
299
|
+
{ className: "discipline-name" },
|
|
300
|
+
selectedDiscipline.specialization,
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
|
|
304
|
+
div(
|
|
305
|
+
{ className: "assessment-items" },
|
|
306
|
+
...sortedItems.map((skill) =>
|
|
307
|
+
createSkillAssessmentItem(
|
|
308
|
+
skill,
|
|
309
|
+
getSkillRelevance(skill),
|
|
310
|
+
assessmentState,
|
|
311
|
+
rerender,
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a behaviour assessment item
|
|
320
|
+
* @param {Object} behaviour - Behaviour data
|
|
321
|
+
* @param {Object} assessmentState
|
|
322
|
+
* @param {Function} rerender
|
|
323
|
+
* @returns {HTMLElement}
|
|
324
|
+
*/
|
|
325
|
+
function createBehaviourAssessmentItem(behaviour, assessmentState, rerender) {
|
|
326
|
+
const currentLevel = assessmentState.behaviours[behaviour.id];
|
|
327
|
+
|
|
328
|
+
return div(
|
|
329
|
+
{ className: `assessment-item ${currentLevel ? "assessed" : ""}` },
|
|
330
|
+
div(
|
|
331
|
+
{ className: "assessment-item-header" },
|
|
332
|
+
div(
|
|
333
|
+
{ className: "assessment-item-title" },
|
|
334
|
+
a({ href: `#/behaviour/${behaviour.id}` }, behaviour.name),
|
|
335
|
+
),
|
|
336
|
+
currentLevel &&
|
|
337
|
+
span({ className: "current-level-badge" }, formatLevel(currentLevel)),
|
|
338
|
+
),
|
|
339
|
+
|
|
340
|
+
p({ className: "assessment-item-description" }, behaviour.description),
|
|
341
|
+
|
|
342
|
+
div(
|
|
343
|
+
{ className: "level-selector" },
|
|
344
|
+
...BEHAVIOUR_MATURITY_ORDER.map((level, index) =>
|
|
345
|
+
createLevelButton(
|
|
346
|
+
behaviour,
|
|
347
|
+
level,
|
|
348
|
+
index,
|
|
349
|
+
"behaviour",
|
|
350
|
+
assessmentState,
|
|
351
|
+
rerender,
|
|
352
|
+
),
|
|
353
|
+
),
|
|
354
|
+
button(
|
|
355
|
+
{
|
|
356
|
+
className: "level-clear-btn",
|
|
357
|
+
title: "Clear selection",
|
|
358
|
+
onClick: () => {
|
|
359
|
+
delete assessmentState.behaviours[behaviour.id];
|
|
360
|
+
rerender();
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
"✕",
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Render behaviours assessment step
|
|
371
|
+
* @param {Object} step - Step configuration
|
|
372
|
+
* @param {Object} data - App data
|
|
373
|
+
* @param {Object} assessmentState
|
|
374
|
+
* @param {Function} rerender
|
|
375
|
+
* @returns {HTMLElement}
|
|
376
|
+
*/
|
|
377
|
+
export function renderBehavioursStep(step, data, assessmentState, rerender) {
|
|
378
|
+
const { items } = step;
|
|
379
|
+
const assessedCount = items.filter(
|
|
380
|
+
(item) => assessmentState.behaviours[item.id],
|
|
381
|
+
).length;
|
|
382
|
+
|
|
383
|
+
return div(
|
|
384
|
+
{ className: "assessment-step" },
|
|
385
|
+
div(
|
|
386
|
+
{ className: "step-header" },
|
|
387
|
+
h2(
|
|
388
|
+
{},
|
|
389
|
+
span(
|
|
390
|
+
{ className: "step-header-icon" },
|
|
391
|
+
getConceptEmoji(data.framework, "behaviour"),
|
|
392
|
+
),
|
|
393
|
+
" Behaviours",
|
|
394
|
+
),
|
|
395
|
+
span(
|
|
396
|
+
{ className: "step-progress" },
|
|
397
|
+
`${assessedCount}/${items.length} rated`,
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
|
|
401
|
+
p(
|
|
402
|
+
{ className: "step-intro" },
|
|
403
|
+
"Behaviours describe how you approach work—your mindsets and ways of working. " +
|
|
404
|
+
"These are equally important as technical skills.",
|
|
405
|
+
),
|
|
406
|
+
|
|
407
|
+
div(
|
|
408
|
+
{ className: "assessment-items" },
|
|
409
|
+
...items.map((behaviour) =>
|
|
410
|
+
createBehaviourAssessmentItem(behaviour, assessmentState, rerender),
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Render results preview before navigating to full results
|
|
418
|
+
* @param {Object} data - App data
|
|
419
|
+
* @param {Object} assessmentState
|
|
420
|
+
* @param {Function} calculateProgress
|
|
421
|
+
* @returns {HTMLElement}
|
|
422
|
+
*/
|
|
423
|
+
export function renderResultsPreview(data, assessmentState, calculateProgress) {
|
|
424
|
+
const progress = calculateProgress(data);
|
|
425
|
+
const skillCount = Object.keys(assessmentState.skills).length;
|
|
426
|
+
const behaviourCount = Object.keys(assessmentState.behaviours).length;
|
|
427
|
+
|
|
428
|
+
if (progress < 20) {
|
|
429
|
+
return div(
|
|
430
|
+
{ className: "assessment-step results-preview" },
|
|
431
|
+
div(
|
|
432
|
+
{ className: "results-incomplete" },
|
|
433
|
+
h2({}, "Complete More of the Assessment"),
|
|
434
|
+
p(
|
|
435
|
+
{},
|
|
436
|
+
"Please complete at least 20% of the assessment to see job matches. " +
|
|
437
|
+
`You've currently assessed ${skillCount} skills and ${behaviourCount} behaviours.`,
|
|
438
|
+
),
|
|
439
|
+
div(
|
|
440
|
+
{ className: "progress-summary" },
|
|
441
|
+
div(
|
|
442
|
+
{ className: "progress-bar large" },
|
|
443
|
+
div({
|
|
444
|
+
className: "progress-bar-fill",
|
|
445
|
+
style: `width: ${progress}%`,
|
|
446
|
+
}),
|
|
447
|
+
),
|
|
448
|
+
span({}, `${progress}% complete`),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return div(
|
|
455
|
+
{ className: "assessment-step results-preview" },
|
|
456
|
+
div(
|
|
457
|
+
{ className: "results-ready" },
|
|
458
|
+
h2({}, "🎉 Assessment Complete!"),
|
|
459
|
+
p({}, "Great work! You're ready to see your job matches."),
|
|
460
|
+
|
|
461
|
+
div(
|
|
462
|
+
{ className: "results-summary" },
|
|
463
|
+
div(
|
|
464
|
+
{ className: "summary-stat" },
|
|
465
|
+
span({ className: "summary-value" }, String(skillCount)),
|
|
466
|
+
span({ className: "summary-label" }, "Skills Assessed"),
|
|
467
|
+
),
|
|
468
|
+
div(
|
|
469
|
+
{ className: "summary-stat" },
|
|
470
|
+
span({ className: "summary-value" }, String(behaviourCount)),
|
|
471
|
+
span({ className: "summary-label" }, "Behaviours Assessed"),
|
|
472
|
+
),
|
|
473
|
+
div(
|
|
474
|
+
{ className: "summary-stat" },
|
|
475
|
+
span({ className: "summary-value" }, `${progress}%`),
|
|
476
|
+
span({ className: "summary-label" }, "Complete"),
|
|
477
|
+
),
|
|
478
|
+
),
|
|
479
|
+
|
|
480
|
+
div(
|
|
481
|
+
{ className: "results-actions" },
|
|
482
|
+
button(
|
|
483
|
+
{
|
|
484
|
+
className: "btn btn-primary btn-lg",
|
|
485
|
+
onClick: () => {
|
|
486
|
+
window.location.hash = "/self-assessment/results";
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
"View My Job Matches →",
|
|
490
|
+
),
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
}
|