diff --git a/csm_web/frontend/src/components/section/Section.tsx b/csm_web/frontend/src/components/section/Section.tsx index 3e6ced65..6c31205d 100644 --- a/csm_web/frontend/src/components/section/Section.tsx +++ b/csm_web/frontend/src/components/section/Section.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { NavLink, useParams } from "react-router-dom"; import StudentSection from "./StudentSection"; import MentorSection from "./MentorSection"; diff --git a/csm_web/frontend/src/components/section/StudentSection.tsx b/csm_web/frontend/src/components/section/StudentSection.tsx index 09229c32..46dbf9f6 100644 --- a/csm_web/frontend/src/components/section/StudentSection.tsx +++ b/csm_web/frontend/src/components/section/StudentSection.tsx @@ -8,6 +8,7 @@ import { import { Mentor, Override, Spacetime } from "../../utils/types"; import Modal from "../Modal"; import { ATTENDANCE_LABELS, InfoCard, ROLES, SectionDetail, SectionSpacetime } from "./Section"; +import { SwapSectionModal } from "./SwapSectionModal"; import { dateSortWord, formatDateISOToWord } from "./utils"; import XIcon from "../../../static/frontend/img/x.svg"; @@ -52,6 +53,7 @@ export default function StudentSection({ index element={

My Section

@@ -92,12 +95,40 @@ function StudentSectionInfo({ mentor, spacetimes, override, associatedProfileId override={override} /> ))} + ); } +interface SwapSectionProps { + sectionId: number; +} + +enum SwapSectionStage { + INITIAL = "INITIAL", + DISPLAY = "DISPLAY" +} + +export function SwapSection({ sectionId }: SwapSectionProps) { + const [stage, setStage] = useState(SwapSectionStage.INITIAL); + + switch (stage) { + case SwapSectionStage.INITIAL: + return ( + +
Swap Section
+ +
+ ); + case SwapSectionStage.DISPLAY: + return setStage(SwapSectionStage.INITIAL)} />; + } +} + interface DropSectionProps { profileId: number; } diff --git a/csm_web/frontend/src/components/section/SwapSectionModal.tsx b/csm_web/frontend/src/components/section/SwapSectionModal.tsx new file mode 100644 index 00000000..99e2fdea --- /dev/null +++ b/csm_web/frontend/src/components/section/SwapSectionModal.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import Modal from "../Modal"; +import { useUserEmails } from "../../utils/queries/base"; +import { + useSwapRequestQuery, + useSwapPostMutation, + useReceivedSwapRequestQuery, + useAllSwapRequestQuery +} from "../../utils/queries/sections"; + +interface SwapSectionModalProps { + sectionId: number; + closeModal: () => void; +} + +export const SwapSectionModal = ({ sectionId, closeModal }: SwapSectionModalProps) => { + // Need API endpoints + + return ( + + + + + ); +}; + +export const SwapRequestDashboard = ({ sectionId }: SwapRequestDashBoardProps) => { + const [myRequest, setMyRequest] = useState([]); + const [sendRequest, setSendRequest] = useState([]); + const requests = useAllSwapRequestQuery(sectionId, studentId).data; + const send_requests = useReceivedSwapRequestQuery(sectionId, studentId).data; + useEffect(() => { + // How do i get student id? + }, []); + + return ( +
+
+
My Swap Requests
+
    + {/* {myRequest.map(request => ( +
  • + {request.sender.user.first_name} {request.sender.user.last_name} (id: {request.sender.id}) +
  • + ))} */} +
+
+
+
Receive Swap Requests
+
    + {/* {myRequest.map(request => ( +
  • + {request.sender.user.first_name} {request.sender.user.last_name} (id: {request.sender.id}) +
  • + ))} */} +
+
+
+ ); +}; +interface SwapRequestDashBoardProps { + sectionId: number; +} + +interface SwapRequestFormProps { + sectionId: number; + email: string; +} + +export const SwapRequestForm = ({ sectionId }: SwapRequestFormProps) => { + const { data: userEmails, isSuccess: userEmailsLoaded } = useUserEmails(); + // const swapSectionMutation = useSwapRequestMutation(sectionId); can be changed, not too sure + const [email, setEmail] = useState(); + const body = { sectionId: sectionId, email: email }; + const studentDropMutation = useSwapPostMutation(sectionId, email); + + const handleSubmit = (event: React.FormEvent): void => { + event.preventDefault(); + }; + + return ( +
+
+ + +
+
+ ); +}; diff --git a/csm_web/frontend/src/utils/queries/sections.tsx b/csm_web/frontend/src/utils/queries/sections.tsx index a4bbcf8c..9737707f 100644 --- a/csm_web/frontend/src/utils/queries/sections.tsx +++ b/csm_web/frontend/src/utils/queries/sections.tsx @@ -274,11 +274,136 @@ export const useStudentSubmitWordOfTheDayMutation = ( return mutationResult; }; +export interface SectionSwapMutationBody { + email: string; +} + +/** + * Hook to request a swap with another student + * Wait, what is this for? I wrote another one for posting a new request below this. -Kevin + */ +export const useSwapRequestQuery = ( + sectionId: number +): UseMutationResult => { + const queryClient = useQueryClient(); + const queryResult = useMutation( + async (body: SectionSwapMutationBody) => { + const response = await fetchWithMethod(`sections//swap`, HTTP_METHODS.GET, body); + if (response.ok) { + return response.json(); + } else { + handlePermissionsError(response.status); + throw new ServerError(`Failed to load swap`); + } + }, + { + retry: handleRetry, + onSuccess: () => { + queryClient.invalidateQueries(["section_swap", sectionId]); + } + } + ); + + handleError(queryResult); + return queryResult; +}; + +/* + *Posting new queries + */ + +export const useSwapPostMutation = ( + sectionId: number, + email: string +): UseMutationResult => { + const queryClient = useQueryClient(); + const queryResult = useMutation( + async (body: SectionSwapMutationBody) => { + const response = await fetchWithMethod(`sections/${sectionId}}/swap`, HTTP_METHODS.POST, body); + if (response.ok) { + return response.json(); + } else { + handlePermissionsError(response.status); + throw new ServerError(`Failed to load swap`); + } + }, + { + retry: handleRetry, + onSuccess: () => { + queryClient.invalidateQueries(["section_swap", sectionId]); + } + } + ); + + handleError(queryResult); + return queryResult; +}; + export interface StudentDropMutationBody { banned: boolean; blacklisted: boolean; } +/** + * Get all swap requests where the user is the receiver + */ +export const useAllSwapRequestQuery = ( + sectionId: number, + studentId: number +): UseMutationResult => { + const queryClient = useQueryClient(); + const queryResult = useMutation( + async (body: SectionSwapMutationBody) => { + const response = await fetchWithMethod(`sections/${sectionId}/my_swap`, HTTP_METHODS.GET, body); + if (response.ok) { + return response.json(); + } else { + handlePermissionsError(response.status); + throw new ServerError(`Failed to load swap`); + } + }, + { + retry: handleRetry, + onSuccess: () => { + queryClient.invalidateQueries(["section_swap", sectionId]); + } + } + ); + + handleError(queryResult); + return queryResult; +}; + +/** + * Get all swap requests where the user is the receiver + */ +export const useReceivedSwapRequestQuery = ( + sectionId: number, + studentId: number +): UseMutationResult => { + const queryClient = useQueryClient(); + const queryResult = useMutation( + async (body: SectionSwapMutationBody) => { + const response = await fetchWithMethod(`sections/${sectionId}/my_swap`, HTTP_METHODS.GET, body); + if (response.ok) { + return response.json(); + } else { + handlePermissionsError(response.status); + throw new ServerError(`Failed to load swap`); + } + }, + { + retry: handleRetry, + onSuccess: () => { + queryClient.invalidateQueries(["section_swap", sectionId]); + } + } + ); + + handleError(queryResult); + return queryResult; +}; + /** * Hook to drop a student from their section. * diff --git a/csm_web/frontend/static/frontend/css/style.css b/csm_web/frontend/static/frontend/css/style.css index 5a557de3..1ddd18e6 100644 --- a/csm_web/frontend/static/frontend/css/style.css +++ b/csm_web/frontend/static/frontend/css/style.css @@ -879,6 +879,12 @@ main { margin: 0 0 20px; } +.section-detail-info-card.swap-section { + flex-basis: 100%; + display: flex; + text-align: center; +} + .section-detail-info-card.drop-section { flex-basis: 100%; display: flex; @@ -1303,6 +1309,92 @@ table tbody tr:last-child > td { margin-bottom: 40px; } +#section-swap { + width: 500px; + display: flex; + border: black; + border-style: solid; + border-width: 1px; + + flex-direction: column; + width: -moz-fit-content; + margin-left: auto; +} + +.view-swap-btn { + display: block; + border: none; + margin: auto; + cursor: pointer; + background-color: var(--csm-green); + border-radius: 6px; + color: white; + font-size: 14px; + padding: 10px 27px; +} + +.swap-display { + width: 750px; + height: 1000px; + text-align: center; +} + +#swap-request-form { + display: flex; + flex-direction: row; +} + +#swap-request-form-contents { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: auto; + max-width: max-content; +} + +#swap-request-form-contents > * { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.create-swap-request { + align-self: left; + flex-direction: column; + display: flex; + flex-direction: column; +} + +.swap-title { + text-align: center; + border: black; + border-style: solid; + border-width: 1px; + height: 20%; +} +.swap-dashboard { + display: flex; + width: 100%; + height: 80%; +} + +.my-request { + width: 50%; + border: black; + border-style: solid; + border-width: 1px; +} + +.received-request { + width: 50%; + border: black; + border-style: solid; + border-width: 1px; +} + #section-detail-sidebar { display: flex; flex-wrap: wrap; diff --git a/csm_web/scheduler/models.py b/csm_web/scheduler/models.py index 02c1dc02..6b181727 100644 --- a/csm_web/scheduler/models.py +++ b/csm_web/scheduler/models.py @@ -34,8 +34,8 @@ def week_bounds(date): return week_start, week_end class Swap(models.Model): - sender = models.ForeignKey(Student, on_delete=models.CASCADE) - receiver = models.ForeignKey(Student, on_delete=models.CASCADE) + sender = models.ForeignKey("Student", on_delete=models.CASCADE, related_name = "sender") + receiver = models.ForeignKey("Student", on_delete=models.CASCADE, related_name = "receiver") class User(AbstractUser): priority_enrollment = models.DateTimeField(null=True, blank=True) diff --git a/csm_web/scheduler/views/swap.py b/csm_web/scheduler/views/swap.py deleted file mode 100644 index 55999c0d..00000000 --- a/csm_web/scheduler/views/swap.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework.exceptions import PermissionDenied -from rest_framework.response import Response -from rest_framework import status -from rest_framework.decorators import api_view - -from .utils import viewset_with -from ..models import Swap, Student -from scheduler.serializers import UserSerializer - -