placementt-core 1.400.1038 → 1.400.1040

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.
@@ -17,12 +17,18 @@ export declare const destinationsConfigCallables: {
17
17
  /** The full DestinationsConfig from the setup wizard. Validated server
18
18
  * side, which is also where provenance fields are stamped. */
19
19
  config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
20
+ /** Trust-only: what this trust imposes on its schools, keyed by feature.
21
+ * Merged into the institute's existing policies, and refused outright
22
+ * from a school. Lives on the institute root, not inside config. */
23
+ trustPolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
24
  }, "strip", z.ZodTypeAny, {
21
25
  config: Record<string, unknown>;
22
26
  oId: string;
27
+ trustPolicies?: Record<string, unknown> | undefined;
23
28
  }, {
24
29
  config: Record<string, unknown>;
25
30
  oId: string;
31
+ trustPolicies?: Record<string, unknown> | undefined;
26
32
  }>;
27
33
  response: z.ZodObject<{
28
34
  saved: z.ZodBoolean;
@@ -39,6 +45,34 @@ export declare const destinationsConfigCallables: {
39
45
  created?: number | undefined;
40
46
  }>;
41
47
  };
48
+ "destinationsConfig-saveLeaverDestinations": {
49
+ request: z.ZodObject<{
50
+ oId: z.ZodString;
51
+ /** The school's own leaver destination categories, keyed by id. The nine
52
+ * NCCIS codes are fixed and are never sent — only what sits under them.
53
+ * Validated server side, which also stamps provenance and turns a
54
+ * removal into an archive. */
55
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
56
+ }, "strip", z.ZodTypeAny, {
57
+ oId: string;
58
+ destinations: Record<string, unknown>;
59
+ }, {
60
+ oId: string;
61
+ destinations: Record<string, unknown>;
62
+ }>;
63
+ response: z.ZodObject<{
64
+ saved: z.ZodBoolean;
65
+ /** The stored map, including anything the server archived rather than
66
+ * deleted — the client adopts this rather than its own copy. */
67
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
68
+ }, "strip", z.ZodTypeAny, {
69
+ destinations: Record<string, unknown>;
70
+ saved: boolean;
71
+ }, {
72
+ destinations: Record<string, unknown>;
73
+ saved: boolean;
74
+ }>;
75
+ };
42
76
  "destinationsConfig-provisionDestinationsNow": {
43
77
  request: z.ZodObject<{
44
78
  oId: z.ZodString;
@@ -20,6 +20,10 @@ exports.destinationsConfigCallables = {
20
20
  /** The full DestinationsConfig from the setup wizard. Validated server
21
21
  * side, which is also where provenance fields are stamped. */
22
22
  config: zod_1.z.record(zod_1.z.unknown()),
23
+ /** Trust-only: what this trust imposes on its schools, keyed by feature.
24
+ * Merged into the institute's existing policies, and refused outright
25
+ * from a school. Lives on the institute root, not inside config. */
26
+ trustPolicies: zod_1.z.record(zod_1.z.unknown()).optional(),
23
27
  }),
24
28
  response: zod_1.z.object({
25
29
  saved: zod_1.z.boolean(),
@@ -28,6 +32,22 @@ exports.destinationsConfigCallables = {
28
32
  created: zod_1.z.number().optional(),
29
33
  }),
30
34
  },
35
+ "destinationsConfig-saveLeaverDestinations": {
36
+ request: zod_1.z.object({
37
+ oId: zod_1.z.string(),
38
+ /** The school's own leaver destination categories, keyed by id. The nine
39
+ * NCCIS codes are fixed and are never sent — only what sits under them.
40
+ * Validated server side, which also stamps provenance and turns a
41
+ * removal into an archive. */
42
+ destinations: zod_1.z.record(zod_1.z.unknown()),
43
+ }),
44
+ response: zod_1.z.object({
45
+ saved: zod_1.z.boolean(),
46
+ /** The stored map, including anything the server archived rather than
47
+ * deleted — the client adopts this rather than its own copy. */
48
+ destinations: zod_1.z.record(zod_1.z.unknown()),
49
+ }),
50
+ },
31
51
  "destinationsConfig-provisionDestinationsNow": {
32
52
  request: zod_1.z.object({
33
53
  oId: zod_1.z.string(),
@@ -1 +1 @@
1
- {"version":3,"file":"destinationsConfig.js","sourceRoot":"","sources":["../../src/callables/destinationsConfig.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;;;;GAUG;AACU,QAAA,2BAA2B,GAAG;IACvC,2CAA2C,EAAE;QACzC,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;YACf;0EAC8D;YAC9D,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC;SAChC,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,OAAO,EAAE;YAClB,2EAA2E;YAC3E,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACjC,CAAC;KACL;IACD,6CAA6C,EAAE;QAC3C,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;YACf,uEAAuE;YACvE,QAAQ,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;YAChC,8EAA8E;YAC9E,KAAK,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE;SACvD,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;YACnB,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE;SAC3B,CAAC;KACL;CACoB,CAAC"}
1
+ {"version":3,"file":"destinationsConfig.js","sourceRoot":"","sources":["../../src/callables/destinationsConfig.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;;;;GAUG;AACU,QAAA,2BAA2B,GAAG;IACvC,2CAA2C,EAAE;QACzC,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;YACf;0EAC8D;YAC9D,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC;YAC7B;;gFAEoE;YACpE,aAAa,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;SAClD,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,OAAO,EAAE;YAClB,2EAA2E;YAC3E,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACjC,CAAC;KACL;IACD,2CAA2C,EAAE;QACzC,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;YACf;;;0CAG8B;YAC9B,YAAY,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC;SACtC,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,KAAK,EAAE,OAAC,CAAC,OAAO,EAAE;YAClB;4EACgE;YAChE,YAAY,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC;SACtC,CAAC;KACL;IACD,6CAA6C,EAAE;QAC3C,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE;YACf,uEAAuE;YACvE,QAAQ,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;YAChC,8EAA8E;YAC9E,KAAK,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE;SACvD,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE;YACxB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;YACnB,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE;SAC3B,CAAC;KACL;CACoB,CAAC"}
@@ -763,12 +763,15 @@ export declare const allCallables: {
763
763
  request: z.ZodObject<{
764
764
  oId: z.ZodString;
765
765
  config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
766
+ trustPolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
766
767
  }, "strip", z.ZodTypeAny, {
767
768
  config: Record<string, unknown>;
768
769
  oId: string;
770
+ trustPolicies?: Record<string, unknown> | undefined;
769
771
  }, {
770
772
  config: Record<string, unknown>;
771
773
  oId: string;
774
+ trustPolicies?: Record<string, unknown> | undefined;
772
775
  }>;
773
776
  response: z.ZodObject<{
774
777
  saved: z.ZodBoolean;
@@ -784,6 +787,28 @@ export declare const allCallables: {
784
787
  created?: number | undefined;
785
788
  }>;
786
789
  };
790
+ "destinationsConfig-saveLeaverDestinations": {
791
+ request: z.ZodObject<{
792
+ oId: z.ZodString;
793
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
794
+ }, "strip", z.ZodTypeAny, {
795
+ oId: string;
796
+ destinations: Record<string, unknown>;
797
+ }, {
798
+ oId: string;
799
+ destinations: Record<string, unknown>;
800
+ }>;
801
+ response: z.ZodObject<{
802
+ saved: z.ZodBoolean;
803
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
804
+ }, "strip", z.ZodTypeAny, {
805
+ destinations: Record<string, unknown>;
806
+ saved: boolean;
807
+ }, {
808
+ destinations: Record<string, unknown>;
809
+ saved: boolean;
810
+ }>;
811
+ };
787
812
  "destinationsConfig-provisionDestinationsNow": {
788
813
  request: z.ZodObject<{
789
814
  oId: z.ZodString;
@@ -833,12 +858,15 @@ export declare const callableGroups: {
833
858
  request: z.ZodObject<{
834
859
  oId: z.ZodString;
835
860
  config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
861
+ trustPolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
836
862
  }, "strip", z.ZodTypeAny, {
837
863
  config: Record<string, unknown>;
838
864
  oId: string;
865
+ trustPolicies?: Record<string, unknown> | undefined;
839
866
  }, {
840
867
  config: Record<string, unknown>;
841
868
  oId: string;
869
+ trustPolicies?: Record<string, unknown> | undefined;
842
870
  }>;
843
871
  response: z.ZodObject<{
844
872
  saved: z.ZodBoolean;
@@ -854,6 +882,28 @@ export declare const callableGroups: {
854
882
  created?: number | undefined;
855
883
  }>;
856
884
  };
885
+ "destinationsConfig-saveLeaverDestinations": {
886
+ request: z.ZodObject<{
887
+ oId: z.ZodString;
888
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
889
+ }, "strip", z.ZodTypeAny, {
890
+ oId: string;
891
+ destinations: Record<string, unknown>;
892
+ }, {
893
+ oId: string;
894
+ destinations: Record<string, unknown>;
895
+ }>;
896
+ response: z.ZodObject<{
897
+ saved: z.ZodBoolean;
898
+ destinations: z.ZodRecord<z.ZodString, z.ZodUnknown>;
899
+ }, "strip", z.ZodTypeAny, {
900
+ destinations: Record<string, unknown>;
901
+ saved: boolean;
902
+ }, {
903
+ destinations: Record<string, unknown>;
904
+ saved: boolean;
905
+ }>;
906
+ };
857
907
  "destinationsConfig-provisionDestinationsNow": {
858
908
  request: z.ZodObject<{
859
909
  oId: z.ZodString;
@@ -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 {};