Skip to content
186 changes: 182 additions & 4 deletions src/features/scheduling/FreeplayScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StaffAttendeeID, AdminAttendeeID, CamperAttendeeID, Freeplay } from "../../types/sessionTypes";
import { StaffAttendeeID, AdminAttendeeID, CamperAttendeeID, Freeplay, PostID } from "../../types/sessionTypes";

export class FreeplayScheduler {
/* The current freeplay schedule */
Expand All @@ -9,9 +9,15 @@ export class FreeplayScheduler {
staff: StaffAttendeeID[] = [];
admins: AdminAttendeeID[] = [];

assignedStaff: StaffAttendeeID[] = [];
assignedAdmin: AdminAttendeeID[] = [];

posts: PostID[] = [];

/* The freeplay buddies from other freeplays in this session */
otherFreeplayBuddies: { [attendeeId: number]: number[] } = {};

// postInfo includes a list of all posts with PostID information (necessary for requiresAdmin flag) --> schedule only includes string of IDs
constructor() { }

withSchedule(schedule: Freeplay): FreeplayScheduler { this.schedule = schedule; return this; }
Expand All @@ -22,11 +28,79 @@ export class FreeplayScheduler {

withAdmins(admins: AdminAttendeeID[]): FreeplayScheduler { this.admins = admins; return this; }

withPosts(posts: PostID[]): FreeplayScheduler { this.posts = posts; return this; }

getCamperById = (id: number) => this.campers.find(c => c.id === id);

getPostByID = (id: string) => this.posts.find(p => p.name === id);

// withOtherFreeplays should build the previousFreeplayBuddies object
withOtherFreeplays(otherFreeplays: Freeplay[]): FreeplayScheduler { return this; }
withOtherFreeplays(otherFreeplays: Freeplay[]): FreeplayScheduler {
for (const freeplay of otherFreeplays) {
for (const buddieID in freeplay.buddies) {
if (buddieID in this.otherFreeplayBuddies) {

const attendees = freeplay.buddies[buddieID];

// add all attendees that don't already exist
for (const att of attendees) {
if (!this.otherFreeplayBuddies[buddieID].includes(att)) {
this.otherFreeplayBuddies[buddieID].push(att);
}
}
} else {
const attendees = freeplay.buddies[buddieID];
this.otherFreeplayBuddies[buddieID] = attendees;
}
}
}

return this;
}
Comment thread
DurjaMan27 marked this conversation as resolved.

/* Assigns ADMINs to all posts that require ADMINs and either STAFF or ADMINs to all other posts */
assignPosts() { return this; }
assignPosts() {

// Keep track of available staff/admins
const availableAdmins = this.admins.filter(admin =>
!this.assignedAdmin.some(assigned => assigned.id === admin.id)
);
const availableStaff = this.staff.filter(staff =>
!this.assignedStaff.some(assigned => assigned.id === staff.id)
)

// assign all ADMIN-only roles first
for (const postID in this.schedule.posts) {
const assigned = this.schedule.posts[postID];
const post = this.getPostByID(postID);
if (assigned.length == 0 && post?.requiresAdmin) {
if (availableAdmins.length > 0) {
const adminID: AdminAttendeeID = availableAdmins.shift()!;
this.schedule.posts[postID] = [adminID.id];
this.assignedAdmin.push(adminID);
}
}
Comment thread
DurjaMan27 marked this conversation as resolved.
}

// assigns all other roles (not Admin-specific) to admins first, then staff
for (const postID in this.schedule.posts) {
const assigned = this.schedule.posts[postID];
if (assigned.length == 0) {
if (availableAdmins.length > 0) {
const adminID: AdminAttendeeID = availableAdmins.shift()!;
this.schedule.posts[postID] = [adminID.id];
this.assignedAdmin.push(adminID);
} else if (availableStaff.length > 0) {
const staffID: StaffAttendeeID = availableStaff.shift()!;
this.schedule.posts[postID] = [staffID.id];
this.assignedStaff.push(staffID);
}
}
}

return this;
}


/*
Assigns campers to remaining ADMIN & STAFF members for freeplay according to the following rules:
Expand All @@ -38,7 +112,111 @@ export class FreeplayScheduler {
to the same staff member.
- Prioritize avoiding assigning the same "freeplay buddy" (previous buddy) if possible.
*/
assignCampers() { return this; }
assignCampers() {
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated
const allAssignedStaffers = [...this.assignedStaff, ...this.assignedAdmin];
// const allAssignedStaffers = [...this.assignedStaff];

// 1. Split campers by gender
const femaleCampers = this.campers.filter(c => c.gender === "Female");
const maleCampers = this.campers.filter(c => c.gender === "Male");
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated

// 2. Assign female campers
for (const camper of femaleCampers) {
let assigned = false;

// Loop through female staffers/admins first
for (const staffer of allAssignedStaffers) {

if (staffer.gender !== "Female") continue;
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated

const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
const prevBuddies = this.otherFreeplayBuddies[staffer.id] || [];

// Check buddy conflict (camper.id appears in staffer's prevBuddies)
const hasConflict = prevBuddies.includes(camper.id);

if (!hasConflict && alreadyAssigned.length == 0) {
if (staffer.role === "STAFF") {
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated
if (staffer.bunk !== camper.bunk) {
this.schedule.buddies[staffer.id] = [camper.id];
assigned = true;
break;
}
} else {
this.schedule.buddies[staffer.id] = [camper.id];
assigned = true;
break;
}
}
}

// Fallback: assign to any female staffer with another camper of the same bunk
if (!assigned) {
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated
for (const staffer of allAssignedStaffers) {
if (staffer.gender !== "Female") continue;

const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
if (alreadyAssigned.length == 1) {

const otherCamper = this.getCamperById(alreadyAssigned[0]) || camper;
if (otherCamper.bunk == camper.bunk && otherCamper.id !== camper.id) {
this.schedule.buddies[staffer.id].push(camper.id);
assigned = true;
break;
}

}
}
}
}

// 3. Assign male campers
for (const camper of maleCampers) {
let assigned = false;

// Loop through staffers/admins and assign to one that has no campers
for (const staffer of allAssignedStaffers) {

const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
const prevBuddies = this.otherFreeplayBuddies[staffer.id] || [];

// Check buddy conflict (camper.id appears in staffer's prevBuddies)
const hasConflict = prevBuddies.includes(camper.id);

if (!hasConflict && alreadyAssigned.length == 0) {
if (staffer.role === "STAFF") {
if (staffer.bunk !== camper.bunk) {
assigned = true;
this.schedule.buddies[staffer.id] = [camper.id];
}
} else {
assigned = true;
this.schedule.buddies[staffer.id] = [camper.id];
}
}
}

// Fallback: assign to any staffer with another camper of the same bunk
if (!assigned) {
Comment thread
DurjaMan27 marked this conversation as resolved.
Outdated
for (const staffer of allAssignedStaffers) {

const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
if (alreadyAssigned.length == 1) {

const otherCamper = this.getCamperById(alreadyAssigned[0]) || camper;
if (otherCamper.bunk == camper.bunk && otherCamper.id !== camper.id) {
this.schedule.buddies[staffer.id].push(camper.id);
assigned = true;
break;
}

}
}
}
}

return this;
}

getSchedule() { return this.schedule; }
}
Loading