Skip to content

Freeplay alg#37

Open
DurjaMan27 wants to merge 10 commits into
mainfrom
freeplay-alg
Open

Freeplay alg#37
DurjaMan27 wants to merge 10 commits into
mainfrom
freeplay-alg

Conversation

@DurjaMan27
Copy link
Copy Markdown
Collaborator

@DurjaMan27 DurjaMan27 commented Sep 28, 2025

Note that, for the constructor, I included a parameters for all necessary information (i.e. freeplayschedule, admins, staffers, campers, etc.)
I also included a variable that includes all PostID objects as a list. This is necessary for "requiresAdmin" lookup, since the only other information about posts is given through the schedule object, which only contains the postID as a string.

Summary by CodeRabbit

Release Notes

  • New Features
    • Enhanced camper assignment with gender-based allocation
    • Admin-only post scheduling support
    • Improved staff-camper pairing with multi-step conflict resolution
    • Better handling of complex scheduling scenarios with fallback logic

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 28, 2025

📝 Walkthrough

Walkthrough

The FreeplayScheduler class has been substantially expanded to support staff and admin assignment tracking, post-based scheduling, and multi-step camper allocation logic with gender-based grouping and conflict detection across multiple assignment fallback strategies.

Changes

Cohort / File(s) Summary
Core State Expansion
src/features/scheduling/generation/FreeplayScheduler.ts
Added assignedStaff, assignedAdmin arrays, and posts field; changed schedule type from Freeplay to FreeplayID.
Lookup & Configuration Methods
src/features/scheduling/generation/FreeplayScheduler.ts
Added withPosts() setter, getCamperById() and getPostByID() lookup helpers; fully implemented withOtherFreeplays() for buddy merging across freeplays.
Post Assignment Logic
src/features/scheduling/generation/FreeplayScheduler.ts
Implemented assignPosts() with ADMIN-only post prioritization, available staff/admin filtering (respecting days off), and fallback logic to fill remaining posts.
Multi-Step Camper Assignment
src/features/scheduling/generation/FreeplayScheduler.ts
Refactored assignCampers() to split by gender and execute gender-specific assignment pipelines; added three helper methods (assignToOpenStaffFirstStep, assignToOpenStaffSecondStep, assignToOpenStaffThirdStep) for progressive camper placement with conflict avoidance.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as FreeplayScheduler
    participant Staff as Staff/Admin Pool
    participant Campers as Camper Queue
    participant FirstStep as First Step<br/>(New Bunk)
    participant SecondStep as Second Step<br/>(Existing Bunk)
    participant ThirdStep as Third Step<br/>(Final Fallback)

    Scheduler->>Staff: Filter available (daysOff)
    Staff-->>Scheduler: Available staff/admins
    
    Scheduler->>Campers: Split by gender
    
    rect rgba(100, 150, 200, 0.5)
    Note over Campers,ThirdStep: For each gender group
    Campers->>FirstStep: Try to place camper<br/>with new staffer
    FirstStep-->>Scheduler: Success? Return true
    end
    
    rect rgba(200, 150, 100, 0.5)
    Note over Campers,ThirdStep: If first step fails
    Campers->>SecondStep: Try to place camper<br/>with existing bunk
    SecondStep-->>Scheduler: Success? Return true
    end
    
    rect rgba(150, 100, 200, 0.5)
    Note over Campers,ThirdStep: If second step fails
    Campers->>ThirdStep: Place camper<br/>with any staffer<br/>(no conflicts)
    ThirdStep-->>Scheduler: Success? Return true
    end
    
    Scheduler->>Scheduler: Log unassigned campers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A bunking puzzle solved with care,
Where staff and campers find their pair,
Three tries, then placement—first choice best,
Gender groups assigned with zest,
No conflicts here, just harmony's cheer! 🎪

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Freeplay alg' is vague and abbreviated, using a non-descriptive shorthand ('alg') that doesn't convey meaningful information about the actual changes. Replace with a more descriptive title that clearly explains the main change, such as 'Implement freeplay scheduling algorithm with staff and camper assignment' or 'Add full freeplay scheduler implementation with gender-based camper allocation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch freeplay-alg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0a9704 and 8a6752c.

📒 Files selected for processing (1)
  • src/features/scheduling/FreeplayScheduler.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/features/scheduling/FreeplayScheduler.ts (1)
src/types/sessionTypes.ts (5)
  • CamperAttendeeID (24-24)
  • StaffAttendeeID (33-33)
  • AdminAttendeeID (39-39)
  • PostID (135-135)
  • Freeplay (125-128)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_preview

Comment thread src/features/scheduling/FreeplayScheduler.ts
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
@Hack4Impact-UMD Hack4Impact-UMD deleted a comment from coderabbitai Bot Sep 29, 2025
Copy link
Copy Markdown
Collaborator

@eshavignesh123 eshavignesh123 left a comment

Choose a reason for hiding this comment

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

Hi Varun! This looks good for the most part! Just some minor edits

Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/features/scheduling/FreeplayScheduler.ts (3)

115-122: Remove the commented-out line.

The active implementation correctly places assignedStaff before assignedAdmin as requested in feedback. Please remove the commented line 117 to keep the code clean.

Apply this diff:

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

153-171: Consider adding buddy conflict check in the fallback logic.

The fallback assignment doesn't verify whether the camper was previously paired with the staffer in otherFreeplayBuddies, which could result in repeated pairings. Based on previous feedback, consider adding this check to further avoid buddy repetition.

Apply this diff to add the conflict check:

         if (!assigned) {
           for (const staffer of allAssignedStaffers) {
             if (staffer.gender !== "Female") continue;

             const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
+            const prevBuddies = this.otherFreeplayBuddies[staffer.id] || [];
+            const hasConflict = prevBuddies.includes(camper.id);
+
+            if (hasConflict) continue;
+
             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;
               }

             }
           }
         }

Based on learnings


199-216: Consider adding buddy conflict check in the male fallback logic.

Similar to the female fallback, this logic doesn't verify otherFreeplayBuddies, which could result in repeated pairings. Consider adding the conflict check here as well.

Apply this diff to add the conflict check:

         if (!assigned) {
           for (const staffer of allAssignedStaffers) {

             const alreadyAssigned = this.schedule.buddies[staffer.id] || [];
+            const prevBuddies = this.otherFreeplayBuddies[staffer.id] || [];
+            const hasConflict = prevBuddies.includes(camper.id);
+
+            if (hasConflict) continue;
+
             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;
               }

             }
           }
         }

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a6752c and 2ae5d76.

📒 Files selected for processing (1)
  • src/features/scheduling/FreeplayScheduler.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/features/scheduling/FreeplayScheduler.ts (1)
src/types/sessionTypes.ts (4)
  • StaffAttendeeID (33-33)
  • AdminAttendeeID (39-39)
  • PostID (135-135)
  • Freeplay (125-128)
🔇 Additional comments (12)
src/features/scheduling/FreeplayScheduler.ts (12)

1-1: LGTM!

The addition of PostID to the imports is necessary for the new posts field and related functionality.


12-15: LGTM!

The new tracking fields (assignedStaff, assignedAdmin, posts) are correctly typed and necessary for the assignment logic.


20-21: LGTM!

The constructor is correctly left blank as requested, and the comment helpfully explains the purpose of the posts field.


31-31: LGTM!

The withPosts method correctly follows the builder pattern and was requested in previous feedback.


33-35: LGTM!

The helper methods correctly use find() and will return undefined if no match is found, which is properly handled by callers.


64-71: LGTM!

The filtering logic correctly excludes already-assigned staff and admins, addressing previous feedback. The use of filtered copies ensures the original arrays remain unchanged.


72-83: LGTM!

The admin-required post assignment logic is correct and now properly records admins in assignedAdmin (line 80), addressing the previous review feedback.


85-99: LGTM!

The general post assignment correctly prioritizes admins over staff and properly tracks assignments in both assignedAdmin and assignedStaff.


100-102: LGTM!

The method correctly returns this for builder pattern chaining.


124-151: LGTM!

The female camper assignment logic correctly breaks out of the inner loop after assignment (lines 143, 148), addressing previous feedback. The gender matching and buddy conflict checks are properly implemented.


217-219: LGTM!

The method correctly returns this for builder pattern chaining.


173-197: Add break statements after male camper assignment.

After setting assigned = true and assigning the camper (lines 190, 194), the loop continues iterating, potentially assigning the same camper to multiple staffers. This is the same issue previously flagged for the female assignment loop.

Apply this diff to add the missing breaks:

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

Comment thread src/features/scheduling/FreeplayScheduler.ts
Copy link
Copy Markdown
Collaborator

@nkanchinadam nkanchinadam left a comment

Choose a reason for hiding this comment

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

Overall, really good! Just left a couple comments noting on stuff that could be simplified/optimized, but other than that, I think it hits the major points that were needed for scheduling Freeplays.

Comment thread src/features/scheduling/generation/FreeplayScheduler.ts
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Comment thread src/features/scheduling/FreeplayScheduler.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/features/scheduling/FreeplayScheduler.ts (4)

48-51: Consider using Sets for efficient duplicate checking.

The current implementation uses includes() for duplicate checking, which has O(n) complexity for each check. As suggested in previous feedback, using Sets would provide O(1) lookups and improve performance when merging large buddy lists.

Here's an optimized approach using Sets:

 withOtherFreeplays(otherFreeplays: Freeplay[]): FreeplayScheduler {
+  // Use Sets for efficient deduplication
+  const buddySets: Map<number, Set<number>> = new Map();
+  
   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) {
-          if (!this.otherFreeplayBuddies[buddieID].includes(att)) {
-            this.otherFreeplayBuddies[buddieID].push(att);
-          }
-        }
-      } else {
-        const attendees = freeplay.buddies[buddieID];
-        this.otherFreeplayBuddies[buddieID] = attendees;
+      
+      if (!buddySets.has(buddieID)) {
+        buddySets.set(buddieID, new Set());
       }
+      
+      const attendees = freeplay.buddies[buddieID];
+      for (const att of attendees) {
+        buddySets.get(buddieID)!.add(att);
+      }
     }
   }
+  
+  // Convert Sets back to arrays
+  for (const [buddieID, attendeeSet] of buddySets) {
+    this.otherFreeplayBuddies[buddieID] = Array.from(attendeeSet);
+  }

   return this;
 }

Based on learnings


117-119: Consider caching gender-split staff at class level.

As suggested in previous feedback, maintaining gender-split staff as class-level variables would eliminate redundant filtering on every call to assignCampers(). This would improve performance if assignCampers() is called multiple times.

Example approach:

// Add to class fields
private femaleStaff: StaffAttendeeID[] = [];
private femaleAdmins: AdminAttendeeID[] = [];

// Update in withStaff/withAdmins
withStaff(staff: StaffAttendeeID[]): FreeplayScheduler { 
  this.staff = staff; 
  this.femaleStaff = staff.filter(s => s.gender === "Female");
  return this; 
}

// Then use directly
const allAssignedFemaleStaffers = [
  ...this.assignedStaff.filter(s => this.femaleStaff.some(f => f.id === s.id)),
  ...this.assignedAdmin.filter(a => this.femaleAdmins.some(f => f.id === a.id))
];

Based on learnings


148-148: Add JSDoc comments for helper methods.

The helper methods assignToOpenStaffFirstStep and assignToOpenStaffSecondStep implement complex assignment logic but lack documentation. Consider adding JSDoc comments to explain:

  • The purpose of each step
  • The buddy conflict checking mechanism
  • The bunk matching constraints
  • Return values and side effects

Example:

+  /**
+   * First assignment step: Attempts to assign a camper to a staffer who:
+   * - Has no campers assigned yet
+   * - Has no previous buddy conflict with this camper
+   * - Is not from the same bunk (for STAFF role only)
+   * 
+   * @returns true if assignment was successful, false otherwise
+   */
   assignToOpenStaffFirstStep(allAssignedStaffers: (StaffAttendeeID | AdminAttendeeID)[], camper: CamperAttendeeID) {

1-1: Remove unused import

The Staff import from @/types/personTypes is unused in FreeplayScheduler.ts. Remove it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae5d76 and de8c4c2.

📒 Files selected for processing (1)
  • src/features/scheduling/FreeplayScheduler.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/features/scheduling/FreeplayScheduler.ts (1)
src/types/sessionTypes.ts (5)
  • StaffAttendeeID (33-33)
  • AdminAttendeeID (39-39)
  • PostID (135-135)
  • Freeplay (125-128)
  • CamperAttendeeID (24-24)
🔇 Additional comments (3)
src/features/scheduling/FreeplayScheduler.ts (3)

64-104: LGTM! Post assignment logic is correct.

The implementation properly:

  • Filters available staff/admins without mutating the original lists
  • Prioritizes admins for admin-required posts (lines 75-85)
  • Records all assignments in assignedAdmin and assignedStaff (lines 82, 94, 98)
  • Falls back to staff for non-admin posts when admins are exhausted

The critical issue from previous feedback about recording admin assignments has been correctly addressed.


161-161: Document the bunk exclusion logic for staff.

The condition (staffer.role === "STAFF" && staffer.bunk !== camper.bunk) || staffer.role !== "STAFF" prevents STAFF members from being assigned to campers from their own bunk, while allowing ADMINs to supervise any bunk. This business rule is not documented and may be unclear to future maintainers.

Please verify this is the intended behavior and add a comment explaining why staff cannot supervise their own bunk during freeplay. For example:

       if (!hasConflict && alreadyAssigned.length == 0) {
+        // Staff members cannot supervise their own bunk during freeplay
+        // Admins (who don't have assigned bunks) can supervise any campers
         if ( (staffer.role === "STAFF" && staffer.bunk !== camper.bunk) || staffer.role !== "STAFF" ) {

178-179: Verify the fallback logic for missing campers.

Line 178 uses || camper as a fallback if getCamperById returns undefined, but this would cause the same-bunk check to compare camper with itself. The subsequent check otherCamper.id !== camper.id would then always fail, silently skipping the assignment.

If a camper ID is in this.schedule.buddies but not found via getCamperById, it indicates a data inconsistency that should probably be handled explicitly:

-        const otherCamper = this.getCamperById(alreadyAssigned[0]) || camper;
+        const otherCamper = this.getCamperById(alreadyAssigned[0]);
+        if (!otherCamper) continue; // Skip if camper not found
         if (otherCamper.bunk == camper.bunk && otherCamper.id !== camper.id) {

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts`:
- Around line 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.
- Around line 126-141: Change the mutable declarations for the loop-local
variable "assigned" to immutable "const" in FreeplayScheduler.ts: in the
femaleCampers loop (where you call
assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper)) and in the
maleCampers loop (where you call assignToOpenStaffFirstStep(allAssignedStaffers,
camper)); both "assigned" variables are never reassigned so replace "let
assigned" with "const assigned" to satisfy ESLint prefer-const while leaving the
fallback calls to assignToOpenStaffSecondStep unchanged.
- Around line 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.
- Line 1: The import of the Staff type in FreeplayScheduler.ts is unused and
causing an ESLint lint failure; remove the unused import line "import { Staff }
from "@/types/personTypes"; from the top of FreeplayScheduler.ts (or
alternatively use the Staff type where appropriate), ensuring no other
references to Staff remain so CI lint passes.
- Around line 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.

Comment thread src/features/scheduling/generation/FreeplayScheduler.ts Outdated
Comment on lines +34 to +36
getCamperById = (id: number) => this.campers.find(c => c.id === id);

getPostByID = (id: string) => this.posts.find(p => p.name === id);
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.

Comment on lines +45 to +56
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;
}
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.

Comment on lines +87 to +99
// 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);
}
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.

Comment on lines +126 to +141
for (const camper of femaleCampers) {
let assigned = this.assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper);

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

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

// Fallback: assign to any staffer with another camper of the same bunk
if (!assigned) {
this.assignToOpenStaffSecondStep(allAssignedStaffers, camper);
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

Use const for assigned to fix lint errors.

Both assigned variables are never reassigned; ESLint prefer-const fails CI.

🧹 Suggested fix
-      let assigned = this.assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper);
+      const assigned = this.assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper);
...
-      let assigned = this.assignToOpenStaffFirstStep(allAssignedStaffers, camper);
+      const assigned = this.assignToOpenStaffFirstStep(allAssignedStaffers, camper);
📝 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
for (const camper of femaleCampers) {
let assigned = this.assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper);
// Fallback: assign to any female staffer with another camper of the same bunk
if (!assigned) {
this.assignToOpenStaffSecondStep(allAssignedFemaleStaffers, camper);
}
}
// 3. Assign male campers
for (const camper of maleCampers) {
let assigned = this.assignToOpenStaffFirstStep(allAssignedStaffers, camper);
// Fallback: assign to any staffer with another camper of the same bunk
if (!assigned) {
this.assignToOpenStaffSecondStep(allAssignedStaffers, camper);
for (const camper of femaleCampers) {
const assigned = this.assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper);
// Fallback: assign to any female staffer with another camper of the same bunk
if (!assigned) {
this.assignToOpenStaffSecondStep(allAssignedFemaleStaffers, camper);
}
}
// 3. Assign male campers
for (const camper of maleCampers) {
const assigned = this.assignToOpenStaffFirstStep(allAssignedStaffers, camper);
// Fallback: assign to any staffer with another camper of the same bunk
if (!assigned) {
this.assignToOpenStaffSecondStep(allAssignedStaffers, camper);
🧰 Tools
🪛 GitHub Actions: Lint & Build on PR

[error] 127-127: ESLint: 'assigned' is never reassigned. Use 'const' instead. (prefer-const)


[error] 137-137: ESLint: 'assigned' is never reassigned. Use 'const' instead. (prefer-const)

🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts` around lines 126 -
141, Change the mutable declarations for the loop-local variable "assigned" to
immutable "const" in FreeplayScheduler.ts: in the femaleCampers loop (where you
call assignToOpenStaffFirstStep(allAssignedFemaleStaffers, camper)) and in the
maleCampers loop (where you call assignToOpenStaffFirstStep(allAssignedStaffers,
camper)); both "assigned" variables are never reassigned so replace "let
assigned" with "const assigned" to satisfy ESLint prefer-const while leaving the
fallback calls to assignToOpenStaffSecondStep unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/features/scheduling/test/generateEmulatorData.tsx`:
- Around line 1-6: The file has many unused imports, helper functions,
parameters and locals (e.g., imports CamperAttendeeID, StaffAttendeeID,
AdminAttendeeID, AgeGroup, SectionSchedule, BundleActivityWithAssignments,
SectionPreferences, BunkID, BunkJamboreeActivityWithAssignments,
NonBunkJamboreeActivityWithAssignments, Freeplay, PostID, SectionID, SessionID,
NightShiftID, ProgramAreaID, SchedulingSectionID, FreeplayID and helpers
generateNonoLists, generateYesyesLists, generateBunks,
generateBundleBlockSchedule, generateBunkJamboBlockSchedule,
generateNonBunkJamboBlockSchedule, generateBlockIDs, generatePrefs, and
locals/params blockID, counselors, nightShifts) — either remove the unused
imports/helpers/params, wire them into generateSession where appropriate, or add
focused eslint-disable-next-line comments only on the specific unused symbols to
silence CI; update imports at top to match what generateSession actually
references and delete or export unused helper functions if intended for later
use.
- Around line 537-548: The generatePosts function currently uses
faker.word.noun() for PostID.id which can produce duplicate IDs; update
generatePosts to guarantee uniqueness by generating IDs with a
collision-resistant generator (e.g., faker.datatype.uuid() or append a
sequential/index suffix) when creating each PostID, and ensure the returned
PostID objects (id, name, requiresAdmin) use that unique id so the schedule map
won't be overwritten; references: function generatePosts and the PostID objects
created inside it.
♻️ Duplicate comments (3)
src/features/scheduling/generation/FreeplayScheduler.ts (3)

38-38: Use PostID.id for lookup.

getPostByID should compare against id, not name, or admin-required posts may be missed.

🛠️ Suggested fix
-  getPostByID = (id: string) => this.posts.find(p => p.name === id);
+  getPostByID = (id: string) => this.posts.find(p => p.id === id);

47-58: Avoid mutating source freeplay.buddies arrays.

Clone before storing/pushing to prevent side effects across schedules.

🧹 Suggested fix
-          const attendees = freeplay.buddies[buddieID];
+          const attendees = [...freeplay.buddies[buddieID]];
...
-          const attendees = freeplay.buddies[buddieID];
-          this.otherFreeplayBuddies[buddieID] = attendees;
+          const attendees = [...freeplay.buddies[buddieID]];
+          this.otherFreeplayBuddies[buddieID] = attendees;

95-107: Prevent STAFF assignment to admin-only posts in pass two.

Second loop can still place staff into requiresAdmin posts once admins are exhausted.

🛠️ 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; // leave unfilled if no admins are available
+        }
+        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);
+        }
+      }
+    }
🧹 Nitpick comments (2)
src/app/LoginPage.tsx (1)

37-45: Gate test-data generation behind a dev-only flag.

This adds a production-visible test utility on the login screen; consider hiding it behind NODE_ENV or a feature flag.

💡 Suggested guard
-  const [error, setError] = useState<string>("");
+  const [error, setError] = useState<string>("");
+  const isDev = process.env.NODE_ENV !== "production";
...
-        <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>
+        {isDev && (
+          <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>
+        )}

Also applies to: 81-88

src/features/scheduling/test/generateEmulatorData.tsx (1)

346-351: Remove duplicate alternatePeriodsOff["RH"] assignment.

It’s set twice in a row, which is redundant.

♻️ Simplify duplicate assignment
-  schedule.alternatePeriodsOff["RH"] = []
-
-  schedule.alternatePeriodsOff["RH"] = []
+  schedule.alternatePeriodsOff["RH"] = []

Comment thread src/features/scheduling/test/generateEmulatorData.tsx Outdated
Comment on lines +537 to +548
function generatePosts(totalPosts: number): PostID[] {
const posts: PostID[] = [];

for (let i = 0; i < totalPosts; i++) {
const name = faker.word.noun();
const requiresAdmin = faker.datatype.boolean();

posts.push({
name,
requiresAdmin,
id: name
});
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

Ensure generated post IDs are unique.

faker.word.noun() can collide, which can overwrite posts in the schedule map.

✅ Safer IDs
-    const name = faker.word.noun();
+    const name = faker.word.noun();
+    const id = `${name}-${i}`;
...
-    posts.push({
-      name,
-      requiresAdmin,
-      id: name
-    });
+    posts.push({
+      name,
+      requiresAdmin,
+      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
function generatePosts(totalPosts: number): PostID[] {
const posts: PostID[] = [];
for (let i = 0; i < totalPosts; i++) {
const name = faker.word.noun();
const requiresAdmin = faker.datatype.boolean();
posts.push({
name,
requiresAdmin,
id: name
});
function generatePosts(totalPosts: number): PostID[] {
const posts: PostID[] = [];
for (let i = 0; i < totalPosts; i++) {
const name = faker.word.noun();
const requiresAdmin = faker.datatype.boolean();
const id = `${name}-${i}`;
posts.push({
name,
requiresAdmin,
id
});
🤖 Prompt for AI Agents
In `@src/features/scheduling/test/generateEmulatorData.tsx` around lines 537 -
548, The generatePosts function currently uses faker.word.noun() for PostID.id
which can produce duplicate IDs; update generatePosts to guarantee uniqueness by
generating IDs with a collision-resistant generator (e.g., faker.datatype.uuid()
or append a sequential/index suffix) when creating each PostID, and ensure the
returned PostID objects (id, name, requiresAdmin) use that unique id so the
schedule map won't be overwritten; references: function generatePosts and the
PostID objects created inside it.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/features/scheduling/generation/FreeplayScheduler.ts`:
- Around line 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.
🧹 Nitpick comments (2)
src/features/scheduling/generation/FreeplayScheduler.ts (2)

78-79: Consider removing debug console.log statements.

These debug logs ("Available Staff: ", "Available Admins: ") are useful during development but should be removed or converted to a proper logging framework with configurable levels before production.


136-137: Remove debug console.log statements.

Similar to lines 78-79, these debug logs should be removed or converted to proper logging for production.

Comment on lines +198 to +208
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;
}
}
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants