placementt-core 1.400.1183 → 1.400.1184
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/eventTimeSlots.d.ts +15 -7
- package/lib/eventTimeSlots.js +31 -22
- package/lib/eventTimeSlots.js.map +1 -1
- package/lib/eventTimeSlots.test.js +46 -17
- package/lib/eventTimeSlots.test.js.map +1 -1
- package/lib/typeDefinitions.d.ts +9 -3
- package/lib/typeDefinitions.js.map +1 -1
- package/package.json +1 -1
- package/src/eventTimeSlots.test.ts +53 -18
- package/src/eventTimeSlots.ts +34 -30
- package/src/typeDefinitions.ts +12 -6
package/lib/eventTimeSlots.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { ExternalEvent } from "./typeDefinitions";
|
|
2
|
-
export type EventTimeSlot = {
|
|
3
|
-
id: string;
|
|
1
|
+
import { EventTimeSlotSetting, ExternalEvent } from "./typeDefinitions";
|
|
2
|
+
export type EventTimeSlot = EventTimeSlotSetting & {
|
|
4
3
|
date: string;
|
|
5
|
-
start: string;
|
|
6
|
-
end: string;
|
|
7
4
|
};
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
5
|
+
export declare const getEventDateRange: (event: Pick<ExternalEvent, "startDate" | "endDate">) => string[];
|
|
6
|
+
export declare const getEventTimeSlots: (event: Pick<ExternalEvent, "timeSlots" | "startDate" | "endDate">) => EventTimeSlot[];
|
|
7
|
+
export declare const getSlotPlacesLeft: (event: Pick<ExternalEvent, "timeSlots" | "slotBookings" | "startDate" | "endDate">, slotId: string) => number;
|
|
8
|
+
export type SlotBookingChange = {
|
|
9
|
+
slotId: string;
|
|
10
|
+
before: number;
|
|
11
|
+
after: number;
|
|
12
|
+
};
|
|
13
|
+
export declare const getSlotBookingChanges: (before?: {
|
|
14
|
+
[slotId: string]: number;
|
|
15
|
+
}, after?: {
|
|
16
|
+
[slotId: string]: number;
|
|
17
|
+
}) => SlotBookingChange[];
|
package/lib/eventTimeSlots.js
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSlotPlacesLeft = exports.getEventTimeSlots = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
const toTime = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
9
|
-
// Every slot for the event, one run of slotMinutes-long slots per day between
|
|
10
|
-
// startDate and endDate (inclusive). A final slot that would overrun endTime is dropped.
|
|
11
|
-
const getEventTimeSlots = (event) => {
|
|
12
|
-
const { timeSlots, startDate, startTime, endTime } = event;
|
|
13
|
-
if (!timeSlots?.slotMinutes || !startDate || !startTime || !endTime)
|
|
3
|
+
exports.getSlotBookingChanges = exports.getSlotPlacesLeft = exports.getEventTimeSlots = exports.getEventDateRange = void 0;
|
|
4
|
+
// Every date from startDate to endDate (inclusive), as "YYYY-MM-DD".
|
|
5
|
+
const getEventDateRange = (event) => {
|
|
6
|
+
if (!event.startDate)
|
|
14
7
|
return [];
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const first = new Date(`${startDate.slice(0, 10)}T00:00:00Z`);
|
|
18
|
-
const last = new Date(`${(event.endDate || startDate).slice(0, 10)}T00:00:00Z`);
|
|
8
|
+
const first = new Date(`${event.startDate.slice(0, 10)}T00:00:00Z`);
|
|
9
|
+
const last = new Date(`${(event.endDate || event.startDate).slice(0, 10)}T00:00:00Z`);
|
|
19
10
|
if (isNaN(first.getTime()) || isNaN(last.getTime()))
|
|
20
11
|
return [];
|
|
21
|
-
const
|
|
12
|
+
const dates = [];
|
|
22
13
|
for (const day = first; day <= last; day.setUTCDate(day.getUTCDate() + 1)) {
|
|
23
|
-
|
|
24
|
-
for (let t = dayStart; t + timeSlots.slotMinutes <= dayEnd; t += timeSlots.slotMinutes) {
|
|
25
|
-
slots.push({ id: `${date}T${toTime(t)}`, date, start: toTime(t), end: toTime(t + timeSlots.slotMinutes) });
|
|
26
|
-
}
|
|
14
|
+
dates.push(day.toISOString().slice(0, 10));
|
|
27
15
|
}
|
|
28
|
-
return
|
|
16
|
+
return dates;
|
|
17
|
+
};
|
|
18
|
+
exports.getEventDateRange = getEventDateRange;
|
|
19
|
+
// Every bookable slot, in date then start time order. Slots set up for a date that is no
|
|
20
|
+
// longer in the event's date range are left out, as are incomplete rows.
|
|
21
|
+
const getEventTimeSlots = (event) => {
|
|
22
|
+
const days = event.timeSlots?.days;
|
|
23
|
+
if (!days)
|
|
24
|
+
return [];
|
|
25
|
+
return (0, exports.getEventDateRange)(event).flatMap((date) => (days[date] || [])
|
|
26
|
+
.filter((slot) => slot.id && slot.start && slot.end && slot.capacity > 0)
|
|
27
|
+
.map((slot) => ({ ...slot, date }))
|
|
28
|
+
.sort((a, b) => a.start.localeCompare(b.start)));
|
|
29
29
|
};
|
|
30
30
|
exports.getEventTimeSlots = getEventTimeSlots;
|
|
31
|
-
const getSlotPlacesLeft = (event, slotId) =>
|
|
31
|
+
const getSlotPlacesLeft = (event, slotId) => {
|
|
32
|
+
const slot = (0, exports.getEventTimeSlots)(event).find((s) => s.id === slotId);
|
|
33
|
+
return Math.max(0, (slot?.capacity || 0) - (event.slotBookings?.[slotId] || 0));
|
|
34
|
+
};
|
|
32
35
|
exports.getSlotPlacesLeft = getSlotPlacesLeft;
|
|
36
|
+
// What changed between two versions of a school's booking, in slot order where the slots still exist.
|
|
37
|
+
const getSlotBookingChanges = (before = {}, after = {}) => [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
38
|
+
.map((slotId) => ({ slotId, before: before[slotId] || 0, after: after[slotId] || 0 }))
|
|
39
|
+
.filter((c) => c.before !== c.after)
|
|
40
|
+
.sort((a, b) => a.slotId.localeCompare(b.slotId));
|
|
41
|
+
exports.getSlotBookingChanges = getSlotBookingChanges;
|
|
33
42
|
//# sourceMappingURL=eventTimeSlots.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventTimeSlots.js","sourceRoot":"","sources":["../src/eventTimeSlots.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"eventTimeSlots.js","sourceRoot":"","sources":["../src/eventTimeSlots.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,29 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const eventTimeSlots_1 = require("./eventTimeSlots");
|
|
4
|
+
const slot = (id, start, end, capacity = 30) => ({ id, start, end, capacity });
|
|
5
|
+
describe("getEventDateRange", () => {
|
|
6
|
+
it("lists every date in the range", () => {
|
|
7
|
+
expect((0, eventTimeSlots_1.getEventDateRange)({ startDate: "2026-11-02", endDate: "2026-11-04" })).toEqual(["2026-11-02", "2026-11-03", "2026-11-04"]);
|
|
8
|
+
});
|
|
9
|
+
it("uses the start date alone for a single-day event", () => {
|
|
10
|
+
expect((0, eventTimeSlots_1.getEventDateRange)({ startDate: "2026-11-02" })).toEqual(["2026-11-02"]);
|
|
11
|
+
expect((0, eventTimeSlots_1.getEventDateRange)({})).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
4
14
|
describe("getEventTimeSlots", () => {
|
|
5
|
-
const timeSlots = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const timeSlots = {
|
|
16
|
+
capacityUnit: "students",
|
|
17
|
+
days: {
|
|
18
|
+
"2026-11-02": [slot("b", "11:00", "12:00"), slot("a", "09:00", "10:00", 20)],
|
|
19
|
+
"2026-11-03": [slot("c", "13:00", "14:00"), slot("incomplete", "", "14:00")],
|
|
20
|
+
"2026-11-09": [slot("outOfRange", "09:00", "10:00")],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
it("returns slots in date and start time order, with the date attached", () => {
|
|
24
|
+
const slots = (0, eventTimeSlots_1.getEventTimeSlots)({ timeSlots, startDate: "2026-11-02", endDate: "2026-11-03" });
|
|
25
|
+
expect(slots.map((s) => s.id)).toEqual(["a", "b", "c"]);
|
|
26
|
+
expect(slots[0]).toEqual({ id: "a", start: "09:00", end: "10:00", capacity: 20, date: "2026-11-02" });
|
|
11
27
|
});
|
|
12
|
-
it("
|
|
13
|
-
const
|
|
14
|
-
expect(
|
|
28
|
+
it("leaves out dates outside the event range and incomplete rows", () => {
|
|
29
|
+
const ids = (0, eventTimeSlots_1.getEventTimeSlots)({ timeSlots, startDate: "2026-11-02", endDate: "2026-11-03" }).map((s) => s.id);
|
|
30
|
+
expect(ids).not.toContain("outOfRange");
|
|
31
|
+
expect(ids).not.toContain("incomplete");
|
|
15
32
|
});
|
|
16
|
-
it("returns nothing without slot settings
|
|
17
|
-
expect((0, eventTimeSlots_1.getEventTimeSlots)({ startDate: "2026-11-02"
|
|
18
|
-
expect((0, eventTimeSlots_1.getEventTimeSlots)({ timeSlots, startDate: "2026-11-02" })).toEqual([]);
|
|
33
|
+
it("returns nothing without slot settings", () => {
|
|
34
|
+
expect((0, eventTimeSlots_1.getEventTimeSlots)({ startDate: "2026-11-02" })).toEqual([]);
|
|
19
35
|
});
|
|
20
36
|
});
|
|
21
37
|
describe("getSlotPlacesLeft", () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
expect((0, eventTimeSlots_1.getSlotPlacesLeft)({
|
|
25
|
-
expect((0, eventTimeSlots_1.getSlotPlacesLeft)({
|
|
26
|
-
expect((0, eventTimeSlots_1.getSlotPlacesLeft)(
|
|
38
|
+
const event = { startDate: "2026-11-02", timeSlots: { capacityUnit: "students", days: { "2026-11-02": [slot("a", "09:00", "10:00", 30)] } } };
|
|
39
|
+
it("subtracts bookings from the slot's own capacity and never goes negative", () => {
|
|
40
|
+
expect((0, eventTimeSlots_1.getSlotPlacesLeft)({ ...event, slotBookings: { a: 12 } }, "a")).toBe(18);
|
|
41
|
+
expect((0, eventTimeSlots_1.getSlotPlacesLeft)({ ...event, slotBookings: { a: 40 } }, "a")).toBe(0);
|
|
42
|
+
expect((0, eventTimeSlots_1.getSlotPlacesLeft)(event, "missing")).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("getSlotBookingChanges", () => {
|
|
46
|
+
it("lists added, removed and resized slots", () => {
|
|
47
|
+
expect((0, eventTimeSlots_1.getSlotBookingChanges)({ a: 10, b: 5, c: 3 }, { a: 10, b: 8, d: 2 })).toEqual([
|
|
48
|
+
{ slotId: "b", before: 5, after: 8 },
|
|
49
|
+
{ slotId: "c", before: 3, after: 0 },
|
|
50
|
+
{ slotId: "d", before: 0, after: 2 },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
it("returns nothing when the booking is unchanged", () => {
|
|
54
|
+
expect((0, eventTimeSlots_1.getSlotBookingChanges)({ a: 10 }, { a: 10 })).toEqual([]);
|
|
55
|
+
expect((0, eventTimeSlots_1.getSlotBookingChanges)()).toEqual([]);
|
|
27
56
|
});
|
|
28
57
|
});
|
|
29
58
|
//# sourceMappingURL=eventTimeSlots.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventTimeSlots.test.js","sourceRoot":"","sources":["../src/eventTimeSlots.test.ts"],"names":[],"mappings":";;AAAA,
|
|
1
|
+
{"version":3,"file":"eventTimeSlots.test.js","sourceRoot":"","sources":["../src/eventTimeSlots.test.ts"],"names":[],"mappings":"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"}
|
package/lib/typeDefinitions.d.ts
CHANGED
|
@@ -2398,10 +2398,10 @@ export type ExternalEvent = {
|
|
|
2398
2398
|
maxSchools?: number;
|
|
2399
2399
|
maxStudents?: number;
|
|
2400
2400
|
timeSlots?: {
|
|
2401
|
-
slotMinutes: number;
|
|
2402
|
-
capacity: number;
|
|
2403
2401
|
capacityUnit: "students" | "schools";
|
|
2404
|
-
|
|
2402
|
+
days?: {
|
|
2403
|
+
[date: string]: EventTimeSlotSetting[];
|
|
2404
|
+
};
|
|
2405
2405
|
};
|
|
2406
2406
|
slotBookings?: {
|
|
2407
2407
|
[slotId: string]: number;
|
|
@@ -3388,6 +3388,12 @@ export type ExternalEventAttendee = {
|
|
|
3388
3388
|
};
|
|
3389
3389
|
};
|
|
3390
3390
|
};
|
|
3391
|
+
export type EventTimeSlotSetting = {
|
|
3392
|
+
id: string;
|
|
3393
|
+
start: string;
|
|
3394
|
+
end: string;
|
|
3395
|
+
capacity: number;
|
|
3396
|
+
};
|
|
3391
3397
|
export type ExternalEventCareersSchool = {
|
|
3392
3398
|
oId: string;
|
|
3393
3399
|
instituteId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typeDefinitions.js","sourceRoot":"","sources":["../src/typeDefinitions.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"typeDefinitions.js","sourceRoot":"","sources":["../src/typeDefinitions.ts"],"names":[],"mappings":";;;AA+pIA,sDASC;AAeD,oDAKC;AAaD,0CAEC;AAlpED;;;;;kFAKkF;AACrE,QAAA,qBAAqB,GAAG,SAAS,CAAC;AAClC,QAAA,iBAAiB,GAAG,OAAO,CAAC;AA8jEzC;yEACyE;AAC5D,QAAA,0BAA0B,GAA2C;IAC9E,gBAAgB,EAAE,2BAA2B;IAC7C,mBAAmB,EAAE,sBAAsB;IAC3C,cAAc,EAAE,iBAAiB;IACjC,kBAAkB,EAAE,qBAAqB;IACzC,UAAU,EAAE,oCAAoC;IAChD,aAAa,EAAE,yBAAyB;IACxC,aAAa,EAAE,wBAAwB;IACvC,KAAK,EAAE,OAAO;CACjB,CAAC;AAEW,QAAA,0BAA0B,GAA+C;IAClF,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,OAAO;CACjB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAgB,qBAAqB,CAAC,SAAsC;IACxE,QAAQ,SAAS,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC;QAC5B,KAAK,OAAO,CAAC,CAAC,OAAO,OAAO,CAAC;QAC7B,KAAK,WAAW,CAAC;QACjB,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;QACb,OAAO,CAAC,CAAC,OAAO,UAAU,CAAC;IAC3B,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,oBAAoB,CAChC,IAAwC,EACxC,MAA6C;IAE7C,OAAO,IAAI,EAAE,WAAW,IAAI,MAAM,EAAE,YAAY,IAAI,KAAK,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,KAA6D;IACzF,OAAO,GAAG,KAAK,CAAC,cAAc,IAAI,EAAE,IAAI,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;AAC/E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,31 +1,66 @@
|
|
|
1
|
-
import {getEventTimeSlots, getSlotPlacesLeft} from "./eventTimeSlots";
|
|
1
|
+
import {getEventDateRange, getEventTimeSlots, getSlotBookingChanges, getSlotPlacesLeft} from "./eventTimeSlots";
|
|
2
|
+
|
|
3
|
+
const slot = (id: string, start: string, end: string, capacity = 30) => ({id, start, end, capacity});
|
|
4
|
+
|
|
5
|
+
describe("getEventDateRange", () => {
|
|
6
|
+
it("lists every date in the range", () => {
|
|
7
|
+
expect(getEventDateRange({startDate: "2026-11-02", endDate: "2026-11-04"})).toEqual(["2026-11-02", "2026-11-03", "2026-11-04"]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("uses the start date alone for a single-day event", () => {
|
|
11
|
+
expect(getEventDateRange({startDate: "2026-11-02"})).toEqual(["2026-11-02"]);
|
|
12
|
+
expect(getEventDateRange({})).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
2
15
|
|
|
3
16
|
describe("getEventTimeSlots", () => {
|
|
4
|
-
const timeSlots = {
|
|
17
|
+
const timeSlots = {
|
|
18
|
+
capacityUnit: "students" as const,
|
|
19
|
+
days: {
|
|
20
|
+
"2026-11-02": [slot("b", "11:00", "12:00"), slot("a", "09:00", "10:00", 20)],
|
|
21
|
+
"2026-11-03": [slot("c", "13:00", "14:00"), slot("incomplete", "", "14:00")],
|
|
22
|
+
"2026-11-09": [slot("outOfRange", "09:00", "10:00")],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
5
25
|
|
|
6
|
-
it("
|
|
7
|
-
const slots = getEventTimeSlots({timeSlots, startDate: "2026-11-02", endDate: "2026-11-
|
|
8
|
-
expect(slots).
|
|
9
|
-
expect(slots[0]).toEqual({id: "
|
|
10
|
-
expect(slots[8].id).toBe("2026-11-04T11:00");
|
|
26
|
+
it("returns slots in date and start time order, with the date attached", () => {
|
|
27
|
+
const slots = getEventTimeSlots({timeSlots, startDate: "2026-11-02", endDate: "2026-11-03"});
|
|
28
|
+
expect(slots.map((s) => s.id)).toEqual(["a", "b", "c"]);
|
|
29
|
+
expect(slots[0]).toEqual({id: "a", start: "09:00", end: "10:00", capacity: 20, date: "2026-11-02"});
|
|
11
30
|
});
|
|
12
31
|
|
|
13
|
-
it("
|
|
14
|
-
const
|
|
15
|
-
expect(
|
|
32
|
+
it("leaves out dates outside the event range and incomplete rows", () => {
|
|
33
|
+
const ids = getEventTimeSlots({timeSlots, startDate: "2026-11-02", endDate: "2026-11-03"}).map((s) => s.id);
|
|
34
|
+
expect(ids).not.toContain("outOfRange");
|
|
35
|
+
expect(ids).not.toContain("incomplete");
|
|
16
36
|
});
|
|
17
37
|
|
|
18
|
-
it("returns nothing without slot settings
|
|
19
|
-
expect(getEventTimeSlots({startDate: "2026-11-02"
|
|
20
|
-
expect(getEventTimeSlots({timeSlots, startDate: "2026-11-02"})).toEqual([]);
|
|
38
|
+
it("returns nothing without slot settings", () => {
|
|
39
|
+
expect(getEventTimeSlots({startDate: "2026-11-02"})).toEqual([]);
|
|
21
40
|
});
|
|
22
41
|
});
|
|
23
42
|
|
|
24
43
|
describe("getSlotPlacesLeft", () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(getSlotPlacesLeft({
|
|
29
|
-
expect(getSlotPlacesLeft({
|
|
44
|
+
const event = {startDate: "2026-11-02", timeSlots: {capacityUnit: "students" as const, days: {"2026-11-02": [slot("a", "09:00", "10:00", 30)]}}};
|
|
45
|
+
|
|
46
|
+
it("subtracts bookings from the slot's own capacity and never goes negative", () => {
|
|
47
|
+
expect(getSlotPlacesLeft({...event, slotBookings: {a: 12}}, "a")).toBe(18);
|
|
48
|
+
expect(getSlotPlacesLeft({...event, slotBookings: {a: 40}}, "a")).toBe(0);
|
|
49
|
+
expect(getSlotPlacesLeft(event, "missing")).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("getSlotBookingChanges", () => {
|
|
54
|
+
it("lists added, removed and resized slots", () => {
|
|
55
|
+
expect(getSlotBookingChanges({a: 10, b: 5, c: 3}, {a: 10, b: 8, d: 2})).toEqual([
|
|
56
|
+
{slotId: "b", before: 5, after: 8},
|
|
57
|
+
{slotId: "c", before: 3, after: 0},
|
|
58
|
+
{slotId: "d", before: 0, after: 2},
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns nothing when the booking is unchanged", () => {
|
|
63
|
+
expect(getSlotBookingChanges({a: 10}, {a: 10})).toEqual([]);
|
|
64
|
+
expect(getSlotBookingChanges()).toEqual([]);
|
|
30
65
|
});
|
|
31
66
|
});
|
package/src/eventTimeSlots.ts
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import {ExternalEvent} from "./typeDefinitions";
|
|
1
|
+
import {EventTimeSlotSetting, ExternalEvent} from "./typeDefinitions";
|
|
2
2
|
|
|
3
|
-
export type EventTimeSlot = {
|
|
4
|
-
id: string, // "YYYY-MM-DDTHH:mm", stable as long as the event dates and times don't change
|
|
3
|
+
export type EventTimeSlot = EventTimeSlotSetting & {
|
|
5
4
|
date: string, // "YYYY-MM-DD"
|
|
6
|
-
start: string, // "HH:mm"
|
|
7
|
-
end: string, // "HH:mm"
|
|
8
5
|
};
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const toTime = (minutes: number) => `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
16
|
-
|
|
17
|
-
// Every slot for the event, one run of slotMinutes-long slots per day between
|
|
18
|
-
// startDate and endDate (inclusive). A final slot that would overrun endTime is dropped.
|
|
19
|
-
export const getEventTimeSlots = (event: Pick<ExternalEvent, "timeSlots"|"startDate"|"endDate"|"startTime"|"endTime">): EventTimeSlot[] => {
|
|
20
|
-
const {timeSlots, startDate, startTime, endTime} = event;
|
|
21
|
-
if (!timeSlots?.slotMinutes || !startDate || !startTime || !endTime) return [];
|
|
22
|
-
|
|
23
|
-
const dayStart = toMinutes(startTime);
|
|
24
|
-
const dayEnd = toMinutes(endTime);
|
|
25
|
-
const first = new Date(`${startDate.slice(0, 10)}T00:00:00Z`);
|
|
26
|
-
const last = new Date(`${(event.endDate || startDate).slice(0, 10)}T00:00:00Z`);
|
|
7
|
+
// Every date from startDate to endDate (inclusive), as "YYYY-MM-DD".
|
|
8
|
+
export const getEventDateRange = (event: Pick<ExternalEvent, "startDate"|"endDate">): string[] => {
|
|
9
|
+
if (!event.startDate) return [];
|
|
10
|
+
const first = new Date(`${event.startDate.slice(0, 10)}T00:00:00Z`);
|
|
11
|
+
const last = new Date(`${(event.endDate || event.startDate).slice(0, 10)}T00:00:00Z`);
|
|
27
12
|
if (isNaN(first.getTime()) || isNaN(last.getTime())) return [];
|
|
28
13
|
|
|
29
|
-
const
|
|
14
|
+
const dates: string[] = [];
|
|
30
15
|
for (const day = first; day <= last; day.setUTCDate(day.getUTCDate() + 1)) {
|
|
31
|
-
|
|
32
|
-
for (let t = dayStart; t + timeSlots.slotMinutes <= dayEnd; t += timeSlots.slotMinutes) {
|
|
33
|
-
slots.push({id: `${date}T${toTime(t)}`, date, start: toTime(t), end: toTime(t + timeSlots.slotMinutes)});
|
|
34
|
-
}
|
|
16
|
+
dates.push(day.toISOString().slice(0, 10));
|
|
35
17
|
}
|
|
36
|
-
return
|
|
18
|
+
return dates;
|
|
37
19
|
};
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
|
|
21
|
+
// Every bookable slot, in date then start time order. Slots set up for a date that is no
|
|
22
|
+
// longer in the event's date range are left out, as are incomplete rows.
|
|
23
|
+
export const getEventTimeSlots = (event: Pick<ExternalEvent, "timeSlots"|"startDate"|"endDate">): EventTimeSlot[] => {
|
|
24
|
+
const days = event.timeSlots?.days;
|
|
25
|
+
if (!days) return [];
|
|
26
|
+
return getEventDateRange(event).flatMap((date) => (days[date] || [])
|
|
27
|
+
.filter((slot) => slot.id && slot.start && slot.end && slot.capacity > 0)
|
|
28
|
+
.map((slot) => ({...slot, date}))
|
|
29
|
+
.sort((a, b) => a.start.localeCompare(b.start)));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getSlotPlacesLeft = (event: Pick<ExternalEvent, "timeSlots"|"slotBookings"|"startDate"|"endDate">, slotId: string) => {
|
|
33
|
+
const slot = getEventTimeSlots(event).find((s) => s.id === slotId);
|
|
34
|
+
return Math.max(0, (slot?.capacity || 0) - (event.slotBookings?.[slotId] || 0));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SlotBookingChange = {slotId: string, before: number, after: number}; // 0 = not booked
|
|
38
|
+
|
|
39
|
+
// What changed between two versions of a school's booking, in slot order where the slots still exist.
|
|
40
|
+
export const getSlotBookingChanges = (before: {[slotId: string]: number} = {}, after: {[slotId: string]: number} = {}): SlotBookingChange[] =>
|
|
41
|
+
[...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
42
|
+
.map((slotId) => ({slotId, before: before[slotId] || 0, after: after[slotId] || 0}))
|
|
43
|
+
.filter((c) => c.before !== c.after)
|
|
44
|
+
.sort((a, b) => a.slotId.localeCompare(b.slotId));
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -2407,14 +2407,13 @@ export type ExternalEvent = {
|
|
|
2407
2407
|
maxExternalAttendees?: number, // max number of employers
|
|
2408
2408
|
maxSchools?: number, // max number of employers
|
|
2409
2409
|
maxStudents?: number, // max number of employers
|
|
2410
|
-
// Timed slots:
|
|
2411
|
-
// and
|
|
2412
|
-
// only happens through the events-bookEventSlots /
|
|
2410
|
+
// Timed slots: the hub lists slots for each event date, each with its own capacity.
|
|
2411
|
+
// Only dates between startDate and endDate are offered (see getEventTimeSlots). Booking
|
|
2412
|
+
// and freeing places only happens through the events-bookEventSlots /
|
|
2413
|
+
// events-removeEventSlotBooking callables.
|
|
2413
2414
|
timeSlots?: {
|
|
2414
|
-
slotMinutes: number,
|
|
2415
|
-
capacity: number, // places per slot
|
|
2416
2415
|
capacityUnit: "students"|"schools",
|
|
2417
|
-
|
|
2416
|
+
days?: {[date: string]: EventTimeSlotSetting[]}, // keyed "YYYY-MM-DD"
|
|
2418
2417
|
},
|
|
2419
2418
|
slotBookings?: {[slotId: string]: number}, // places used per slot
|
|
2420
2419
|
studentJoinCode: number,
|
|
@@ -3376,6 +3375,13 @@ export type ExternalEventAttendee = {
|
|
|
3376
3375
|
|
|
3377
3376
|
|
|
3378
3377
|
|
|
3378
|
+
export type EventTimeSlotSetting = {
|
|
3379
|
+
id: string, // Kept when the times change, so bookings follow the slot
|
|
3380
|
+
start: string, // "HH:mm"
|
|
3381
|
+
end: string, // "HH:mm"
|
|
3382
|
+
capacity: number, // places in this slot
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3379
3385
|
export type ExternalEventCareersSchool = {
|
|
3380
3386
|
oId: string,
|
|
3381
3387
|
instituteId: string,
|