Skip to content
Merged

merge #535

Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion .babelrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-react", "@babel/preset-typescript"],
"plugins": ["transform-class-properties", "lodash", "@babel/plugin-transform-runtime", "dynamic-import-node"]
"plugins": ["transform-class-properties", "@babel/plugin-transform-runtime", "dynamic-import-node"]
}
6 changes: 5 additions & 1 deletion csm_web/frontend/src/components/course/Course.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):

let currDaySections = sections && sections[currDayGroup];
if (currDaySections && !showUnavailable) {
currDaySections = currDaySections.filter(({ numStudentsEnrolled, capacity }) => numStudentsEnrolled < capacity);
currDaySections = currDaySections.filter(
({ numStudentsEnrolled, capacity, numStudentsWaitlisted, waitlistCapacity }) =>
numStudentsEnrolled < capacity || numStudentsWaitlisted < waitlistCapacity
);
}

const enrollmentDate =
Expand Down Expand Up @@ -206,6 +209,7 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):
key={section.id}
userIsCoordinator={userIsCoordinator}
courseOpen={course.enrollmentOpen}
courseId={course.id}
{...section}
/>
))
Expand Down
122 changes: 107 additions & 15 deletions csm_web/frontend/src/components/course/SectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import React, { useState } from "react";
import { Link, Navigate } from "react-router-dom";

import { formatSpacetimeInterval } from "../../utils/datetime";
import { EnrollUserMutationResponse, useEnrollUserMutation } from "../../utils/queries/sections";
import { Mentor, Spacetime } from "../../utils/types";
import { useProfiles } from "../../utils/queries/base";
import {
EnrollUserMutationResponse,
useEnrollUserMutation,
useEnrollStudentToWaitlistMutation
} from "../../utils/queries/sections";
import { Mentor, Role, Spacetime } from "../../utils/types";
import Modal, { ModalCloser } from "../Modal";

import CheckCircle from "../../../static/frontend/img/check_circle.svg";
import ClockIcon from "../../../static/frontend/img/clock.svg";
import GroupIcon from "../../../static/frontend/img/group.svg";
import LocationIcon from "../../../static/frontend/img/location.svg";
import UserIcon from "../../../static/frontend/img/user.svg";
import WaitlistIcon from "../../../static/frontend/img/waitlist.svg";
import XCircle from "../../../static/frontend/img/x_circle.svg";

interface SectionCardProps {
Expand All @@ -22,6 +28,9 @@ interface SectionCardProps {
description: string;
userIsCoordinator: boolean;
courseOpen: boolean;
numStudentsWaitlisted: number;
waitlistCapacity: number;
courseId: number;
}

export const SectionCard = ({
Expand All @@ -32,12 +41,21 @@ export const SectionCard = ({
capacity,
description,
userIsCoordinator,
courseOpen
courseOpen,
numStudentsWaitlisted,
waitlistCapacity,
courseId
}: SectionCardProps): React.ReactElement => {
/**
* Mutation to enroll a student in the section.
*/
const enrollStudentMutation = useEnrollUserMutation(id);
/**
* Mutation to enroll a student in the section's waitlist.
*/
const enrollStudentWaitlistMutation = useEnrollStudentToWaitlistMutation(id);

const { data: profiles } = useProfiles();

/**
* Whether to show the modal (after an attempt to enroll).
Expand All @@ -51,19 +69,26 @@ export const SectionCard = ({
* The error message if the enrollment failed.
*/
const [errorMessage, setErrorMessage] = useState<string>("");
/**
* Whether to show the swap/waitlist confirmation modal.
*/
const [showSwapConfirm, setShowSwapConfirm] = useState<boolean>(false);
/**
* Whether the pending action is a waitlist join (vs direct enroll).
*/
const [pendingIsWaitlist, setPendingIsWaitlist] = useState<boolean>(false);

/**
* Handle enrollment in the section.
* Check if the user is already enrolled in another section of this course.
*/
const enroll = () => {
if (!courseOpen) {
setShowModal(true);
setEnrollmentSuccessful(false);
setErrorMessage("The course is not open for enrollment.");
return;
}
const isAlreadyEnrolled = profiles?.some(p => p.courseId === courseId && p.role === Role.STUDENT) ?? false;

enrollStudentMutation.mutate(undefined, {
/**
* Perform the actual mutation (enroll or waitlist).
*/
const performEnroll = (useWaitlist: boolean) => {
const mutation = useWaitlist ? enrollStudentWaitlistMutation : enrollStudentMutation;
mutation.mutate(undefined, {
onSuccess: () => {
setEnrollmentSuccessful(true);
setShowModal(true);
Expand All @@ -76,6 +101,31 @@ export const SectionCard = ({
});
};

/**
* Handle enrollment in the section.
*/
const enroll = () => {
if (!courseOpen) {
setShowModal(true);
setEnrollmentSuccessful(false);
setErrorMessage("The course is not open for enrollment.");
return;
}

// Determine if we should use waitlist mutation (enrolled capacity is full but waitlist is not full)
const isEnrolledFull = numStudentsEnrolled >= capacity;
const shouldUseWaitlist = isEnrolledFull && numStudentsWaitlisted < waitlistCapacity;

// If user is already enrolled in another section, show a confirmation warning
if (isAlreadyEnrolled) {
setPendingIsWaitlist(shouldUseWaitlist);
setShowSwapConfirm(true);
return;
}

performEnroll(shouldUseWaitlist);
};

/**
* Handle closeing of the modal.
*/
Expand Down Expand Up @@ -114,7 +164,8 @@ export const SectionCard = ({

const iconWidth = "1.3em";
const iconHeight = "1.3em";
const isFull = numStudentsEnrolled >= capacity;
const isFull = numStudentsEnrolled >= capacity && numStudentsWaitlisted >= waitlistCapacity;
const isEnrolledFull = numStudentsEnrolled >= capacity;
if (!showModal && enrollmentSuccessful) {
// redirect to the section page if the user was successfully enrolled in the section
return <Navigate to="/" />;
Expand All @@ -130,6 +181,43 @@ export const SectionCard = ({

return (
<React.Fragment>
{showSwapConfirm && (
<Modal closeModal={() => setShowSwapConfirm(false)}>
<div className="enroll-confirm-modal-contents">
{pendingIsWaitlist ? (
<>
<h3>Join waitlist?</h3>
<p style={{ margin: "0.5em 1.5em", textAlign: "center" }}>
You are currently enrolled in another section of this course. When a spot opens up on this waitlist,
you will be <strong>automatically dropped</strong> from your current section and enrolled in this one.
</p>
</>
) : (
<>
<h3>Switch sections?</h3>
<p style={{ margin: "0.5em 1.5em", textAlign: "center" }}>
You are currently enrolled in another section of this course. Enrolling here will{" "}
<strong>drop you from your current section</strong> and enroll you in this one.
</p>
</>
)}
<div style={{ display: "flex", gap: "1em", marginTop: "1em" }}>
<button className="secondary-btn" onClick={() => setShowSwapConfirm(false)}>
Cancel
</button>
<button
className="primary-btn"
onClick={() => {
setShowSwapConfirm(false);
performEnroll(pendingIsWaitlist);
}}
>
Confirm
</button>
</div>
</div>
</Modal>
)}
{showModal && <Modal closeModal={closeModal}>{modalContents()}</Modal>}
<section className={`section-card ${isFull ? "full" : ""}`}>
<div className="section-card-contents">
Expand Down Expand Up @@ -171,7 +259,11 @@ export const SectionCard = ({
<UserIcon width={iconWidth} height={iconHeight} /> {mentor.name}
</p>
<p title="Current enrollment">
<GroupIcon width={iconWidth} height={iconHeight} /> {`${numStudentsEnrolled}/${capacity}`}
<GroupIcon width={iconWidth} height={iconHeight} /> {`Enrolled: ${numStudentsEnrolled}/${capacity}`}
</p>
<p title="Current waitlist">
<WaitlistIcon width={iconWidth} height={iconHeight} />{" "}
{`Waitlisted: ${numStudentsWaitlisted}/${waitlistCapacity}`}
</p>
</div>
{userIsCoordinator ? (
Expand All @@ -184,7 +276,7 @@ export const SectionCard = ({
disabled={!courseOpen || isFull}
onClick={isFull ? undefined : enroll}
>
ENROLL
{isFull ? "FULL" : isEnrolledFull ? "JOIN WAITLIST" : "ENROLL"}
</button>
)}
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useState } from "react";
import { Link } from "react-router-dom";

import { useUserEmails } from "../../utils/queries/base";
import { useEnrollStudentMutation } from "../../utils/queries/sections";
import { useEnrollStudentMutation, useCoordEnrollStudentToWaitlistMutation } from "../../utils/queries/sections";
import LoadingSpinner from "../LoadingSpinner";
import Modal from "../Modal";

Expand All @@ -22,6 +22,10 @@ enum CoordModalStates {
interface CoordinatorAddStudentModalProps {
closeModal: (arg0?: boolean) => void;
sectionId: number;
title: string;
mutation: (
sectionId: number
) => ReturnType<typeof useEnrollStudentMutation> | ReturnType<typeof useCoordEnrollStudentToWaitlistMutation>;
}

interface RequestType {
Expand All @@ -39,7 +43,10 @@ interface ResponseType {
progress?: Array<{
email: string;
status: string;
detail?: any;
detail?: {
reason?: string;
section?: { id: number; mentor: { name: string } };
};
}>;
}

Expand All @@ -50,10 +57,12 @@ interface ActionType {

export function CoordinatorAddStudentModal({
closeModal,
sectionId
sectionId,
title,
mutation
}: CoordinatorAddStudentModalProps): React.ReactElement {
const { data: userEmails, isSuccess: userEmailsLoaded } = useUserEmails();
const enrollStudentMutation = useEnrollStudentMutation(sectionId);
const enrollMutation = mutation(sectionId);

const [emailsToAdd, setEmailsToAdd] = useState<string[]>([""]);
const [response, setResponse] = useState<ResponseType>({} as ResponseType);
Expand Down Expand Up @@ -127,8 +136,8 @@ export function CoordinatorAddStudentModal({
request.actions["capacity"] = responseActions.get("capacity") as string;
}

enrollStudentMutation.mutate(request, {
onError: ({ status, json }) => {
enrollMutation.mutate(request, {
onError: ({ status, json }: { status: number; json: ResponseType }) => {
if (status === 500) {
// internal error
setResponse({
Expand Down Expand Up @@ -182,7 +191,7 @@ export function CoordinatorAddStudentModal({

const initial_component = (
<React.Fragment>
<h2>Add new students</h2>
<h2>Add new {title}</h2>
<div className="coordinator-email-content">
<div className="coordinator-email-input-list">
{emailsToAdd.map((email, index) => (
Expand Down Expand Up @@ -286,28 +295,32 @@ export function CoordinatorAddStudentModal({
</div>
<div className="coordinator-email-response-item-container">
{conflict_arr.map(email_obj => {
const detail = email_obj.detail;
let conflictDetail: React.ReactNode = "";
let drop_disabled = false;
if (!email_obj.detail.section) {
if (!detail || !detail.section) {
// look at reason
if (!email_obj.detail.reason || email_obj.detail.reason === "other") {
if (!detail?.reason || detail.reason === "other") {
// unknown reason
conflictDetail = "Unable to enroll user in section!";
} else if (email_obj.detail.reason === "coordinator") {
} else if (detail.reason === "coordinator") {
conflictDetail = "User is already a coordinator for the course!";
} else if (email_obj.detail.reason === "mentor") {
} else if (detail.reason === "mentor") {
conflictDetail = "User is already a mentor for the course!";
} else {
// display the reason string directly (e.g. from waitlist coord-add)
conflictDetail = detail.reason;
}
drop_disabled = true;
} else if (email_obj.detail.section.id == sectionId) {
} else if (detail.section.id == sectionId) {
conflictDetail = "Already enrolled!";
drop_disabled = true;
} else {
conflictDetail = (
<React.Fragment>
Conflict:{" "}
<Link to={`/sections/${email_obj.detail.section.id}`} target="_blank" rel="noopener noreferrer">
{email_obj.detail.section.mentor.name}
<Link to={`/sections/${detail.section.id}`} target="_blank" rel="noopener noreferrer">
{detail.section.mentor.name}
</Link>
</React.Fragment>
);
Expand Down
5 changes: 4 additions & 1 deletion csm_web/frontend/src/components/section/MentorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface MentorSectionProps {
capacity: number;
description: string;
courseRestricted: boolean;
waitlistCapacity: number;
}

export default function MentorSection({
Expand All @@ -28,7 +29,8 @@ export default function MentorSection({
capacity,
description,
userRole,
mentor
mentor,
waitlistCapacity
}: MentorSectionProps) {
return (
<SectionDetail
Expand All @@ -55,6 +57,7 @@ export default function MentorSection({
description={description}
id={id}
courseRestricted={courseRestricted}
waitlistCapacity={waitlistCapacity}
/>
}
/>
Expand Down
Loading
Loading