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.
@@ -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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"}
@@ -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
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "placementt-core",
4
4
  "author": "Placementt",
5
- "version": "1.400.939",
5
+ "version": "1.400.941",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -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
+ });