@forwardimpact/pathway 0.1.0
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/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- package/package.json +68 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-assessment results page
|
|
3
|
+
* Displays job matches, gaps, and development recommendations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
render,
|
|
8
|
+
div,
|
|
9
|
+
h1,
|
|
10
|
+
h2,
|
|
11
|
+
h3,
|
|
12
|
+
h4,
|
|
13
|
+
p,
|
|
14
|
+
span,
|
|
15
|
+
button,
|
|
16
|
+
a,
|
|
17
|
+
} from "../lib/render.js";
|
|
18
|
+
import { getState } from "../lib/state.js";
|
|
19
|
+
import { createBadge } from "../components/card.js";
|
|
20
|
+
import { formatLevel } from "../lib/render.js";
|
|
21
|
+
import { getAssessmentState, resetAssessment } from "./self-assessment.js";
|
|
22
|
+
import { findRealisticMatches } from "../model/matching.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the assessment results page
|
|
26
|
+
*/
|
|
27
|
+
export function renderAssessmentResults() {
|
|
28
|
+
const { data } = getState();
|
|
29
|
+
const assessmentState = getAssessmentState();
|
|
30
|
+
|
|
31
|
+
// Check if there's any assessment data
|
|
32
|
+
const hasSkills = Object.keys(assessmentState.skills).length > 0;
|
|
33
|
+
const hasBehaviours = Object.keys(assessmentState.behaviours).length > 0;
|
|
34
|
+
|
|
35
|
+
if (!hasSkills && !hasBehaviours) {
|
|
36
|
+
renderNoAssessment();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create self-assessment object in the expected format
|
|
41
|
+
const selfAssessment = {
|
|
42
|
+
id: "current-assessment",
|
|
43
|
+
skills: assessmentState.skills,
|
|
44
|
+
behaviours: assessmentState.behaviours,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Find matching jobs with realistic scoring
|
|
48
|
+
const { matches, matchesByTier, estimatedGrade } = findRealisticMatches({
|
|
49
|
+
selfAssessment,
|
|
50
|
+
disciplines: data.disciplines,
|
|
51
|
+
grades: data.grades,
|
|
52
|
+
tracks: data.tracks,
|
|
53
|
+
skills: data.skills,
|
|
54
|
+
behaviours: data.behaviours,
|
|
55
|
+
filterByGrade: false, // Show all grades but group by tier
|
|
56
|
+
topN: 20,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const page = div(
|
|
60
|
+
{ className: "assessment-results-page" },
|
|
61
|
+
// Header
|
|
62
|
+
div(
|
|
63
|
+
{ className: "page-header" },
|
|
64
|
+
a(
|
|
65
|
+
{ href: "#/self-assessment", className: "back-link" },
|
|
66
|
+
"← Back to Assessment",
|
|
67
|
+
),
|
|
68
|
+
h1({ className: "page-title" }, "Your Job Matches"),
|
|
69
|
+
p(
|
|
70
|
+
{ className: "page-description" },
|
|
71
|
+
"Based on your self-assessment, here are the roles that best match your current skills and behaviours.",
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
|
|
75
|
+
// Summary stats
|
|
76
|
+
createSummaryStats(assessmentState, data, estimatedGrade),
|
|
77
|
+
|
|
78
|
+
// Top matches grouped by tier
|
|
79
|
+
createMatchesSection(matches, matchesByTier, selfAssessment, data),
|
|
80
|
+
|
|
81
|
+
// Actions
|
|
82
|
+
div(
|
|
83
|
+
{ className: "results-actions-footer" },
|
|
84
|
+
button(
|
|
85
|
+
{
|
|
86
|
+
className: "btn btn-secondary",
|
|
87
|
+
onClick: () => {
|
|
88
|
+
window.location.hash = "/self-assessment";
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"← Edit Assessment",
|
|
92
|
+
),
|
|
93
|
+
button(
|
|
94
|
+
{
|
|
95
|
+
className: "btn btn-secondary",
|
|
96
|
+
onClick: () => {
|
|
97
|
+
if (
|
|
98
|
+
confirm(
|
|
99
|
+
"Are you sure you want to start over? This will clear your assessment.",
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
resetAssessment();
|
|
103
|
+
window.location.hash = "/self-assessment";
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
"Start Over",
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
render(page);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render message when no assessment data exists
|
|
117
|
+
*/
|
|
118
|
+
function renderNoAssessment() {
|
|
119
|
+
render(
|
|
120
|
+
div(
|
|
121
|
+
{ className: "assessment-results-page" },
|
|
122
|
+
div(
|
|
123
|
+
{ className: "no-assessment-message" },
|
|
124
|
+
h1({}, "No Assessment Data"),
|
|
125
|
+
p(
|
|
126
|
+
{},
|
|
127
|
+
"You haven't completed a self-assessment yet. Complete the assessment to see your job matches.",
|
|
128
|
+
),
|
|
129
|
+
a(
|
|
130
|
+
{ href: "#/self-assessment", className: "btn btn-primary btn-lg" },
|
|
131
|
+
"Start Self-Assessment →",
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create summary statistics section
|
|
140
|
+
* @param {Object} assessmentState - Current assessment state
|
|
141
|
+
* @param {Object} data - App data
|
|
142
|
+
* @param {{grade: Object, confidence: number}} estimatedGrade - Estimated best-fit grade
|
|
143
|
+
* @returns {HTMLElement}
|
|
144
|
+
*/
|
|
145
|
+
function createSummaryStats(assessmentState, data, estimatedGrade) {
|
|
146
|
+
const skillCount = Object.keys(assessmentState.skills).length;
|
|
147
|
+
const behaviourCount = Object.keys(assessmentState.behaviours).length;
|
|
148
|
+
|
|
149
|
+
// Calculate average levels
|
|
150
|
+
const avgSkillLevel = calculateAverageLevel(
|
|
151
|
+
Object.values(assessmentState.skills),
|
|
152
|
+
["awareness", "foundational", "working", "practitioner", "expert"],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Get grade name based on track (default to professional)
|
|
156
|
+
const gradeName =
|
|
157
|
+
estimatedGrade.grade.professionalTitle ||
|
|
158
|
+
estimatedGrade.grade.name ||
|
|
159
|
+
estimatedGrade.grade.id;
|
|
160
|
+
const confidenceLabel =
|
|
161
|
+
estimatedGrade.confidence >= 0.7
|
|
162
|
+
? "High"
|
|
163
|
+
: estimatedGrade.confidence >= 0.4
|
|
164
|
+
? "Medium"
|
|
165
|
+
: "Low";
|
|
166
|
+
|
|
167
|
+
return div(
|
|
168
|
+
{ className: "results-summary-section" },
|
|
169
|
+
h2({}, "Assessment Summary"),
|
|
170
|
+
div(
|
|
171
|
+
{ className: "auto-grid-xs" },
|
|
172
|
+
createStatBox(
|
|
173
|
+
String(skillCount),
|
|
174
|
+
`of ${data.skills.length} Skills`,
|
|
175
|
+
"📊",
|
|
176
|
+
),
|
|
177
|
+
createStatBox(
|
|
178
|
+
String(behaviourCount),
|
|
179
|
+
`of ${data.behaviours.length} Behaviours`,
|
|
180
|
+
"🧠",
|
|
181
|
+
),
|
|
182
|
+
createStatBox(formatLevel(avgSkillLevel), "Avg Skill Level", "💡"),
|
|
183
|
+
createStatBox(gradeName, `Estimated Level (${confidenceLabel})`, "🎯"),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a stat box
|
|
190
|
+
* @param {string} value
|
|
191
|
+
* @param {string} label
|
|
192
|
+
* @param {string} icon
|
|
193
|
+
* @returns {HTMLElement}
|
|
194
|
+
*/
|
|
195
|
+
function createStatBox(value, label, icon) {
|
|
196
|
+
return div(
|
|
197
|
+
{ className: "result-stat-box" },
|
|
198
|
+
span({ className: "stat-icon" }, icon),
|
|
199
|
+
span({ className: "stat-value" }, value),
|
|
200
|
+
span({ className: "stat-label" }, label),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Calculate average level from array of levels
|
|
206
|
+
* @param {string[]} levels
|
|
207
|
+
* @param {string[]} levelOrder
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
function calculateAverageLevel(levels, levelOrder) {
|
|
211
|
+
if (levels.length === 0) return levelOrder[0];
|
|
212
|
+
|
|
213
|
+
const sum = levels.reduce((acc, level) => {
|
|
214
|
+
const index = levelOrder.indexOf(level);
|
|
215
|
+
return acc + (index >= 0 ? index : 0);
|
|
216
|
+
}, 0);
|
|
217
|
+
|
|
218
|
+
const avgIndex = Math.round(sum / levels.length);
|
|
219
|
+
return levelOrder[Math.min(avgIndex, levelOrder.length - 1)];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create the matches section grouped by tier
|
|
224
|
+
* @param {Array} matches - Job matches from findRealisticMatches
|
|
225
|
+
* @param {Object} matchesByTier - Matches grouped by tier
|
|
226
|
+
* @param {Object} selfAssessment - Self-assessment data
|
|
227
|
+
* @param {Object} data - App data
|
|
228
|
+
* @returns {HTMLElement}
|
|
229
|
+
*/
|
|
230
|
+
function createMatchesSection(matches, matchesByTier, selfAssessment, data) {
|
|
231
|
+
if (matches.length === 0) {
|
|
232
|
+
return div(
|
|
233
|
+
{ className: "no-matches" },
|
|
234
|
+
h2({}, "No Matches Found"),
|
|
235
|
+
p(
|
|
236
|
+
{},
|
|
237
|
+
"We couldn't find any suitable job matches. Try completing more of the assessment.",
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Create tier sections
|
|
243
|
+
const tierSections = [];
|
|
244
|
+
|
|
245
|
+
// Tier 1: Strong Matches
|
|
246
|
+
if (matchesByTier[1].length > 0) {
|
|
247
|
+
tierSections.push(
|
|
248
|
+
createTierSection(
|
|
249
|
+
1,
|
|
250
|
+
"Strong Matches",
|
|
251
|
+
"green",
|
|
252
|
+
"Ready for these roles now",
|
|
253
|
+
matchesByTier[1],
|
|
254
|
+
selfAssessment,
|
|
255
|
+
data,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Tier 2: Good Matches
|
|
261
|
+
if (matchesByTier[2].length > 0) {
|
|
262
|
+
tierSections.push(
|
|
263
|
+
createTierSection(
|
|
264
|
+
2,
|
|
265
|
+
"Good Matches",
|
|
266
|
+
"blue",
|
|
267
|
+
"Ready within 6-12 months of focused growth",
|
|
268
|
+
matchesByTier[2],
|
|
269
|
+
selfAssessment,
|
|
270
|
+
data,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Tier 3: Stretch Roles
|
|
276
|
+
if (matchesByTier[3].length > 0) {
|
|
277
|
+
tierSections.push(
|
|
278
|
+
createTierSection(
|
|
279
|
+
3,
|
|
280
|
+
"Stretch Roles",
|
|
281
|
+
"amber",
|
|
282
|
+
"Ambitious but achievable with dedicated development",
|
|
283
|
+
matchesByTier[3],
|
|
284
|
+
selfAssessment,
|
|
285
|
+
data,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Tier 4: Aspirational (show fewer)
|
|
291
|
+
if (matchesByTier[4].length > 0) {
|
|
292
|
+
tierSections.push(
|
|
293
|
+
createTierSection(
|
|
294
|
+
4,
|
|
295
|
+
"Aspirational",
|
|
296
|
+
"gray",
|
|
297
|
+
"Long-term career goals requiring significant growth",
|
|
298
|
+
matchesByTier[4].slice(0, 3),
|
|
299
|
+
selfAssessment,
|
|
300
|
+
data,
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return div(
|
|
306
|
+
{ className: "matches-section" },
|
|
307
|
+
h2({}, "Job Matches by Readiness"),
|
|
308
|
+
p(
|
|
309
|
+
{ className: "text-muted" },
|
|
310
|
+
"Jobs are grouped by how ready you are for them based on your current skills and behaviours.",
|
|
311
|
+
),
|
|
312
|
+
...tierSections,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create a tier section
|
|
318
|
+
* @param {number} tierNum - Tier number (1-4)
|
|
319
|
+
* @param {string} title - Section title
|
|
320
|
+
* @param {string} color - Color class
|
|
321
|
+
* @param {string} description - Tier description
|
|
322
|
+
* @param {Array} matches - Matches in this tier
|
|
323
|
+
* @param {Object} selfAssessment - Self-assessment data
|
|
324
|
+
* @param {Object} data - App data
|
|
325
|
+
* @returns {HTMLElement}
|
|
326
|
+
*/
|
|
327
|
+
function createTierSection(
|
|
328
|
+
tierNum,
|
|
329
|
+
title,
|
|
330
|
+
color,
|
|
331
|
+
description,
|
|
332
|
+
matches,
|
|
333
|
+
selfAssessment,
|
|
334
|
+
data,
|
|
335
|
+
) {
|
|
336
|
+
return div(
|
|
337
|
+
{ className: `tier-section tier-${tierNum} tier-color-${color}` },
|
|
338
|
+
div(
|
|
339
|
+
{ className: "tier-header" },
|
|
340
|
+
h3({ className: "tier-title" }, title),
|
|
341
|
+
span(
|
|
342
|
+
{ className: "tier-count" },
|
|
343
|
+
`${matches.length} role${matches.length !== 1 ? "s" : ""}`,
|
|
344
|
+
),
|
|
345
|
+
),
|
|
346
|
+
p({ className: "tier-description" }, description),
|
|
347
|
+
div(
|
|
348
|
+
{ className: "matches-list" },
|
|
349
|
+
...matches.map((match, index) =>
|
|
350
|
+
createMatchCard(match, index, selfAssessment, data),
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create a match card
|
|
358
|
+
* @param {Object} match - Job match object
|
|
359
|
+
* @param {number} index - Match index
|
|
360
|
+
* @param {Object} selfAssessment - Self-assessment data
|
|
361
|
+
* @param {Object} data - App data
|
|
362
|
+
* @returns {HTMLElement}
|
|
363
|
+
*/
|
|
364
|
+
function createMatchCard(match, _index, _selfAssessment, _data) {
|
|
365
|
+
const { job, analysis } = match;
|
|
366
|
+
const matchPercent = Math.round(analysis.overallScore * 100);
|
|
367
|
+
|
|
368
|
+
// Use tier from model for consistent classification
|
|
369
|
+
const tierColor = analysis.tier.color;
|
|
370
|
+
|
|
371
|
+
return div(
|
|
372
|
+
{ className: `match-card match-tier-${tierColor}` },
|
|
373
|
+
// Header
|
|
374
|
+
div(
|
|
375
|
+
{ className: "match-card-header" },
|
|
376
|
+
div(
|
|
377
|
+
{ className: "match-title-area" },
|
|
378
|
+
h3(
|
|
379
|
+
{ className: "match-job-title" },
|
|
380
|
+
a(
|
|
381
|
+
{
|
|
382
|
+
href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
|
|
383
|
+
},
|
|
384
|
+
job.title,
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
div(
|
|
388
|
+
{ className: "match-badges" },
|
|
389
|
+
createBadge(job.discipline.name, "default"),
|
|
390
|
+
createBadge(job.grade.name, "secondary"),
|
|
391
|
+
createBadge(job.track.name, "broad"),
|
|
392
|
+
),
|
|
393
|
+
),
|
|
394
|
+
div(
|
|
395
|
+
{ className: "match-score-area" },
|
|
396
|
+
div(
|
|
397
|
+
{ className: `match-score match-score-${tierColor}` },
|
|
398
|
+
span({ className: "score-value" }, `${matchPercent}%`),
|
|
399
|
+
span({ className: "score-label" }, "Match"),
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
),
|
|
403
|
+
|
|
404
|
+
// Score breakdown
|
|
405
|
+
div(
|
|
406
|
+
{ className: "auto-grid-sm" },
|
|
407
|
+
createScoreBar(
|
|
408
|
+
"Skills",
|
|
409
|
+
analysis.skillScore,
|
|
410
|
+
analysis.weightsUsed.skills,
|
|
411
|
+
),
|
|
412
|
+
createScoreBar(
|
|
413
|
+
"Behaviours",
|
|
414
|
+
analysis.behaviourScore,
|
|
415
|
+
analysis.weightsUsed.behaviours,
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
|
|
419
|
+
// Priority gaps section (use priorityGaps from model - top 3)
|
|
420
|
+
analysis.priorityGaps.length > 0 &&
|
|
421
|
+
div(
|
|
422
|
+
{ className: "match-gaps" },
|
|
423
|
+
h4({}, "Priority Development Areas"),
|
|
424
|
+
div(
|
|
425
|
+
{ className: "gaps-list" },
|
|
426
|
+
...analysis.priorityGaps.map((gap) => createGapItem(gap)),
|
|
427
|
+
analysis.gaps.length > 3 &&
|
|
428
|
+
span(
|
|
429
|
+
{ className: "more-gaps" },
|
|
430
|
+
`+${analysis.gaps.length - 3} more areas`,
|
|
431
|
+
),
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
|
|
435
|
+
// Actions
|
|
436
|
+
div(
|
|
437
|
+
{ className: "match-card-actions" },
|
|
438
|
+
a(
|
|
439
|
+
{
|
|
440
|
+
href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
|
|
441
|
+
className: "btn btn-secondary btn-sm",
|
|
442
|
+
},
|
|
443
|
+
"View Job Details",
|
|
444
|
+
),
|
|
445
|
+
a(
|
|
446
|
+
{
|
|
447
|
+
href: `#/interview/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
|
|
448
|
+
className: "btn btn-secondary btn-sm",
|
|
449
|
+
},
|
|
450
|
+
"Interview Prep",
|
|
451
|
+
),
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Create a score bar
|
|
458
|
+
* @param {string} label
|
|
459
|
+
* @param {number} score - Score from 0 to 1
|
|
460
|
+
* @param {number} weight - Weight factor
|
|
461
|
+
* @returns {HTMLElement}
|
|
462
|
+
*/
|
|
463
|
+
function createScoreBar(label, score, weight) {
|
|
464
|
+
const percent = Math.round(score * 100);
|
|
465
|
+
const weightPercent = Math.round(weight * 100);
|
|
466
|
+
|
|
467
|
+
return div(
|
|
468
|
+
{ className: "score-bar-item" },
|
|
469
|
+
div(
|
|
470
|
+
{ className: "score-bar-header" },
|
|
471
|
+
span({ className: "score-bar-label" }, label),
|
|
472
|
+
span(
|
|
473
|
+
{ className: "score-bar-values" },
|
|
474
|
+
`${percent}% (${weightPercent}% weight)`,
|
|
475
|
+
),
|
|
476
|
+
),
|
|
477
|
+
div(
|
|
478
|
+
{ className: "progress-bar" },
|
|
479
|
+
div({ className: "progress-bar-fill", style: `width: ${percent}%` }),
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Create a gap item
|
|
486
|
+
* @param {Object} gap - Gap information
|
|
487
|
+
* @returns {HTMLElement}
|
|
488
|
+
*/
|
|
489
|
+
function createGapItem(gap) {
|
|
490
|
+
const isSkill = gap.type === "skill";
|
|
491
|
+
|
|
492
|
+
return div(
|
|
493
|
+
{ className: "gap-item" },
|
|
494
|
+
span({ className: "gap-type-icon" }, isSkill ? "💡" : "🧠"),
|
|
495
|
+
span({ className: "gap-name" }, gap.name),
|
|
496
|
+
span(
|
|
497
|
+
{ className: "gap-levels" },
|
|
498
|
+
span({ className: "gap-current" }, formatLevel(gap.current)),
|
|
499
|
+
span({ className: "gap-arrow" }, "→"),
|
|
500
|
+
span({ className: "gap-required" }, formatLevel(gap.required)),
|
|
501
|
+
),
|
|
502
|
+
span(
|
|
503
|
+
{ className: `gap-size gap-size-${gap.gap}` },
|
|
504
|
+
`+${gap.gap} level${gap.gap > 1 ? "s" : ""}`,
|
|
505
|
+
),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviours pages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, div, h1, p } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createCardList } from "../components/list.js";
|
|
8
|
+
import { renderNotFound } from "../components/error-page.js";
|
|
9
|
+
import { prepareBehavioursList } from "../formatters/behaviour/shared.js";
|
|
10
|
+
import { behaviourToDOM } from "../formatters/behaviour/dom.js";
|
|
11
|
+
import { behaviourToCardConfig } from "../lib/card-mappers.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render behaviours list page
|
|
15
|
+
*/
|
|
16
|
+
export function renderBehavioursList() {
|
|
17
|
+
const { data } = getState();
|
|
18
|
+
const { framework } = data;
|
|
19
|
+
|
|
20
|
+
// Transform data for list view
|
|
21
|
+
const { items } = prepareBehavioursList(data.behaviours);
|
|
22
|
+
|
|
23
|
+
const page = div(
|
|
24
|
+
{ className: "behaviours-page" },
|
|
25
|
+
// Header
|
|
26
|
+
div(
|
|
27
|
+
{ className: "page-header" },
|
|
28
|
+
h1(
|
|
29
|
+
{ className: "page-title" },
|
|
30
|
+
framework.entityDefinitions.behaviour.title,
|
|
31
|
+
),
|
|
32
|
+
p(
|
|
33
|
+
{ className: "page-description" },
|
|
34
|
+
framework.entityDefinitions.behaviour.description.trim(),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
// Behaviours list
|
|
39
|
+
createCardList(items, behaviourToCardConfig, "No behaviours found."),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
render(page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render behaviour detail page
|
|
47
|
+
* @param {Object} params - Route params
|
|
48
|
+
*/
|
|
49
|
+
export function renderBehaviourDetail(params) {
|
|
50
|
+
const { data } = getState();
|
|
51
|
+
const behaviour = data.behaviours.find((b) => b.id === params.id);
|
|
52
|
+
|
|
53
|
+
if (!behaviour) {
|
|
54
|
+
renderNotFound({
|
|
55
|
+
entityType: "Behaviour",
|
|
56
|
+
entityId: params.id,
|
|
57
|
+
backPath: "/behaviour",
|
|
58
|
+
backText: "← Back to Behaviours",
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Use DOM formatter - it handles transformation internally
|
|
64
|
+
render(
|
|
65
|
+
behaviourToDOM(behaviour, {
|
|
66
|
+
drivers: data.drivers,
|
|
67
|
+
framework: data.framework,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disciplines pages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, div, h1, p } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createCardList } from "../components/list.js";
|
|
8
|
+
import { renderNotFound } from "../components/error-page.js";
|
|
9
|
+
import { prepareDisciplinesList } from "../formatters/discipline/shared.js";
|
|
10
|
+
import { disciplineToDOM } from "../formatters/discipline/dom.js";
|
|
11
|
+
import { disciplineToCardConfig } from "../lib/card-mappers.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render disciplines list page
|
|
15
|
+
*/
|
|
16
|
+
export function renderDisciplinesList() {
|
|
17
|
+
const { data } = getState();
|
|
18
|
+
const { framework } = data;
|
|
19
|
+
|
|
20
|
+
// Transform data for list view
|
|
21
|
+
const { items } = prepareDisciplinesList(data.disciplines);
|
|
22
|
+
|
|
23
|
+
const page = div(
|
|
24
|
+
{ className: "disciplines-page" },
|
|
25
|
+
// Header
|
|
26
|
+
div(
|
|
27
|
+
{ className: "page-header" },
|
|
28
|
+
h1(
|
|
29
|
+
{ className: "page-title" },
|
|
30
|
+
framework.entityDefinitions.discipline.title,
|
|
31
|
+
),
|
|
32
|
+
p(
|
|
33
|
+
{ className: "page-description" },
|
|
34
|
+
framework.entityDefinitions.discipline.description.trim(),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
// Disciplines list
|
|
39
|
+
createCardList(items, disciplineToCardConfig, "No disciplines found."),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
render(page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render discipline detail page
|
|
47
|
+
* @param {Object} params - Route params
|
|
48
|
+
*/
|
|
49
|
+
export function renderDisciplineDetail(params) {
|
|
50
|
+
const { data } = getState();
|
|
51
|
+
const discipline = data.disciplines.find((d) => d.id === params.id);
|
|
52
|
+
|
|
53
|
+
if (!discipline) {
|
|
54
|
+
renderNotFound({
|
|
55
|
+
entityType: "Discipline",
|
|
56
|
+
entityId: params.id,
|
|
57
|
+
backPath: "/discipline",
|
|
58
|
+
backText: "← Back to Disciplines",
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Use DOM formatter - it handles transformation internally
|
|
64
|
+
render(
|
|
65
|
+
disciplineToDOM(discipline, {
|
|
66
|
+
skills: data.skills,
|
|
67
|
+
behaviours: data.behaviours,
|
|
68
|
+
framework: data.framework,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|