Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/app/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAuth } from "@/auth/useAuth";
import { signInWithGooglePopup } from "@/auth/authN";
import Image from "next/image";
import { signInWithMicrosoftPopup } from "@/auth/authN";
import { generateSession } from "@/features/scheduling/test/generateEmulatorData";

export default function LoginPage() {
const [error, setError] = useState<string>("");
Expand All @@ -33,6 +34,16 @@ export default function LoginPage() {
}
}

const generateTest = async () => {
try {
const data = await generateSession();
console.log(data);
}
catch {
setError("An error occurred while trying to sign in. Please try again.");
}
}

return (
<div className="relative min-h-full w-full flex items-center justify-center bg-primary-300 overflow-hidden">
<div className="absolute inset-0 h-full">
Expand Down Expand Up @@ -67,6 +78,15 @@ export default function LoginPage() {
Sign in with Microsoft
</button>

<button
onClick={generateTest}
className="flex flex-row justify-around items-center w-5/6 max-w-[344px] bg-white
mt-5 py-4 px-12 rounded-full shadow-[0_4px_4px_-1px_rgba(0,0,0,0.2)] font-lato text-xl text-gray-600"
>
<Image src={MicrosoftIcon.src} alt="Microsoft" width={32} height={32} />
Generate Test Data
</button>


{/* Error Message */}
{errorDisplay && (
Expand Down
254 changes: 247 additions & 7 deletions src/features/scheduling/generation/FreeplayScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,116 @@
import { StaffAttendeeID, AdminAttendeeID, CamperAttendeeID, Freeplay } from "../../../types/sessionTypes";
import { Staff } from "@/types/personTypes";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import { StaffAttendeeID, AdminAttendeeID, CamperAttendeeID, Freeplay, PostID, FreeplayID } from "@/types/sessionTypes";
import { a } from "framer-motion/dist/types.d-BJcRxCew";
import { doesConflictExist } from "./schedulingUtils";

export class FreeplayScheduler {
/* The current freeplay schedule */
schedule: Freeplay = { posts: {}, buddies: {} };
schedule: FreeplayID = { posts: {}, buddies: {}, id: "", sessionId: "" };

/* The session attendees that still need to be assigned */
campers: CamperAttendeeID[] = [];
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; }
withSchedule(schedule: FreeplayID): FreeplayScheduler { this.schedule = schedule; return this; }

withCampers(campers: CamperAttendeeID[]): FreeplayScheduler { this.campers = campers; return this; }

withStaff(staff: StaffAttendeeID[]): FreeplayScheduler { this.staff = staff; return this; }

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);
Comment on lines +34 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Post lookup should match PostID.id, not name.

Schedule keys are post IDs; using name risks missing requiresAdmin and mis-assigning staff.

🔧 Suggested fix
-  getPostByID = (id: string) => this.posts.find(p => p.name === id);
+  getPostByID = (id: string) => this.posts.find(p => p.id === id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getCamperById = (id: number) => this.campers.find(c => c.id === id);
getPostByID = (id: string) => this.posts.find(p => p.name === id);
getCamperById = (id: number) => this.campers.find(c => c.id === id);
getPostByID = (id: string) => this.posts.find(p => p.id === id);
🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts` around lines 34 -
36, getPostByID currently finds posts by p.name which is wrong for schedule keys
— match PostID.id instead; update the getPostByID implementation to compare the
incoming id to the post's id field (e.g., this.posts.find(p => p.id === id)),
ensure the parameter type aligns with PostID.id, and leave posts/requiredAdmin
checks intact so requiresAdmin-based assignments work correctly.


// withOtherFreeplays should build the previousFreeplayBuddies object
// eslint-disable-next-line @typescript-eslint/no-unused-vars
withOtherFreeplays(otherFreeplays: Freeplay[]): FreeplayScheduler { return this; }
withOtherFreeplays(otherFreeplays: Freeplay[]): FreeplayScheduler {
for (const freeplay of otherFreeplays) {
for (const buddieIDStr in freeplay.buddies) {
const buddieID = Number(buddieIDStr);
if (buddieID in this.otherFreeplayBuddies) {

const attendees = freeplay.buddies[buddieID];

// add all attendees that don't already exist
for (const att of attendees) {
Comment thread
DurjaMan27 marked this conversation as resolved.
if (!this.otherFreeplayBuddies[buddieID].includes(att)) {
this.otherFreeplayBuddies[buddieID].push(att);
}
}
} else {
const attendees = freeplay.buddies[buddieID];
this.otherFreeplayBuddies[buddieID] = attendees;
}
Comment on lines +45 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid mutating input freeplay buddy arrays.

Assigning and then pushing into arrays sourced from freeplay.buddies can mutate the input objects. Clone them first.

🧹 Suggested fix
-          const attendees = freeplay.buddies[buddieID];
+          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;
+          const attendees = [...freeplay.buddies[buddieID]];
+          this.otherFreeplayBuddies[buddieID] = attendees;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
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;
}
🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts` around lines 45 -
56, The code mutates arrays from freeplay.buddies by assigning references and
then pushing into them; fix by cloning buddy arrays before storing or extending
them: when setting this.otherFreeplayBuddies[buddieID] in the else branch assign
a shallow copy of freeplay.buddies[buddieID] (e.g., create a new array from
attendees) and in the branch where you extend the existing
this.otherFreeplayBuddies[buddieID] ensure you are comparing against the cloned
target and only push items into that cloned array (do not push into the original
freeplay.buddies array); reference symbols: FreeplayScheduler,
this.otherFreeplayBuddies, freeplay.buddies, buddieID, attendees.

}
}

return this;
}

/* 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)
).filter(admin => !admin.daysOff.includes(this.schedule.id));



const availableStaff = this.staff.filter(staff =>
!this.assignedStaff.some(assigned => assigned.id === staff.id)
).filter(staff => !staff.daysOff.includes(this.schedule.id));


console.log("Available Staff: ", availableStaff);
console.log("Available Admins: ", availableAdmins);
// 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);
}
}
}

// 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);
}
Comment on lines +93 to +105
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t assign STAFF to admin-only posts in the second pass.

The second loop assigns staff to any empty post, including requiresAdmin posts when admins are exhausted. That violates the admin-only constraint.

🛠️ Suggested fix
-    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);
-        }
-      }
-    }
+    for (const postID in this.schedule.posts) {
+      const assigned = this.schedule.posts[postID];
+      if (assigned.length == 0) {
+        const post = this.getPostByID(postID);
+        if (post?.requiresAdmin) {
+          if (availableAdmins.length > 0) {
+            const adminID: AdminAttendeeID = availableAdmins.shift()!;
+            this.schedule.posts[postID] = [adminID.id];
+            this.assignedAdmin.push(adminID);
+          }
+          continue; // keep empty if no admins left
+        }
+        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);
+        }
+      }
+    }
🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts` around lines 87 -
99, The loop in FreeplayScheduler that fills empty this.schedule.posts currently
falls back to assigning staff to any empty post, which can assign STAFF into
admin-only posts; update the assignment logic in the post-filling loop inside
FreeplayScheduler to check each post's requiresAdmin flag (or equivalent
metadata) before assigning from availableStaff: if the post.requiresAdmin is
true and availableAdmins is empty, skip assigning staff (leave unassigned or
handle via existing unfilled-post flow); ensure you still assign admins when
available (using availableAdmins.shift() and this.assignedAdmin.push) and only
use availableStaff for posts where requiresAdmin is false.

}
}

return this;
}


/*
Assigns campers to remaining ADMIN & STAFF members for freeplay according to the following rules:
Expand All @@ -39,7 +122,164 @@ export class FreeplayScheduler {
to the same staff member.
- Prioritize avoiding assigning the same "freeplay buddy" (previous buddy) if possible.
*/
assignCampers() { return this; }
assignCampers() {
// First filter out by days off
const availableStaff = this.staff.filter(staff => !staff.daysOff.includes(this.schedule.id)).filter(staff => !this.assignedStaff.some(assigned => assigned.id === staff.id) );


//const allAssignedStaffers = [...this.assignedStaff, ...this.assignedAdmin];


const allFemaleStaff = availableStaff.filter(c => c.gender == "Female");
const allfemaleCampers = this.campers.filter(c => c.gender === "Female");

const allOtherCampers = this.campers.filter(c => c.gender !== "Female" );

console.log("Female Campers: ", allfemaleCampers);
console.log("Female Staff: ", allFemaleStaff);

// 2. Assign female campers
for (const camper of allfemaleCampers) {
let assigned = this.assignToOpenStaffFirstStep(allFemaleStaff, camper);

// Fallback: assign to any female staffer with another camper of the same bunk
if (!assigned) {
assigned = this.assignToOpenStaffSecondStep(allFemaleStaff, camper);
}

if (!assigned) {
assigned = this.assignToOpenStaffThirdStep(allFemaleStaff, camper);
}

if (!assigned) {
console.warn("No staffer available for camper: ", camper);
}
}

const allOtherStaff = availableStaff.filter(c => !this.schedule.buddies[c.id] || this.schedule.buddies[c.id].length == 0);


// 3. Assign male campers
for (const camper of allOtherCampers) {
let assigned = this.assignToOpenStaffFirstStep(allOtherStaff, camper);

// Fallback: assign to any staffer with another camper of the same bunk
if (!assigned) {
assigned = this.assignToOpenStaffSecondStep(allOtherStaff, camper);
}

if (!assigned) {
assigned = this.assignToOpenStaffThirdStep(allOtherStaff, camper);
}

if (!assigned) {
console.warn("No staffer available for camper: ", camper);
}
}

return this;
}

// Assigns camper to any staffer (not in same bunk)that they weren't assigned to before
assignToOpenStaffFirstStep(allAssignedStaffers: (StaffAttendeeID | AdminAttendeeID)[], camper: CamperAttendeeID) {
let assigned = false;

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


// Checks if staffer is already assigned to a camper
const alreadyAssigned = this.schedule.buddies[staffer.id] || [];

// Checks if staffer was already assigned to this camper in a previous iteration
const prevBuddies = this.otherFreeplayBuddies[staffer.id] || [];

// Check buddy conflict (camper.id appears in staffer's prevBuddies) and if camper-staff conflict exists
const hasConflict = prevBuddies.includes(camper.id) || doesConflictExist(staffer, [camper.id]);

if (!hasConflict && alreadyAssigned.length == 0) {
if ( (staffer.role === "STAFF" && staffer.bunk !== camper.bunk)) {

if (!this.schedule.buddies[staffer.id]) {
this.schedule.buddies[staffer.id] = [];
}
this.schedule.buddies[staffer.id].push(camper.id);
assigned = true;
break;
}
}
Comment on lines +198 to +208
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the FreeplayScheduler.ts file and extract the function signature
fd -e ts -n FreeplayScheduler && head -230 src/features/scheduling/generation/FreeplayScheduler.ts | tail -50

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 303


🏁 Script executed:

# Search for AdminAttendee interface definition
rg -n "interface AdminAttendee" -A 10

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 748


🏁 Script executed:

# Check if AdminAttendee has a bunk property
rg -n "bunk" --type=ts -A 2 -B 2 | head -80

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 4890


🏁 Script executed:

# Find all calls to assignToOpenStaffFirstStep to see what's passed
rg -n "assignToOpenStaffFirstStep" -B 3 -A 3

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 2151


🏁 Script executed:

# Check the commented-out line 128 context
sed -n '120,135p' src/features/scheduling/generation/FreeplayScheduler.ts

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 756


🏁 Script executed:

# Find AdminAttendee interface (not AdminAttendeeID)
rg -n "interface AdminAttendee[^I]" -A 15

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 55


🏁 Script executed:

# Check the specific lines 198-208 in FreeplayScheduler.ts to see the exact code
sed -n '198,208p' src/features/scheduling/generation/FreeplayScheduler.ts

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 444


🏁 Script executed:

# Check if AdminAttendee has role property and what values it can have
rg -n "role.*ADMIN" --type=ts -B 2 -A 2

Repository: Hack4Impact-UMD/camp-starfish

Length of output: 3833


ADMINs are silently skipped due to role check.

The function signature accepts (StaffAttendeeID | AdminAttendeeID)[], but line 199 only processes staffers where staffer.role === "STAFF". AdminAttendees have role "ADMIN" and lack a bunk property entirely, so they would be silently ignored if ever passed to this function. The commented-out line 128 suggests admins were considered as input.

Either narrow the type signature to StaffAttendeeID[] to match the actual behavior, or add explicit handling for ADMINs if they should be supported.

🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts` around lines 198 -
208, The code currently only handles staffers with staffer.role === "STAFF" and
accesses staffer.bunk, so AdminAttendeeID (role "ADMIN") entries are silently
ignored; either restrict the function parameter type to StaffAttendeeID[] to
match the implementation, or add explicit ADMIN handling: update the loop in
FreeplayScheduler to branch on staffer.role (e.g., if "STAFF" keep the bunk !=
camper.bunk check, if "ADMIN" skip the bunk check or apply the intended admin
logic), ensure any use of staffer.bunk is guarded or typed, and keep
schedule.buddies[staffer.id] initialization/assignment logic for both branches.

}

return assigned
}

// Assigns camper to any staffer with another camper of the same bunk
assignToOpenStaffSecondStep(allAssignedStaffers: (StaffAttendeeID | AdminAttendeeID)[], camper: CamperAttendeeID) {

let assigned = false;


for (const staffer of allAssignedStaffers) {


const prevBuddies = this.otherFreeplayBuddies[staffer.id] || []
const hasConflict = prevBuddies.includes(camper.id) || doesConflictExist(staffer, [camper.id]);

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

const otherCamper = this.getCamperById(alreadyAssigned[0]);

if (!otherCamper) {
continue;
}

if (otherCamper.bunk == camper.bunk && otherCamper.id !== camper.id) {
this.schedule.buddies[staffer.id].push(camper.id);
assigned = true;

break;
}

}
}

return assigned;
}

assignToOpenStaffThirdStep(allAssignedStaffers: (StaffAttendeeID | AdminAttendeeID)[], camper: CamperAttendeeID) {

let assigned = false;

for (const staffer of allAssignedStaffers) {

if (doesConflictExist(staffer, [camper.id])) {
continue;
}
const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
if (alreadyAssigned.length == 1) {

const otherCamper = this.getCamperById(alreadyAssigned[0]);

if (!otherCamper) {
continue;
}

if (!doesConflictExist(otherCamper, [camper.id])) {
this.schedule.buddies[staffer.id].push(camper.id);
assigned = true;

break;
}

}
}

return assigned;
}

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