Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
52 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
4d6adc6
Merge branch 'main' into feature/session-wide-algorithm
nkanchinadam Jun 11, 2026
6e89704
setup
nkanchinadam Jun 11, 2026
c60587a
delete test file
nkanchinadam Jun 12, 2026
15b2745
change date strings to Moment type
nkanchinadam Jun 12, 2026
4de56f0
outline algorithm
nkanchinadam Jun 12, 2026
e16c74c
create CounselorAttendee type
nkanchinadam Jun 12, 2026
8878335
determine days off in session
nkanchinadam Jun 12, 2026
dac6a7e
create counselor data structures
nkanchinadam Jun 12, 2026
c499a7b
assign days off
nkanchinadam Jun 12, 2026
5c6357a
add error case & clean up
nkanchinadam Jun 12, 2026
8f4b407
Merge branch 'main' into feature/session-wide-algorithm
nkanchinadam Jun 14, 2026
55c8325
rename
nkanchinadam Jun 14, 2026
320051d
clean up dead code
nkanchinadam Jun 14, 2026
ae15a0a
fix lint & compilation issues
nkanchinadam Jun 14, 2026
475e66f
fix emulator data
nkanchinadam Jun 14, 2026
770f144
Merge branch 'main' into feature/session-wide-algorithm
nkanchinadam Jun 22, 2026
714dc44
remove TypeScript directive
nkanchinadam Jun 22, 2026
7a0067c
add todo
nkanchinadam Jun 22, 2026
81d995f
add another todo
nkanchinadam Jun 22, 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.

283 changes: 279 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,283 @@ export class SessionScheduler {

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

assignDaysOff(): SessionScheduler { return this; }

assignNightShifts(): 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 getDateRange(startDate: Moment, endDate: Moment) {
let fromDate = startDate
let toDate = endDate
let diff = toDate.diff(fromDate, "day")
let range = []
for (let i = 0; i < diff; i++) {
range.push(moment(startDate).add(i, "day"))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return range
}

private isDateEligible(date: Moment, eligibleDates: Set<Moment>): boolean {
for (const eligibleDate of eligibleDates) {
if (eligibleDate.isSame(date, 'day')) {
return true;
}
}
return false;
}

//Convert moment string to iso string
private toISODate(date: Moment): string {
return date.format("YYYY-MM-DD");
}

// Helper function to check if a date is a Jamboree day
private isJamboreeDay(date: Moment, sections: SectionID[]): boolean {
const dateStr = this.toISODate(date);
return sections.some(section => {
if (!('type' in section)) {
return false;
}
const sectionStart = moment(section.startDate);
const sectionEnd = moment(section.endDate).subtract(1, 'day');
return (
(section.type === 'BUNK-JAMBO' || section.type === 'NON-BUNK-JAMBO') &&
date.isSameOrAfter(sectionStart) &&
date.isSameOrBefore(sectionEnd)
);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}



private assignDaysOffForWeek(
weekStart: Moment,
weekEnd: Moment,
eligibleDates: Set<Moment>,
employees: (StaffAttendeeID | AdminAttendeeID)[],
employeesNeedingDayOff: Set<StaffAttendeeID | AdminAttendeeID>,
maxStaffOffPerDay: number
): void {
// Get all eligible dates in this week
const datesInWeek: Moment[] = [];
const currentDate = weekStart.clone();

// Add all the dates that are in this week to the list
while (currentDate.isSameOrBefore(weekEnd)) {
if (this.isDateEligible(currentDate, eligibleDates)) {
datesInWeek.push(currentDate.clone());
}
currentDate.add(1, 'day');
}

if (datesInWeek.length === 0) return; // invalid start and end date

// Track days off assigned per day
const daysOffCount = new Map<string, number>();
datesInWeek.forEach(date => daysOffCount.set(this.toISODate(date), 0));

// Separate Program Counselors and others
const programCounselors = Array.from(employeesNeedingDayOff).filter(e =>
'programCounselor' in e && e.programCounselor
);
const otherEmployees = Array.from(employeesNeedingDayOff).filter(e =>
!('programCounselor' in e && e.programCounselor)
);

// Assign Program Counselors first, prioritizing Jamboree days
for (const employee of this.shuffleArray(programCounselors)) {
const jamboreeeDates = datesInWeek.filter(date =>
this.isJamboreeDay(date, this.sections) // get all jamboree dates for this section in the week
);
const preferredDates = jamboreeeDates.length > 0 ? jamboreeeDates : datesInWeek;

// Assign day off
if (this.assignDayOffToEmployee(employee, preferredDates, daysOffCount, maxStaffOffPerDay, employees)) {
employeesNeedingDayOff.delete(employee);
}
}

// Assign other employees
for (const employee of this.shuffleArray(otherEmployees)) {
if (this.assignDayOffToEmployee(employee, datesInWeek, daysOffCount, maxStaffOffPerDay, employees)) {
employeesNeedingDayOff.delete(employee);
}
}
}

private assignDayOffToEmployee(
employee: StaffAttendeeID | AdminAttendeeID,
availableDates: Moment[],
daysOffCount: Map<string, number>,
maxStaffOffPerDay: number,
allEmployees: (StaffAttendeeID | AdminAttendeeID)[]
): boolean {


// gets all the dates from available dates that dont have the max staff off per day.
const unfilledDates = availableDates.filter((date) => {
const dateStr = this.toISODate(date);
const currentCount = daysOffCount.get(dateStr) || 0;
return currentCount < maxStaffOffPerDay && !employee.daysOff.includes(dateStr);
})

// sort the unfilledDates by unfilledDates[0] being the date where the most number of available employees in a bunk
// need a day off

unfilledDates.sort((date1: Moment, date2: Moment) => {
const dateStrA = this.toISODate(date1);
const dateStrB = this.toISODate(date2);
const countA = daysOffCount.get(dateStrA) || 0;
const countB = daysOffCount.get(dateStrB) || 0;

// sorts the array so that the dates with the least amoutnt of employees off appear first.
if (countA !== countB) {
return countA - countB;
}

// for equal counts of employees off on two dates, sort by the bunks with the least amount of employees off
else {
let bunk = null;
if ('bunk' in employee) {
bunk = employee.bunk
}
if (bunk) {
// gets the number of people off on dateStrA in the employee bunk
const numOffBunkA = allEmployees.filter(e =>
'bunk' in e && e.bunk === bunk && e.daysOff.includes(dateStrA)
).length;
// gets the number of people off on dateStrB in the employee bunk
const numBunkOffB = allEmployees.filter(e =>
'bunk' in e && e.bunk === bunk && e.daysOff.includes(dateStrB)
).length;
// If dateA already has an employee off in the same bunk, put dateB before dateA. Otherwise,
// put dateA before dateB.
if (numOffBunkA > numBunkOffB) return 1;
if (numOffBunkA < numBunkOffB) return -1;
}

}
return 0; // equal dates, and both dates have someone off on that bunk.
})

// At the end of this sorting algorithm, unfilledDates will include all of the dates that are not
// filled with the max number of staff with days off allowed in the session, and will be sorted on
// two keys:
// 1. Dates with less employees off on that day will appear first
// 2. Dates with less bunk members off on that day will appear first

if (unfilledDates.length === 0) return false; // if no dates are avaiable due to our criteria.

const selectedDate = unfilledDates[0]; // first element will have most optimal day off to assign
const dateStr = this.toISODate(selectedDate);

employee.daysOff.push(dateStr); // assign date to employee
daysOffCount.set(dateStr, (daysOffCount.get(dateStr) || 0) + 1);

return true;


}


assignDaysOff(session: SessionID, employees: (StaffAttendeeID | AdminAttendeeID)[]): SessionScheduler {
const start = moment(session.startDate); // ISO 8601 string
const end = moment(session.endDate).subtract(1, 'day');

const NUM_WEEKS = Math.ceil(end.diff(start, "week", true));
const NUM_DAYS = (end.diff(start, "day")) + 1
const MAX_STAFF_OFF_PER_DAY = Math.ceil(employees.length / NUM_DAYS);
const employeesNeedingDayOff = new Set(employees);

let currDate = start;
// Need a future implementation for this date range to only include dates that allow for a day off.
const sessionDateRange: Set<Moment> = new Set(this.getDateRange(start, end))

for (let i = 0; i < NUM_WEEKS; i++) {
if (currDate.clone().add(1, "week").isBefore(end)) {
this.assignDaysOffForWeek(currDate, currDate.clone().add(6, "day"), sessionDateRange, employees, employeesNeedingDayOff, MAX_STAFF_OFF_PER_DAY)
currDate.add(6, "day")
}
else {
this.assignDaysOffForWeek(currDate, end, sessionDateRange, employees, employeesNeedingDayOff, MAX_STAFF_OFF_PER_DAY)
currDate = end
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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);
}



let currDate = startDate;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Create a night bunk object for each night
while (currDate.isSameOrBefore(endDate)) {
const shift: NightShiftID = {
sessionId: session.id,
id: `night-${this.toISODate(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);
if (employeesInBunk.length === 0) continue;
const shuffled = this.shuffleArray(employeesInBunk);
const selected = shuffled.slice(0, 4);

for (const employee of selected) {
const prevDate = currDate.clone().subtract(1, 'day');
const nextDate = currDate.clone().add(1, 'day');

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

// If no adjacent day off, eligible for COD
// Otherwise, assign to NBD
if (!hasAdjacentDayOff) {
shift[bunkNumber].counselorsOnDuty.push(employee.id);
} else {
shift[bunkNumber].nightBunkDuty.push(employee.id);
}

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




}

getSchedule(): { nightShifts: NightShiftID[] } { return { nightShifts: this.nightShifts }; }
}
1 change: 1 addition & 0 deletions src/types/sessionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface CommonSection {
type: 'COMMON';
startDate: string; // ISO-8601
endDate: string; // ISO-8601, exclusive
hasDaysOff: boolean;
}
export interface CommonSectionID extends CommonSection, ID<string> { sessionId: string; };

Expand Down
2 changes: 1 addition & 1 deletion storage.rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if true;
allow read, write: if false;

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 | 🔴 Critical

🧩 Analysis chain

Critical: Verify that denying all storage access is intentional.

This change denies all read/write access to Firebase Storage, which will break any storage-dependent functionality in the application. This seems unrelated to the scheduling algorithm work described in the PR objectives.

If this is a temporary development/testing change, it should not be committed to the main branch. If intentional, please clarify the reasoning.

Consider reverting this change unless there's a specific reason to deny all storage access:

-      allow read, write: if false;
+      allow read, write: if true;

Or implement proper access control rules based on your security requirements instead of blanket denial.


I'll gather the necessary information to verify this review comment. Let me inspect the storage rules file and the context around this change.


🏁 Script executed:

#!/bin/bash
# Examine the storage.rules file to understand the full context
echo "=== Full storage.rules file ==="
cat storage.rules

echo -e "\n=== Git diff for storage.rules ==="
git diff storage.rules

echo -e "\n=== Git log for storage.rules (last 5 commits) ==="
git log --oneline -5 -- storage.rules

echo -e "\n=== Checking for any related documentation or comments ==="
rg -n "storage" --type-list | head -20

Length of output: 1056


🏁 Script executed:

#!/bin/bash
# Search for any references to storage rules or access control in the codebase
echo "=== References to Firebase Storage or access control ==="
rg -i "storage" -B2 -A2 --type=md --type=json --type=ts --type=js -g '!node_modules' | head -50

Length of output: 1891


Let me search for actual Firebase Storage usage in the codebase to understand if this rule change would genuinely break functionality.


🏁 Script executed:

#!/bin/bash
# Search for Firebase Storage method calls and operations
echo "=== Firebase Storage operations in application code ==="
rg -i "uploadBytes|download|getBytes|put|get\(|ref\(" -B2 -A2 --type=ts --type=js -g '!node_modules' | head -80

echo -e "\n=== Direct storage references ==="
rg "firebase\.storage|getStorage|ref\(|uploadFile|readFile" -B1 -A1 --type=ts --type=js -g '!node_modules'

echo -e "\n=== Check PR or commit context ==="
git show --stat 445ab62 | head -30

Length of output: 7799


🏁 Script executed:

#!/bin/bash
# Look for README or documentation about this change
echo "=== Checking README or docs ==="
find . -maxdepth 2 -name "README*" -o -name "CHANGELOG*" -o -name "*.md" | head -10 | xargs grep -l "storage" 2>/dev/null || echo "No matches found in docs"

echo -e "\n=== Checking for TODOs or FIXMEs related to storage ==="
rg "TODO|FIXME" -i --type=ts --type=js --type=rules -B1 -A1 -g '!node_modules' | grep -i storage -B2 -A2

Length of output: 130


Critical: Storage rules deny all access while application code actively uploads and downloads files.

Evidence shows the application uses Firebase Storage operations:

  • src/data/storage/fileOperations.ts implements uploadFile(), getFileURL(), and getFileURLs()
  • src/app/albums/[albumId]/AlbumPage.tsx calls uploadFiles() to store album images
  • src/components/FileUploadModal.tsx provides the UI for uploads

The current rule allow read, write: if false; will break all these operations. Revert this change and implement proper access control rules instead:

-      allow read, write: if false;
+      allow read, write: if request.auth != null;

Or use authentication-based rules as suggested by the commented code in the file referencing admin checks.

📝 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
allow read, write: if false;
allow read, write: if request.auth != null;
🤖 Prompt for AI Agents
In storage.rules around line 9, the rule "allow read, write: if false;" blocks
all storage operations and prevents the app from uploading/downloading files;
revert that blanket denial and replace it with authentication-based rules: allow
read/write when request.auth != null (or more specific checks) and, where
applicable, enforce ownership by matching request.auth.uid to a
metadata/filename or path segment (e.g., only allow writes to /users/{uid}/...
when request.auth.uid == uid) or check a custom admin claim for admin-only
actions; restore the previously commented admin-check logic if needed and ensure
public read-only content is explicitly allowed if required by the app.

}
}
}
1 change: 0 additions & 1 deletion test/emulatorData/auth_export/accounts.json

This file was deleted.

1 change: 0 additions & 1 deletion test/emulatorData/auth_export/config.json

This file was deleted.

16 changes: 0 additions & 16 deletions test/emulatorData/firebase-export-metadata.json

This file was deleted.

7 changes: 0 additions & 7 deletions test/emulatorData/storage_export/buckets.json

This file was deleted.

Loading