diff --git a/src/api/klass.ts b/src/api/klass.ts index 8b5cd96..def4fe8 100644 --- a/src/api/klass.ts +++ b/src/api/klass.ts @@ -1,11 +1,3 @@ -import { type Class, urls } from "codeforlife/api" -import getReadClassEndpoints, { - CLASS_TAG, - type ListClassesArg, - type ListClassesResult, - type RetrieveClassArg, - type RetrieveClassResult, -} from "codeforlife/api/endpoints/klass" import { type BulkUpdateArg, type BulkUpdateResult, @@ -18,6 +10,14 @@ import { buildUrl, tagData, } from "codeforlife/utils/api" +import { type Class, urls } from "codeforlife/api" +import getReadClassEndpoints, { + CLASS_TAG, + type ListClassesArg, + type ListClassesResult, + type RetrieveClassArg, + type RetrieveClassResult, +} from "codeforlife/api/endpoints/klass" import api from "." diff --git a/src/api/school.ts b/src/api/school.ts index 62e5b0c..307b76f 100644 --- a/src/api/school.ts +++ b/src/api/school.ts @@ -1,9 +1,3 @@ -import { type School, urls } from "codeforlife/api" -import getReadSchoolEndpoints, { - type RetrieveSchoolArg, - type RetrieveSchoolResult, - SCHOOL_TAG, -} from "codeforlife/api/endpoints/school" import { type CreateArg, type CreateResult, @@ -12,6 +6,12 @@ import { buildUrl, tagData, } from "codeforlife/utils/api" +import { type School, urls } from "codeforlife/api" +import getReadSchoolEndpoints, { + type RetrieveSchoolArg, + type RetrieveSchoolResult, + SCHOOL_TAG, +} from "codeforlife/api/endpoints/school" import api from "." diff --git a/src/api/schoolTeacherInvitation.ts b/src/api/schoolTeacherInvitation.ts index ea11e4e..aaa2920 100644 --- a/src/api/schoolTeacherInvitation.ts +++ b/src/api/schoolTeacherInvitation.ts @@ -1,4 +1,3 @@ -import { type User } from "codeforlife/api" import { type Arg, type CreateArg, @@ -15,6 +14,7 @@ import { buildUrl, tagData, } from "codeforlife/utils/api" +import { type User } from "codeforlife/api" import api from "." diff --git a/src/api/user.ts b/src/api/user.ts index 6fdb9e3..29b5d34 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,11 +1,3 @@ -import { type User, urls } from "codeforlife/api" -import getReadUserEndpoints, { - type ListUsersArg, - type ListUsersResult, - type RetrieveUserArg, - type RetrieveUserResult, - USER_TAG, -} from "codeforlife/api/endpoints/user" import { type Arg, type CreateArg, @@ -17,6 +9,14 @@ import { buildUrl, tagData, } from "codeforlife/utils/api" +import { type User, urls } from "codeforlife/api" +import getReadUserEndpoints, { + type ListUsersArg, + type ListUsersResult, + type RetrieveUserArg, + type RetrieveUserResult, + USER_TAG, +} from "codeforlife/api/endpoints/user" import api from "." diff --git a/src/pages/teacherDashboard/TeacherDashboard.tsx b/src/pages/teacherDashboard/TeacherDashboard.tsx index 9e66185..0abfc50 100644 --- a/src/pages/teacherDashboard/TeacherDashboard.tsx +++ b/src/pages/teacherDashboard/TeacherDashboard.tsx @@ -5,18 +5,30 @@ import { CircularProgress } from "@mui/material" import { type SchoolTeacherUser } from "codeforlife/api" import { getParam } from "codeforlife/utils/router" +import Account, { type AccountProps } from "./account/Account" +import Classes, { type ClassesProps } from "./classes/Classes" import { type RetrieveUserResult, useLazyRetrieveUserQuery, } from "../../api/user" -import Account from "./account/Account" -import Classes from "./classes/Classes" -import School from "./school/School" +import School, { type SchoolProps } from "./school/School" import { paths } from "../../router" -export interface TeacherDashboardProps {} +export type TeacherDashboardProps = + | { + tab: "school" + view?: SchoolProps["view"] + } + | { + tab: "classes" + view?: ClassesProps["view"] + } + | { + tab: "account" + view?: AccountProps["view"] + } -const TeacherDashboard: FC = () => { +const TeacherDashboard: FC = ({ tab, view }) => { let [retrieveUser, { data: authUser, isError }] = useLazyRetrieveUserQuery() const navigate = useNavigate() @@ -41,27 +53,45 @@ const TeacherDashboard: FC = () => { const authSchoolTeacherUser = authUser as SchoolTeacherUser + const tabs: page.TabBarProps["tabs"] = [ + { + label: "Your school", + children: ( + + ), + path: getParam(paths.teacher.dashboard.tab.school, "tab"), + }, + { + label: "Your classes", + children: ( + + ), + path: getParam(paths.teacher.dashboard.tab.classes, "tab"), + }, + { + label: "Your account", + children: ( + + ), + path: getParam(paths.teacher.dashboard.tab.account, "tab"), + }, + ] + return ( , - path: getParam(paths.teacher.dashboard.tab.school, "tab"), - }, - { - label: "Your classes", - children: , - path: getParam(paths.teacher.dashboard.tab.classes, "tab"), - }, - { - label: "Your account", - children: , - path: getParam(paths.teacher.dashboard.tab.account, "tab"), - }, - ]} + value={tabs.findIndex(t => t.path === tab)} + tabs={tabs} /> ) } diff --git a/src/pages/teacherDashboard/account/Account.tsx b/src/pages/teacherDashboard/account/Account.tsx index 4b2eab4..dfbd3fd 100644 --- a/src/pages/teacherDashboard/account/Account.tsx +++ b/src/pages/teacherDashboard/account/Account.tsx @@ -5,6 +5,7 @@ import { type RetrieveUserResult } from "../../../api/user" export interface AccountProps { authUser: SchoolTeacherUser + view?: "otp" } const Account: FC = ({ authUser }) => { diff --git a/src/pages/teacherDashboard/classes/Class.tsx b/src/pages/teacherDashboard/classes/Class.tsx new file mode 100644 index 0000000..96c79fc --- /dev/null +++ b/src/pages/teacherDashboard/classes/Class.tsx @@ -0,0 +1,9 @@ +import { type FC } from "react" + +export interface ClassProps {} + +const Class: FC = () => { + return <>Class +} + +export default Class diff --git a/src/pages/teacherDashboard/classes/Classes.tsx b/src/pages/teacherDashboard/classes/Classes.tsx index 9a57039..60b0d4f 100644 --- a/src/pages/teacherDashboard/classes/Classes.tsx +++ b/src/pages/teacherDashboard/classes/Classes.tsx @@ -1,14 +1,24 @@ import { type FC } from "react" import { type SchoolTeacherUser } from "codeforlife/api" +import Class from "./Class" +import JoinClassRequest from "./JoinClassRequest" import { type RetrieveUserResult } from "../../../api/user" export interface ClassesProps { authUser: SchoolTeacherUser + view?: "class" | "join-class-request" } -const Classes: FC = ({ authUser }) => { - return <>TODO +const Classes: FC = ({ authUser, view }) => { + if (view) { + return { + class: , + "join-class-request": , + }[view] + } + + return <>Classes } export default Classes diff --git a/src/pages/teacherDashboard/classes/JoinClassRequest.tsx b/src/pages/teacherDashboard/classes/JoinClassRequest.tsx new file mode 100644 index 0000000..f3cfad1 --- /dev/null +++ b/src/pages/teacherDashboard/classes/JoinClassRequest.tsx @@ -0,0 +1,231 @@ +import * as form from "codeforlife/components/form" +import * as page from "codeforlife/components/page" +import * as yup from "yup" +import { + CircularProgress, + Unstable_Grid2 as Grid, + Stack, + Typography, +} from "@mui/material" +import { type FC, useEffect, useState } from "react" +import { type IndependentUser, type StudentUser } from "codeforlife/api" +import { Link, LinkButton } from "codeforlife/components/router" +import { useNavigate, useParams } from "codeforlife/hooks" +import { TablePagination } from "codeforlife/components" +import { generatePath } from "react-router" +import { submitForm } from "codeforlife/utils/form" + +import { + type RetrieveClassResult, + useLazyRetrieveClassQuery, +} from "../../../api/klass" +import { + type RetrieveUserResult, + useHandleJoinClassRequestMutation, + useLazyListUsersQuery, + useLazyRetrieveUserQuery, +} from "../../../api/user" +import { classIdSchema } from "../../../app/schemas" +import { paths } from "../../../router" + +const AddedStudent: FC<{ + klass: RetrieveClassResult + user: StudentUser +}> = ({ klass, user }) => ( + <> + + + External student added to class {klass.name} ({klass.id}) + + + + + The student has been successfully added to the class {klass.name}. + + + Please provide the student with their new login details: + + + + + Class Access Code: {klass.id} + + + Name: {user.first_name} + + + + + {user.first_name} should now login as a student with these details. + + + {user.first_name}'s password is unchanged. You may manage this + student, including changing their name and password, as with other + students. + + + Class + + + +) + +const HandleRequest: FC<{ + klass: RetrieveClassResult + user: IndependentUser + onAcceptRequest: () => void +}> = ({ klass, user, onAcceptRequest }) => { + const [handleJoinClassRequest] = useHandleJoinClassRequestMutation() + + return ( + <> + + + Add external student to class {klass.name} ({klass.id}) + + + + + + + {studentUsers => ( + + + Students currently in class + + {studentUsers.length ? ( + <> + + {user.first_name}, the new external student, will be + joining students in the class {klass.name} ({klass.id}) + + Student Name + {studentUsers.map((studentUser, index) => ( + + {studentUser.first_name} + + ))} + + ) : ( + + The new external student {user.first_name} is joining the + class {klass.name} ({klass.id}) in which there are + currently no other students. + + )} + + )} + + + + + Add external student + + Please confirm the name of the new external student joining your + class. Their name will be used in their new login details, so + please ensure it is different from any other existing student in + the class. + + + + + + Cancel + + Save + + + + + + + + Class + + + + + ) +} + +export interface JoinClassRequestProps {} + +const JoinClassRequest: FC = () => { + const navigate = useNavigate() + const [wasAccepted, setWasAccepted] = useState(false) + const params = useParams({ + classId: classIdSchema().required(), + userId: yup.number().required(), + }) + + const [ + retrieveUser, + { data: user, isLoading: userIsLoading, isError: userIsError }, + ] = useLazyRetrieveUserQuery() + const [ + retrieveClass, + { data: klass, isLoading: classIsLoading, isError: classIsError }, + ] = useLazyRetrieveClassQuery() + + useEffect(() => { + if (params) { + retrieveUser(params.userId) + retrieveClass(params.classId) + } else navigate(paths.error.type.pageNotFound._) + }, [params, navigate, retrieveUser, retrieveClass]) + + if (!params) return <> + + if (!user || userIsLoading || !klass || classIsLoading) + return + + if (userIsError || classIsError) alert("TODO: handle error") + + return wasAccepted ? ( + } + /> + ) : ( + } + onAcceptRequest={() => { + setWasAccepted(true) + }} + /> + ) +} + +export default JoinClassRequest diff --git a/src/pages/teacherDashboard/school/Leave.tsx b/src/pages/teacherDashboard/school/Leave.tsx new file mode 100644 index 0000000..a92cc00 --- /dev/null +++ b/src/pages/teacherDashboard/school/Leave.tsx @@ -0,0 +1,183 @@ +import * as form from "codeforlife/components/form" +import * as page from "codeforlife/components/page" +import * as yup from "yup" +import { CircularProgress, Stack, Typography } from "@mui/material" +import { type FC, useEffect } from "react" +import { Link, LinkButton } from "codeforlife/components/router" +import { type SchoolTeacher, type User } from "codeforlife/api" +import { useNavigate, useParams } from "codeforlife/hooks" +import { TablePagination } from "codeforlife/components" + +import * as table from "../../../components/table" +import { + useLazyListClassesQuery, + useUpdateClassesMutation, +} from "../../../api/klass" +import { + useLazyListUsersQuery, + useLazyRetrieveUserQuery, +} from "../../../api/user" +import { paths } from "../../../router" +import { submitForm } from "codeforlife/utils/form" +import { useRemoveTeacherFromSchoolMutation } from "../../../api/teacher" + +export interface LeaveProps { + authUserId: User["id"] +} + +const Leave: FC = ({ authUserId }) => { + const [updateClasses] = useUpdateClassesMutation() + const [removeTeacherFromSchool] = useRemoveTeacherFromSchoolMutation() + const [retrieveUser, { data: user, isLoading, isError }] = + useLazyRetrieveUserQuery() + + const navigate = useNavigate() + const params = useParams({ userId: yup.number().required() }) + + useEffect(() => { + function navigateToSchoolTabWithErrorNotification(message: string) { + navigate(paths.teacher.dashboard.tab.school._, { + state: { + notifications: [ + { + props: { children: message, error: true }, + }, + ], + }, + }) + } + + if (!params) navigate(paths.error.type.pageNotFound._) + else if (isError) + navigateToSchoolTabWithErrorNotification("Failed to retrieve user.") + else if (!isLoading && !user) retrieveUser(params.userId) + else if (user && !user.teacher) + navigateToSchoolTabWithErrorNotification("This user is not a teacher.") + }, [params, navigate, isError, isLoading, user, retrieveUser]) + + if (!params || isError) return <> + if (isLoading || !user) return + if (!user.teacher) return <> + + const isSelf = params.userId === authUserId + + function handleRemoveTeacherFromSchool() { + removeTeacherFromSchool(user!.teacher!.id) + .unwrap() + .then(() => { + if (isSelf) { + navigate(paths.teacher.onboarding._) + } else { + navigate(paths.teacher.dashboard.tab.school._, { + state: { + notifications: [ + { + props: { + children: + "The teacher has been successfully removed from your school or club, and their classes were successfully transferred.", + }, + }, + ], + }, + }) + } + }) + .catch(() => { + // TODO: error handling strategy. + alert("Failed to remove teacher from school") + }) + } + + return ( + <> + + {isSelf + ? "You still have classes, you must first move them to another teacher within your school or club." + : "This teacher still has classes assigned to them. You must first move them to another teacher in your school or club."} + + + + Move all classes for teacher {user.first_name} {user.last_name} + + + dashboard + + + Please specify which teacher you would like the classes below to be + moved to. + + + {classes => { + if (!classes.length) { + handleRemoveTeacherFromSchool() + return <> + } + + return ( + ({ + ...values, + [klass.id]: { teacher: undefined }, + }), + {}, + )} + onSubmit={submitForm(updateClasses)} + > + + {classes.map(klass => ( + + + + {klass.name} + + + + + `${first_name} ${last_name}` + } + getOptionKey={({ teacher }) => + (teacher as SchoolTeacher).id + } + textFieldProps={{ + required: true, + name: `${klass.id}.teacher`, + }} + searchKey="name" + /> + + + ))} + + + + Cancel + + + {isSelf + ? "Move classes and leave" + : "Move classes and remove teacher"} + + + + ) + }} + + + + ) +} + +export default Leave diff --git a/src/pages/teacherDashboard/school/School.tsx b/src/pages/teacherDashboard/school/School.tsx index fec7d04..dc41353 100644 --- a/src/pages/teacherDashboard/school/School.tsx +++ b/src/pages/teacherDashboard/school/School.tsx @@ -6,37 +6,35 @@ import { Stack, Typography, } from "@mui/material" -import { useLocation, useNavigate } from "codeforlife/hooks" import { type FC } from "react" import { type SchoolTeacherUser } from "codeforlife/api" +import { generatePath } from "react-router" +import { useNavigate } from "codeforlife/hooks" -import TransferClasses, { type TransferClassesProps } from "./TransferClasses" import InviteTeacherForm from "./InviteTeacherForm" +import Leave from "./Leave" import { type RetrieveUserResult } from "../../../api/user" import TeacherInvitationTable from "./TeacherInvitationTable" import TeacherTable from "./TeacherTable" import UpdateSchoolForm from "./UpdateSchoolForm" +import { paths } from "../../../router" import { useRetrieveSchoolQuery } from "../../../api/school" export interface SchoolProps { authUser: SchoolTeacherUser + view?: "leave" } -const School: FC = ({ authUser }) => { +const School: FC = ({ authUser, view }) => { + const navigate = useNavigate() const { data: school, isError } = useRetrieveSchoolQuery( authUser.teacher.school, ) - const { state } = useLocation<{ - transferClasses: TransferClassesProps["user"] - }>() - const navigate = useNavigate<{ - transferClasses?: TransferClassesProps["user"] - }>() - if (state?.transferClasses) { - return ( - - ) + if (view) { + return { + leave: , + }[view] } // TODO: handle this better @@ -75,7 +73,11 @@ const School: FC = ({ authUser }) => {