placementt-core 1.400.939 → 1.400.941
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/lib/benchmarkEvidenceRules.d.ts +113 -0
- package/lib/benchmarkEvidenceRules.js +201 -0
- package/lib/benchmarkEvidenceRules.js.map +1 -0
- package/lib/benchmarkEvidenceRules.test.d.ts +1 -0
- package/lib/benchmarkEvidenceRules.test.js +77 -0
- package/lib/benchmarkEvidenceRules.test.js.map +1 -0
- package/lib/compassExport.d.ts +64 -0
- package/lib/compassExport.js +127 -0
- package/lib/compassExport.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/portfolioAggregate.d.ts +169 -0
- package/lib/portfolioAggregate.js +344 -0
- package/lib/portfolioAggregate.js.map +1 -0
- package/lib/typeDefinitions.d.ts +12 -0
- package/package.json +1 -1
- package/src/benchmarkEvidenceRules.test.ts +98 -0
- package/src/benchmarkEvidenceRules.ts +247 -0
- package/src/compassExport.ts +161 -0
- package/src/index.ts +3 -0
- package/src/portfolioAggregate.ts +515 -0
- package/src/typeDefinitions.ts +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Student Portfolio — shared aggregation layer.
|
|
3
|
+
*
|
|
4
|
+
* Design principle (see feature brief): "build one aggregation, three renders".
|
|
5
|
+
* This module is the single aggregation. It is deliberately PURE — no Firebase,
|
|
6
|
+
* no React, no Node — so it can run identically in:
|
|
7
|
+
* - the web client (usePortfolioData hook → StudentPortfolio page + student PDF),
|
|
8
|
+
* - the backend export callables (portfolio PDF, PfA annual-review pack, careers CSV).
|
|
9
|
+
*
|
|
10
|
+
* Callers are responsible for fetching the raw documents (they live in different
|
|
11
|
+
* places on client vs server); this module only shapes them into a portfolio.
|
|
12
|
+
*/
|
|
13
|
+
import { AspirationEntry, ExternalEvent, ExternalEventStudent, SkillAssessment, SkillLabel, StudentActivity, StudentPlacementData, UserData } from "./typeDefinitions";
|
|
14
|
+
export declare const PORTFOLIO_PRIMARY_COLOUR = "#1F68B3";
|
|
15
|
+
export declare const PORTFOLIO_SECONDARY_COLOUR = "#0AA4DE";
|
|
16
|
+
export declare const PORTFOLIO_TERTIARY_COLOUR = "#00C0C7";
|
|
17
|
+
export declare const PORTFOLIO_SECTION_LABELS: {
|
|
18
|
+
readonly header: "About me";
|
|
19
|
+
readonly skills: "My skills";
|
|
20
|
+
readonly experiences: "Things I've done";
|
|
21
|
+
readonly aspirations: "What I want to do next";
|
|
22
|
+
readonly evidence: "My evidence";
|
|
23
|
+
};
|
|
24
|
+
export declare const POST_SIXTEEN_LABELS: Record<string, string>;
|
|
25
|
+
export declare const ACTIVITY_TYPE_LABELS: Record<StudentActivity["activityType"], string>;
|
|
26
|
+
export type PfaPathway = "employmentAndFurtherLearning" | "independentLiving" | "communityInclusion" | "goodHealth";
|
|
27
|
+
export declare const PFA_PATHWAYS: PfaPathway[];
|
|
28
|
+
export declare const PFA_PATHWAY_LABELS: Record<PfaPathway, string>;
|
|
29
|
+
export declare const PFA_DEFAULT_PATHWAY: PfaPathway;
|
|
30
|
+
/** Extracurricular activity type → PfA pathway. */
|
|
31
|
+
export declare const ACTIVITY_TYPE_TO_PFA: Record<StudentActivity["activityType"], PfaPathway>;
|
|
32
|
+
/**
|
|
33
|
+
* Skills-framework category → PfA pathway. Hardcoded sensible default for now;
|
|
34
|
+
* the brief calls for this to be per-institute configurable later.
|
|
35
|
+
* TODO: make this mapping overridable per-institute (institute.pfaSkillMap).
|
|
36
|
+
*/
|
|
37
|
+
export declare const SKILL_CATEGORY_TO_PFA: Record<SkillLabel["category"], PfaPathway>;
|
|
38
|
+
export type PortfolioAspirationHeadline = {
|
|
39
|
+
/** e.g. "College", "Apprenticeship" — plain-English post-16 intent. */
|
|
40
|
+
route?: string;
|
|
41
|
+
/** Free-text "dream job" if the student has given one. */
|
|
42
|
+
dreamJob?: string;
|
|
43
|
+
};
|
|
44
|
+
export type PortfolioHeader = {
|
|
45
|
+
name: string;
|
|
46
|
+
school?: string;
|
|
47
|
+
yearGroup?: string | number;
|
|
48
|
+
cohortName?: string;
|
|
49
|
+
aspiration?: PortfolioAspirationHeadline;
|
|
50
|
+
};
|
|
51
|
+
/** One rater's score timeline for a skill (chronological, oldest → newest). */
|
|
52
|
+
export type PortfolioSkillTimeline = {
|
|
53
|
+
date: string;
|
|
54
|
+
score: number;
|
|
55
|
+
}[];
|
|
56
|
+
export type PortfolioSkill = {
|
|
57
|
+
skillId: string;
|
|
58
|
+
label: string;
|
|
59
|
+
category: SkillLabel["category"];
|
|
60
|
+
color: string;
|
|
61
|
+
/** Latest student self-score (0 when never assessed). */
|
|
62
|
+
latestScore: number;
|
|
63
|
+
/** First student score this academic year (for distance-travelled). */
|
|
64
|
+
firstScore: number;
|
|
65
|
+
/** latestScore − firstScore. */
|
|
66
|
+
distanceTravelled: number;
|
|
67
|
+
/** Latest score per rater — populated only where multi-rater data exists. */
|
|
68
|
+
byRater: {
|
|
69
|
+
students: number;
|
|
70
|
+
parents: number;
|
|
71
|
+
employers: number;
|
|
72
|
+
};
|
|
73
|
+
/** True when at least one parent/employer assessment exists. */
|
|
74
|
+
hasMultiRater: boolean;
|
|
75
|
+
timeline: PortfolioSkillTimeline;
|
|
76
|
+
};
|
|
77
|
+
export type PortfolioExperienceKind = "placement" | "event" | "activity";
|
|
78
|
+
export type PortfolioExperience = {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: PortfolioExperienceKind;
|
|
81
|
+
title: string;
|
|
82
|
+
organisation?: string;
|
|
83
|
+
/** dbstring — used for sorting. Falls back to created date. */
|
|
84
|
+
startDate?: string;
|
|
85
|
+
endDate?: string;
|
|
86
|
+
/** Human-readable date range for display. */
|
|
87
|
+
dateLabel?: string;
|
|
88
|
+
typeLabel?: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
reflection?: string;
|
|
91
|
+
/** Employer/provider feedback quotes, plain strings. */
|
|
92
|
+
feedbackQuotes: string[];
|
|
93
|
+
skillsDeveloped: string[];
|
|
94
|
+
evidenceFileIds: string[];
|
|
95
|
+
/** Original activity type where kind === "activity" (drives PfA mapping). */
|
|
96
|
+
activityType?: StudentActivity["activityType"];
|
|
97
|
+
/** Gatsby benchmarks where kind === "event" (informational). */
|
|
98
|
+
gatsbyBenchmarks?: number[];
|
|
99
|
+
};
|
|
100
|
+
export type PortfolioAspiration = AspirationEntry & {
|
|
101
|
+
routeLabel?: string;
|
|
102
|
+
};
|
|
103
|
+
export type PortfolioEvidenceItem = {
|
|
104
|
+
fileId: string;
|
|
105
|
+
/** Where this evidence came from, for grouping/labelling. */
|
|
106
|
+
source: "activity" | "placement" | "event";
|
|
107
|
+
sourceTitle?: string;
|
|
108
|
+
};
|
|
109
|
+
export type PortfolioAggregate = {
|
|
110
|
+
header: PortfolioHeader;
|
|
111
|
+
skills: PortfolioSkill[];
|
|
112
|
+
/** Reverse-chronological (newest first). */
|
|
113
|
+
experiences: PortfolioExperience[];
|
|
114
|
+
/** Reverse-chronological by year group / date (newest first). */
|
|
115
|
+
aspirations: PortfolioAspiration[];
|
|
116
|
+
evidence: PortfolioEvidenceItem[];
|
|
117
|
+
meta: {
|
|
118
|
+
generatedAt: string;
|
|
119
|
+
/** Earliest → latest dates seen across experiences, for cover pages. */
|
|
120
|
+
dateRange?: {
|
|
121
|
+
start?: string;
|
|
122
|
+
end?: string;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
/** Raw inputs the aggregation needs. Callers fetch these however they can. */
|
|
127
|
+
export type PortfolioAggregateInput = {
|
|
128
|
+
student: Pick<UserData, "details" | "cohort" | "userType"> & {
|
|
129
|
+
[k: string]: unknown;
|
|
130
|
+
};
|
|
131
|
+
schoolName?: string;
|
|
132
|
+
cohortName?: string;
|
|
133
|
+
skillsAssessments: SkillAssessment[];
|
|
134
|
+
/** skillsLabels keyed by their document id, already filtered to institute.skills. */
|
|
135
|
+
skillsLabels: {
|
|
136
|
+
[skillId: string]: SkillLabel;
|
|
137
|
+
};
|
|
138
|
+
activities: StudentActivity[];
|
|
139
|
+
/** Per-student event invites joined to their event doc. */
|
|
140
|
+
events: {
|
|
141
|
+
invite: ExternalEventStudent;
|
|
142
|
+
event?: ExternalEvent | false;
|
|
143
|
+
}[];
|
|
144
|
+
placements: (Partial<StudentPlacementData> & {
|
|
145
|
+
id: string;
|
|
146
|
+
})[];
|
|
147
|
+
aspirationHistory: AspirationEntry[];
|
|
148
|
+
/** Optional academic-year cutoff (dbstring) for "distance travelled". */
|
|
149
|
+
academicYearStart?: string;
|
|
150
|
+
generatedAt?: string;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Resolves the "current aspiration" headline from the most recent entry.
|
|
154
|
+
* Mirrors the fields AspirationsSurveyPage reads (postSixteenIntent, dreamJob).
|
|
155
|
+
*/
|
|
156
|
+
export declare function resolveAspirationHeadline(history: AspirationEntry[]): PortfolioAspirationHeadline | undefined;
|
|
157
|
+
export declare function buildPortfolioAggregate(input: PortfolioAggregateInput): PortfolioAggregate;
|
|
158
|
+
export declare function mapExperienceToPfaPathway(exp: PortfolioExperience): PfaPathway;
|
|
159
|
+
export declare function mapSkillToPfaPathway(skill: PortfolioSkill): PfaPathway;
|
|
160
|
+
export type PfaGroupedPortfolio = Record<PfaPathway, {
|
|
161
|
+
experiences: PortfolioExperience[];
|
|
162
|
+
skills: PortfolioSkill[];
|
|
163
|
+
}>;
|
|
164
|
+
/**
|
|
165
|
+
* Re-groups the aggregate under the four PfA pathways for the annual-review
|
|
166
|
+
* evidence pack. Unmapped items land in employmentAndFurtherLearning rather
|
|
167
|
+
* than disappearing.
|
|
168
|
+
*/
|
|
169
|
+
export declare function buildAnnualReviewGroups(aggregate: PortfolioAggregate): PfaGroupedPortfolio;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Student Portfolio — shared aggregation layer.
|
|
4
|
+
*
|
|
5
|
+
* Design principle (see feature brief): "build one aggregation, three renders".
|
|
6
|
+
* This module is the single aggregation. It is deliberately PURE — no Firebase,
|
|
7
|
+
* no React, no Node — so it can run identically in:
|
|
8
|
+
* - the web client (usePortfolioData hook → StudentPortfolio page + student PDF),
|
|
9
|
+
* - the backend export callables (portfolio PDF, PfA annual-review pack, careers CSV).
|
|
10
|
+
*
|
|
11
|
+
* Callers are responsible for fetching the raw documents (they live in different
|
|
12
|
+
* places on client vs server); this module only shapes them into a portfolio.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SKILL_CATEGORY_TO_PFA = exports.ACTIVITY_TYPE_TO_PFA = exports.PFA_DEFAULT_PATHWAY = exports.PFA_PATHWAY_LABELS = exports.PFA_PATHWAYS = exports.ACTIVITY_TYPE_LABELS = exports.POST_SIXTEEN_LABELS = exports.PORTFOLIO_SECTION_LABELS = exports.PORTFOLIO_TERTIARY_COLOUR = exports.PORTFOLIO_SECONDARY_COLOUR = exports.PORTFOLIO_PRIMARY_COLOUR = void 0;
|
|
16
|
+
exports.resolveAspirationHeadline = resolveAspirationHeadline;
|
|
17
|
+
exports.buildPortfolioAggregate = buildPortfolioAggregate;
|
|
18
|
+
exports.mapExperienceToPfaPathway = mapExperienceToPfaPathway;
|
|
19
|
+
exports.mapSkillToPfaPathway = mapSkillToPfaPathway;
|
|
20
|
+
exports.buildAnnualReviewGroups = buildAnnualReviewGroups;
|
|
21
|
+
const constants_1 = require("./constants");
|
|
22
|
+
// ── Brand ────────────────────────────────────────────────────────────────────
|
|
23
|
+
// The portfolio uses the CareerThread platform palette, which is distinct from
|
|
24
|
+
// the app-wide PRIMARY_COLOUR (#397DF2). These are the brand colours the export
|
|
25
|
+
// templates and on-screen page share.
|
|
26
|
+
exports.PORTFOLIO_PRIMARY_COLOUR = "#1F68B3";
|
|
27
|
+
exports.PORTFOLIO_SECONDARY_COLOUR = "#0AA4DE";
|
|
28
|
+
exports.PORTFOLIO_TERTIARY_COLOUR = "#00C0C7";
|
|
29
|
+
// ── Plain-English labels (SEND/SEMH register) ─────────────────────────────────
|
|
30
|
+
// The trust's cohort is SEND/SEMH, so section headings avoid jargon. Kept here
|
|
31
|
+
// so the page and the PDF stay in lockstep.
|
|
32
|
+
exports.PORTFOLIO_SECTION_LABELS = {
|
|
33
|
+
header: "About me",
|
|
34
|
+
skills: "My skills",
|
|
35
|
+
experiences: "Things I've done",
|
|
36
|
+
aspirations: "What I want to do next",
|
|
37
|
+
evidence: "My evidence",
|
|
38
|
+
};
|
|
39
|
+
exports.POST_SIXTEEN_LABELS = {
|
|
40
|
+
sixthForm: "Sixth form",
|
|
41
|
+
college: "College",
|
|
42
|
+
apprenticeship: "Apprenticeship",
|
|
43
|
+
employment: "Employment",
|
|
44
|
+
notSure: "Not sure yet",
|
|
45
|
+
};
|
|
46
|
+
exports.ACTIVITY_TYPE_LABELS = {
|
|
47
|
+
partTimeWork: "Part-time work",
|
|
48
|
+
volunteering: "Volunteering",
|
|
49
|
+
workshop: "Workshop",
|
|
50
|
+
employerTalk: "Employer talk",
|
|
51
|
+
onlineProgramme: "Online programme",
|
|
52
|
+
workShadowing: "Work shadowing",
|
|
53
|
+
other: "Other",
|
|
54
|
+
};
|
|
55
|
+
exports.PFA_PATHWAYS = [
|
|
56
|
+
"employmentAndFurtherLearning",
|
|
57
|
+
"independentLiving",
|
|
58
|
+
"communityInclusion",
|
|
59
|
+
"goodHealth",
|
|
60
|
+
];
|
|
61
|
+
exports.PFA_PATHWAY_LABELS = {
|
|
62
|
+
employmentAndFurtherLearning: "Employment & further learning",
|
|
63
|
+
independentLiving: "Independent living",
|
|
64
|
+
communityInclusion: "Community inclusion",
|
|
65
|
+
goodHealth: "Good health",
|
|
66
|
+
};
|
|
67
|
+
// Unmapped items fall here rather than disappearing (per brief).
|
|
68
|
+
exports.PFA_DEFAULT_PATHWAY = "employmentAndFurtherLearning";
|
|
69
|
+
/** Extracurricular activity type → PfA pathway. */
|
|
70
|
+
exports.ACTIVITY_TYPE_TO_PFA = {
|
|
71
|
+
partTimeWork: "employmentAndFurtherLearning",
|
|
72
|
+
workShadowing: "employmentAndFurtherLearning",
|
|
73
|
+
employerTalk: "employmentAndFurtherLearning",
|
|
74
|
+
onlineProgramme: "employmentAndFurtherLearning",
|
|
75
|
+
workshop: "employmentAndFurtherLearning",
|
|
76
|
+
volunteering: "communityInclusion",
|
|
77
|
+
other: exports.PFA_DEFAULT_PATHWAY,
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Skills-framework category → PfA pathway. Hardcoded sensible default for now;
|
|
81
|
+
* the brief calls for this to be per-institute configurable later.
|
|
82
|
+
* TODO: make this mapping overridable per-institute (institute.pfaSkillMap).
|
|
83
|
+
*/
|
|
84
|
+
exports.SKILL_CATEGORY_TO_PFA = {
|
|
85
|
+
["Communication"]: "communityInclusion",
|
|
86
|
+
["Teamwork"]: "communityInclusion",
|
|
87
|
+
["Self-Management"]: "independentLiving",
|
|
88
|
+
["Thinking & Problem Solving"]: "employmentAndFurtherLearning",
|
|
89
|
+
["Attitude & Resilience"]: "goodHealth",
|
|
90
|
+
["Work Readiness"]: "employmentAndFurtherLearning",
|
|
91
|
+
["Career Development"]: "employmentAndFurtherLearning",
|
|
92
|
+
};
|
|
93
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
94
|
+
const timestampToMs = (t) => {
|
|
95
|
+
if (!t)
|
|
96
|
+
return 0;
|
|
97
|
+
return t.seconds * 1000 + t.nanoseconds / 1e6;
|
|
98
|
+
};
|
|
99
|
+
const timestampToDbString = (t) => {
|
|
100
|
+
const d = new Date(timestampToMs(t));
|
|
101
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
102
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the "current aspiration" headline from the most recent entry.
|
|
106
|
+
* Mirrors the fields AspirationsSurveyPage reads (postSixteenIntent, dreamJob).
|
|
107
|
+
*/
|
|
108
|
+
function resolveAspirationHeadline(history) {
|
|
109
|
+
if (!history?.length)
|
|
110
|
+
return undefined;
|
|
111
|
+
const latest = [...history].sort((a, b) => (b.yearGroup || 0) - (a.yearGroup || 0) ||
|
|
112
|
+
(b.date || "").localeCompare(a.date || ""))[0];
|
|
113
|
+
if (!latest)
|
|
114
|
+
return undefined;
|
|
115
|
+
const route = latest.postSixteenIntent ?
|
|
116
|
+
(exports.POST_SIXTEEN_LABELS[latest.postSixteenIntent] ?? latest.postSixteenIntent) :
|
|
117
|
+
undefined;
|
|
118
|
+
return { route, dreamJob: latest.dreamJob };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Latest score per rater for a skill id, using the newest completed assessment
|
|
122
|
+
* of that rater type. Mirrors SkillsAssessmentUserOverview.fetchMostRecentScore.
|
|
123
|
+
*/
|
|
124
|
+
function latestScore(assessments, rater, skillId) {
|
|
125
|
+
return [...assessments]
|
|
126
|
+
.filter((v) => v.dateCompleted && v.userType === rater)
|
|
127
|
+
.sort((a, b) => timestampToMs(b.dateCompleted) - timestampToMs(a.dateCompleted))
|
|
128
|
+
.find((v) => v.scores?.[skillId] !== undefined)
|
|
129
|
+
?.scores[skillId] ?? 0;
|
|
130
|
+
}
|
|
131
|
+
function buildSkill(skillId, label, assessments, academicYearStart) {
|
|
132
|
+
const studentAssessments = assessments
|
|
133
|
+
.filter((v) => v.dateCompleted && v.userType === "students" && v.scores?.[skillId] !== undefined)
|
|
134
|
+
.sort((a, b) => timestampToMs(a.dateCompleted) - timestampToMs(b.dateCompleted)); // oldest → newest
|
|
135
|
+
const inYear = academicYearStart ?
|
|
136
|
+
studentAssessments.filter((v) => timestampToDbString(v.dateCompleted) >= academicYearStart) :
|
|
137
|
+
studentAssessments;
|
|
138
|
+
const timeline = studentAssessments.map((v) => ({
|
|
139
|
+
date: timestampToDbString(v.dateCompleted),
|
|
140
|
+
score: v.scores[skillId],
|
|
141
|
+
}));
|
|
142
|
+
const firstScore = inYear[0]?.scores[skillId] ?? 0;
|
|
143
|
+
const latestStudent = latestScore(assessments, "students", skillId);
|
|
144
|
+
const parent = latestScore(assessments, "parents", skillId);
|
|
145
|
+
const employer = latestScore(assessments, "employers", skillId);
|
|
146
|
+
return {
|
|
147
|
+
skillId,
|
|
148
|
+
label: label.label,
|
|
149
|
+
category: label.category,
|
|
150
|
+
color: constants_1.skillLabelColours[label.category],
|
|
151
|
+
latestScore: latestStudent,
|
|
152
|
+
firstScore,
|
|
153
|
+
distanceTravelled: Math.round((latestStudent - firstScore) * 100) / 100,
|
|
154
|
+
byRater: { students: latestStudent, parents: parent, employers: employer },
|
|
155
|
+
hasMultiRater: parent > 0 || employer > 0,
|
|
156
|
+
timeline,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Formats a dbstring range for display (kept local to avoid a util import).
|
|
160
|
+
function formatRange(start, end) {
|
|
161
|
+
const fmt = (s) => {
|
|
162
|
+
if (!s)
|
|
163
|
+
return undefined;
|
|
164
|
+
const d = new Date(s);
|
|
165
|
+
if (isNaN(d.getTime()))
|
|
166
|
+
return undefined;
|
|
167
|
+
return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
168
|
+
};
|
|
169
|
+
const s = fmt(start);
|
|
170
|
+
const e = fmt(end);
|
|
171
|
+
if (s && e && s !== e)
|
|
172
|
+
return `${s} – ${e}`;
|
|
173
|
+
return s || e || undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Pulls plain-string feedback quotes out of a placement's provider feedback
|
|
177
|
+
* data. The feedback form schema is dynamic, so we take any non-empty string
|
|
178
|
+
* values rather than assume field ids.
|
|
179
|
+
*/
|
|
180
|
+
function extractPlacementFeedback(placement) {
|
|
181
|
+
const data = placement.feedback?.provider?.completed ?
|
|
182
|
+
placement.feedback?.provider?.data : undefined;
|
|
183
|
+
if (!data)
|
|
184
|
+
return [];
|
|
185
|
+
return Object.values(data)
|
|
186
|
+
.filter((v) => typeof v === "string" && v.trim().length > 3)
|
|
187
|
+
.map((v) => v.trim());
|
|
188
|
+
}
|
|
189
|
+
// ── Main aggregation ───────────────────────────────────────────────────────────
|
|
190
|
+
function buildPortfolioAggregate(input) {
|
|
191
|
+
const generatedAt = input.generatedAt ||
|
|
192
|
+
new Date().toISOString().split("T")[0];
|
|
193
|
+
// Header
|
|
194
|
+
const details = input.student.details;
|
|
195
|
+
const header = {
|
|
196
|
+
name: [details?.forename, details?.surname].filter(Boolean).join(" ").trim() || "Student",
|
|
197
|
+
school: input.schoolName,
|
|
198
|
+
yearGroup: details?.year,
|
|
199
|
+
cohortName: input.cohortName,
|
|
200
|
+
aspiration: resolveAspirationHeadline(input.aspirationHistory),
|
|
201
|
+
};
|
|
202
|
+
// Skills
|
|
203
|
+
const skills = Object.entries(input.skillsLabels)
|
|
204
|
+
.map(([skillId, label]) => buildSkill(skillId, label, input.skillsAssessments, input.academicYearStart))
|
|
205
|
+
// Keep skills that have at least one score, or leave all if none scored
|
|
206
|
+
// (the page/PDF decides how to render an all-zero set).
|
|
207
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
208
|
+
// Experiences — merge placements + events + activities
|
|
209
|
+
const experiences = [];
|
|
210
|
+
input.placements.forEach((p) => {
|
|
211
|
+
const start = p.startDate;
|
|
212
|
+
const end = p.endDate;
|
|
213
|
+
// On a placement, `name` is the employer/company name (PlacementList
|
|
214
|
+
// shows it to the student as their placement employer). Fall back to
|
|
215
|
+
// the placement title, then sector.
|
|
216
|
+
const org = p.name || p.title || p.sector || undefined;
|
|
217
|
+
experiences.push({
|
|
218
|
+
id: p.id,
|
|
219
|
+
kind: "placement",
|
|
220
|
+
title: p.title ? `${p.title}${org && org !== p.title ? ` at ${org}` : ""}` : (org ? `Work experience at ${org}` : "Work experience placement"),
|
|
221
|
+
organisation: org,
|
|
222
|
+
startDate: start,
|
|
223
|
+
endDate: end,
|
|
224
|
+
dateLabel: formatRange(start, end),
|
|
225
|
+
typeLabel: "Work experience",
|
|
226
|
+
description: p.studentDescription,
|
|
227
|
+
feedbackQuotes: extractPlacementFeedback(p),
|
|
228
|
+
skillsDeveloped: [],
|
|
229
|
+
evidenceFileIds: [],
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
input.events.forEach(({ invite, event }) => {
|
|
233
|
+
if (!event)
|
|
234
|
+
return;
|
|
235
|
+
const start = event.startDate || invite.eventDates?.startDate;
|
|
236
|
+
const end = event.endDate || invite.eventDates?.endDate;
|
|
237
|
+
experiences.push({
|
|
238
|
+
id: invite.eventId,
|
|
239
|
+
kind: "event",
|
|
240
|
+
title: event.name || "Event",
|
|
241
|
+
organisation: undefined,
|
|
242
|
+
startDate: start,
|
|
243
|
+
endDate: end,
|
|
244
|
+
dateLabel: formatRange(start, end),
|
|
245
|
+
typeLabel: "Event",
|
|
246
|
+
description: event.description,
|
|
247
|
+
feedbackQuotes: [],
|
|
248
|
+
skillsDeveloped: [],
|
|
249
|
+
evidenceFileIds: [],
|
|
250
|
+
gatsbyBenchmarks: invite.gatsbyBenchmarks || event.gatsbyBenchmarks,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
input.activities.forEach((a) => {
|
|
254
|
+
experiences.push({
|
|
255
|
+
id: a.id,
|
|
256
|
+
kind: "activity",
|
|
257
|
+
title: a.title,
|
|
258
|
+
organisation: a.organisationName,
|
|
259
|
+
startDate: a.startDate || a.dateCreated,
|
|
260
|
+
endDate: a.endDate,
|
|
261
|
+
dateLabel: formatRange(a.startDate, a.endDate) ||
|
|
262
|
+
(a.isOngoing ? "Ongoing" : undefined),
|
|
263
|
+
typeLabel: exports.ACTIVITY_TYPE_LABELS[a.activityType] || "Activity",
|
|
264
|
+
description: a.description,
|
|
265
|
+
reflection: a.reflection,
|
|
266
|
+
feedbackQuotes: [],
|
|
267
|
+
skillsDeveloped: a.skillsDeveloped || [],
|
|
268
|
+
evidenceFileIds: a.evidenceFileIds || [],
|
|
269
|
+
activityType: a.activityType,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
// Reverse-chronological. Missing dates sort last.
|
|
273
|
+
experiences.sort((a, b) => (b.startDate || "").localeCompare(a.startDate || ""));
|
|
274
|
+
// Aspirations — newest first
|
|
275
|
+
const aspirations = [...input.aspirationHistory]
|
|
276
|
+
.sort((a, b) => (b.yearGroup || 0) - (a.yearGroup || 0) || (b.date || "").localeCompare(a.date || ""))
|
|
277
|
+
.map((e) => ({
|
|
278
|
+
...e,
|
|
279
|
+
routeLabel: e.postSixteenIntent ?
|
|
280
|
+
(exports.POST_SIXTEEN_LABELS[e.postSixteenIntent] ?? e.postSixteenIntent) : undefined,
|
|
281
|
+
}));
|
|
282
|
+
// Evidence — collected from experiences that carry file ids
|
|
283
|
+
const evidence = [];
|
|
284
|
+
experiences.forEach((e) => {
|
|
285
|
+
e.evidenceFileIds.forEach((fileId) => {
|
|
286
|
+
evidence.push({ fileId, source: e.kind, sourceTitle: e.title });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// Date range across experiences
|
|
290
|
+
const allDates = experiences
|
|
291
|
+
.flatMap((e) => [e.startDate, e.endDate])
|
|
292
|
+
.filter((d) => Boolean(d))
|
|
293
|
+
.sort();
|
|
294
|
+
return {
|
|
295
|
+
header,
|
|
296
|
+
skills,
|
|
297
|
+
experiences,
|
|
298
|
+
aspirations,
|
|
299
|
+
evidence,
|
|
300
|
+
meta: {
|
|
301
|
+
generatedAt,
|
|
302
|
+
dateRange: allDates.length ?
|
|
303
|
+
{ start: allDates[0], end: allDates[allDates.length - 1] } : undefined,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// ── PfA re-grouping (second render) ─────────────────────────────────────────────
|
|
308
|
+
function mapExperienceToPfaPathway(exp) {
|
|
309
|
+
if (exp.kind === "activity" && exp.activityType) {
|
|
310
|
+
return exports.ACTIVITY_TYPE_TO_PFA[exp.activityType] ?? exports.PFA_DEFAULT_PATHWAY;
|
|
311
|
+
}
|
|
312
|
+
// Events default to community inclusion (clubs/social/talks) unless clearly
|
|
313
|
+
// employer-facing; placements are employment. Both fall back to default.
|
|
314
|
+
if (exp.kind === "event")
|
|
315
|
+
return "communityInclusion";
|
|
316
|
+
return exports.PFA_DEFAULT_PATHWAY; // placements → employmentAndFurtherLearning
|
|
317
|
+
}
|
|
318
|
+
function mapSkillToPfaPathway(skill) {
|
|
319
|
+
return exports.SKILL_CATEGORY_TO_PFA[skill.category] ?? exports.PFA_DEFAULT_PATHWAY;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Re-groups the aggregate under the four PfA pathways for the annual-review
|
|
323
|
+
* evidence pack. Unmapped items land in employmentAndFurtherLearning rather
|
|
324
|
+
* than disappearing.
|
|
325
|
+
*/
|
|
326
|
+
function buildAnnualReviewGroups(aggregate) {
|
|
327
|
+
const groups = exports.PFA_PATHWAYS.reduce((acc, p) => {
|
|
328
|
+
acc[p] = { experiences: [], skills: [] };
|
|
329
|
+
return acc;
|
|
330
|
+
}, {});
|
|
331
|
+
aggregate.experiences.forEach((e) => groups[mapExperienceToPfaPathway(e)].experiences.push(e));
|
|
332
|
+
aggregate.skills
|
|
333
|
+
.filter((s) => s.latestScore > 0)
|
|
334
|
+
.forEach((s) => groups[mapSkillToPfaPathway(s)].skills.push(s));
|
|
335
|
+
return groups;
|
|
336
|
+
}
|
|
337
|
+
// ── Careers record (third render) ────────────────────────────────────────────────
|
|
338
|
+
// The careers-record CSV reuses the single-source Compass mapping
|
|
339
|
+
// (buildCompassActivityRows / compassActivityRowToCsvArray from compassExport.ts)
|
|
340
|
+
// filtered to one learner. It lives in the backend export callable, which passes
|
|
341
|
+
// this student's UPN as the only learner per event so the row output stays
|
|
342
|
+
// byte-identical to the bulk Compass export. Nothing to add here — this comment
|
|
343
|
+
// documents that the mapping is intentionally NOT duplicated in this module.
|
|
344
|
+
//# sourceMappingURL=portfolioAggregate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portfolioAggregate.js","sourceRoot":"","sources":["../src/portfolioAggregate.ts"],"names":[],"mappings":"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"}
|
package/lib/typeDefinitions.d.ts
CHANGED
|
@@ -367,6 +367,8 @@ export type UserData = {
|
|
|
367
367
|
[key: string]: any;
|
|
368
368
|
};
|
|
369
369
|
parentEmails?: WondeParentContact[];
|
|
370
|
+
upn?: string;
|
|
371
|
+
aspirationHistory?: AspirationEntry[];
|
|
370
372
|
wonde?: {
|
|
371
373
|
upi: string;
|
|
372
374
|
id: string;
|
|
@@ -1658,6 +1660,16 @@ export type ExternalEvent = {
|
|
|
1658
1660
|
activityId?: string;
|
|
1659
1661
|
visibleTo: string[];
|
|
1660
1662
|
gatsbyBenchmarks?: number[];
|
|
1663
|
+
gatsbyCategories?: {
|
|
1664
|
+
[benchmark: number]: string;
|
|
1665
|
+
};
|
|
1666
|
+
palCompliant?: boolean;
|
|
1667
|
+
parentAttendance?: boolean;
|
|
1668
|
+
compassExport?: {
|
|
1669
|
+
exportedAt: string;
|
|
1670
|
+
exportedBy: string;
|
|
1671
|
+
batchId?: string;
|
|
1672
|
+
};
|
|
1661
1673
|
oId: string;
|
|
1662
1674
|
submissionClose: string;
|
|
1663
1675
|
acceptingEmployers?: boolean;
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
academicYearDateRange,
|
|
3
|
+
academicYearForDate,
|
|
4
|
+
activityEvidenceBenchmarks,
|
|
5
|
+
BENCHMARK_COVERAGE_NUMBERS,
|
|
6
|
+
BENCHMARK_META,
|
|
7
|
+
coverageBand,
|
|
8
|
+
dateInAcademicYear,
|
|
9
|
+
eventEvidenceBenchmarks,
|
|
10
|
+
eventProvidesBenchmark,
|
|
11
|
+
isPalCompliantProviderEncounter,
|
|
12
|
+
palPhaseForYearGroup,
|
|
13
|
+
} from "./benchmarkEvidenceRules";
|
|
14
|
+
import {COMPASS_PAL_BENCHMARKS, COMPASS_VALID_ACTIVITY_BENCHMARKS} from "./compassExport";
|
|
15
|
+
|
|
16
|
+
describe("benchmarkEvidenceRules single-source consistency", () => {
|
|
17
|
+
it("stays consistent with the Compass export's valid activity benchmarks (2-8)", () => {
|
|
18
|
+
// Coverage numbers are the export-valid set minus BM8 (untracked on CareerThread).
|
|
19
|
+
expect(BENCHMARK_COVERAGE_NUMBERS).toEqual(
|
|
20
|
+
COMPASS_VALID_ACTIVITY_BENCHMARKS.filter((b) => b !== 8));
|
|
21
|
+
expect(BENCHMARK_COVERAGE_NUMBERS).not.toContain(1);
|
|
22
|
+
expect(BENCHMARK_COVERAGE_NUMBERS).not.toContain(8);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("marks BM1 as provision-level and BM8 as untracked — neither counts toward coverage", () => {
|
|
26
|
+
expect(BENCHMARK_META[1].countsTowardCoverage).toBe(false);
|
|
27
|
+
expect(BENCHMARK_META[1].source).toBe("provision");
|
|
28
|
+
expect(BENCHMARK_META[8].countsTowardCoverage).toBe(false);
|
|
29
|
+
expect(BENCHMARK_META[8].source).toBe("untracked");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("flags PAL relevance for exactly benchmarks 5-7", () => {
|
|
33
|
+
[1, 2, 3, 4, 5, 6, 7, 8].forEach((b) => {
|
|
34
|
+
expect(BENCHMARK_META[b as 1].palRelevant)
|
|
35
|
+
.toBe(COMPASS_PAL_BENCHMARKS.includes(b));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("event evidence classification", () => {
|
|
41
|
+
it("keeps tagged 2-8 benchmarks and drops GB1", () => {
|
|
42
|
+
expect(eventEvidenceBenchmarks({gatsbyBenchmarks: [1, 5, 7]})).toEqual([5, 7]);
|
|
43
|
+
expect(eventProvidesBenchmark({gatsbyBenchmarks: [1]}, 1 as never)).toBe(false);
|
|
44
|
+
expect(eventProvidesBenchmark({gatsbyBenchmarks: [5]}, 5)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("counts a PAL encounter only when flagged AND tagged 5-7", () => {
|
|
48
|
+
expect(isPalCompliantProviderEncounter({palCompliant: true, gatsbyBenchmarks: [7]})).toBe(true);
|
|
49
|
+
expect(isPalCompliantProviderEncounter({palCompliant: true, gatsbyBenchmarks: [4]})).toBe(false);
|
|
50
|
+
expect(isPalCompliantProviderEncounter({palCompliant: false, gatsbyBenchmarks: [7]})).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("student activity classification", () => {
|
|
55
|
+
it("maps employer encounters to BM5 and workplace experiences to BM6", () => {
|
|
56
|
+
expect(activityEvidenceBenchmarks({activityType: "employerTalk"})).toEqual([5]);
|
|
57
|
+
expect(activityEvidenceBenchmarks({activityType: "workShadowing"})).toEqual([6]);
|
|
58
|
+
expect(activityEvidenceBenchmarks({activityType: "partTimeWork"})).toEqual([6]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("never fabricates a benchmark from an untyped 'other' activity", () => {
|
|
62
|
+
expect(activityEvidenceBenchmarks({activityType: "other"})).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("PAL phases", () => {
|
|
67
|
+
it("assigns year groups to the three statutory phases and leaves Year 7 unphased", () => {
|
|
68
|
+
expect(palPhaseForYearGroup(9)?.phase).toBe(1);
|
|
69
|
+
expect(palPhaseForYearGroup(11)?.phase).toBe(2);
|
|
70
|
+
expect(palPhaseForYearGroup(13)?.phase).toBe(3);
|
|
71
|
+
expect(palPhaseForYearGroup(7)).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("academic year", () => {
|
|
76
|
+
it("rolls over at August", () => {
|
|
77
|
+
expect(academicYearForDate("2025-09-15")).toBe("2025-26");
|
|
78
|
+
expect(academicYearForDate("2026-07-02")).toBe("2025-26");
|
|
79
|
+
expect(academicYearForDate("2026-08-01")).toBe("2026-27");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("produces an inclusive-start, exclusive-end range and tests membership", () => {
|
|
83
|
+
expect(academicYearDateRange("2025-26")).toEqual({start: "2025-08-01", end: "2026-08-01"});
|
|
84
|
+
expect(dateInAcademicYear("2025-12-01", "2025-26")).toBe(true);
|
|
85
|
+
expect(dateInAcademicYear("2026-08-01", "2025-26")).toBe(false);
|
|
86
|
+
expect(dateInAcademicYear(undefined, "2025-26")).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("coverage bands are neutral (no pass/fail theatre)", () => {
|
|
91
|
+
it("buckets fractions onto a none→full scale", () => {
|
|
92
|
+
expect(coverageBand(0)).toBe("none");
|
|
93
|
+
expect(coverageBand(0.2)).toBe("low");
|
|
94
|
+
expect(coverageBand(0.5)).toBe("partial");
|
|
95
|
+
expect(coverageBand(0.9)).toBe("strong");
|
|
96
|
+
expect(coverageBand(1)).toBe("full");
|
|
97
|
+
});
|
|
98
|
+
});
|