placementt-core 1.400.939 → 1.400.940

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { ExternalEvent } from "./typeDefinitions";
2
+ /**
3
+ * Benchmarks Compass+ accepts on an individual Activity row. GB1 ("a stable careers programme")
4
+ * describes whole-provision leadership, not a single encounter — CEC's Activities Upload template
5
+ * only offers Benchmarks 2-8 in its Benchmark dropdown, so GB1-only events have nothing to export.
6
+ */
7
+ export declare const COMPASS_VALID_ACTIVITY_BENCHMARKS: number[];
8
+ /**
9
+ * Benchmarks where the PAL Compliance column is meaningful. Provider Access Legislation covers
10
+ * encounters with technical education/apprenticeship providers, which sit under GB5-7 — Compass+
11
+ * only reflects this field on activities tagged with one of these three.
12
+ */
13
+ export declare const COMPASS_PAL_BENCHMARKS: number[];
14
+ /**
15
+ * Whether CareerThread is currently a CEC-recognised Compass+ "Partner Platform" (the status
16
+ * Unifrog, Xello/Cascaid, Morrisby etc. have). While this is false, Compass+ can't auto-detect our
17
+ * CSV as a known source on upload — schools may hit a "Missing Activity Source Data" error and have
18
+ * no listed option to pick us from the resolution dropdown. Flip this once CEC confirms we're
19
+ * listed as a partner; until then, exports carry a standing warning so staff don't assume the file
20
+ * will upload cleanly.
21
+ */
22
+ export declare const COMPASS_PARTNER_PLATFORM_RECOGNISED = false;
23
+ export declare const COMPASS_ACTIVITY_CSV_HEADERS: string[];
24
+ export type CompassActivityRow = {
25
+ benchmark: number;
26
+ category: string;
27
+ activityName: string;
28
+ startDateTime: string;
29
+ endDateTime: string;
30
+ learnerUpns: string;
31
+ schoolLead: string;
32
+ notes: string;
33
+ palCompliance: "Yes" | "";
34
+ parentAttendance: "Yes" | "";
35
+ };
36
+ export type CompassExportIssue = {
37
+ eventId: string;
38
+ eventName: string;
39
+ level: "blocking" | "warning";
40
+ message: string;
41
+ };
42
+ /**
43
+ * Checks whether an event is safe to include in a Compass+ export. "blocking" issues mean the
44
+ * event is left out of the file entirely (it would fail Compass+'s own validation and, worse,
45
+ * could silently upload with the wrong benchmark/category). "warning" issues still get exported,
46
+ * but should be shown to staff before they send the file on to a school.
47
+ */
48
+ export declare function validateCompassExportEvent(event: Partial<ExternalEvent> & {
49
+ id?: string;
50
+ }, learnerCount: number, learnersWithUpnCount: number): CompassExportIssue[];
51
+ /**
52
+ * Expands a single event into one Compass+ Activities-Upload row per valid benchmark. Compass+'s
53
+ * template is one benchmark + one category per row, so an event tagged with multiple benchmarks
54
+ * (e.g. an employer talk logged as both GB5 and GB7) becomes multiple rows sharing the same
55
+ * activity name, dates and learner list. Rows for benchmarks without a mandatory category are
56
+ * skipped — callers should run validateCompassExportEvent first and surface that as a blocking
57
+ * issue rather than silently dropping the row here.
58
+ */
59
+ export declare function buildCompassActivityRows(event: Partial<ExternalEvent>, learnerUpns: string[]): CompassActivityRow[];
60
+ export declare function compassActivityRowToCsvArray(row: CompassActivityRow): string[];
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMPASS_ACTIVITY_CSV_HEADERS = exports.COMPASS_PARTNER_PLATFORM_RECOGNISED = exports.COMPASS_PAL_BENCHMARKS = exports.COMPASS_VALID_ACTIVITY_BENCHMARKS = void 0;
4
+ exports.validateCompassExportEvent = validateCompassExportEvent;
5
+ exports.buildCompassActivityRows = buildCompassActivityRows;
6
+ exports.compassActivityRowToCsvArray = compassActivityRowToCsvArray;
7
+ /**
8
+ * Benchmarks Compass+ accepts on an individual Activity row. GB1 ("a stable careers programme")
9
+ * describes whole-provision leadership, not a single encounter — CEC's Activities Upload template
10
+ * only offers Benchmarks 2-8 in its Benchmark dropdown, so GB1-only events have nothing to export.
11
+ */
12
+ exports.COMPASS_VALID_ACTIVITY_BENCHMARKS = [2, 3, 4, 5, 6, 7, 8];
13
+ /**
14
+ * Benchmarks where the PAL Compliance column is meaningful. Provider Access Legislation covers
15
+ * encounters with technical education/apprenticeship providers, which sit under GB5-7 — Compass+
16
+ * only reflects this field on activities tagged with one of these three.
17
+ */
18
+ exports.COMPASS_PAL_BENCHMARKS = [5, 6, 7];
19
+ /**
20
+ * Whether CareerThread is currently a CEC-recognised Compass+ "Partner Platform" (the status
21
+ * Unifrog, Xello/Cascaid, Morrisby etc. have). While this is false, Compass+ can't auto-detect our
22
+ * CSV as a known source on upload — schools may hit a "Missing Activity Source Data" error and have
23
+ * no listed option to pick us from the resolution dropdown. Flip this once CEC confirms we're
24
+ * listed as a partner; until then, exports carry a standing warning so staff don't assume the file
25
+ * will upload cleanly.
26
+ */
27
+ exports.COMPASS_PARTNER_PLATFORM_RECOGNISED = false;
28
+ exports.COMPASS_ACTIVITY_CSV_HEADERS = [
29
+ "Benchmark", "Main category", "Activity name", "Start Date and Time",
30
+ "End Date and Time", "Learners involved", "School Lead", "Notes",
31
+ "PAL Compliance", "Parent Attendance",
32
+ ];
33
+ const formatDbDateTime = (dateStr, timeStr) => {
34
+ if (!dateStr)
35
+ return "";
36
+ const [y, m, d] = dateStr.split("-");
37
+ if (!y || !m || !d)
38
+ return "";
39
+ return `${d}/${m}/${y} ${timeStr || "09:00"}`;
40
+ };
41
+ /**
42
+ * Checks whether an event is safe to include in a Compass+ export. "blocking" issues mean the
43
+ * event is left out of the file entirely (it would fail Compass+'s own validation and, worse,
44
+ * could silently upload with the wrong benchmark/category). "warning" issues still get exported,
45
+ * but should be shown to staff before they send the file on to a school.
46
+ */
47
+ function validateCompassExportEvent(event, learnerCount, learnersWithUpnCount) {
48
+ const issues = [];
49
+ const eventId = event.id || "";
50
+ const eventName = event.name || "Untitled event";
51
+ const push = (level, message) => issues.push({ eventId, eventName, level, message });
52
+ const validBenchmarks = (event.gatsbyBenchmarks || [])
53
+ .filter((b) => exports.COMPASS_VALID_ACTIVITY_BENCHMARKS.includes(b));
54
+ if (!validBenchmarks.length) {
55
+ push("blocking", "No Gatsby Benchmark (2-8) selected — Benchmark 1 alone can't be uploaded as an individual Compass+ activity.");
56
+ }
57
+ validBenchmarks.forEach((b) => {
58
+ if (!event.gatsbyCategories?.[b]) {
59
+ push("blocking", `Benchmark ${b} is selected but has no Compass+ category. Category is mandatory — Compass+ will reject this row.`);
60
+ }
61
+ });
62
+ if (!event.name)
63
+ push("blocking", "Event has no name.");
64
+ if (!event.startDate)
65
+ push("blocking", "Event has no start date.");
66
+ if (learnerCount > 0 && learnersWithUpnCount === 0) {
67
+ push("warning", "None of the attending learners have a UPN on file — Compass+ won't be able to match this activity to individual learner profiles.");
68
+ }
69
+ else if (learnersWithUpnCount < learnerCount) {
70
+ push("warning", `${learnerCount - learnersWithUpnCount} of ${learnerCount} attending learners are missing a UPN and will be left off this row.`);
71
+ }
72
+ if (!exports.COMPASS_PARTNER_PLATFORM_RECOGNISED) {
73
+ push("warning", "CareerThread isn't yet a CEC-recognised Compass+ partner platform. The school may need to manually pick a source on upload, or the upload may be rejected until we're listed.");
74
+ }
75
+ return issues;
76
+ }
77
+ /**
78
+ * Expands a single event into one Compass+ Activities-Upload row per valid benchmark. Compass+'s
79
+ * template is one benchmark + one category per row, so an event tagged with multiple benchmarks
80
+ * (e.g. an employer talk logged as both GB5 and GB7) becomes multiple rows sharing the same
81
+ * activity name, dates and learner list. Rows for benchmarks without a mandatory category are
82
+ * skipped — callers should run validateCompassExportEvent first and surface that as a blocking
83
+ * issue rather than silently dropping the row here.
84
+ */
85
+ function buildCompassActivityRows(event, learnerUpns) {
86
+ const validBenchmarks = (event.gatsbyBenchmarks || [])
87
+ .filter((b) => exports.COMPASS_VALID_ACTIVITY_BENCHMARKS.includes(b) && event.gatsbyCategories?.[b]);
88
+ return validBenchmarks.map((b) => ({
89
+ benchmark: b,
90
+ category: event.gatsbyCategories?.[b] || "",
91
+ activityName: event.name || "",
92
+ startDateTime: formatDbDateTime(event.startDate, event.startTime),
93
+ endDateTime: formatDbDateTime(event.endDate || event.startDate, event.endTime || event.startTime),
94
+ learnerUpns: learnerUpns.join(";"),
95
+ schoolLead: event.designatedStaffText || "",
96
+ notes: event.description || "",
97
+ palCompliance: (event.palCompliant && exports.COMPASS_PAL_BENCHMARKS.includes(b)) ? "Yes" : "",
98
+ parentAttendance: event.parentAttendance ? "Yes" : "",
99
+ }));
100
+ }
101
+ function compassActivityRowToCsvArray(row) {
102
+ return [
103
+ String(row.benchmark),
104
+ row.category,
105
+ row.activityName,
106
+ row.startDateTime,
107
+ row.endDateTime,
108
+ row.learnerUpns,
109
+ row.schoolLead,
110
+ row.notes,
111
+ row.palCompliance,
112
+ row.parentAttendance,
113
+ ];
114
+ }
115
+ //# sourceMappingURL=compassExport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compassExport.js","sourceRoot":"","sources":["../src/compassExport.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./firebase/firebaseQuery";
9
9
  export * from "./typeDefinitions";
10
10
  export * from "./emailSections";
11
11
  export * from "./exportTransformers";
12
+ export * from "./compassExport";
12
13
  export * from "./tutorialTips";
13
14
  export * from "./constants";
14
15
  export * from "./hooks";
package/lib/index.js CHANGED
@@ -29,6 +29,7 @@ __exportStar(require("./firebase/firebaseQuery"), exports);
29
29
  __exportStar(require("./typeDefinitions"), exports);
30
30
  __exportStar(require("./emailSections"), exports);
31
31
  __exportStar(require("./exportTransformers"), exports);
32
+ __exportStar(require("./compassExport"), exports);
32
33
  __exportStar(require("./tutorialTips"), exports);
33
34
  __exportStar(require("./constants"), exports);
34
35
  __exportStar(require("./hooks"), 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,kDAAgC;AAChC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,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,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,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,kDAAgC;AAChC,uDAAqC;AACrC,kDAAgC;AAChC,iDAA+B;AAC/B,8CAA4B;AAC5B,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,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
@@ -367,6 +367,7 @@ export type UserData = {
367
367
  [key: string]: any;
368
368
  };
369
369
  parentEmails?: WondeParentContact[];
370
+ upn?: string;
370
371
  wonde?: {
371
372
  upi: string;
372
373
  id: string;
@@ -1658,6 +1659,16 @@ export type ExternalEvent = {
1658
1659
  activityId?: string;
1659
1660
  visibleTo: string[];
1660
1661
  gatsbyBenchmarks?: number[];
1662
+ gatsbyCategories?: {
1663
+ [benchmark: number]: string;
1664
+ };
1665
+ palCompliant?: boolean;
1666
+ parentAttendance?: boolean;
1667
+ compassExport?: {
1668
+ exportedAt: string;
1669
+ exportedBy: string;
1670
+ batchId?: string;
1671
+ };
1661
1672
  oId: string;
1662
1673
  submissionClose: string;
1663
1674
  acceptingEmployers?: boolean;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "placementt-core",
4
4
  "author": "Placementt",
5
- "version": "1.400.939",
5
+ "version": "1.400.940",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -0,0 +1,148 @@
1
+ import {ExternalEvent} from "./typeDefinitions";
2
+
3
+ /**
4
+ * Benchmarks Compass+ accepts on an individual Activity row. GB1 ("a stable careers programme")
5
+ * describes whole-provision leadership, not a single encounter — CEC's Activities Upload template
6
+ * only offers Benchmarks 2-8 in its Benchmark dropdown, so GB1-only events have nothing to export.
7
+ */
8
+ export const COMPASS_VALID_ACTIVITY_BENCHMARKS = [2, 3, 4, 5, 6, 7, 8];
9
+
10
+ /**
11
+ * Benchmarks where the PAL Compliance column is meaningful. Provider Access Legislation covers
12
+ * encounters with technical education/apprenticeship providers, which sit under GB5-7 — Compass+
13
+ * only reflects this field on activities tagged with one of these three.
14
+ */
15
+ export const COMPASS_PAL_BENCHMARKS = [5, 6, 7];
16
+
17
+ /**
18
+ * Whether CareerThread is currently a CEC-recognised Compass+ "Partner Platform" (the status
19
+ * Unifrog, Xello/Cascaid, Morrisby etc. have). While this is false, Compass+ can't auto-detect our
20
+ * CSV as a known source on upload — schools may hit a "Missing Activity Source Data" error and have
21
+ * no listed option to pick us from the resolution dropdown. Flip this once CEC confirms we're
22
+ * listed as a partner; until then, exports carry a standing warning so staff don't assume the file
23
+ * will upload cleanly.
24
+ */
25
+ export const COMPASS_PARTNER_PLATFORM_RECOGNISED = false;
26
+
27
+ export const COMPASS_ACTIVITY_CSV_HEADERS = [
28
+ "Benchmark", "Main category", "Activity name", "Start Date and Time",
29
+ "End Date and Time", "Learners involved", "School Lead", "Notes",
30
+ "PAL Compliance", "Parent Attendance",
31
+ ];
32
+
33
+ export type CompassActivityRow = {
34
+ benchmark: number,
35
+ category: string,
36
+ activityName: string,
37
+ startDateTime: string,
38
+ endDateTime: string,
39
+ learnerUpns: string,
40
+ schoolLead: string,
41
+ notes: string,
42
+ palCompliance: "Yes"|"",
43
+ parentAttendance: "Yes"|"",
44
+ }
45
+
46
+ export type CompassExportIssue = {
47
+ eventId: string,
48
+ eventName: string,
49
+ level: "blocking"|"warning",
50
+ message: string,
51
+ }
52
+
53
+ const formatDbDateTime = (dateStr?: string, timeStr?: string): string => {
54
+ if (!dateStr) return "";
55
+ const [y, m, d] = dateStr.split("-");
56
+ if (!y || !m || !d) return "";
57
+ return `${d}/${m}/${y} ${timeStr || "09:00"}`;
58
+ };
59
+
60
+ /**
61
+ * Checks whether an event is safe to include in a Compass+ export. "blocking" issues mean the
62
+ * event is left out of the file entirely (it would fail Compass+'s own validation and, worse,
63
+ * could silently upload with the wrong benchmark/category). "warning" issues still get exported,
64
+ * but should be shown to staff before they send the file on to a school.
65
+ */
66
+ export function validateCompassExportEvent(
67
+ event: Partial<ExternalEvent>&{id?: string},
68
+ learnerCount: number,
69
+ learnersWithUpnCount: number,
70
+ ): CompassExportIssue[] {
71
+ const issues: CompassExportIssue[] = [];
72
+ const eventId = event.id || "";
73
+ const eventName = event.name || "Untitled event";
74
+ const push = (level: CompassExportIssue["level"], message: string) =>
75
+ issues.push({eventId, eventName, level, message});
76
+
77
+ const validBenchmarks = (event.gatsbyBenchmarks || [])
78
+ .filter((b) => COMPASS_VALID_ACTIVITY_BENCHMARKS.includes(b));
79
+
80
+ if (!validBenchmarks.length) {
81
+ push("blocking", "No Gatsby Benchmark (2-8) selected — Benchmark 1 alone can't be uploaded as an individual Compass+ activity.");
82
+ }
83
+
84
+ validBenchmarks.forEach((b) => {
85
+ if (!event.gatsbyCategories?.[b]) {
86
+ push("blocking", `Benchmark ${b} is selected but has no Compass+ category. Category is mandatory — Compass+ will reject this row.`);
87
+ }
88
+ });
89
+
90
+ if (!event.name) push("blocking", "Event has no name.");
91
+ if (!event.startDate) push("blocking", "Event has no start date.");
92
+
93
+ if (learnerCount > 0 && learnersWithUpnCount === 0) {
94
+ push("warning", "None of the attending learners have a UPN on file — Compass+ won't be able to match this activity to individual learner profiles.");
95
+ } else if (learnersWithUpnCount < learnerCount) {
96
+ push("warning", `${learnerCount - learnersWithUpnCount} of ${learnerCount} attending learners are missing a UPN and will be left off this row.`);
97
+ }
98
+
99
+ if (!COMPASS_PARTNER_PLATFORM_RECOGNISED) {
100
+ push("warning", "CareerThread isn't yet a CEC-recognised Compass+ partner platform. The school may need to manually pick a source on upload, or the upload may be rejected until we're listed.");
101
+ }
102
+
103
+ return issues;
104
+ }
105
+
106
+ /**
107
+ * Expands a single event into one Compass+ Activities-Upload row per valid benchmark. Compass+'s
108
+ * template is one benchmark + one category per row, so an event tagged with multiple benchmarks
109
+ * (e.g. an employer talk logged as both GB5 and GB7) becomes multiple rows sharing the same
110
+ * activity name, dates and learner list. Rows for benchmarks without a mandatory category are
111
+ * skipped — callers should run validateCompassExportEvent first and surface that as a blocking
112
+ * issue rather than silently dropping the row here.
113
+ */
114
+ export function buildCompassActivityRows(
115
+ event: Partial<ExternalEvent>,
116
+ learnerUpns: string[],
117
+ ): CompassActivityRow[] {
118
+ const validBenchmarks = (event.gatsbyBenchmarks || [])
119
+ .filter((b) => COMPASS_VALID_ACTIVITY_BENCHMARKS.includes(b) && event.gatsbyCategories?.[b]);
120
+
121
+ return validBenchmarks.map((b) => ({
122
+ benchmark: b,
123
+ category: event.gatsbyCategories?.[b] || "",
124
+ activityName: event.name || "",
125
+ startDateTime: formatDbDateTime(event.startDate, event.startTime),
126
+ endDateTime: formatDbDateTime(event.endDate || event.startDate, event.endTime || event.startTime),
127
+ learnerUpns: learnerUpns.join(";"),
128
+ schoolLead: event.designatedStaffText || "",
129
+ notes: event.description || "",
130
+ palCompliance: (event.palCompliant && COMPASS_PAL_BENCHMARKS.includes(b)) ? "Yes" : "",
131
+ parentAttendance: event.parentAttendance ? "Yes" : "",
132
+ }));
133
+ }
134
+
135
+ export function compassActivityRowToCsvArray(row: CompassActivityRow): string[] {
136
+ return [
137
+ String(row.benchmark),
138
+ row.category,
139
+ row.activityName,
140
+ row.startDateTime,
141
+ row.endDateTime,
142
+ row.learnerUpns,
143
+ row.schoolLead,
144
+ row.notes,
145
+ row.palCompliance,
146
+ row.parentAttendance,
147
+ ];
148
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./firebase/firebaseQuery";
11
11
  export * from "./typeDefinitions";
12
12
  export * from "./emailSections";
13
13
  export * from "./exportTransformers";
14
+ export * from "./compassExport";
14
15
  export * from "./tutorialTips";
15
16
  export * from "./constants";
16
17
  export * from "./hooks";
@@ -379,6 +379,7 @@ export type UserData = {
379
379
  [key: string]: any
380
380
  },
381
381
  parentEmails?: WondeParentContact[],
382
+ upn?: string, // DfE Unique Pupil Number — needed to match learners for Compass+ activity exports. Populated from Wonde's education_details include (Wonde-integrated schools only; not yet collected via CSV upload).
382
383
  wonde?: {
383
384
  upi: string,
384
385
  id: string,
@@ -1615,6 +1616,22 @@ export type ExternalEvent = {
1615
1616
  activityId?: string,
1616
1617
  visibleTo: string[] // All institutes that can see this event. E.g. Careers Hubs and schools
1617
1618
  gatsbyBenchmarks?: number[], // String array
1619
+ // Compass+ "Main category" per selected benchmark (keyed by benchmark number, e.g. 5). Left as free text
1620
+ // for now — CEC's category options are benchmark-specific and only visible inside the Compass+ Activities
1621
+ // Upload template itself, so this isn't a closed enum yet. Tighten to a per-benchmark union once we've
1622
+ // sourced the authoritative list.
1623
+ gatsbyCategories?: {[benchmark: number]: string},
1624
+ palCompliant?: boolean, // Provider Access Legislation compliance (Compass+ Activities Upload Column I). Only meaningful when gatsbyBenchmarks includes 5, 6 or 7.
1625
+ parentAttendance?: boolean, // Did a parent/carer attend this event (Compass+ Activities Upload Column J).
1626
+ // Dedup guardrail: stamped whenever this event is included in a generated Compass+ export, from
1627
+ // either the single-event export or the bulk Exports tab. Means "a file containing this event was
1628
+ // generated", not "this reached Compass+" — we have no way to confirm the latter. Both export
1629
+ // surfaces must check and set this so an event can't be silently re-exported.
1630
+ compassExport?: {
1631
+ exportedAt: string,
1632
+ exportedBy: string, // uid of the staff member who triggered the export
1633
+ batchId?: string, // links back to the export run, for tracing duplicate-data reports
1634
+ },
1618
1635
  oId: string,
1619
1636
  submissionClose: string,
1620
1637
  acceptingEmployers?: boolean,