Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stevenh/availability #17

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
12 changes: 6 additions & 6 deletions src/features/shift/shiftApiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export const shiftsApiSlice = apiSlice.injectEndpoints({
const loaddedShifts = responseData.map((entity) => {
// console.log('[loaddedShifts] entity: ', entity)
entity.id = entity.id
if (!entity.timeWindowDisplay) {
entity.timeWindowDisplay =
formatMilitaryTime(entity.timeWindow.startTime) +
' - ' +
formatMilitaryTime(entity.timeWindow.endTime)
}
// if (!entity.timeWindowDisplay) {
// entity.timeWindowDisplay =
// formatMilitaryTime(entity.timeWindow.startTime) +
// ' - ' +
// formatMilitaryTime(entity.timeWindow.endTime)
// }
return entity
})
console.debug(loaddedShifts)
Expand Down
23 changes: 19 additions & 4 deletions src/features/userAssignment/tables/AvailableUsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { RootState } from '@/store/store'
import SortedTable from '@/components/shared/tables/SortedTable'
import { selectSelectedUserId, setSelectedUserId } from '../userAssignmentSlice'
import { selectCurrentHouse } from '@/features/auth/authSlice'
import { createListOfUsersForShift } from '@/sprintFiles/availabilityHelper'

type AvailableUsersTableProps = {
day: string
Expand Down Expand Up @@ -107,7 +108,7 @@ const AvailableUsersTable: React.FC<AvailableUsersTableProps> = ({

// define state variables
const authHouse = useSelector(selectCurrentHouse) as House
const [listOfUserIds, setLitsOfUserIds] = useState<EntityId[]>([])
const [listOfUserIds, setListOfUserIds] = useState<EntityId[]>([])
const [disableTable, setDisableTable] = useState(
selectedUserId ? true : false
)
Expand Down Expand Up @@ -157,10 +158,24 @@ const AvailableUsersTable: React.FC<AvailableUsersTableProps> = ({
useEffect(() => {
// would filter the userIds here, but not doing any filtering or sorting right now
if (isUsersDataSuccess && usersData) {
let userIds = usersData.ids
setLitsOfUserIds(userIds)
let days = []
if (day === 'All') {
days = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
]
} else {
days = [day]
}
let userIds = createListOfUsersForShift(shiftObject, usersData, days)
setListOfUserIds(userIds)
}
}, [isUsersDataSuccess, usersData])
}, [day, isUsersDataSuccess, shiftObject, usersData])

useEffect(() => {
if (selectedUserId) {
Expand Down
58 changes: 58 additions & 0 deletions src/pages/account/testing/ShiftAssignmentHelper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useGetUsersQuery } from "@/features/user/userApiSlice";
import { createListOfUsersForShift } from "@/sprintFiles/availabilityHelper";
import { useEffect } from "react";

const ShiftAssignmentHelper = () => {
const {
data: dataUsers,
isLoading,
isSuccess,
isError,
error,
} = useGetUsersQuery({})

const testFunction = () => {
let testShiftObject = {
id: "1",
name: "Clean the house",
shiftID: "4iWhGXXz65H0aPfaF3RJ",
description: "Clean house pls ggreg sucks",
possibleDays: ["Monday", "Tuesday", "Wednesday"],
timeWindow: {
startTime: "1930",
endTime: "2300"
},
timeWindowDisplay: "",
assignedDay: "",
assignedUser: "maybe populate this with user id",
hours: 1.5,
verificationBuffer: 1,
verification: false,
category: "hehe",
preferences: {
preferredBy: [""],
dislikedBy: [""]
}
}

let testDays = ["Sunday", "Monday"];
if (dataUsers !== undefined) {
console.log(createListOfUsersForShift(testShiftObject, dataUsers, testDays));
}

// console.log(dayjs('1900', 'HHmm').format('HHmm'))
// const num = 1900
// console.log(dayjs(num.toString(), 'HHmm'))
}

useEffect(() => {
testFunction();
}, [dataUsers])
return (
<div>
hello
</div>
)
}

export default ShiftAssignmentHelper
181 changes: 181 additions & 0 deletions src/sprintFiles/availabilityHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { Dictionary, EntityId, EntityState } from "@reduxjs/toolkit";
import {Shift, User} from "../types/schema";
import dayjs, { Dayjs } from "dayjs";
import { HOURS_REQUIRED, NEUTRAL_PREFERENCE, LIKED_PREFERENCE, DISLIKED_PREFERENCE } from "@/utils/constants";

// TODO: DOESN'T WORK IF SOMEONE IS ALREADY ASSIGNED DURING THEIR AVAILABILITY PERIOD TO A DIFFERENT SHIFT

/**
* Returns a list of user ids that are available to complete the shift
*
* @param shiftObject - The shift object
* @param days - The days that we iterate through
* @param entityState - the entity state from redux
* @returns A list of user ids that are available to complete the shift
*/
const filterUsersByAvailability = (shiftObject: Shift, days: string[], entityState: EntityState<User>) => {
const numHours = Math.floor(shiftObject.hours);
const numMinutes = (shiftObject.hours - numHours) * 60;
const shiftStart: Dayjs = dayjs(shiftObject.timeWindow.startTime, 'HHmm')
const shiftEnd: Dayjs = dayjs(shiftObject.timeWindow.endTime, 'HHmm')
const shiftID = shiftObject.shiftID;
let totalUsersInHouse: EntityId[] = entityState.ids;
let availableUserIDs: EntityId[] = [];
let idToObjectDictionary: Dictionary<User> = entityState.entities;
// iterate thru all users in the house
for (let i = 0; i < totalUsersInHouse.length; i++) {
const userID = totalUsersInHouse[i];
const userObject = idToObjectDictionary[userID];
if (userObject === undefined) {
continue;
}
// if this user has already been assigned to this shift, display them regardless of hours
if (userObject.assignedScheduledShifts !== undefined && userObject.assignedScheduledShifts.includes(shiftID)) {
availableUserIDs.push(userID)
continue;
}
const currAvailabilities: { [key: string]: { startTime: string; endTime: string }[] } = userObject.availabilities;
if (currAvailabilities === undefined) {
continue;
}
// flag to determine if user id has been added to the list of available user ids already so that we only add a user id to the list once
let isAdded = false;
// iterate thru all days for a user
for (let j = 0; j < days.length; j++) {
const day = days[j];
if (!(day.toLowerCase() in currAvailabilities)) {
continue;
}

const perDayAvailability = currAvailabilities[day.toLowerCase()];
if (perDayAvailability === undefined) {
continue;
}
// iterate thru all availabilities per day for a user
// TODO: CHECK IF THEYRE ASSIGNED TO A DIFFERENT SHIFT DURING THE AVAILABILITY WINDOW
for (let k = 0; k < perDayAvailability.length; k++) {
const currInterval = perDayAvailability[k];
// start of the user availability window
let userStart: Dayjs = dayjs("" + currInterval.startTime, 'HHmm')
// end of the user availability window
let userEnd: Dayjs = dayjs("" + currInterval.endTime, 'HHmm')
// if user's availability window ends before the start of the shift, can continue
if (userEnd.isBefore(shiftStart) || userEnd.isSame(shiftStart)) {
continue;
}
// calculatedStart = either the start of the shift or the start of the user's availability, whichever comes later

let calculatedStart: Dayjs = shiftStart;
if (userStart.isAfter(shiftStart)) {
calculatedStart = userStart;
}
// add the number of hours required to complete to the calculatedStart
const calculatedEnd = calculatedStart.add(numHours, 'hour').add(numMinutes, 'minute');
// calculatedEndRequirement = either the end of the shift or the end of the user's availability, whichever comes earlier
let calculatedEndRequirement: Dayjs = shiftEnd;
if (userEnd.isBefore(shiftEnd)) {
calculatedEndRequirement = userEnd;
}
// only add the user id if the calculated end of when they would finish the shift is before the end of the shift or the end of their availability, whichever comes first
if (calculatedEnd.isBefore(calculatedEndRequirement) || calculatedEnd.isSame(calculatedEndRequirement)) {
availableUserIDs.push(userID);
isAdded = true;
break;
}
}
if (isAdded) {
break;
}
}
}
return availableUserIDs;
}

/**
* Returns a list of user ids that are eligible to complete the shift (hours unassigned >= credit hours/already assigned to the shift)
*
* @param ids - A list of user entity ids
* @param entityState - the entity state from redux
* @param creditHours - the credit hours for the shift
* @param shiftID - the id of the shift
* @returns A list of user ids that are eligible to complete the shift
*/
const filterUsersByEligibility = (ids: EntityId[], entityState: EntityState<User>, creditHours: number, shiftID: string) => {
const eligibleIDs: EntityId[] = [];
const idToObjectDictionary: Dictionary<User> = entityState.entities;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const userObject = idToObjectDictionary[id];
if (userObject === undefined) {
continue;
}
// put the assigned user in eligibleIDs, regardless
if (userObject.assignedScheduledShifts !== undefined && userObject.assignedScheduledShifts.includes(shiftID)) {
eligibleIDs.push(id)
continue;
}
// push them on the list if the hours they need >= creditHours
const assignableHours: number = HOURS_REQUIRED - userObject.hoursAssigned;
if (assignableHours >= creditHours) {
eligibleIDs.push(id);
}
}
return eligibleIDs;
}

/**
* Returns a list of user ids that are sorted, first on whether they prefer the shift and second as a tiebreaker on the number of unassigned hours (higher is higher priority)
*
* @param shiftObject - The shift object
* @param ids - The list of entity ids
* @param entityState - the entity state from redux
* @returns A sorted list of the entity ids that are passed in
*/
const sortUsersByNeededHoursAndPreference = (ids: EntityId[], entityState: EntityState<User>, shiftObject: Shift) => {
const idToObjectDictionary: Dictionary<User> = entityState.entities;
const likedList = shiftObject.preferences.preferredBy;
const dislikedList = shiftObject.preferences.dislikedBy;
const sorted = ids.sort((id1, id2) => {
const user1 = idToObjectDictionary[id1];
const user2 = idToObjectDictionary[id2];
if (user1 === undefined || user2 === undefined) {
return 0;
}
// First sort on preferences
let user1Preference = NEUTRAL_PREFERENCE;
if (likedList.includes("" + id1)) {;
user1Preference = LIKED_PREFERENCE;
} else if (dislikedList.includes("" + id1)) {
user1Preference = DISLIKED_PREFERENCE;
}
let user2Preference = NEUTRAL_PREFERENCE;
if (likedList.includes("" + id2)) {
user2Preference = LIKED_PREFERENCE;
} else if (dislikedList.includes("" + id2)) {
user2Preference = DISLIKED_PREFERENCE;
}
if (user2Preference - user1Preference != 0) {
return user2Preference - user1Preference;
}
// Second sort on hours assignable leftr, prioritizing people with higher preferences (user2 - user1)
const user1HoursLeft = HOURS_REQUIRED - user1.hoursAssigned
const user2HoursLeft = HOURS_REQUIRED - user2.hoursAssigned
return user2HoursLeft - user1HoursLeft;
})
return sorted;
}

/**
* Returns a list of user ids that are available and eligible to complete the shift; returns it in sorted order
*
* @param shiftObject - The shift object
* @param days - The days that we iterate through
* @param entityState - the entity state from redux
* @returns A list of user ids that are available and eligible to complete the shift; returns it in sorted order
*/
export const createListOfUsersForShift = (shiftObject: Shift, entityState: EntityState<User>, days: string[]) => {
const availableIDs = filterUsersByAvailability(shiftObject, days, entityState);
const availableAndEligibleIDs = filterUsersByEligibility(availableIDs, entityState, shiftObject.hours, shiftObject.shiftID);
const sortedAvailableAndEligibleIDs = sortUsersByNeededHoursAndPreference(availableAndEligibleIDs, entityState, shiftObject);
return sortedAvailableAndEligibleIDs;
}
2 changes: 1 addition & 1 deletion src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export type Shift = {
// Possible days that the shift can be done on
possibleDays: string[]
// Time window that this shift must be done in [startTime, endTime]
timeWindow: { startTime: number; endTime: number }
timeWindow: { startTime: string; endTime: string }
// property to display timeWindow
timeWindowDisplay: string //Todo: Maybe delete this property
// Day that the shift is assigned
Expand Down
7 changes: 6 additions & 1 deletion src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,9 @@ export const DAYS = [
'thursday',
'friday',
'saturday',
]
]

export const HOURS_REQUIRED = 5;
export const NEUTRAL_PREFERENCE = 1;
export const LIKED_PREFERENCE = 2;
export const DISLIKED_PREFERENCE = 0;