Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
28714c6
added function declarations and started initial algorthim for filling…
Psangs1 Sep 23, 2025
d2588ee
edited assignDaysOff function to account for no days off on days 2, 1…
Psangs1 Sep 26, 2025
974f949
changed employee parameter type to staffsessionattendee, and created …
Psangs1 Sep 27, 2025
356ae5a
Added functions to generate data to test Scheduler functions
eshavignesh123 Sep 28, 2025
3e6ab9b
Merge branch 'main' of https://github.com/Hack4Impact-UMD/camp-starfi…
eshavignesh123 Sep 28, 2025
7b4b6ea
Fixed generateCamperPrefs() and added a function to create the Bundle…
eshavignesh123 Sep 28, 2025
d2ca704
Added swimOptOut generation to test data
eshavignesh123 Sep 28, 2025
8872031
Added BlockId as a param when generating activities
eshavignesh123 Sep 29, 2025
04b2007
Merge remote-tracking branch 'origin/main' into feature/session-wide-…
Psangs1 Sep 29, 2025
705b8d7
refactored sessionwidealgorithm to move code into session scheduleler…
Psangs1 Sep 29, 2025
da2baeb
removed inital sessionwidealgorithm file
Psangs1 Sep 29, 2025
fa4ff29
fixed some issues with npm run build and unisnstalled node dependenci…
Psangs1 Sep 30, 2025
573adef
added hasDaysOff field to Section type
nkanchinadam Oct 2, 2025
8862067
refactored scheduling algorithm to take into account program counsole…
Psangs1 Oct 3, 2025
16fb835
Merge branch 'main' of https://github.com/Hack4Impact-UMD/camp-starfi…
eshavignesh123 Oct 14, 2025
ff504c9
Finished adding testdata info to emulator
eshavignesh123 Oct 16, 2025
54b156d
delete unused file
nkanchinadam Oct 18, 2025
8e4c621
Exported test data from emulators and removed button on Login page
eshavignesh123 Oct 18, 2025
cd05c4a
Merge branch 'test/testData' of https://github.com/Hack4Impact-UMD/ca…
eshavignesh123 Oct 18, 2025
45888ce
remove temporary file
nkanchinadam Oct 20, 2025
efe2242
moved emulator data into root-level test folder
nkanchinadam Oct 20, 2025
3ddbf98
deny all Firestore requests
nkanchinadam Oct 20, 2025
445ab62
deny all Cloud Storage requests
nkanchinadam Oct 20, 2025
6a642b3
import emulator data on terminal launch
nkanchinadam Oct 20, 2025
e451a8c
Merge branch 'main' into test/testData
nkanchinadam Oct 20, 2025
628c4f4
Merge remote-tracking branch 'origin/test/testData' into feature/sess…
Psangs1 Oct 20, 2025
54b7d67
Merge branch 'main' into feature/session-wide-algorithm
eshavignesh123 Jan 20, 2026
ba22470
Removed temp testdata
eshavignesh123 Jan 20, 2026
c20df47
Finished assigning periodsOff
eshavignesh123 Jan 21, 2026
e0c47ee
Fixed minor mistakes with assignDaysOff
eshavignesh123 Jan 22, 2026
5fb1b9f
Fixed assignNightShifts
eshavignesh123 Jan 22, 2026
32560d5
Lint issues
eshavignesh123 Jan 22, 2026
1028ff4
Lint issues
eshavignesh123 Jan 25, 2026
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
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 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 generateTestData = async () => {
try {
const data = await generateSession();
console.log(data);
}
catch {
setError("An error occurred while trying to sign in. Please try again.");
}
}
Comment on lines +37 to +45
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

Fix the error message - it references sign-in instead of test data generation.

The catch block reuses the sign-in error message, which would be confusing to users/developers debugging test data generation failures.

Proposed fix
   const generateTestData = async () => {
     try {
       const data = await generateSession();
       console.log(data);
     }
     catch {
-      setError("An error occurred while trying to sign in. Please try again.");
+      setError("An error occurred while generating test data. Please try again.");
     }
   }
📝 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 generateTestData = async () => {
try {
const data = await generateSession();
console.log(data);
}
catch {
setError("An error occurred while trying to sign in. Please try again.");
}
}
const generateTestData = async () => {
try {
const data = await generateSession();
console.log(data);
}
catch {
setError("An error occurred while generating test data. Please try again.");
}
}
🤖 Prompt for AI Agents
In `@src/app/LoginPage.tsx` around lines 37 - 45, The catch in generateTestData
incorrectly sets a sign-in message; change it to handle errors from
generateSession by catching the error (catch (err)) and calling setError with a
message like "An error occurred while generating test data. Please try again."
and optionally log the actual error via console.error(err) to aid debugging;
update references in generateTestData and ensure setError usage is consistent.


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,14 @@ export default function LoginPage() {
Sign in with Microsoft
</button>

<button
onClick={generateTestData}
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
</button>
Comment on lines +81 to +88
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

Test button should not be on the production login page.

This "Generate Test" button appears to be debug/development tooling. Including it on the login page has several issues:

  1. It uses the Microsoft icon which is misleading
  2. Test data generation shouldn't be accessible to end users in production
  3. It only logs to console, providing no user feedback on success

Consider:

  • Removing this button entirely and using a separate admin/dev route
  • Conditionally rendering based on environment (e.g., process.env.NODE_ENV === 'development')
  • Using an appropriate icon if kept
Proposed fix (conditional rendering)
-        <button
-          onClick={generateTestData}
-          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
-        </button>
+        {process.env.NODE_ENV === 'development' && (
+          <button
+            onClick={generateTestData}
+            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"
+          >
+            Generate Test Data
+          </button>
+        )}
📝 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
<button
onClick={generateTestData}
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
</button>
{process.env.NODE_ENV === 'development' && (
<button
onClick={generateTestData}
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"
>
Generate Test Data
</button>
)}
🤖 Prompt for AI Agents
In `@src/app/LoginPage.tsx` around lines 81 - 88, Remove or hide the debug
"Generate Test" button from the production LoginPage: locate the button using
the generateTestData function and MicrosoftIcon in LoginPage.tsx and either
delete it or wrap its JSX in a condition that only renders in development (e.g.,
check process.env.NODE_ENV === 'development'); if you keep it for dev use,
replace the MicrosoftIcon with a neutral dev icon and modify generateTestData to
provide user-visible feedback (toast or inline message) instead of only
console.log, and ensure this functionality is moved to an admin/dev-only route
if it needs to exist long-term.


{/* Error Message */}
{errorDisplay && (
Expand Down
226 changes: 222 additions & 4 deletions src/features/scheduling/generation/SessionScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import { StaffAttendeeID, AdminAttendeeID, NightShiftID, SessionID, SectionID } from "@/types/sessionTypes";
import { StaffAttendeeID, AdminAttendeeID, NightShiftID, SessionID, SectionID, NightShift } from "@/types/sessionTypes";
import { Moment } from "moment";
import moment from "moment";

export class SessionScheduler {
session: SessionID | undefined;
Expand All @@ -17,9 +18,226 @@ export class SessionScheduler {

withNightShifts(nightShifts: NightShiftID[]): SessionScheduler { this.nightShifts = nightShifts; return this; }

assignDaysOff(): SessionScheduler { return this; }
private shuffleArray<T>(arr: T[]): T[] {
return arr .map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
}
private toISO(date: Moment): string {
return date.format("YYYY-MM-DD");
}

private getDateStringsInRange(start: Moment, end: Moment): string[] {
const dates: string[] = [];
const curr = start.clone();
while (curr.isSameOrBefore(end)) {
dates.push(this.toISO(curr));
curr.add(1, "day");
}
return dates;
}

private isJamboreeISO(dateISO: string, sections: SectionID[]): boolean {
const date = moment(dateISO);
return sections.some(section => {
if (!('type' in section)) return false;
const s = moment(section.startDate);
const e = moment(section.endDate).subtract(1, "day");
return (
(section.type === "BUNK-JAMBO" || section.type === "NON-BUNK-JAMBO") &&
date.isSameOrAfter(s) &&
date.isSameOrBefore(e)
);
});
}


private assignOneDayOff(
employee: StaffAttendeeID | AdminAttendeeID,
candidateDates: string[],
dayCounts: Map<string, number>,
allEmployees: (StaffAttendeeID | AdminAttendeeID)[],
maxStaffOffPerDay: number
): boolean {

for (const date of candidateDates) {
if ((dayCounts.get(date) ?? 0) >= maxStaffOffPerDay){
console.log("Max staff off per day reached");
continue;
};
// bunk constraint (HARD)
if ('bunk' in employee && maxStaffOffPerDay !== Infinity) {
const bunkConflict = allEmployees.some(e =>
'bunk' in e &&
e.bunk === employee.bunk &&
e.daysOff.includes(date)
);

if (bunkConflict) continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// assign
employee.daysOff.push(date);
dayCounts.set(date, (dayCounts.get(date) ?? 0) + 1);
return true;
}



return false;
}



assignDaysOff(session: SessionID, employees: (StaffAttendeeID | AdminAttendeeID)[]): SessionScheduler {
try {
const start = moment(session.startDate);

// Define windows (days 2–6 and 7–12) (TEMP Set for default session. Need to change to handle multiple sessions)
const window1 = this.getDateStringsInRange(start.clone().add(1, "day"), start.clone().add(5, "day"));
const window2 = this.getDateStringsInRange(start.clone().add(6, "day"), start.clone().add(11, "day"));

const allDates = [...window1, ...window2];
let maxStaffOffPerDay = Math.ceil(employees.length / allDates.length);

const dayCounts = new Map<string, number>();
allDates.forEach(d => dayCounts.set(d, 0));

// Separate program counselors
const nonWFProgramCounselors = employees.filter(
e => 'programCounselor' in e && e.programCounselor && e.programCounselor.id !== "WF"
);
const WFProgramCounselors = employees.filter(
e => 'programCounselor' in e && e.programCounselor && e.programCounselor.id === "WF"
);
// ---- Choose exact number of WF counselors for Jamboree ----
const numWFWindow1 = Math.ceil(WFProgramCounselors.length/window1.length); // example: 3 WF on Jamboree in window1
const numWFWindow2 = Math.ceil(WFProgramCounselors.length/window2.length); // example: 2 WF on Jamboree in window2

const shuffledWF = this.shuffleArray(WFProgramCounselors);
const selectedWFWindow1 = shuffledWF.slice(0, numWFWindow1);
const selectedWFWindow2 = shuffledWF.slice(numWFWindow1, numWFWindow1 + numWFWindow2);

// ---- Build Jamboree sets ----
const jamboreeWindow1 = new Set([...nonWFProgramCounselors, ...selectedWFWindow1]);
const jamboreeWindow2 = new Set([...nonWFProgramCounselors, ...selectedWFWindow2]);

// ---- PASS 1: window1 ----
for (const employee of employees) {
maxStaffOffPerDay = Math.ceil(employees.length / window1.length);
const dates =
'programCounselor' in employee && jamboreeWindow1.has(employee)
? window1.filter(d => this.isJamboreeISO(d, this.sections))
: window1;

if (dates.length === 1){
// set maxStaffOffPerDay to Infinity
maxStaffOffPerDay = Infinity;
};

if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
console.warn(`Failed to assign window-1 day off for ${employee.id}`);
}
}
Comment thread
eshavignesh123 marked this conversation as resolved.

// ---- PASS 2: window2 ----
for (const employee of employees) {
maxStaffOffPerDay = Math.ceil(employees.length / window2.length);

const dates =
'programCounselor' in employee && jamboreeWindow2.has(employee)
? window2.filter(d => this.isJamboreeISO(d, this.sections))
: window2;

if (dates.length === 1){

maxStaffOffPerDay = Infinity;
};

if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
console.warn(`Failed to assign window-2 day off for ${employee.id}`);
}
}
Comment on lines +95 to +159
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

Clamp day‑off windows to session dates and guard empty windows.

The fixed windows (days 2–6, 7–12) can extend beyond the session end. For shorter sessions this creates out‑of‑range day‑offs and can also lead to window2.length === 0, which yields Math.ceil(employees.length / 0) = Infinity and noisy warnings.

✅ Proposed fix (clip windows + skip empty pass)
-      // Define windows (days 2–6 and 7–12) (TEMP Set for default session. Need to change to handle multiple sessions)
-      const window1 = this.getDateStringsInRange(start.clone().add(1, "day"), start.clone().add(5, "day"));
-      const window2 = this.getDateStringsInRange(start.clone().add(6, "day"), start.clone().add(11, "day"));
+      // Define windows (days 2–6 and 7–12), clipped to session end
+      const sessionEnd = moment(session.endDate).subtract(1, "day");
+      const window1Start = start.clone().add(1, "day");
+      const window1End = moment.min(start.clone().add(5, "day"), sessionEnd);
+      const window1 = window1Start.isSameOrBefore(window1End)
+        ? this.getDateStringsInRange(window1Start, window1End)
+        : [];
+
+      const window2Start = start.clone().add(6, "day");
+      const window2End = moment.min(start.clone().add(11, "day"), sessionEnd);
+      const window2 = window2Start.isSameOrBefore(window2End)
+        ? this.getDateStringsInRange(window2Start, window2End)
+        : [];
@@
-      for (const employee of employees) {
+      if (window1.length > 0) {
+        for (const employee of employees) {
           maxStaffOffPerDay = Math.ceil(employees.length / window1.length);
@@
-        if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
+          if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
             console.warn(`Failed to assign window-1 day off for ${employee.id}`);
-        }
+          }
+        }
       }
@@
-      for (const employee of employees) {
+      if (window2.length > 0) {
+        for (const employee of employees) {
           maxStaffOffPerDay = Math.ceil(employees.length / window2.length);
@@
-        if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
+          if (!this.assignOneDayOff(employee, dates, dayCounts, employees, maxStaffOffPerDay)) {
             console.warn(`Failed to assign window-2 day off for ${employee.id}`);
-        }
+          }
+        }
       }
🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/SessionScheduler.ts` around lines 95 -
159, The fixed windows (window1/window2 created via getDateStringsInRange) can
extend past the session end causing out‑of‑range dates and window2.length === 0
which leads to division by zero and Infinity for maxStaffOffPerDay; clamp each
window to the session end (e.g., compute end-bound when calling
getDateStringsInRange or trim the returned arrays against the session end/last
valid date) and skip a pass when a window is empty (if window1.length===0 or
window2.length===0, do not run the corresponding for-loop), and also guard
before computing maxStaffOffPerDay to avoid dividing by zero (only compute
Math.ceil(employees.length / window.length) when window.length>0); apply the
same guards to the jamboree filtering that uses isJamboreeISO and to calls to
assignOneDayOff so you never pass empty date arrays.


} catch (e) {
console.error(e);
}

return this;
}



assignNightShifts(session: SessionID, employees: StaffAttendeeID[]): SessionScheduler {
const bunks = new Set<number>();
const endDate = moment(session.endDate).subtract(1, 'day');
const startDate = moment(session.startDate);


// Collect unique bunks
for (let i = 0; i < employees.length; i++) {
bunks.add(employees[i].bunk);
}



const currDate = startDate;

// Create a night bunk object for each night
while (currDate.isSameOrBefore(endDate)) {
const shift: NightShiftID = {
sessionId: session.id,
id: `night-${this.toISO(currDate)}`
};
// looping through all unique bunks that the employees have
for (const bunkNumber of bunks) {
// Create NightShift object for this bunk
shift[bunkNumber] = {
counselorsOnDuty: [],
nightBunkDuty: [],
};

// choosing 4 employees
const employeesInBunk = employees.filter((e) => e.bunk === bunkNumber);
const PER_SPLIT = Math.ceil(employeesInBunk.length / 2);

if (employeesInBunk.length === 0) continue;
let selected: StaffAttendeeID[] = []

for (const employee of employeesInBunk) {

if (employee.daysOff.includes(this.toISO(currDate))) {
continue;
}
const prevDate = currDate.clone().subtract(1, 'day');
const nextDate = currDate.clone().add(1, 'day');

const hasAdjacentDayOff =
employee.daysOff.includes(this.toISO(prevDate)) ||
employee.daysOff.includes(this.toISO(nextDate));

if (!hasAdjacentDayOff && shift[bunkNumber].counselorsOnDuty.length + 1 < PER_SPLIT ) {
shift[bunkNumber].counselorsOnDuty.push(employee.id);
selected.push(employee);
}

else {
shift[bunkNumber].nightBunkDuty.push(employee.id);
selected.push(employee);
}
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

Fix night‑shift split and make selected const.

length + 1 < PER_SPLIT under‑allocates on‑duty by one, and selected is never reassigned (lint failure). If the intent is “pick 4 employees,” add a cap or update the comment.

✅ Proposed fix
-        let selected: StaffAttendeeID[] = []
+        const selected: StaffAttendeeID[] = []
@@
-          if (!hasAdjacentDayOff && shift[bunkNumber].counselorsOnDuty.length + 1 < PER_SPLIT ) {
+          if (!hasAdjacentDayOff && shift[bunkNumber].counselorsOnDuty.length < PER_SPLIT) {
             shift[bunkNumber].counselorsOnDuty.push(employee.id);
             selected.push(employee);
           }
🧰 Tools
🪛 ESLint

[error] 205-205: 'selected' is never reassigned. Use 'const' instead.

(prefer-const)

🪛 GitHub Actions: Lint & Build on PR

[error] 205-205: 'selected' is never reassigned. Use 'const' instead. prefer-const

🤖 Prompt for AI Agents
In `@src/features/scheduling/generation/SessionScheduler.ts` around lines 200 -
227, The on‑duty split logic under‑allocates because it uses "length + 1 <
PER_SPLIT" and `selected` is never reassigned; change `selected` to a const
array and fix the condition to compare the current count directly (e.g., use
"shift[bunkNumber].counselorsOnDuty.length < PER_SPLIT") or explicitly enforce a
cap (introduce a MAX_ON_DUTY constant if the intent is to "pick 4 employees")
before pushing into shift[bunkNumber].counselorsOnDuty; ensure fallback pushes
go to shift[bunkNumber].nightBunkDuty, and keep all other checks (daysOff via
this.toISO(currDate), prevDate, nextDate) the same while referencing
employeesInBunk, PER_SPLIT, selected, and the shift[...] arrays to locate the
change.

}

}
// Save the shift object
this.nightShifts.push(shift);
currDate.add(1, "day");
}
return this;




assignNightShifts(): SessionScheduler { return this; }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

getSchedule(): { nightShifts: NightShiftID[] } { return { nightShifts: this.nightShifts }; }
}
Loading
Loading