From 461aca2187cb7388ae8a1c5094271b7554b22675 Mon Sep 17 00:00:00 2001 From: Ryan Chen <103216376+ryanyychen@users.noreply.github.com> Date: Fri, 20 Dec 2024 23:15:00 -0500 Subject: [PATCH] Interview scheduler bug fixes and features update (#178) * basic frontend stuff complete :3. Finished the general ui * first attempt, implementing inductee view of their schedule backend only * Push some frontend changes * update * Finish all availabilities display page * Added backend for getting all availabilities, not single yet * Update styling * some post hopefully * Update all availabilities page * Add utils file for interview-related pages * Update sidebar navigation * Start edit own availability from scratch * Update response type to const * Update app url pathing * Map correct url to schedule overview page * Move utils file * inducteeviewset post delete * Update app routing * Update user views * Implement interview get api backends * Update overall interview schedule * Skeleton for individual interview schedule * function for editschedule * addavailability function * Complete edit schedule page * Edit InductionClassViewSet * Update interview related functions * Complete initial interview schedule overview * Finish up interview system * Delete .venv * Add comments on functions and files * Fix clicking on timeslot does not update availability * Add functionality to see individual inductee availabilities * Remove TODO comment --------- Co-authored-by: jyeh2 Co-authored-by: niannianwang --- frontend/src/Pages/EditSchedule.svelte | 4 + frontend/src/Pages/InterviewSchedule.svelte | 170 +++++++++++++++++-- frontend/src/Pages/interviewscheduleutils.js | 1 + myapp/api/views/user_views.py | 23 +++ 4 files changed, 183 insertions(+), 15 deletions(-) diff --git a/frontend/src/Pages/EditSchedule.svelte b/frontend/src/Pages/EditSchedule.svelte index 06848f9d..66b8caac 100644 --- a/frontend/src/Pages/EditSchedule.svelte +++ b/frontend/src/Pages/EditSchedule.svelte @@ -30,12 +30,16 @@ * Changes 'available' attribute of timeslot to true if available, false if unavailable */ timeslot.addEventListener('click', (event) => { + let day = timeslot.id.split('-')[0]; + let slotNum = timeslot.id.split('-')[1]; if (event.target.getAttribute('available') == 'false') { event.target.setAttribute('available', true); event.target.style.background = AVAILABLE_COLOR; + availability[day][slotNum] = 1; } else if (event.target.getAttribute('available') == 'true') { event.target.setAttribute('available', false); event.target.style.background = UNAVAILABLE_COLOR; + availability[day][slotNum] = 0; } }); diff --git a/frontend/src/Pages/InterviewSchedule.svelte b/frontend/src/Pages/InterviewSchedule.svelte index 0b12c919..6cadb179 100644 --- a/frontend/src/Pages/InterviewSchedule.svelte +++ b/frontend/src/Pages/InterviewSchedule.svelte @@ -4,21 +4,28 @@ import { onMount } from "svelte"; import { generateSchedule, UNAVAILABLE_COLOR, AVAILABLE_COLOR, SELECTED_COLOR, NUM_DAYS, NUM_SLOTS } from "./interviewscheduleutils.js" - let availabilities = null; + let availabilities; + let inductee_availabilities = {}; + let inductees; let selected_slot = null; + let loaded = false; onMount(async () => { // Retrieve availabilities of all inductees and officers from backend await getAvailabilities(); + await getInducteeAvailabilities(); // Generate table for schedule - generateSchedule(); + loaded = generateSchedule(); // Populate the schedule according to availabilities retrieved if (availabilities != null) { populateSchedule(); } + document.getElementById('slot_availability').style.display = 'flex'; + document.getElementById('schedule').style.display = 'flex'; + /* * Add event listener to document to manage clicks on timeslots * If a timeslot is clicked, lock 'avaiilability' display to that timeslot @@ -95,6 +102,30 @@ } } + /** + * Make an api call to the backend to retrieve all inductee availabilities + * Format of inductee_availabilities: dictionary of user_id: availability + */ + async function getInducteeAvailabilities() { + const response = await fetch(`api/inductionclasses/inductee_availabilities/`); + let list; + if (response.ok) { + list = await response.json(); + } else { + list = null; + } + + if (list != null) { + inductees = []; + for (let user_id in list) { + inductees.push([user_id, list[user_id][0]]); + inductee_availabilities[user_id] = list[user_id][1]; + } + } else { + inductee_availabilities = null; + } + } + /* * Sets the availability display to show the inductees and officers available at the selected timeslot */ @@ -106,7 +137,7 @@ let inductees; let officers; try { - inductees = availabilities[day][slot]['inductees']; + inductees = availabilities[day][slot]['inductees'].filter(inductee => inductee == inductee_option[1] || inductee_option == "all"); officers = availabilities[day][slot]['officers']; } catch { return; @@ -199,6 +230,98 @@ } } } + + /* + * Populate the schedule with individual inductee's availabilities + * Attach mouseover and mouseout events on slots with availabilties + * Mouseover event displays inductees and officers available at that timeslot in the availability display + * Set on click event to only display the selected inductee + */ + function populateInducteeSchedule(inductee_availability) { + for (let day = 0; day < NUM_DAYS; day++) { + for (let slotNum = 0; slotNum < NUM_SLOTS; slotNum++) { + let timeslot = document.getElementById(`${day}-${slotNum}`); + + // Make timeslot colored if an inductee has availability at that time + if (inductee_availability[day][slotNum] == 1) { + timeslot.style.background = AVAILABLE_COLOR; + timeslot.setAttribute('available', true); + } + + // Add mouseover event listener to display inductees and officers at timeslot + timeslot.addEventListener('mouseover', function() { + const P_STYLE = "margin: 1px 0px 1px 0px;"; + if (selected_slot != null) { + return; + } + clearAvailabilityDisplay(); + + // Populate availability display with inductees available at that time + let inductees = availabilities[day][slotNum]['inductees'].filter(inductee => inductee == inductee_option[1]); + let available_inductees = document.getElementById('available_inductees'); + inductees.forEach(inductee => { + let name = document.createElement('p'); + name.innerText = inductee; + name.style = P_STYLE; + available_inductees.appendChild(name); + }); + + // Populate availability display with officers available at that time + let officers = availabilities[day][slotNum]['officers']; + let available_officers = document.getElementById('available_officers'); + officers.forEach(officer => { + let name = document.createElement('p'); + name.innerText = officer; + name.style = P_STYLE; + available_officers.appendChild(name); + }) + }); + + // Add mouseout event listener to clear availability display + timeslot.addEventListener('mouseleave', function() { + if (selected_slot != null) { + return; + } + clearAvailabilityDisplay(); + }); + } + } + } + + /** + * Clear schedule + */ + function clear_schedule() { + for (let day = 0; day < NUM_DAYS; day++) { + for (let slotNum = 0; slotNum < NUM_SLOTS; slotNum++) { + let timeslot = document.getElementById(`${day}-${slotNum}`); + timeslot.style.background = UNAVAILABLE_COLOR; + timeslot.setAttribute('available', false); + } + } + } + + + let inductee_option; + + /* + * Filter out the selected inductee's availabilities + */ + function filter() { + if (inductee_availabilities[inductee_option[0]] != null) { + clear_schedule(); + populateInducteeSchedule(inductee_availabilities[inductee_option[0]]); + } else { + clear_schedule(); + populateSchedule(); + } + } + + // Filter the data when schedule if loaded and any inductee is selected from dropdown + $: { + inductee_option; + if (loaded && inductee_availabilities) filter(); + } @@ -211,36 +334,53 @@

Overall Schedule

-
-
-

Available

-

Inductees:

-
-

Officers:

-
+
+ {#if inductees} +
+
+ +
+
+ {:else} +

Loading

+ {/if} +
+
+
+

Available

+

Inductees:

+
+

Officers:

+
+
-
\ No newline at end of file diff --git a/frontend/src/Pages/interviewscheduleutils.js b/frontend/src/Pages/interviewscheduleutils.js index 2ed5199b..1c1af382 100644 --- a/frontend/src/Pages/interviewscheduleutils.js +++ b/frontend/src/Pages/interviewscheduleutils.js @@ -113,4 +113,5 @@ export function generateSchedule() { dayCol.appendChild(timeslot); } } + return true; } \ No newline at end of file diff --git a/myapp/api/views/user_views.py b/myapp/api/views/user_views.py index 14c708d0..0f4368e8 100644 --- a/myapp/api/views/user_views.py +++ b/myapp/api/views/user_views.py @@ -280,6 +280,29 @@ def list_all_availabilities(self, request, pk=None): elif (user_type == 'officer'): overall_availability[i][j]['officers'].append(name) return Response(overall_availability, status=status.HTTP_200_OK) + + @action(detail=False, methods=['GET'], url_path='inductee_availabilities') + def get_inductee_availabilities(self, request, pk=None): + ''' + Retrieve all inductees who filled out availabilities for a specific induction class. + ''' + # Find current induction class + induction_classes = InductionClass.objects.all() + curr_induction_class = None + for induction_class in induction_classes: + if (datetime.now().date() > induction_class.start_date and datetime.now().date() < induction_class.end_date): + curr_induction_class = induction_class + + if (curr_induction_class == None): + return Response(status=status.HTTP_400_BAD_REQUEST) + + inductees = {} + for (user_id, availability) in curr_induction_class.availabilities.items(): + user = CustomUser.objects.get(user_id=user_id) + if user.groups.filter(name='inductee').exists(): + inductees[user_id] = [f'{user.preferred_name} {user.last_name}', availability] + + return Response(inductees, status=status.HTTP_200_OK) @action(detail=False, methods=['GET'], url_path='get_availability') def individual_availability(self, request, pk=None):