placementt-core 1.400.1039 → 1.400.1041
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/destinations.d.ts +140 -0
- package/lib/destinations.js +220 -0
- package/lib/destinations.js.map +1 -0
- package/lib/destinations.test.d.ts +1 -0
- package/lib/destinations.test.js +121 -0
- package/lib/destinations.test.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/typeDefinitions.d.ts +52 -6
- package/package.json +1 -1
- package/src/destinations.test.ts +149 -0
- package/src/destinations.ts +249 -0
- package/src/index.ts +1 -0
- package/src/typeDefinitions.ts +53 -6
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { DestinationCode, DestinationValue, InstituteData, LeaverDestination } from "./typeDefinitions";
|
|
2
|
+
/**
|
|
3
|
+
* destinations.ts — the nine NCCIS codes plus whatever a school has added to them.
|
|
4
|
+
*
|
|
5
|
+
* NCCIS gives every leaver one of nine activity codes, and two of them — NEET and
|
|
6
|
+
* NK — cover situations that could hardly be less alike: a leaver in hospital, one
|
|
7
|
+
* in care, one in custody and one who simply hasn't decided are all the same row of
|
|
8
|
+
* the return. Schools with SEND cohorts need that row broken up, so a school can
|
|
9
|
+
* define its own categories (`InstituteData.leaverDestinations`) and every one of
|
|
10
|
+
* them names the NCCIS code it counts within.
|
|
11
|
+
*
|
|
12
|
+
* That mapping is the whole design: a custom category's id is what gets stored and
|
|
13
|
+
* counted, so the school's own charts get the finer breakdown, while anything
|
|
14
|
+
* statutory (Compass+, the LA MI return) resolves it back to one of the nine.
|
|
15
|
+
*/
|
|
16
|
+
export type LeaverDestinationMap = {
|
|
17
|
+
[id: string]: LeaverDestination;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One option as the forms and charts see it — standard and custom side by side, so
|
|
21
|
+
* nothing downstream has to know which it is unless it cares.
|
|
22
|
+
*/
|
|
23
|
+
export type DestinationOption = {
|
|
24
|
+
/** What gets stored: an NCCIS code, or a custom category's id. */
|
|
25
|
+
code: DestinationValue;
|
|
26
|
+
label: string;
|
|
27
|
+
/** The NCCIS code this counts within — its own code, for the standard nine. */
|
|
28
|
+
nccisCode: DestinationCode;
|
|
29
|
+
custom: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
archived?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** Ids become Firestore field-path segments (`aggregates.destinations.<id>`), so
|
|
34
|
+
* they may not contain a dot, a slash or anything else a path splits on. */
|
|
35
|
+
export declare const DESTINATION_ID_PATTERN: RegExp;
|
|
36
|
+
/** Custom ids carry a prefix so nothing can shadow a standard code, now or when
|
|
37
|
+
* NCCIS adds one. */
|
|
38
|
+
export declare const CUSTOM_DESTINATION_PREFIX = "custom_";
|
|
39
|
+
/** The NCCIS codes that mean "not in education, employment or training, or we don't
|
|
40
|
+
* know" — the pair schools break down, and the pair a positive-destination rate
|
|
41
|
+
* counts against. */
|
|
42
|
+
export declare const NEGATIVE_DESTINATION_CODES: DestinationCode[];
|
|
43
|
+
/**
|
|
44
|
+
* A stable id for a new custom category, derived from its label.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} label the category's display name.
|
|
47
|
+
* @param {object} [existing] categories already defined, so ids stay unique.
|
|
48
|
+
* @return {string} a field-path-safe id.
|
|
49
|
+
*/
|
|
50
|
+
export declare const customDestinationId: (label: string, existing?: LeaverDestinationMap) => string;
|
|
51
|
+
/** The nine NCCIS codes as options, in the order the MI return lists them. */
|
|
52
|
+
export declare const standardDestinationOptions: () => DestinationOption[];
|
|
53
|
+
/**
|
|
54
|
+
* Every destination a school offers: the nine standard codes, then its own
|
|
55
|
+
* categories grouped after the NCCIS code each one belongs to.
|
|
56
|
+
*
|
|
57
|
+
* Archived categories are left out unless asked for — they exist so that historical
|
|
58
|
+
* records keep their label, not so that new leavers can pick them.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
61
|
+
* @param {object} [opts] `includeArchived` to keep withdrawn categories in the list.
|
|
62
|
+
* @return {DestinationOption[]} the ordered option list.
|
|
63
|
+
*/
|
|
64
|
+
export declare const destinationOptions: (customs?: LeaverDestinationMap, opts?: {
|
|
65
|
+
includeArchived?: boolean;
|
|
66
|
+
}) => DestinationOption[];
|
|
67
|
+
/**
|
|
68
|
+
* A school's categories, plus the ones its trust supplies.
|
|
69
|
+
*
|
|
70
|
+
* A trust's categories are extra vocabulary, not a mandate: they are added to
|
|
71
|
+
* whatever the school has of its own, and the school can neither edit nor remove
|
|
72
|
+
* them. Nothing is copied onto the school — resolution is a read-time merge, so a
|
|
73
|
+
* school that leaves the trust simply stops seeing them (see ./trustPolicies for why
|
|
74
|
+
* that is how every trust-supplied setting works here).
|
|
75
|
+
*
|
|
76
|
+
* On an id collision the SCHOOL's entry wins. Both keys are the same, so responses
|
|
77
|
+
* and aggregates land in the same place either way; what differs is that the school
|
|
78
|
+
* keeps the category it made, and keeps being able to edit it.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} [school] the school's own institute doc.
|
|
81
|
+
* @param {object} [trust] the trust's institute doc, when the school is in one.
|
|
82
|
+
* @return {object} the merged map, and the ids the trust owns.
|
|
83
|
+
*/
|
|
84
|
+
export declare const resolveLeaverDestinations: (school?: Pick<InstituteData, "leaverDestinations">, trust?: Pick<InstituteData, "leaverDestinations">) => {
|
|
85
|
+
destinations: LeaverDestinationMap;
|
|
86
|
+
trustIds: string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* What to show for a stored destination — a standard label, or a school's own.
|
|
90
|
+
*
|
|
91
|
+
* A category defined by a DIFFERENT school (a student browsing trust-wide alumni
|
|
92
|
+
* meets these) isn't in the map we hold, so its id is turned back into words rather
|
|
93
|
+
* than shown raw: an id is derived from the label it was created with, which makes
|
|
94
|
+
* "custom_in_hospital" recoverable as "In hospital".
|
|
95
|
+
*
|
|
96
|
+
* @param {string} [code] the stored destination.
|
|
97
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
98
|
+
* @return {string|undefined} the display label.
|
|
99
|
+
*/
|
|
100
|
+
export declare const destinationLabel: (code?: DestinationValue, customs?: LeaverDestinationMap) => string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The NCCIS code a stored destination counts within. Statutory exports and the
|
|
103
|
+
* NEET/NK figures go through this, never through the raw value.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} [code] the stored destination.
|
|
106
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
107
|
+
* @return {DestinationCode|undefined} the NCCIS code to report it as.
|
|
108
|
+
*/
|
|
109
|
+
export declare const destinationNccisCode: (code?: DestinationValue, customs?: LeaverDestinationMap) => DestinationCode | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Whether a destination is one of the two the school is trying to shrink. Custom
|
|
112
|
+
* categories count exactly as the NCCIS code they name does.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} [code] the stored destination.
|
|
115
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
116
|
+
* @return {boolean} true for NEET / NK and anything mapped to them.
|
|
117
|
+
*/
|
|
118
|
+
export declare const isNegativeDestination: (code?: DestinationValue, customs?: LeaverDestinationMap) => boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Re-keys a `{destination: count}` aggregate onto the nine NCCIS codes, folding each
|
|
121
|
+
* custom category into its parent. For anything that has to report in NCCIS terms
|
|
122
|
+
* while the raw aggregate stays as granular as the school made it.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} [counts] the raw aggregate.
|
|
125
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
126
|
+
* @return {object} counts keyed by NCCIS code.
|
|
127
|
+
*/
|
|
128
|
+
export declare const rollUpDestinationCounts: (counts?: {
|
|
129
|
+
[code: string]: number;
|
|
130
|
+
}, customs?: LeaverDestinationMap) => {
|
|
131
|
+
[code: string]: number;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Validates a set of custom categories before it is stored. Shared by the setup card
|
|
135
|
+
* and the callable so both refuse the same things.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} destinations the map to check.
|
|
138
|
+
* @return {string|undefined} the first problem found, or undefined if it's valid.
|
|
139
|
+
*/
|
|
140
|
+
export declare const validateLeaverDestinations: (destinations: LeaverDestinationMap) => string | undefined;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateLeaverDestinations = exports.rollUpDestinationCounts = exports.isNegativeDestination = exports.destinationNccisCode = exports.destinationLabel = exports.resolveLeaverDestinations = exports.destinationOptions = exports.standardDestinationOptions = exports.customDestinationId = exports.NEGATIVE_DESTINATION_CODES = exports.CUSTOM_DESTINATION_PREFIX = exports.DESTINATION_ID_PATTERN = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
/** Ids become Firestore field-path segments (`aggregates.destinations.<id>`), so
|
|
6
|
+
* they may not contain a dot, a slash or anything else a path splits on. */
|
|
7
|
+
exports.DESTINATION_ID_PATTERN = /^[A-Za-z0-9_]{1,60}$/;
|
|
8
|
+
/** Custom ids carry a prefix so nothing can shadow a standard code, now or when
|
|
9
|
+
* NCCIS adds one. */
|
|
10
|
+
exports.CUSTOM_DESTINATION_PREFIX = "custom_";
|
|
11
|
+
/** The NCCIS codes that mean "not in education, employment or training, or we don't
|
|
12
|
+
* know" — the pair schools break down, and the pair a positive-destination rate
|
|
13
|
+
* counts against. */
|
|
14
|
+
exports.NEGATIVE_DESTINATION_CODES = ["NEET", "NK"];
|
|
15
|
+
/**
|
|
16
|
+
* A stable id for a new custom category, derived from its label.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} label the category's display name.
|
|
19
|
+
* @param {object} [existing] categories already defined, so ids stay unique.
|
|
20
|
+
* @return {string} a field-path-safe id.
|
|
21
|
+
*/
|
|
22
|
+
const customDestinationId = (label, existing) => {
|
|
23
|
+
const slug = (label || "")
|
|
24
|
+
.trim().toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
26
|
+
.replace(/^_+|_+$/g, "")
|
|
27
|
+
.slice(0, 40) || "category";
|
|
28
|
+
const base = `${exports.CUSTOM_DESTINATION_PREFIX}${slug}`;
|
|
29
|
+
if (!existing?.[base])
|
|
30
|
+
return base;
|
|
31
|
+
// Two categories can legitimately slug the same ("In care" / "In-care"), so the
|
|
32
|
+
// second one gets a suffix rather than silently overwriting the first.
|
|
33
|
+
let n = 2;
|
|
34
|
+
while (existing[`${base}_${n}`])
|
|
35
|
+
n++;
|
|
36
|
+
return `${base}_${n}`;
|
|
37
|
+
};
|
|
38
|
+
exports.customDestinationId = customDestinationId;
|
|
39
|
+
/** The nine NCCIS codes as options, in the order the MI return lists them. */
|
|
40
|
+
const standardDestinationOptions = () => Object.entries(constants_1.DESTINATION_CODES).map(([code, label]) => ({
|
|
41
|
+
code, label, nccisCode: code, custom: false,
|
|
42
|
+
}));
|
|
43
|
+
exports.standardDestinationOptions = standardDestinationOptions;
|
|
44
|
+
/**
|
|
45
|
+
* Every destination a school offers: the nine standard codes, then its own
|
|
46
|
+
* categories grouped after the NCCIS code each one belongs to.
|
|
47
|
+
*
|
|
48
|
+
* Archived categories are left out unless asked for — they exist so that historical
|
|
49
|
+
* records keep their label, not so that new leavers can pick them.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
52
|
+
* @param {object} [opts] `includeArchived` to keep withdrawn categories in the list.
|
|
53
|
+
* @return {DestinationOption[]} the ordered option list.
|
|
54
|
+
*/
|
|
55
|
+
const destinationOptions = (customs, opts) => {
|
|
56
|
+
const custom = Object.entries(customs ?? {})
|
|
57
|
+
.filter(([, d]) => d && (opts?.includeArchived || !d.archived))
|
|
58
|
+
.map(([id, d]) => ({
|
|
59
|
+
code: id,
|
|
60
|
+
label: d.label,
|
|
61
|
+
nccisCode: (constants_1.DESTINATION_CODES[d.nccisCode] ? d.nccisCode : "NEET"),
|
|
62
|
+
custom: true,
|
|
63
|
+
description: d.description,
|
|
64
|
+
archived: d.archived,
|
|
65
|
+
}))
|
|
66
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
67
|
+
// Grouped under their parent so "In hospital" reads as a kind of NEET rather than
|
|
68
|
+
// as a tenth code of its own.
|
|
69
|
+
return (0, exports.standardDestinationOptions)().flatMap((std) => [
|
|
70
|
+
std, ...custom.filter((c) => c.nccisCode === std.code),
|
|
71
|
+
]);
|
|
72
|
+
};
|
|
73
|
+
exports.destinationOptions = destinationOptions;
|
|
74
|
+
/**
|
|
75
|
+
* A school's categories, plus the ones its trust supplies.
|
|
76
|
+
*
|
|
77
|
+
* A trust's categories are extra vocabulary, not a mandate: they are added to
|
|
78
|
+
* whatever the school has of its own, and the school can neither edit nor remove
|
|
79
|
+
* them. Nothing is copied onto the school — resolution is a read-time merge, so a
|
|
80
|
+
* school that leaves the trust simply stops seeing them (see ./trustPolicies for why
|
|
81
|
+
* that is how every trust-supplied setting works here).
|
|
82
|
+
*
|
|
83
|
+
* On an id collision the SCHOOL's entry wins. Both keys are the same, so responses
|
|
84
|
+
* and aggregates land in the same place either way; what differs is that the school
|
|
85
|
+
* keeps the category it made, and keeps being able to edit it.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} [school] the school's own institute doc.
|
|
88
|
+
* @param {object} [trust] the trust's institute doc, when the school is in one.
|
|
89
|
+
* @return {object} the merged map, and the ids the trust owns.
|
|
90
|
+
*/
|
|
91
|
+
const resolveLeaverDestinations = (school, trust) => {
|
|
92
|
+
const own = school?.leaverDestinations ?? {};
|
|
93
|
+
const fromTrust = trust?.leaverDestinations ?? {};
|
|
94
|
+
const trustIds = Object.keys(fromTrust).filter((id) => !own[id]);
|
|
95
|
+
return {
|
|
96
|
+
destinations: { ...fromTrust, ...own },
|
|
97
|
+
trustIds,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
exports.resolveLeaverDestinations = resolveLeaverDestinations;
|
|
101
|
+
/**
|
|
102
|
+
* What to show for a stored destination — a standard label, or a school's own.
|
|
103
|
+
*
|
|
104
|
+
* A category defined by a DIFFERENT school (a student browsing trust-wide alumni
|
|
105
|
+
* meets these) isn't in the map we hold, so its id is turned back into words rather
|
|
106
|
+
* than shown raw: an id is derived from the label it was created with, which makes
|
|
107
|
+
* "custom_in_hospital" recoverable as "In hospital".
|
|
108
|
+
*
|
|
109
|
+
* @param {string} [code] the stored destination.
|
|
110
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
111
|
+
* @return {string|undefined} the display label.
|
|
112
|
+
*/
|
|
113
|
+
const destinationLabel = (code, customs) => {
|
|
114
|
+
if (!code)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (constants_1.DESTINATION_CODES[code])
|
|
117
|
+
return constants_1.DESTINATION_CODES[code];
|
|
118
|
+
const known = customs?.[code]?.label;
|
|
119
|
+
if (known)
|
|
120
|
+
return known;
|
|
121
|
+
if (!code.startsWith(exports.CUSTOM_DESTINATION_PREFIX))
|
|
122
|
+
return code;
|
|
123
|
+
const words = code.slice(exports.CUSTOM_DESTINATION_PREFIX.length).replace(/_/g, " ").trim();
|
|
124
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : code;
|
|
125
|
+
};
|
|
126
|
+
exports.destinationLabel = destinationLabel;
|
|
127
|
+
/**
|
|
128
|
+
* The NCCIS code a stored destination counts within. Statutory exports and the
|
|
129
|
+
* NEET/NK figures go through this, never through the raw value.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} [code] the stored destination.
|
|
132
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
133
|
+
* @return {DestinationCode|undefined} the NCCIS code to report it as.
|
|
134
|
+
*/
|
|
135
|
+
const destinationNccisCode = (code, customs) => {
|
|
136
|
+
if (!code)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (constants_1.DESTINATION_CODES[code])
|
|
139
|
+
return code;
|
|
140
|
+
const custom = customs?.[code];
|
|
141
|
+
// An id we no longer recognise is still somebody's leaver: "not known" is the
|
|
142
|
+
// honest reading, and it keeps them in the cohort rather than dropping them.
|
|
143
|
+
if (!custom)
|
|
144
|
+
return "NK";
|
|
145
|
+
return constants_1.DESTINATION_CODES[custom.nccisCode] ? custom.nccisCode : "NEET";
|
|
146
|
+
};
|
|
147
|
+
exports.destinationNccisCode = destinationNccisCode;
|
|
148
|
+
/**
|
|
149
|
+
* Whether a destination is one of the two the school is trying to shrink. Custom
|
|
150
|
+
* categories count exactly as the NCCIS code they name does.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} [code] the stored destination.
|
|
153
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
154
|
+
* @return {boolean} true for NEET / NK and anything mapped to them.
|
|
155
|
+
*/
|
|
156
|
+
const isNegativeDestination = (code, customs) => {
|
|
157
|
+
const nccis = (0, exports.destinationNccisCode)(code, customs);
|
|
158
|
+
return Boolean(nccis && exports.NEGATIVE_DESTINATION_CODES.includes(nccis));
|
|
159
|
+
};
|
|
160
|
+
exports.isNegativeDestination = isNegativeDestination;
|
|
161
|
+
/**
|
|
162
|
+
* Re-keys a `{destination: count}` aggregate onto the nine NCCIS codes, folding each
|
|
163
|
+
* custom category into its parent. For anything that has to report in NCCIS terms
|
|
164
|
+
* while the raw aggregate stays as granular as the school made it.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} [counts] the raw aggregate.
|
|
167
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
168
|
+
* @return {object} counts keyed by NCCIS code.
|
|
169
|
+
*/
|
|
170
|
+
const rollUpDestinationCounts = (counts, customs) => {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [code, n] of Object.entries(counts ?? {})) {
|
|
173
|
+
const key = (0, exports.destinationNccisCode)(code, customs) ?? "NK";
|
|
174
|
+
out[key] = (out[key] ?? 0) + (n ?? 0);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
};
|
|
178
|
+
exports.rollUpDestinationCounts = rollUpDestinationCounts;
|
|
179
|
+
/**
|
|
180
|
+
* Validates a set of custom categories before it is stored. Shared by the setup card
|
|
181
|
+
* and the callable so both refuse the same things.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} destinations the map to check.
|
|
184
|
+
* @return {string|undefined} the first problem found, or undefined if it's valid.
|
|
185
|
+
*/
|
|
186
|
+
const validateLeaverDestinations = (destinations) => {
|
|
187
|
+
const entries = Object.entries(destinations ?? {});
|
|
188
|
+
if (entries.length > 50)
|
|
189
|
+
return "You can add up to 50 destination categories.";
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
for (const [id, d] of entries) {
|
|
192
|
+
if (!d)
|
|
193
|
+
return "One of your categories is empty.";
|
|
194
|
+
if (!exports.DESTINATION_ID_PATTERN.test(id) || id !== d.id)
|
|
195
|
+
return "A category id is invalid.";
|
|
196
|
+
if (!id.startsWith(exports.CUSTOM_DESTINATION_PREFIX))
|
|
197
|
+
return "A category id is invalid.";
|
|
198
|
+
if (constants_1.DESTINATION_CODES[id])
|
|
199
|
+
return "A category can't reuse a standard destination code.";
|
|
200
|
+
const label = (d.label ?? "").trim();
|
|
201
|
+
if (!label)
|
|
202
|
+
return "Give every category a name.";
|
|
203
|
+
if (label.length > 60)
|
|
204
|
+
return "Category names must be 60 characters or fewer.";
|
|
205
|
+
if (!constants_1.DESTINATION_CODES[d.nccisCode])
|
|
206
|
+
return `Choose which group "${label}" counts within.`;
|
|
207
|
+
const key = label.toLowerCase();
|
|
208
|
+
if (seen.has(key))
|
|
209
|
+
return `You already have a category called "${label}".`;
|
|
210
|
+
// A category that duplicates a standard label would show twice in the picker
|
|
211
|
+
// with no way to tell them apart.
|
|
212
|
+
if (Object.values(constants_1.DESTINATION_CODES).some((l) => l.toLowerCase() === key)) {
|
|
213
|
+
return `"${label}" is already a standard destination.`;
|
|
214
|
+
}
|
|
215
|
+
seen.add(key);
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
};
|
|
219
|
+
exports.validateLeaverDestinations = validateLeaverDestinations;
|
|
220
|
+
//# sourceMappingURL=destinations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destinations.js","sourceRoot":"","sources":["../src/destinations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const destinations_1 = require("./destinations");
|
|
4
|
+
const customs = {
|
|
5
|
+
custom_in_hospital: { id: "custom_in_hospital", label: "In hospital", nccisCode: "NEET" },
|
|
6
|
+
custom_in_custody: { id: "custom_in_custody", label: "In custody", nccisCode: "NEET" },
|
|
7
|
+
custom_supported_internship: { id: "custom_supported_internship", label: "Supported internship", nccisCode: "EMP" },
|
|
8
|
+
custom_moved_away: { id: "custom_moved_away", label: "Moved away", nccisCode: "NK", archived: true },
|
|
9
|
+
};
|
|
10
|
+
describe("destinationOptions", () => {
|
|
11
|
+
it("lists the nine NCCIS codes when a school has added nothing", () => {
|
|
12
|
+
const options = (0, destinations_1.destinationOptions)();
|
|
13
|
+
expect(options).toHaveLength(9);
|
|
14
|
+
expect(options.every((o) => !o.custom)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("puts each custom category directly after the code it counts within", () => {
|
|
17
|
+
const codes = (0, destinations_1.destinationOptions)(customs).map((o) => o.code);
|
|
18
|
+
expect(codes.indexOf("custom_supported_internship")).toEqual(codes.indexOf("EMP") + 1);
|
|
19
|
+
// Both NEET categories follow NEET, alphabetically.
|
|
20
|
+
expect(codes.slice(codes.indexOf("NEET"), codes.indexOf("NEET") + 3))
|
|
21
|
+
.toEqual(["NEET", "custom_in_custody", "custom_in_hospital"]);
|
|
22
|
+
});
|
|
23
|
+
it("hides archived categories unless asked for them", () => {
|
|
24
|
+
expect((0, destinations_1.destinationOptions)(customs).map((o) => o.code)).not.toContain("custom_moved_away");
|
|
25
|
+
expect((0, destinations_1.destinationOptions)(customs, { includeArchived: true }).map((o) => o.code))
|
|
26
|
+
.toContain("custom_moved_away");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("resolveLeaverDestinations", () => {
|
|
30
|
+
const trust = { leaverDestinations: {
|
|
31
|
+
custom_in_care: { id: "custom_in_care", label: "In care", nccisCode: "NEET" },
|
|
32
|
+
custom_in_hospital: { id: "custom_in_hospital", label: "In hospital (trust)", nccisCode: "NEET" },
|
|
33
|
+
} };
|
|
34
|
+
it("offers the trust's categories alongside the school's, and names the trust's", () => {
|
|
35
|
+
const { destinations, trustIds } = (0, destinations_1.resolveLeaverDestinations)({ leaverDestinations: customs }, trust);
|
|
36
|
+
expect(Object.keys(destinations).sort()).toEqual([
|
|
37
|
+
"custom_in_care", "custom_in_custody", "custom_in_hospital",
|
|
38
|
+
"custom_moved_away", "custom_supported_internship",
|
|
39
|
+
]);
|
|
40
|
+
expect(trustIds).toEqual(["custom_in_care"]);
|
|
41
|
+
});
|
|
42
|
+
it("leaves the school's own version of a shared id in the school's hands", () => {
|
|
43
|
+
const { destinations, trustIds } = (0, destinations_1.resolveLeaverDestinations)({ leaverDestinations: customs }, trust);
|
|
44
|
+
expect(destinations.custom_in_hospital.label).toEqual("In hospital");
|
|
45
|
+
expect(trustIds).not.toContain("custom_in_hospital");
|
|
46
|
+
});
|
|
47
|
+
it("is the school's own map when there is no trust", () => {
|
|
48
|
+
expect((0, destinations_1.resolveLeaverDestinations)({ leaverDestinations: customs }).trustIds).toEqual([]);
|
|
49
|
+
expect((0, destinations_1.resolveLeaverDestinations)().destinations).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("destinationLabel", () => {
|
|
53
|
+
it("reads standard codes and the school's own categories", () => {
|
|
54
|
+
expect((0, destinations_1.destinationLabel)("APP")).toEqual("Apprenticeship");
|
|
55
|
+
expect((0, destinations_1.destinationLabel)("custom_in_hospital", customs)).toEqual("In hospital");
|
|
56
|
+
});
|
|
57
|
+
it("recovers words from an id defined by another school", () => {
|
|
58
|
+
// A student browsing trust-wide alumni meets categories we don't hold.
|
|
59
|
+
expect((0, destinations_1.destinationLabel)("custom_in_care")).toEqual("In care");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("destinationNccisCode", () => {
|
|
63
|
+
it("maps a custom category onto the code it reports as", () => {
|
|
64
|
+
expect((0, destinations_1.destinationNccisCode)("custom_in_hospital", customs)).toEqual("NEET");
|
|
65
|
+
expect((0, destinations_1.destinationNccisCode)("custom_supported_internship", customs)).toEqual("EMP");
|
|
66
|
+
});
|
|
67
|
+
it("treats an unknown id as not known rather than dropping the leaver", () => {
|
|
68
|
+
expect((0, destinations_1.destinationNccisCode)("custom_gone", customs)).toEqual("NK");
|
|
69
|
+
});
|
|
70
|
+
it("leaves standard codes alone", () => {
|
|
71
|
+
expect((0, destinations_1.destinationNccisCode)("HE", customs)).toEqual("HE");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("isNegativeDestination", () => {
|
|
75
|
+
it("counts a school's NEET-mapped categories as NEET", () => {
|
|
76
|
+
expect((0, destinations_1.isNegativeDestination)("custom_in_hospital", customs)).toBe(true);
|
|
77
|
+
expect((0, destinations_1.isNegativeDestination)("custom_supported_internship", customs)).toBe(false);
|
|
78
|
+
expect((0, destinations_1.isNegativeDestination)("NK")).toBe(true);
|
|
79
|
+
expect((0, destinations_1.isNegativeDestination)("HE")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("rollUpDestinationCounts", () => {
|
|
83
|
+
it("folds custom categories into their NCCIS code", () => {
|
|
84
|
+
const rolled = (0, destinations_1.rollUpDestinationCounts)({ HE: 10, NEET: 2, custom_in_hospital: 3, custom_in_custody: 1, custom_supported_internship: 4 }, customs);
|
|
85
|
+
expect(rolled).toEqual({ HE: 10, NEET: 6, EMP: 4 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("customDestinationId", () => {
|
|
89
|
+
it("derives a field-path-safe id from the label", () => {
|
|
90
|
+
expect((0, destinations_1.customDestinationId)("In hospital")).toEqual("custom_in_hospital");
|
|
91
|
+
});
|
|
92
|
+
it("suffixes rather than overwriting a category that slugs the same", () => {
|
|
93
|
+
expect((0, destinations_1.customDestinationId)("In-hospital", customs)).toEqual("custom_in_hospital_2");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("validateLeaverDestinations", () => {
|
|
97
|
+
it("accepts a well-formed set", () => {
|
|
98
|
+
expect((0, destinations_1.validateLeaverDestinations)(customs)).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
it("rejects a category with no name, a bad group, or a duplicate label", () => {
|
|
101
|
+
expect((0, destinations_1.validateLeaverDestinations)({
|
|
102
|
+
custom_x: { id: "custom_x", label: " ", nccisCode: "NEET" },
|
|
103
|
+
})).toBeDefined();
|
|
104
|
+
expect((0, destinations_1.validateLeaverDestinations)({
|
|
105
|
+
custom_x: { id: "custom_x", label: "In hospital", nccisCode: "XX" },
|
|
106
|
+
})).toBeDefined();
|
|
107
|
+
expect((0, destinations_1.validateLeaverDestinations)({
|
|
108
|
+
...customs,
|
|
109
|
+
custom_in_hospital_2: { id: "custom_in_hospital_2", label: "in hospital", nccisCode: "NEET" },
|
|
110
|
+
})).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it("rejects an id that could collide with a standard code or a field path", () => {
|
|
113
|
+
expect((0, destinations_1.validateLeaverDestinations)({
|
|
114
|
+
HE: { id: "HE", label: "Uni", nccisCode: "HE" },
|
|
115
|
+
})).toBeDefined();
|
|
116
|
+
expect((0, destinations_1.validateLeaverDestinations)({
|
|
117
|
+
"custom_a.b": { id: "custom_a.b", label: "Dotted", nccisCode: "NEET" },
|
|
118
|
+
})).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=destinations.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destinations.test.js","sourceRoot":"","sources":["../src/destinations.test.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from "./groups";
|
|
|
21
21
|
export * from "./emailSections";
|
|
22
22
|
export * from "./exportTransformers";
|
|
23
23
|
export * from "./compassExport";
|
|
24
|
+
export * from "./destinations";
|
|
24
25
|
export * from "./benchmarkEvidenceRules";
|
|
25
26
|
export * from "./portfolioAggregate";
|
|
26
27
|
export * from "./tutorialTips";
|
package/lib/index.js
CHANGED
|
@@ -41,6 +41,7 @@ __exportStar(require("./groups"), exports);
|
|
|
41
41
|
__exportStar(require("./emailSections"), exports);
|
|
42
42
|
__exportStar(require("./exportTransformers"), exports);
|
|
43
43
|
__exportStar(require("./compassExport"), exports);
|
|
44
|
+
__exportStar(require("./destinations"), exports);
|
|
44
45
|
__exportStar(require("./benchmarkEvidenceRules"), exports);
|
|
45
46
|
__exportStar(require("./portfolioAggregate"), exports);
|
|
46
47
|
__exportStar(require("./tutorialTips"), exports);
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,mDAAiC;AACjC,+CAA6B;AAC7B,wCAAsB;AACtB,oDAAkC;AAClC,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,kDAAgC;AAChC,2DAAyC;AACzC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,mDAAiC;AACjC,+CAA6B;AAC7B,wCAAsB;AACtB,oDAAkC;AAClC,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,kDAAgC;AAChC,iDAA+B;AAC/B,2DAAyC;AACzC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
|
package/lib/typeDefinitions.d.ts
CHANGED
|
@@ -1077,6 +1077,14 @@ export type InstituteData = {
|
|
|
1077
1077
|
closeAfterDays?: number;
|
|
1078
1078
|
};
|
|
1079
1079
|
destinationsConfig?: DestinationsConfig;
|
|
1080
|
+
/** School-defined leaver destination categories, keyed by id, added from the
|
|
1081
|
+
* Destinations Setup tab. They sit alongside the nine NCCIS codes on every
|
|
1082
|
+
* leaver form and each one names the NCCIS code it counts within, so a school
|
|
1083
|
+
* can break "NEET" down into the situations it actually needs to track
|
|
1084
|
+
* (in hospital, in care, in custody) without breaking statutory reporting. */
|
|
1085
|
+
leaverDestinations?: {
|
|
1086
|
+
[id: string]: LeaverDestination;
|
|
1087
|
+
};
|
|
1080
1088
|
/** Trust-authored policy, one entry per feature a trust can impose on the schools
|
|
1081
1089
|
* beneath it. Set on the TRUST's own institute doc and read by its schools through
|
|
1082
1090
|
* their approved trust link — never copied onto the school, so there is one source
|
|
@@ -2227,6 +2235,44 @@ export type ExternalEventRequest = {
|
|
|
2227
2235
|
};
|
|
2228
2236
|
/** NCCIS-compatible destination activity codes (MI Requirement 2026–27) */
|
|
2229
2237
|
export type DestinationCode = "HE" | "FE" | "APP" | "EWBL" | "EMP" | "VOL" | "OTH" | "NEET" | "NK";
|
|
2238
|
+
/**
|
|
2239
|
+
* A destination as it is actually stored on a response, an alumni record or a
|
|
2240
|
+
* cycle aggregate: one of the nine NCCIS codes above, or the id of a school-defined
|
|
2241
|
+
* category from `InstituteData.leaverDestinations`.
|
|
2242
|
+
*
|
|
2243
|
+
* Custom categories exist because NCCIS's NEET and NK are a single bucket for very
|
|
2244
|
+
* different situations — a SEND leaver in hospital, in care or in custody is "NEET"
|
|
2245
|
+
* to the local authority and nothing more, which is exactly the granularity schools
|
|
2246
|
+
* lose. Every custom category still carries the NCCIS code it counts within
|
|
2247
|
+
* (`LeaverDestination.nccisCode`), so statutory returns are unaffected: only the
|
|
2248
|
+
* school's own reporting gets finer.
|
|
2249
|
+
*
|
|
2250
|
+
* The `(string & {})` half keeps editor autocomplete for the nine standard codes
|
|
2251
|
+
* while still accepting a custom id.
|
|
2252
|
+
*/
|
|
2253
|
+
export type DestinationValue = DestinationCode | (string & {});
|
|
2254
|
+
/**
|
|
2255
|
+
* A school-defined leaver destination category, stored on
|
|
2256
|
+
* `InstituteData.leaverDestinations` keyed by `id`.
|
|
2257
|
+
*
|
|
2258
|
+
* `id` is what lands in responses and in `aggregates.destinations.<id>`, so it is
|
|
2259
|
+
* restricted to `[A-Za-z0-9_]` (see DESTINATION_ID_PATTERN) — a Firestore field path
|
|
2260
|
+
* segment can't contain a dot or a slash, and these keys become one.
|
|
2261
|
+
*/
|
|
2262
|
+
export type LeaverDestination = {
|
|
2263
|
+
id: string;
|
|
2264
|
+
label: string;
|
|
2265
|
+
/** Which of the nine NCCIS codes this rolls up into for statutory reporting. */
|
|
2266
|
+
nccisCode: DestinationCode;
|
|
2267
|
+
/** Optional helper text shown under the option on the leaver forms. */
|
|
2268
|
+
description?: string;
|
|
2269
|
+
/** Removed from the forms, but kept here so records and charts that already
|
|
2270
|
+
* carry the id keep their label. Deleting outright would leave past responses
|
|
2271
|
+
* showing a raw id. */
|
|
2272
|
+
archived?: boolean;
|
|
2273
|
+
createdAt?: string;
|
|
2274
|
+
createdBy?: string;
|
|
2275
|
+
};
|
|
2230
2276
|
/** SOC 2020 major group number */
|
|
2231
2277
|
export type SOCMajorGroup = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
2232
2278
|
/** Employment detail captured in Form C and stored in CheckInEntry */
|
|
@@ -2270,7 +2316,7 @@ export type ApprenticeshipDetails = {
|
|
|
2270
2316
|
* Appended on every check-in; source distinguishes self-report from staff/trusted-contact entry.
|
|
2271
2317
|
*/
|
|
2272
2318
|
export type CheckInEntry = {
|
|
2273
|
-
destinationCode:
|
|
2319
|
+
destinationCode: DestinationValue;
|
|
2274
2320
|
type?: "annual" | "midYear" | "leaverTransition" | "resultsDay" | "christmas";
|
|
2275
2321
|
sector?: string;
|
|
2276
2322
|
date: string;
|
|
@@ -2611,7 +2657,7 @@ export type LeaverTransitionData = {
|
|
|
2611
2657
|
mobile?: string;
|
|
2612
2658
|
linkedinUrl?: string;
|
|
2613
2659
|
activities?: string[];
|
|
2614
|
-
intendedDestination:
|
|
2660
|
+
intendedDestination: DestinationValue;
|
|
2615
2661
|
intendedDestinationDetails?: EducationDetails | EmploymentDetails | ApprenticeshipDetails;
|
|
2616
2662
|
intendedStartDate?: string;
|
|
2617
2663
|
trustedContact?: TrustedContact;
|
|
@@ -2652,7 +2698,7 @@ export type LeaverConsentCapture = {
|
|
|
2652
2698
|
*/
|
|
2653
2699
|
export type LeaverCheckpointResponse = {
|
|
2654
2700
|
checkpoint: "resultsDay" | "christmas";
|
|
2655
|
-
destination:
|
|
2701
|
+
destination: DestinationValue;
|
|
2656
2702
|
destinationDetails?: EducationDetails | EmploymentDetails | ApprenticeshipDetails;
|
|
2657
2703
|
/** resultsDay only — did results change their plan vs intendedDestination? */
|
|
2658
2704
|
changedFromIntended?: boolean;
|
|
@@ -2713,13 +2759,13 @@ export type Alumni = {
|
|
|
2713
2759
|
status: "pending" | "inviteSent" | "leaverReviewed" | "uploaded" | "alumniReviewed" | "declined" | "approved";
|
|
2714
2760
|
source?: "selfRegistered" | "staffAdded" | "leaverExport";
|
|
2715
2761
|
/** Intended destination captured via Form B before leaving */
|
|
2716
|
-
intendedDestination?:
|
|
2762
|
+
intendedDestination?: DestinationValue;
|
|
2717
2763
|
intendedDestinationDetails?: EducationDetails | EmploymentDetails | ApprenticeshipDetails;
|
|
2718
2764
|
intendedStartDate?: string;
|
|
2719
2765
|
/** October confirmed actual destination snapshot */
|
|
2720
|
-
confirmedDestination?:
|
|
2766
|
+
confirmedDestination?: DestinationValue;
|
|
2721
2767
|
/** Most recent known destination — updated on each check-in */
|
|
2722
|
-
currentDestination?:
|
|
2768
|
+
currentDestination?: DestinationValue;
|
|
2723
2769
|
sector?: string;
|
|
2724
2770
|
subsector?: string;
|
|
2725
2771
|
businessSize?: string;
|
package/package.json
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
customDestinationId,
|
|
3
|
+
resolveLeaverDestinations,
|
|
4
|
+
destinationLabel,
|
|
5
|
+
destinationNccisCode,
|
|
6
|
+
destinationOptions,
|
|
7
|
+
isNegativeDestination,
|
|
8
|
+
LeaverDestinationMap,
|
|
9
|
+
rollUpDestinationCounts,
|
|
10
|
+
validateLeaverDestinations,
|
|
11
|
+
} from "./destinations";
|
|
12
|
+
|
|
13
|
+
const customs: LeaverDestinationMap = {
|
|
14
|
+
custom_in_hospital: {id: "custom_in_hospital", label: "In hospital", nccisCode: "NEET"},
|
|
15
|
+
custom_in_custody: {id: "custom_in_custody", label: "In custody", nccisCode: "NEET"},
|
|
16
|
+
custom_supported_internship: {id: "custom_supported_internship", label: "Supported internship", nccisCode: "EMP"},
|
|
17
|
+
custom_moved_away: {id: "custom_moved_away", label: "Moved away", nccisCode: "NK", archived: true},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("destinationOptions", () => {
|
|
21
|
+
it("lists the nine NCCIS codes when a school has added nothing", () => {
|
|
22
|
+
const options = destinationOptions();
|
|
23
|
+
expect(options).toHaveLength(9);
|
|
24
|
+
expect(options.every((o) => !o.custom)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("puts each custom category directly after the code it counts within", () => {
|
|
28
|
+
const codes = destinationOptions(customs).map((o) => o.code);
|
|
29
|
+
expect(codes.indexOf("custom_supported_internship")).toEqual(codes.indexOf("EMP") + 1);
|
|
30
|
+
// Both NEET categories follow NEET, alphabetically.
|
|
31
|
+
expect(codes.slice(codes.indexOf("NEET"), codes.indexOf("NEET") + 3))
|
|
32
|
+
.toEqual(["NEET", "custom_in_custody", "custom_in_hospital"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("hides archived categories unless asked for them", () => {
|
|
36
|
+
expect(destinationOptions(customs).map((o) => o.code)).not.toContain("custom_moved_away");
|
|
37
|
+
expect(destinationOptions(customs, {includeArchived: true}).map((o) => o.code))
|
|
38
|
+
.toContain("custom_moved_away");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("resolveLeaverDestinations", () => {
|
|
43
|
+
const trust = {leaverDestinations: {
|
|
44
|
+
custom_in_care: {id: "custom_in_care", label: "In care", nccisCode: "NEET" as const},
|
|
45
|
+
custom_in_hospital: {id: "custom_in_hospital", label: "In hospital (trust)", nccisCode: "NEET" as const},
|
|
46
|
+
}};
|
|
47
|
+
|
|
48
|
+
it("offers the trust's categories alongside the school's, and names the trust's", () => {
|
|
49
|
+
const {destinations, trustIds} = resolveLeaverDestinations({leaverDestinations: customs}, trust);
|
|
50
|
+
expect(Object.keys(destinations).sort()).toEqual([
|
|
51
|
+
"custom_in_care", "custom_in_custody", "custom_in_hospital",
|
|
52
|
+
"custom_moved_away", "custom_supported_internship",
|
|
53
|
+
]);
|
|
54
|
+
expect(trustIds).toEqual(["custom_in_care"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("leaves the school's own version of a shared id in the school's hands", () => {
|
|
58
|
+
const {destinations, trustIds} = resolveLeaverDestinations({leaverDestinations: customs}, trust);
|
|
59
|
+
expect(destinations.custom_in_hospital.label).toEqual("In hospital");
|
|
60
|
+
expect(trustIds).not.toContain("custom_in_hospital");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("is the school's own map when there is no trust", () => {
|
|
64
|
+
expect(resolveLeaverDestinations({leaverDestinations: customs}).trustIds).toEqual([]);
|
|
65
|
+
expect(resolveLeaverDestinations().destinations).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("destinationLabel", () => {
|
|
70
|
+
it("reads standard codes and the school's own categories", () => {
|
|
71
|
+
expect(destinationLabel("APP")).toEqual("Apprenticeship");
|
|
72
|
+
expect(destinationLabel("custom_in_hospital", customs)).toEqual("In hospital");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("recovers words from an id defined by another school", () => {
|
|
76
|
+
// A student browsing trust-wide alumni meets categories we don't hold.
|
|
77
|
+
expect(destinationLabel("custom_in_care")).toEqual("In care");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("destinationNccisCode", () => {
|
|
82
|
+
it("maps a custom category onto the code it reports as", () => {
|
|
83
|
+
expect(destinationNccisCode("custom_in_hospital", customs)).toEqual("NEET");
|
|
84
|
+
expect(destinationNccisCode("custom_supported_internship", customs)).toEqual("EMP");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("treats an unknown id as not known rather than dropping the leaver", () => {
|
|
88
|
+
expect(destinationNccisCode("custom_gone", customs)).toEqual("NK");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("leaves standard codes alone", () => {
|
|
92
|
+
expect(destinationNccisCode("HE", customs)).toEqual("HE");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("isNegativeDestination", () => {
|
|
97
|
+
it("counts a school's NEET-mapped categories as NEET", () => {
|
|
98
|
+
expect(isNegativeDestination("custom_in_hospital", customs)).toBe(true);
|
|
99
|
+
expect(isNegativeDestination("custom_supported_internship", customs)).toBe(false);
|
|
100
|
+
expect(isNegativeDestination("NK")).toBe(true);
|
|
101
|
+
expect(isNegativeDestination("HE")).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("rollUpDestinationCounts", () => {
|
|
106
|
+
it("folds custom categories into their NCCIS code", () => {
|
|
107
|
+
const rolled = rollUpDestinationCounts(
|
|
108
|
+
{HE: 10, NEET: 2, custom_in_hospital: 3, custom_in_custody: 1, custom_supported_internship: 4}, customs);
|
|
109
|
+
expect(rolled).toEqual({HE: 10, NEET: 6, EMP: 4});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("customDestinationId", () => {
|
|
114
|
+
it("derives a field-path-safe id from the label", () => {
|
|
115
|
+
expect(customDestinationId("In hospital")).toEqual("custom_in_hospital");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("suffixes rather than overwriting a category that slugs the same", () => {
|
|
119
|
+
expect(customDestinationId("In-hospital", customs)).toEqual("custom_in_hospital_2");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("validateLeaverDestinations", () => {
|
|
124
|
+
it("accepts a well-formed set", () => {
|
|
125
|
+
expect(validateLeaverDestinations(customs)).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("rejects a category with no name, a bad group, or a duplicate label", () => {
|
|
129
|
+
expect(validateLeaverDestinations({
|
|
130
|
+
custom_x: {id: "custom_x", label: " ", nccisCode: "NEET"},
|
|
131
|
+
})).toBeDefined();
|
|
132
|
+
expect(validateLeaverDestinations({
|
|
133
|
+
custom_x: {id: "custom_x", label: "In hospital", nccisCode: "XX" as never},
|
|
134
|
+
})).toBeDefined();
|
|
135
|
+
expect(validateLeaverDestinations({
|
|
136
|
+
...customs,
|
|
137
|
+
custom_in_hospital_2: {id: "custom_in_hospital_2", label: "in hospital", nccisCode: "NEET"},
|
|
138
|
+
})).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("rejects an id that could collide with a standard code or a field path", () => {
|
|
142
|
+
expect(validateLeaverDestinations({
|
|
143
|
+
HE: {id: "HE", label: "Uni", nccisCode: "HE"},
|
|
144
|
+
})).toBeDefined();
|
|
145
|
+
expect(validateLeaverDestinations({
|
|
146
|
+
"custom_a.b": {id: "custom_a.b", label: "Dotted", nccisCode: "NEET"},
|
|
147
|
+
})).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {DESTINATION_CODES} from "./constants";
|
|
2
|
+
import {DestinationCode, DestinationValue, InstituteData, LeaverDestination} from "./typeDefinitions";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* destinations.ts — the nine NCCIS codes plus whatever a school has added to them.
|
|
6
|
+
*
|
|
7
|
+
* NCCIS gives every leaver one of nine activity codes, and two of them — NEET and
|
|
8
|
+
* NK — cover situations that could hardly be less alike: a leaver in hospital, one
|
|
9
|
+
* in care, one in custody and one who simply hasn't decided are all the same row of
|
|
10
|
+
* the return. Schools with SEND cohorts need that row broken up, so a school can
|
|
11
|
+
* define its own categories (`InstituteData.leaverDestinations`) and every one of
|
|
12
|
+
* them names the NCCIS code it counts within.
|
|
13
|
+
*
|
|
14
|
+
* That mapping is the whole design: a custom category's id is what gets stored and
|
|
15
|
+
* counted, so the school's own charts get the finer breakdown, while anything
|
|
16
|
+
* statutory (Compass+, the LA MI return) resolves it back to one of the nine.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type LeaverDestinationMap = {[id: string]: LeaverDestination};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One option as the forms and charts see it — standard and custom side by side, so
|
|
23
|
+
* nothing downstream has to know which it is unless it cares.
|
|
24
|
+
*/
|
|
25
|
+
export type DestinationOption = {
|
|
26
|
+
/** What gets stored: an NCCIS code, or a custom category's id. */
|
|
27
|
+
code: DestinationValue,
|
|
28
|
+
label: string,
|
|
29
|
+
/** The NCCIS code this counts within — its own code, for the standard nine. */
|
|
30
|
+
nccisCode: DestinationCode,
|
|
31
|
+
custom: boolean,
|
|
32
|
+
description?: string,
|
|
33
|
+
archived?: boolean,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Ids become Firestore field-path segments (`aggregates.destinations.<id>`), so
|
|
37
|
+
* they may not contain a dot, a slash or anything else a path splits on. */
|
|
38
|
+
export const DESTINATION_ID_PATTERN = /^[A-Za-z0-9_]{1,60}$/;
|
|
39
|
+
|
|
40
|
+
/** Custom ids carry a prefix so nothing can shadow a standard code, now or when
|
|
41
|
+
* NCCIS adds one. */
|
|
42
|
+
export const CUSTOM_DESTINATION_PREFIX = "custom_";
|
|
43
|
+
|
|
44
|
+
/** The NCCIS codes that mean "not in education, employment or training, or we don't
|
|
45
|
+
* know" — the pair schools break down, and the pair a positive-destination rate
|
|
46
|
+
* counts against. */
|
|
47
|
+
export const NEGATIVE_DESTINATION_CODES: DestinationCode[] = ["NEET", "NK"];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A stable id for a new custom category, derived from its label.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} label the category's display name.
|
|
53
|
+
* @param {object} [existing] categories already defined, so ids stay unique.
|
|
54
|
+
* @return {string} a field-path-safe id.
|
|
55
|
+
*/
|
|
56
|
+
export const customDestinationId = (label: string, existing?: LeaverDestinationMap): string => {
|
|
57
|
+
const slug = (label || "")
|
|
58
|
+
.trim().toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
60
|
+
.replace(/^_+|_+$/g, "")
|
|
61
|
+
.slice(0, 40) || "category";
|
|
62
|
+
const base = `${CUSTOM_DESTINATION_PREFIX}${slug}`;
|
|
63
|
+
if (!existing?.[base]) return base;
|
|
64
|
+
// Two categories can legitimately slug the same ("In care" / "In-care"), so the
|
|
65
|
+
// second one gets a suffix rather than silently overwriting the first.
|
|
66
|
+
let n = 2;
|
|
67
|
+
while (existing[`${base}_${n}`]) n++;
|
|
68
|
+
return `${base}_${n}`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The nine NCCIS codes as options, in the order the MI return lists them. */
|
|
72
|
+
export const standardDestinationOptions = (): DestinationOption[] =>
|
|
73
|
+
Object.entries(DESTINATION_CODES).map(([code, label]) => ({
|
|
74
|
+
code, label, nccisCode: code as DestinationCode, custom: false,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every destination a school offers: the nine standard codes, then its own
|
|
79
|
+
* categories grouped after the NCCIS code each one belongs to.
|
|
80
|
+
*
|
|
81
|
+
* Archived categories are left out unless asked for — they exist so that historical
|
|
82
|
+
* records keep their label, not so that new leavers can pick them.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
85
|
+
* @param {object} [opts] `includeArchived` to keep withdrawn categories in the list.
|
|
86
|
+
* @return {DestinationOption[]} the ordered option list.
|
|
87
|
+
*/
|
|
88
|
+
export const destinationOptions = (
|
|
89
|
+
customs?: LeaverDestinationMap, opts?: {includeArchived?: boolean},
|
|
90
|
+
): DestinationOption[] => {
|
|
91
|
+
const custom = Object.entries(customs ?? {})
|
|
92
|
+
.filter(([, d]) => d && (opts?.includeArchived || !d.archived))
|
|
93
|
+
.map(([id, d]) => ({
|
|
94
|
+
code: id,
|
|
95
|
+
label: d.label,
|
|
96
|
+
nccisCode: (DESTINATION_CODES[d.nccisCode] ? d.nccisCode : "NEET") as DestinationCode,
|
|
97
|
+
custom: true,
|
|
98
|
+
description: d.description,
|
|
99
|
+
archived: d.archived,
|
|
100
|
+
}))
|
|
101
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
102
|
+
|
|
103
|
+
// Grouped under their parent so "In hospital" reads as a kind of NEET rather than
|
|
104
|
+
// as a tenth code of its own.
|
|
105
|
+
return standardDestinationOptions().flatMap((std) => [
|
|
106
|
+
std, ...custom.filter((c) => c.nccisCode === std.code),
|
|
107
|
+
]);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A school's categories, plus the ones its trust supplies.
|
|
112
|
+
*
|
|
113
|
+
* A trust's categories are extra vocabulary, not a mandate: they are added to
|
|
114
|
+
* whatever the school has of its own, and the school can neither edit nor remove
|
|
115
|
+
* them. Nothing is copied onto the school — resolution is a read-time merge, so a
|
|
116
|
+
* school that leaves the trust simply stops seeing them (see ./trustPolicies for why
|
|
117
|
+
* that is how every trust-supplied setting works here).
|
|
118
|
+
*
|
|
119
|
+
* On an id collision the SCHOOL's entry wins. Both keys are the same, so responses
|
|
120
|
+
* and aggregates land in the same place either way; what differs is that the school
|
|
121
|
+
* keeps the category it made, and keeps being able to edit it.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} [school] the school's own institute doc.
|
|
124
|
+
* @param {object} [trust] the trust's institute doc, when the school is in one.
|
|
125
|
+
* @return {object} the merged map, and the ids the trust owns.
|
|
126
|
+
*/
|
|
127
|
+
export const resolveLeaverDestinations = (
|
|
128
|
+
school?: Pick<InstituteData, "leaverDestinations">,
|
|
129
|
+
trust?: Pick<InstituteData, "leaverDestinations">,
|
|
130
|
+
): {destinations: LeaverDestinationMap, trustIds: string[]} => {
|
|
131
|
+
const own = school?.leaverDestinations ?? {};
|
|
132
|
+
const fromTrust = trust?.leaverDestinations ?? {};
|
|
133
|
+
const trustIds = Object.keys(fromTrust).filter((id) => !own[id]);
|
|
134
|
+
return {
|
|
135
|
+
destinations: {...fromTrust, ...own},
|
|
136
|
+
trustIds,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What to show for a stored destination — a standard label, or a school's own.
|
|
142
|
+
*
|
|
143
|
+
* A category defined by a DIFFERENT school (a student browsing trust-wide alumni
|
|
144
|
+
* meets these) isn't in the map we hold, so its id is turned back into words rather
|
|
145
|
+
* than shown raw: an id is derived from the label it was created with, which makes
|
|
146
|
+
* "custom_in_hospital" recoverable as "In hospital".
|
|
147
|
+
*
|
|
148
|
+
* @param {string} [code] the stored destination.
|
|
149
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
150
|
+
* @return {string|undefined} the display label.
|
|
151
|
+
*/
|
|
152
|
+
export const destinationLabel = (
|
|
153
|
+
code?: DestinationValue, customs?: LeaverDestinationMap,
|
|
154
|
+
): string|undefined => {
|
|
155
|
+
if (!code) return undefined;
|
|
156
|
+
if (DESTINATION_CODES[code]) return DESTINATION_CODES[code];
|
|
157
|
+
const known = customs?.[code]?.label;
|
|
158
|
+
if (known) return known;
|
|
159
|
+
if (!code.startsWith(CUSTOM_DESTINATION_PREFIX)) return code;
|
|
160
|
+
const words = code.slice(CUSTOM_DESTINATION_PREFIX.length).replace(/_/g, " ").trim();
|
|
161
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : code;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The NCCIS code a stored destination counts within. Statutory exports and the
|
|
166
|
+
* NEET/NK figures go through this, never through the raw value.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} [code] the stored destination.
|
|
169
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
170
|
+
* @return {DestinationCode|undefined} the NCCIS code to report it as.
|
|
171
|
+
*/
|
|
172
|
+
export const destinationNccisCode = (
|
|
173
|
+
code?: DestinationValue, customs?: LeaverDestinationMap,
|
|
174
|
+
): DestinationCode|undefined => {
|
|
175
|
+
if (!code) return undefined;
|
|
176
|
+
if (DESTINATION_CODES[code]) return code as DestinationCode;
|
|
177
|
+
const custom = customs?.[code];
|
|
178
|
+
// An id we no longer recognise is still somebody's leaver: "not known" is the
|
|
179
|
+
// honest reading, and it keeps them in the cohort rather than dropping them.
|
|
180
|
+
if (!custom) return "NK";
|
|
181
|
+
return DESTINATION_CODES[custom.nccisCode] ? custom.nccisCode : "NEET";
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether a destination is one of the two the school is trying to shrink. Custom
|
|
186
|
+
* categories count exactly as the NCCIS code they name does.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} [code] the stored destination.
|
|
189
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
190
|
+
* @return {boolean} true for NEET / NK and anything mapped to them.
|
|
191
|
+
*/
|
|
192
|
+
export const isNegativeDestination = (
|
|
193
|
+
code?: DestinationValue, customs?: LeaverDestinationMap,
|
|
194
|
+
): boolean => {
|
|
195
|
+
const nccis = destinationNccisCode(code, customs);
|
|
196
|
+
return Boolean(nccis && NEGATIVE_DESTINATION_CODES.includes(nccis));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Re-keys a `{destination: count}` aggregate onto the nine NCCIS codes, folding each
|
|
201
|
+
* custom category into its parent. For anything that has to report in NCCIS terms
|
|
202
|
+
* while the raw aggregate stays as granular as the school made it.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} [counts] the raw aggregate.
|
|
205
|
+
* @param {object} [customs] the institute's leaverDestinations map.
|
|
206
|
+
* @return {object} counts keyed by NCCIS code.
|
|
207
|
+
*/
|
|
208
|
+
export const rollUpDestinationCounts = (
|
|
209
|
+
counts?: {[code: string]: number}, customs?: LeaverDestinationMap,
|
|
210
|
+
): {[code: string]: number} => {
|
|
211
|
+
const out: {[code: string]: number} = {};
|
|
212
|
+
for (const [code, n] of Object.entries(counts ?? {})) {
|
|
213
|
+
const key = destinationNccisCode(code, customs) ?? "NK";
|
|
214
|
+
out[key] = (out[key] ?? 0) + (n ?? 0);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validates a set of custom categories before it is stored. Shared by the setup card
|
|
221
|
+
* and the callable so both refuse the same things.
|
|
222
|
+
*
|
|
223
|
+
* @param {object} destinations the map to check.
|
|
224
|
+
* @return {string|undefined} the first problem found, or undefined if it's valid.
|
|
225
|
+
*/
|
|
226
|
+
export const validateLeaverDestinations = (destinations: LeaverDestinationMap): string|undefined => {
|
|
227
|
+
const entries = Object.entries(destinations ?? {});
|
|
228
|
+
if (entries.length > 50) return "You can add up to 50 destination categories.";
|
|
229
|
+
const seen = new Set<string>();
|
|
230
|
+
for (const [id, d] of entries) {
|
|
231
|
+
if (!d) return "One of your categories is empty.";
|
|
232
|
+
if (!DESTINATION_ID_PATTERN.test(id) || id !== d.id) return "A category id is invalid.";
|
|
233
|
+
if (!id.startsWith(CUSTOM_DESTINATION_PREFIX)) return "A category id is invalid.";
|
|
234
|
+
if (DESTINATION_CODES[id]) return "A category can't reuse a standard destination code.";
|
|
235
|
+
const label = (d.label ?? "").trim();
|
|
236
|
+
if (!label) return "Give every category a name.";
|
|
237
|
+
if (label.length > 60) return "Category names must be 60 characters or fewer.";
|
|
238
|
+
if (!DESTINATION_CODES[d.nccisCode]) return `Choose which group "${label}" counts within.`;
|
|
239
|
+
const key = label.toLowerCase();
|
|
240
|
+
if (seen.has(key)) return `You already have a category called "${label}".`;
|
|
241
|
+
// A category that duplicates a standard label would show twice in the picker
|
|
242
|
+
// with no way to tell them apart.
|
|
243
|
+
if (Object.values(DESTINATION_CODES).some((l) => l.toLowerCase() === key)) {
|
|
244
|
+
return `"${label}" is already a standard destination.`;
|
|
245
|
+
}
|
|
246
|
+
seen.add(key);
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from "./groups";
|
|
|
23
23
|
export * from "./emailSections";
|
|
24
24
|
export * from "./exportTransformers";
|
|
25
25
|
export * from "./compassExport";
|
|
26
|
+
export * from "./destinations";
|
|
26
27
|
export * from "./benchmarkEvidenceRules";
|
|
27
28
|
export * from "./portfolioAggregate";
|
|
28
29
|
export * from "./tutorialTips";
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -1106,6 +1106,12 @@ export type InstituteData = {
|
|
|
1106
1106
|
// Written by the setup wizard (aspirations-saveDestinationsConfig); read by
|
|
1107
1107
|
// destinationsProvisionCron each September to create that year's surveys/groups.
|
|
1108
1108
|
destinationsConfig?: DestinationsConfig,
|
|
1109
|
+
/** School-defined leaver destination categories, keyed by id, added from the
|
|
1110
|
+
* Destinations Setup tab. They sit alongside the nine NCCIS codes on every
|
|
1111
|
+
* leaver form and each one names the NCCIS code it counts within, so a school
|
|
1112
|
+
* can break "NEET" down into the situations it actually needs to track
|
|
1113
|
+
* (in hospital, in care, in custody) without breaking statutory reporting. */
|
|
1114
|
+
leaverDestinations?: {[id: string]: LeaverDestination},
|
|
1109
1115
|
/** Trust-authored policy, one entry per feature a trust can impose on the schools
|
|
1110
1116
|
* beneath it. Set on the TRUST's own institute doc and read by its schools through
|
|
1111
1117
|
* their approved trust link — never copied onto the school, so there is one source
|
|
@@ -2188,6 +2194,47 @@ export type ExternalEventRequest = {
|
|
|
2188
2194
|
/** NCCIS-compatible destination activity codes (MI Requirement 2026–27) */
|
|
2189
2195
|
export type DestinationCode = "HE"|"FE"|"APP"|"EWBL"|"EMP"|"VOL"|"OTH"|"NEET"|"NK";
|
|
2190
2196
|
|
|
2197
|
+
/**
|
|
2198
|
+
* A destination as it is actually stored on a response, an alumni record or a
|
|
2199
|
+
* cycle aggregate: one of the nine NCCIS codes above, or the id of a school-defined
|
|
2200
|
+
* category from `InstituteData.leaverDestinations`.
|
|
2201
|
+
*
|
|
2202
|
+
* Custom categories exist because NCCIS's NEET and NK are a single bucket for very
|
|
2203
|
+
* different situations — a SEND leaver in hospital, in care or in custody is "NEET"
|
|
2204
|
+
* to the local authority and nothing more, which is exactly the granularity schools
|
|
2205
|
+
* lose. Every custom category still carries the NCCIS code it counts within
|
|
2206
|
+
* (`LeaverDestination.nccisCode`), so statutory returns are unaffected: only the
|
|
2207
|
+
* school's own reporting gets finer.
|
|
2208
|
+
*
|
|
2209
|
+
* The `(string & {})` half keeps editor autocomplete for the nine standard codes
|
|
2210
|
+
* while still accepting a custom id.
|
|
2211
|
+
*/
|
|
2212
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
2213
|
+
export type DestinationValue = DestinationCode | (string & {});
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* A school-defined leaver destination category, stored on
|
|
2217
|
+
* `InstituteData.leaverDestinations` keyed by `id`.
|
|
2218
|
+
*
|
|
2219
|
+
* `id` is what lands in responses and in `aggregates.destinations.<id>`, so it is
|
|
2220
|
+
* restricted to `[A-Za-z0-9_]` (see DESTINATION_ID_PATTERN) — a Firestore field path
|
|
2221
|
+
* segment can't contain a dot or a slash, and these keys become one.
|
|
2222
|
+
*/
|
|
2223
|
+
export type LeaverDestination = {
|
|
2224
|
+
id: string,
|
|
2225
|
+
label: string,
|
|
2226
|
+
/** Which of the nine NCCIS codes this rolls up into for statutory reporting. */
|
|
2227
|
+
nccisCode: DestinationCode,
|
|
2228
|
+
/** Optional helper text shown under the option on the leaver forms. */
|
|
2229
|
+
description?: string,
|
|
2230
|
+
/** Removed from the forms, but kept here so records and charts that already
|
|
2231
|
+
* carry the id keep their label. Deleting outright would leave past responses
|
|
2232
|
+
* showing a raw id. */
|
|
2233
|
+
archived?: boolean,
|
|
2234
|
+
createdAt?: string,
|
|
2235
|
+
createdBy?: string,
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2191
2238
|
/** SOC 2020 major group number */
|
|
2192
2239
|
export type SOCMajorGroup = 1|2|3|4|5|6|7|8|9;
|
|
2193
2240
|
|
|
@@ -2235,7 +2282,7 @@ export type ApprenticeshipDetails = {
|
|
|
2235
2282
|
* Appended on every check-in; source distinguishes self-report from staff/trusted-contact entry.
|
|
2236
2283
|
*/
|
|
2237
2284
|
export type CheckInEntry = {
|
|
2238
|
-
destinationCode:
|
|
2285
|
+
destinationCode: DestinationValue,
|
|
2239
2286
|
type?: "annual"|"midYear"|"leaverTransition"|"resultsDay"|"christmas", // Timepoint type; defaults to "annual"
|
|
2240
2287
|
sector?: string,
|
|
2241
2288
|
date: string, // ISO date string
|
|
@@ -2559,7 +2606,7 @@ export type LeaverTransitionData = {
|
|
|
2559
2606
|
activities?: string[],
|
|
2560
2607
|
|
|
2561
2608
|
// Destination (NCCIS / Gatsby BM3)
|
|
2562
|
-
intendedDestination:
|
|
2609
|
+
intendedDestination: DestinationValue,
|
|
2563
2610
|
intendedDestinationDetails?: EducationDetails|EmploymentDetails|ApprenticeshipDetails,
|
|
2564
2611
|
intendedStartDate?: string,
|
|
2565
2612
|
|
|
@@ -2606,7 +2653,7 @@ export type LeaverConsentCapture = {
|
|
|
2606
2653
|
*/
|
|
2607
2654
|
export type LeaverCheckpointResponse = {
|
|
2608
2655
|
checkpoint: "resultsDay"|"christmas",
|
|
2609
|
-
destination:
|
|
2656
|
+
destination: DestinationValue,
|
|
2610
2657
|
destinationDetails?: EducationDetails|EmploymentDetails|ApprenticeshipDetails,
|
|
2611
2658
|
/** resultsDay only — did results change their plan vs intendedDestination? */
|
|
2612
2659
|
changedFromIntended?: boolean,
|
|
@@ -2680,13 +2727,13 @@ export type Alumni = {
|
|
|
2680
2727
|
|
|
2681
2728
|
// Destination tracking
|
|
2682
2729
|
/** Intended destination captured via Form B before leaving */
|
|
2683
|
-
intendedDestination?:
|
|
2730
|
+
intendedDestination?: DestinationValue,
|
|
2684
2731
|
intendedDestinationDetails?: EducationDetails|EmploymentDetails|ApprenticeshipDetails,
|
|
2685
2732
|
intendedStartDate?: string,
|
|
2686
2733
|
/** October confirmed actual destination snapshot */
|
|
2687
|
-
confirmedDestination?:
|
|
2734
|
+
confirmedDestination?: DestinationValue,
|
|
2688
2735
|
/** Most recent known destination — updated on each check-in */
|
|
2689
|
-
currentDestination?:
|
|
2736
|
+
currentDestination?: DestinationValue,
|
|
2690
2737
|
|
|
2691
2738
|
// Legacy fields — retained for backwards compatibility; use CheckInEntry going forward
|
|
2692
2739
|
sector?: string,
|