From 6c101e39d855f441538d0fe33fd85a433df1f921 Mon Sep 17 00:00:00 2001 From: Dmytro Doronin <138795636+Dmytro-Doronin@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:24:37 +0100 Subject: [PATCH 1/4] feat: moderator routing --- client/src/api/auth/types.ts | 3 +- client/src/api/moderator/moderator.type.ts | 9 +++ client/src/api/teacher/teacher.api.ts | 14 +++++ client/src/api/teacher/teacher.type.ts | 8 +++ .../components/sidebar/sidebarMenuItems.ts | 15 +++++ client/src/features/queryKeys.ts | 8 ++- .../query/useTeacherForModeratorQuery.tsx | 30 +++++++++ client/src/layouts/PrivateLayout.tsx | 16 +++-- .../ModeratorTeaschersPage.tsx | 63 +++++++++++++++++++ client/src/router/RoleIndexRedirect.tsx | 30 ++++++--- client/src/router/router.tsx | 12 ++++ .../moderatorPrivateRoutes.tsx | 28 +++++++++ .../router/routesVariables/pathVariables.ts | 6 ++ client/src/store/authSession.store.ts | 2 +- server/src/controllers/teacher.controller.ts | 24 +++++++ .../queryRepositories/teacher.query.ts | 34 ++++++++++ server/src/routes/teacherRoute.ts | 7 +++ server/src/types/teacher/teacher.types.ts | 7 +++ 18 files changed, 297 insertions(+), 19 deletions(-) create mode 100644 client/src/api/moderator/moderator.type.ts create mode 100644 client/src/features/teachers/query/useTeacherForModeratorQuery.tsx create mode 100644 client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx create mode 100644 client/src/router/routesVariables/moderatorPrivateRoutes.tsx diff --git a/client/src/api/auth/types.ts b/client/src/api/auth/types.ts index 35eab2ff..47b0bbbb 100644 --- a/client/src/api/auth/types.ts +++ b/client/src/api/auth/types.ts @@ -1,5 +1,6 @@ import { StudentType } from "../student/student.type"; import { TeacherType } from "../teacher/teacher.type"; +import { ModeratorType } from "../moderator/moderator.type.ts"; export type Role = "student" | "teacher" | "moderator"; @@ -14,7 +15,7 @@ export type RegisterFinalType = RegisterFormTypes & { role: Role; }; -export type UserType = StudentType | TeacherType; +export type UserType = StudentType | TeacherType | ModeratorType; export type LoginFormTypes = { email: string; diff --git a/client/src/api/moderator/moderator.type.ts b/client/src/api/moderator/moderator.type.ts new file mode 100644 index 00000000..33908dba --- /dev/null +++ b/client/src/api/moderator/moderator.type.ts @@ -0,0 +1,9 @@ +export type ModeratorType = { + id: string; + firstName: string; + lastName: string; + email: string; + profileImageUrl: string | null; + createdAt: Date; + role: "moderator"; +}; diff --git a/client/src/api/teacher/teacher.api.ts b/client/src/api/teacher/teacher.api.ts index 9ff67cf7..43aa4f32 100644 --- a/client/src/api/teacher/teacher.api.ts +++ b/client/src/api/teacher/teacher.api.ts @@ -1,6 +1,7 @@ import { apiProtected, apiPublic } from "../api.ts"; import { TeacherOutputModel, + TeachersForModeratorQuery, TeachersQuery, TeacherType, UpdateTeacherProfileInput, @@ -14,6 +15,19 @@ export async function getAllTeachersApi(query: TeachersQuery) { return res.data; } +export async function getAllTeachersForModeratorApi( + query: TeachersForModeratorQuery, +) { + const res = await apiProtected.get( + "/api/teachers/get-teachers-moderator", + { + params: query, + }, + ); + + return res.data; +} + export async function getTeacherByIdApi(teacherId: string) { const res = await apiPublic.get(`/api/teachers/${teacherId}`); return res.data; diff --git a/client/src/api/teacher/teacher.type.ts b/client/src/api/teacher/teacher.type.ts index c81fdbb5..c0c4f66d 100644 --- a/client/src/api/teacher/teacher.type.ts +++ b/client/src/api/teacher/teacher.type.ts @@ -71,6 +71,7 @@ export type TeacherOutputModel = { export type SortDirection = "asc" | "desc"; export type SortBy = "createdAt" | "priceFrom" | "rating"; +export type SortByTeachersForModerator = "status" | "createdAt"; export type TeachersQuery = { subject?: string; @@ -83,6 +84,13 @@ export type TeachersQuery = { pageSize?: number; }; +export type TeachersForModeratorQuery = { + sortBy?: SortByTeachersForModerator; + sortDirection?: SortDirection; + pageNumber?: number; + pageSize?: number; +}; + export type UpdateTeacherProfileInput = { firstName?: string; lastName?: string; diff --git a/client/src/components/sidebar/sidebarMenuItems.ts b/client/src/components/sidebar/sidebarMenuItems.ts index 007d5b44..a2db76a0 100644 --- a/client/src/components/sidebar/sidebarMenuItems.ts +++ b/client/src/components/sidebar/sidebarMenuItems.ts @@ -5,6 +5,8 @@ import Chat from "../icons/Chat"; import UsersIcon from "../icons/UsersIcon"; import { chatRoutes, + moderatorBase, + moderatorPrivatesRoutesVariables, studentBase, studentPrivatesRoutesVariables, teacherBase, @@ -63,3 +65,16 @@ export const defaultTeacherMenuItems: MenuItem[] = [ icon: Chat, }, ]; + +export const defaultModeratorMenuItems: MenuItem[] = [ + { + name: "Teachers", + link: joinPath(moderatorBase, moderatorPrivatesRoutesVariables.teachers), + icon: DashboardIcon, + }, + { + name: "Chat", + link: joinPath(moderatorBase, chatRoutes.root), + icon: Chat, + }, +]; diff --git a/client/src/features/queryKeys.ts b/client/src/features/queryKeys.ts index 528d5267..e386ac8e 100644 --- a/client/src/features/queryKeys.ts +++ b/client/src/features/queryKeys.ts @@ -1,4 +1,7 @@ -import { TeachersQuery } from "../api/teacher/teacher.type.ts"; +import { + TeachersForModeratorQuery, + TeachersQuery, +} from "../api/teacher/teacher.type.ts"; export const queryKeys = { me: ["auth", "me"] as const, @@ -13,7 +16,8 @@ export const queryKeys = { list: (params: TeachersQuery) => ["teachers", "list", params] as const, }, teacher: (id: string) => ["teachers", id] as const, - teachersList: (params: TeachersQuery) => ["teachers", params] as const, + teachersList: (params: TeachersQuery | TeachersForModeratorQuery) => + ["teachers", params] as const, appointments: ["appointments"] as const, teacherAppointments: (teacherId: string, page?: number, limit?: number) => diff --git a/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx new file mode 100644 index 00000000..7cc84f63 --- /dev/null +++ b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx @@ -0,0 +1,30 @@ +import { TeachersForModeratorQuery } from "../../../api/teacher/teacher.type.ts"; +import { useNotificationStore } from "../../../store/notification.store.ts"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { queryKeys } from "../../queryKeys.ts"; +import { getAllTeachersForModeratorApi } from "../../../api/teacher/teacher.api.ts"; +import { useEffect } from "react"; +import { getErrorMessage } from "../../../util/ErrorUtil.ts"; + +export function useTeachersForModeratorQuery( + params: TeachersForModeratorQuery, +) { + const notifyError = useNotificationStore((s) => s.error); + + const query = useQuery({ + queryKey: queryKeys.teachersList(params), + queryFn: () => getAllTeachersForModeratorApi(params), + retry: false, + placeholderData: keepPreviousData, + staleTime: 20 * 60 * 1000, + }); + + useEffect(() => { + if (query.isError) { + const msg = getErrorMessage(query.error); + notifyError(msg ?? "Failed to load teachers"); + } + }, [query.isError, query.isSuccess, query.error, notifyError]); + + return query; +} diff --git a/client/src/layouts/PrivateLayout.tsx b/client/src/layouts/PrivateLayout.tsx index 9dae692a..4771340d 100644 --- a/client/src/layouts/PrivateLayout.tsx +++ b/client/src/layouts/PrivateLayout.tsx @@ -1,6 +1,7 @@ import { Outlet } from "react-router-dom"; import { Sidebar } from "../components/sidebar/Sidebar.tsx"; import { + defaultModeratorMenuItems, defaultStudentMenuItems, defaultTeacherMenuItems, } from "../components/sidebar/sidebarMenuItems.ts"; @@ -8,19 +9,22 @@ import { TopBar } from "../components/headerPrivate/TopBar.tsx"; import { useAuthSessionStore } from "../store/authSession.store.ts"; import { useEffect } from "react"; import { useSocketStore } from "../store/socket.store.ts"; -import { usePresenceSubscribe } from "../hooks/usePresenceSubscribe.ts"; export const PrivateLayout = () => { const user = useAuthSessionStore((s) => s.user); const accessToken = useAuthSessionStore((s) => s.accessToken); const connect = useSocketStore((s) => s.connect); const disconnect = useSocketStore((s) => s.disconnect); - const items = - user?.role === "teacher" - ? defaultTeacherMenuItems - : defaultStudentMenuItems; - usePresenceSubscribe(); + let items; + + if (user?.role === "teacher") { + items = defaultTeacherMenuItems; + } else if (user?.role === "student") { + items = defaultStudentMenuItems; + } else { + items = defaultModeratorMenuItems; + } useEffect(() => { if (!accessToken) { diff --git a/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx b/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx new file mode 100644 index 00000000..22f09073 --- /dev/null +++ b/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx @@ -0,0 +1,63 @@ +import { CardsList } from "../../components/cardsList/CardsList"; +import { Pagination } from "../../components/ui/pagination/Pagination"; +import { useRef, useState } from "react"; +import { TeachersCardsSkeletonList } from "../../components/skeletons/TeachersCardsSkeletonList.tsx"; +import { SortByTeachersForModerator } from "../../api/teacher/teacher.type.ts"; +import { useTeachersForModeratorQuery } from "../../features/teachers/query/useTeacherForModeratorQuery.tsx"; + +export const ModeratorTeachersPage = () => { + const [page, setPage] = useState(1); + const [sortBy] = useState("status"); + const listTopRef = useRef(null); + + const handlePageChange = (page: number) => { + setPage(page); + + requestAnimationFrame(() => { + listTopRef.current?.scrollIntoView({ + block: "start", + }); + }); + }; + + const { data, isFetching } = useTeachersForModeratorQuery({ + pageNumber: page, + pageSize: 10, + sortBy, + }); + + return ( +
+

TEACHERS

+
+
+ {isFetching ? ( + + ) : data?.items?.length ? ( + + ) : ( +
No teachers
+ )} +
+ +
+
+
+
+ ); +}; diff --git a/client/src/router/RoleIndexRedirect.tsx b/client/src/router/RoleIndexRedirect.tsx index c0eeee68..d60d927e 100644 --- a/client/src/router/RoleIndexRedirect.tsx +++ b/client/src/router/RoleIndexRedirect.tsx @@ -1,6 +1,9 @@ import { useAuthSessionStore } from "../store/authSession.store.ts"; import { Navigate, useLocation } from "react-router-dom"; -import { teacherPrivatesRoutesVariables } from "./routesVariables/pathVariables.ts"; +import { + moderatorPrivatesRoutesVariables, + teacherPrivatesRoutesVariables, +} from "./routesVariables/pathVariables.ts"; export const RoleIndexRedirect = () => { const user = useAuthSessionStore((s) => s.user); @@ -9,12 +12,21 @@ export const RoleIndexRedirect = () => { return ; } - return user.role === "teacher" ? ( - - ) : ( - - ); + if (user.role === "teacher") { + return ( + + ); + } else if (user.role === "student") { + return ; + } else if (user.role === "moderator") { + return ( + + ); + } }; diff --git a/client/src/router/router.tsx b/client/src/router/router.tsx index d5ec1c1f..215b543e 100644 --- a/client/src/router/router.tsx +++ b/client/src/router/router.tsx @@ -11,6 +11,7 @@ import { RequireRole } from "./RequireRole.tsx"; import { RoleIndexRedirect } from "./RoleIndexRedirect.tsx"; import { teacherPrivateRoutes } from "./routesVariables/teacherPrivateRoutes.tsx"; import { VideoCallPage } from "../pages/videoCall/VideoCallPage.tsx"; +import { moderatorPrivateRoutes } from "./routesVariables/moderatorPrivateRoutes.tsx"; export const router = createBrowserRouter([ { @@ -33,6 +34,17 @@ export const router = createBrowserRouter([ ), }, + { + path: "/moderator", + element: ( + + + + + + ), + children: moderatorPrivateRoutes, + }, { path: "/clients-dashboard", element: ( diff --git a/client/src/router/routesVariables/moderatorPrivateRoutes.tsx b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx new file mode 100644 index 00000000..3886b78a --- /dev/null +++ b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx @@ -0,0 +1,28 @@ +import { RouteObject } from "react-router-dom"; +import { + chatRoutes, + moderatorPrivatesRoutesVariables, +} from "./pathVariables.ts"; +import { ChatPage } from "../../pages/chat/chatPage/ChatPage.tsx"; +import { EmptyChat } from "../../pages/chat/EmptyChat/EmptyChat.tsx"; +import { ChatDialogPage } from "../../pages/chat/chatDialogPage/ChatDialogPage.tsx"; +import { ModeratorTeachersPage } from "../../pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx"; + +export const moderatorPrivateRoutes: RouteObject[] = [ + { + path: moderatorPrivatesRoutesVariables.teachers, + element: , + }, + // { + // path: studentPrivatesRoutesVariables.appointments, + // element: , + // }, + { + path: chatRoutes.root, + element: , + children: [ + { index: true, element: }, + { path: chatRoutes.dialog, element: }, + ], + }, +]; diff --git a/client/src/router/routesVariables/pathVariables.ts b/client/src/router/routesVariables/pathVariables.ts index bc2d3b15..c9a1fcd5 100644 --- a/client/src/router/routesVariables/pathVariables.ts +++ b/client/src/router/routesVariables/pathVariables.ts @@ -1,5 +1,6 @@ export const studentBase = "/clients-dashboard"; export const teacherBase = "/teacher"; +export const moderatorBase = "/moderator"; export const publicRoutesVariables = { home: "/", @@ -41,3 +42,8 @@ export const teacherPrivatesRoutesVariables = { profile: "profile", appointments: "teacher-appointments", }; + +export const moderatorPrivatesRoutesVariables = { + teachers: "teachers", + teacher: "teachers/:id", +}; diff --git a/client/src/store/authSession.store.ts b/client/src/store/authSession.store.ts index 6e7a218b..b2089c37 100644 --- a/client/src/store/authSession.store.ts +++ b/client/src/store/authSession.store.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; import type { UserType } from "../api/auth/types"; -type AccountType = "student" | "teacher"; +type AccountType = "student" | "teacher" | "moderator"; type AuthSessionState = { user: UserType | null; diff --git a/server/src/controllers/teacher.controller.ts b/server/src/controllers/teacher.controller.ts index c1a4dc00..19eec0b5 100644 --- a/server/src/controllers/teacher.controller.ts +++ b/server/src/controllers/teacher.controller.ts @@ -15,6 +15,7 @@ import { QueryTeacherInput, TeacherOutputModel, UpdateTeacherProfileInput, + QueryTeacherForModeratorInput, } from "../types/teacher/teacher.types.js"; @injectable() @@ -62,6 +63,29 @@ export class TeacherController { return next(err); } } + + async getAllTeachersForModerator( + req: RequestWithQuery, + res: ResponseWithData, + next: NextFunction, + ) { + try { + const sortData: QueryTeacherForModeratorInput = { + sortBy: req.query.sortBy, + sortDirection: req.query.sortDirection, + pageNumber: req.query.pageNumber, + pageSize: req.query.pageSize, + }; + + const teachers = + await this.teacherQuery.getAllTeachersForModerator(sortData); + + return res.status(200).send(teachers); + } catch (err) { + return next(err); + } + } + async getTeacherById( req: RequestWithParams, res: Response, diff --git a/server/src/repositories/queryRepositories/teacher.query.ts b/server/src/repositories/queryRepositories/teacher.query.ts index 63005d05..f4e847cf 100644 --- a/server/src/repositories/queryRepositories/teacher.query.ts +++ b/server/src/repositories/queryRepositories/teacher.query.ts @@ -5,6 +5,7 @@ import { TeacherViewType, AvailabilityView, UpdateTeacherProfileInput, + QueryTeacherForModeratorInput, } from "../../types/teacher/teacher.types.js"; import { TeacherModel } from "../../db/schemes/teacherSchema.js"; import { teacherMapper } from "../../utils/mappers/teacher.mapper.js"; @@ -47,6 +48,39 @@ export class TeacherQuery { } } + async getAllTeachersForModerator( + queries: QueryTeacherForModeratorInput, + ): Promise { + try { + const pageNumber = queries.pageNumber ?? 1; + const pageSize = queries.pageSize ?? 10; + const sortBy = queries.sortBy ?? "createdAt"; + const sortDirection = queries.sortDirection ?? "desc"; + + const items = await TeacherModel.find() + .sort(filterForSort(sortBy, sortDirection)) + .skip((pageNumber - 1) * +pageSize) + .limit(+pageSize) + .lean(); + + const totalCount = await TeacherModel.countDocuments(); + + const pagesCount = Math.ceil(totalCount / +pageSize); + + return { + pagesCount, + page: pageNumber, + pageSize, + totalCount, + items: items.map(teacherMapper), + }; + } catch (err: unknown) { + throw new Error("Something went wrong with getting all teachers", { + cause: err, + }); + } + } + async getTeacherByEmail(email: string): Promise { try { const teacher = await TeacherModel.findOne({ email }).lean(); diff --git a/server/src/routes/teacherRoute.ts b/server/src/routes/teacherRoute.ts index aa270bbe..bab5129e 100644 --- a/server/src/routes/teacherRoute.ts +++ b/server/src/routes/teacherRoute.ts @@ -21,6 +21,13 @@ teacherRouter.get( teacherController.getAllTeachers.bind(teacherController), ); +teacherRouter.get( + "/get-teachers-moderator", + authMiddleware.handle, + requireRole("moderator"), + teacherController.getAllTeachersForModerator.bind(teacherController), +); + teacherRouter.get( "/me", authMiddleware.handle, diff --git a/server/src/types/teacher/teacher.types.ts b/server/src/types/teacher/teacher.types.ts index c4e5cb84..f8ae9c55 100644 --- a/server/src/types/teacher/teacher.types.ts +++ b/server/src/types/teacher/teacher.types.ts @@ -84,6 +84,13 @@ export type QueryTeacherInput = { pageSize?: number; }; +export type QueryTeacherForModeratorInput = { + sortBy?: SortBy; + sortDirection?: SortDirection; + pageNumber?: number; + pageSize?: number; +}; + export type TeacherOutputModel = { pagesCount?: number; page?: number; From fe5ff9bcbda6bf59f38589b17e134043fdd781fa Mon Sep 17 00:00:00 2001 From: Dmytro Doronin <138795636+Dmytro-Doronin@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:19:25 +0100 Subject: [PATCH 2/4] feat: teacher status bouutons and getAllActiveTeachers query repo --- client/src/api/teacher/teacher.type.ts | 8 +- .../components/teacherCard/teacherCard.tsx | 197 +++++++++++------- .../moderatorPrivateRoutes.tsx | 9 +- client/src/util/statusButtons.tsx | 56 +++++ server/src/controllers/teacher.controller.ts | 2 +- .../queryRepositories/teacher.query.ts | 4 +- 6 files changed, 192 insertions(+), 84 deletions(-) create mode 100644 client/src/util/statusButtons.tsx diff --git a/client/src/api/teacher/teacher.type.ts b/client/src/api/teacher/teacher.type.ts index c0c4f66d..65f73fbe 100644 --- a/client/src/api/teacher/teacher.type.ts +++ b/client/src/api/teacher/teacher.type.ts @@ -1,5 +1,10 @@ import { Role } from "../auth/types"; - +export type TeacherStatus = + | "draft" + | "pending" + | "active" + | "rejected" + | "blocked"; type EducationItem = { degree: string; institution: string; @@ -58,6 +63,7 @@ export type TeacherType = { availability: AvailabilityItem; address: AddressItem; createdAt: Date; + status: TeacherStatus; role: Role; }; diff --git a/client/src/components/teacherCard/teacherCard.tsx b/client/src/components/teacherCard/teacherCard.tsx index 7621e0c4..7d7a3af8 100644 --- a/client/src/components/teacherCard/teacherCard.tsx +++ b/client/src/components/teacherCard/teacherCard.tsx @@ -4,12 +4,42 @@ import { TeacherType } from "../../api/teacher/teacher.type"; import { useNavigate } from "react-router-dom"; import { getAvatarUrl } from "../../api/upload/upload.api"; import DefaultAvatarIcon from "../icons/DefaultAvatarIcon"; +import { cva, VariantProps } from "class-variance-authority"; +import { twMerge } from "tailwind-merge"; +import { useAuthSessionStore } from "../../store/authSession.store.ts"; +import { + getStatusButtonClass, + statusOptions, + statusUi, +} from "../../util/statusButtons.tsx"; type TeacherCardType = { teacher: TeacherType; showBookButton?: boolean; }; +const teacherCardVariants = cva( + "flex flex-col border bg-[#15141D80] px-4 sm:px-6 md:px-9 py-6 md:py-9 rounded-[25px]", + { + variants: { + status: { + active: "border-blue-500", + rejected: "border-danger", + blocked: "border-light-600", + draft: "border-yellow-400", + pending: "border-purple-400", + }, + }, + defaultVariants: { status: "active" }, + }, +); + +export type TeacherCardVariantsProps = VariantProps; +const teacherCardClassName = ( + props: TeacherCardVariantsProps, + className?: string, +) => twMerge(teacherCardVariants(props), className); + export const TeacherCard = ({ teacher, showBookButton = true, @@ -25,27 +55,41 @@ export const TeacherCard = ({ priceFrom, bio, rating, + status, } = teacher; const navigate = useNavigate(); - + const user = useAuthSessionStore((state) => state.user); const avatarUrl = getAvatarUrl(profileImageUrl || null); const handleBookClick = () => { navigate(`/teacher/${id}`); }; return ( -
-
+ {user?.role === "moderator" && ( +
+ {statusOptions.map((s) => ( + + ))} +
+ )} +
+
-
-
+
- {avatarUrl ? ( - person - ) : ( - - )} -
-
-

- {firstName} {lastName} -

-
-
-
- {subjects.length > 0 && ( -
- {subjects.map((subject) => ( - - {subject.subjectName} teacher - - ))} + > + {avatarUrl ? ( + person + ) : ( + + )}
- )} -
-

- Experience — {experience} {experience === 1 ? "year" : "years"} -

-

- Education —{" "} - {education.length > 0 - ? education.map((item, index) => ( - - {item.institution} - {index < education.length - 1 ? ", " : ""} - - )) - : "Not specified"} -

-

+

+ {firstName} {lastName} +

+
+
+
+ {subjects.length > 0 && ( +
+ {subjects.map((subject) => ( + + {subject.subjectName} teacher + + ))} +
+ )} +
+

+ Experience — {experience} {experience === 1 ? "year" : "years"} +

+

+ Education —{" "} + {education.length > 0 + ? education.map((item, index) => ( + + {item.institution} + {index < education.length - 1 ? ", " : ""} + + )) + : "Not specified"} +

+

- {bio} -

+ > + {bio} +

+
-
-
-
- - {priceFrom} euro - - - 1 hour - - - {showBookButton && ( - - )} - First lesson - free +
+
+ + {priceFrom} euro + + + 1 hour + + + {showBookButton && ( + + )} + First lesson - free +
); diff --git a/client/src/router/routesVariables/moderatorPrivateRoutes.tsx b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx index 3886b78a..d509bd62 100644 --- a/client/src/router/routesVariables/moderatorPrivateRoutes.tsx +++ b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx @@ -7,16 +7,17 @@ import { ChatPage } from "../../pages/chat/chatPage/ChatPage.tsx"; import { EmptyChat } from "../../pages/chat/EmptyChat/EmptyChat.tsx"; import { ChatDialogPage } from "../../pages/chat/chatDialogPage/ChatDialogPage.tsx"; import { ModeratorTeachersPage } from "../../pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx"; +import { TeacherDetail } from "../../pages/teacherDetail/teacherDetail.tsx"; export const moderatorPrivateRoutes: RouteObject[] = [ { path: moderatorPrivatesRoutesVariables.teachers, element: , }, - // { - // path: studentPrivatesRoutesVariables.appointments, - // element: , - // }, + { + path: moderatorPrivatesRoutesVariables.teacher, + element: , + }, { path: chatRoutes.root, element: , diff --git a/client/src/util/statusButtons.tsx b/client/src/util/statusButtons.tsx new file mode 100644 index 00000000..53cb3805 --- /dev/null +++ b/client/src/util/statusButtons.tsx @@ -0,0 +1,56 @@ +import { twMerge } from "tailwind-merge"; +import { TeacherStatus } from "../api/teacher/teacher.type.ts"; + +export const statusUi: Record< + TeacherStatus, + { label: string; ring: string; bg: string; text: string } +> = { + active: { + label: "Active", + ring: "border-blue-500", + bg: "bg-blue-500", + text: "text-white", + }, + rejected: { + label: "Rejected", + ring: "border-red-500", + bg: "bg-red-500", + text: "text-white", + }, + blocked: { + label: "Blocked", + ring: "border-amber-900", + bg: "bg-amber-900", + text: "text-white", + }, + draft: { + label: "Draft", + ring: "border-yellow-400", + bg: "bg-yellow-400", + text: "text-dark-900", + }, + pending: { + label: "Pending", + ring: "border-purple-400", + bg: "bg-purple-400", + text: "text-white", + }, +}; +export const statusOptions: TeacherStatus[] = [ + "draft", + "pending", + "active", + "rejected", + "blocked", +]; +export const getStatusButtonClass = (s: TeacherStatus, isActive: boolean) => { + const ui = statusUi[s]; + + const base = "min-h-8 px-4 py-1 rounded-full border-2 transition"; + + const active = `${ui.bg} ${ui.text} ${ui.ring} shadow-sm`; + + const inactive = `bg-transparent ${ui.ring} text-light-100 hover:${ui.bg} hover:${ui.text}`; + + return twMerge(base, isActive ? active : inactive); +}; diff --git a/server/src/controllers/teacher.controller.ts b/server/src/controllers/teacher.controller.ts index 19eec0b5..4b663af0 100644 --- a/server/src/controllers/teacher.controller.ts +++ b/server/src/controllers/teacher.controller.ts @@ -56,7 +56,7 @@ export class TeacherController { maxRating: req.query.maxRating, }; - const teachers = await this.teacherQuery.getAllTeachers(sortData); + const teachers = await this.teacherQuery.getAllActiveTeachers(sortData); return res.status(200).send(teachers); } catch (err) { diff --git a/server/src/repositories/queryRepositories/teacher.query.ts b/server/src/repositories/queryRepositories/teacher.query.ts index f4e847cf..96000fd3 100644 --- a/server/src/repositories/queryRepositories/teacher.query.ts +++ b/server/src/repositories/queryRepositories/teacher.query.ts @@ -14,7 +14,7 @@ import { buildTeacherFilter } from "../../utils/teachersFilter.js"; @injectable() export class TeacherQuery { - async getAllTeachers( + async getAllActiveTeachers( queries: QueryTeacherInput, ): Promise { try { @@ -23,7 +23,7 @@ export class TeacherQuery { const sortBy = queries.sortBy ?? "createdAt"; const sortDirection = queries.sortDirection ?? "desc"; const filter = buildTeacherFilter(queries); - + filter.status = "active"; const items = await TeacherModel.find(filter) .sort(filterForSort(sortBy, sortDirection)) .skip((pageNumber - 1) * +pageSize) From da0d782dcda50caacda525aeba3adb599e737387 Mon Sep 17 00:00:00 2001 From: Dmytro Doronin <138795636+Dmytro-Doronin@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:39:40 +0100 Subject: [PATCH 3/4] fet: change status for teacher --- client/src/api/moderator/moderator.api.ts | 15 ++- client/src/api/teacher/teacher.api.ts | 7 ++ client/src/components/cardsList/CardsList.tsx | 16 ++- .../components/statusChanger/StatusChange.tsx | 37 ++++++ .../components/teacherCard/teacherCard.tsx | 39 +++--- .../moderator/mutation/useChangeStatus.ts | 117 ++++++++++++++++++ .../query/useTeacherForModeratorQuery.tsx | 1 + .../teachers/query/useTeacherQuery.ts | 14 ++- .../teachers/query/useTeachersQuery.tsx | 1 + .../ModeratorTeaschersPage.tsx | 18 ++- .../src/pages/teacherDetail/teacherDetail.tsx | 19 ++- client/src/util/patchTeachersList.util.ts | 24 ++++ server/src/controllers/teacher.controller.ts | 18 ++- server/src/routes/teacherRoute.ts | 7 ++ 14 files changed, 300 insertions(+), 33 deletions(-) create mode 100644 client/src/components/statusChanger/StatusChange.tsx create mode 100644 client/src/features/moderator/mutation/useChangeStatus.ts create mode 100644 client/src/util/patchTeachersList.util.ts diff --git a/client/src/api/moderator/moderator.api.ts b/client/src/api/moderator/moderator.api.ts index e9500000..6b312eea 100644 --- a/client/src/api/moderator/moderator.api.ts +++ b/client/src/api/moderator/moderator.api.ts @@ -1,7 +1,20 @@ import { LoginFormTypes } from "../auth/types.ts"; -import { apiPublic } from "../api.ts"; +import { apiProtected, apiPublic } from "../api.ts"; +import { TeacherStatus } from "../teacher/teacher.type.ts"; export async function loginModeratorApi(data: LoginFormTypes) { const res = await apiPublic.post("/api/moderator/auth/login", data); return res.data as { accessToken: string }; } + +export async function moderatorChangeTeacherStatusApi({ + id, + status, +}: { + id: string; + status: TeacherStatus; +}) { + return await apiProtected.patch(`/api/moderator/teachers/${id}/status`, { + status, + }); +} diff --git a/client/src/api/teacher/teacher.api.ts b/client/src/api/teacher/teacher.api.ts index 43aa4f32..ff89df7b 100644 --- a/client/src/api/teacher/teacher.api.ts +++ b/client/src/api/teacher/teacher.api.ts @@ -33,6 +33,13 @@ export async function getTeacherByIdApi(teacherId: string) { return res.data; } +export async function getTeacherByIdForModeratorApi(teacherId: string) { + const res = await apiProtected.get( + `/api/teachers/${teacherId}/moderator`, + ); + return res.data; +} + type ApiSlot = { start: string; end: string }; type ApiAvailability = { monday: ApiSlot[]; diff --git a/client/src/components/cardsList/CardsList.tsx b/client/src/components/cardsList/CardsList.tsx index 715b8884..7cbfb78f 100644 --- a/client/src/components/cardsList/CardsList.tsx +++ b/client/src/components/cardsList/CardsList.tsx @@ -1,12 +1,18 @@ import { TeacherCard } from "../teacherCard/teacherCard"; -import { TeacherType } from "../../api/teacher/teacher.type"; +import { TeacherStatus, TeacherType } from "../../api/teacher/teacher.type"; import { motion, type Variants, useReducedMotion } from "framer-motion"; import { useRef } from "react"; type CardsListType = { cards: TeacherType[]; + changeStatus?: (id: string, status: TeacherStatus) => void; + isStatusPending?: boolean; }; -export const CardsList = ({ cards }: CardsListType) => { +export const CardsList = ({ + cards, + changeStatus, + isStatusPending, +}: CardsListType) => { const reduceMotion = useReducedMotion(); const listVariants: Variants = { @@ -44,7 +50,11 @@ export const CardsList = ({ cards }: CardsListType) => { > {cards.map((teacher) => ( - + ))} diff --git a/client/src/components/statusChanger/StatusChange.tsx b/client/src/components/statusChanger/StatusChange.tsx new file mode 100644 index 00000000..a42d9fca --- /dev/null +++ b/client/src/components/statusChanger/StatusChange.tsx @@ -0,0 +1,37 @@ +import { + getStatusButtonClass, + statusOptions, + statusUi, +} from "../../util/statusButtons.tsx"; +import { Button } from "../ui/button/Button.tsx"; +import { TeacherStatus } from "../../api/teacher/teacher.type.ts"; + +type StatusChangeProps = { + id: string; + changeStatus: (id: string, status: TeacherStatus) => void; + status: TeacherStatus; +}; + +export const StatusChange = ({ + changeStatus, + id, + status, +}: StatusChangeProps) => { + return ( + <> +
+ {statusOptions.map((s) => ( + + ))} +
+ + ); +}; diff --git a/client/src/components/teacherCard/teacherCard.tsx b/client/src/components/teacherCard/teacherCard.tsx index 7d7a3af8..227f67dd 100644 --- a/client/src/components/teacherCard/teacherCard.tsx +++ b/client/src/components/teacherCard/teacherCard.tsx @@ -1,21 +1,20 @@ import { Button } from "../ui/button/Button"; import { Rating } from "../rating/Rating"; -import { TeacherType } from "../../api/teacher/teacher.type"; +import { TeacherStatus, TeacherType } from "../../api/teacher/teacher.type"; import { useNavigate } from "react-router-dom"; import { getAvatarUrl } from "../../api/upload/upload.api"; import DefaultAvatarIcon from "../icons/DefaultAvatarIcon"; import { cva, VariantProps } from "class-variance-authority"; import { twMerge } from "tailwind-merge"; import { useAuthSessionStore } from "../../store/authSession.store.ts"; -import { - getStatusButtonClass, - statusOptions, - statusUi, -} from "../../util/statusButtons.tsx"; +import { Loader } from "../loader/Loader.tsx"; +import { StatusChange } from "../statusChanger/StatusChange.tsx"; type TeacherCardType = { teacher: TeacherType; showBookButton?: boolean; + changeStatus?: (id: string, status: TeacherStatus) => void; + isStatusPending?: boolean; }; const teacherCardVariants = cva( @@ -42,7 +41,9 @@ const teacherCardClassName = ( export const TeacherCard = ({ teacher, + changeStatus, showBookButton = true, + isStatusPending, }: TeacherCardType) => { const { id, @@ -63,24 +64,21 @@ export const TeacherCard = ({ const avatarUrl = getAvatarUrl(profileImageUrl || null); const handleBookClick = () => { + if (user?.role === "moderator") { + navigate(`/moderator/teachers/${id}`); + return; + } navigate(`/teacher/${id}`); }; + + const onChangeStatus = (id: string, status: TeacherStatus) => { + changeStatus?.(id, status); + }; + return (
- {user?.role === "moderator" && ( -
- {statusOptions.map((s) => ( - - ))} -
+ {Boolean(changeStatus) && ( + )}
First lesson - free
+ {isStatusPending && }
); }; diff --git a/client/src/features/moderator/mutation/useChangeStatus.ts b/client/src/features/moderator/mutation/useChangeStatus.ts new file mode 100644 index 00000000..dbc39db4 --- /dev/null +++ b/client/src/features/moderator/mutation/useChangeStatus.ts @@ -0,0 +1,117 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useNotificationStore } from "../../../store/notification.store.ts"; +import { queryKeys } from "../../queryKeys.ts"; +import { getErrorMessage } from "../../../util/ErrorUtil.ts"; +import { moderatorChangeTeacherStatusApi } from "../../../api/moderator/moderator.api.ts"; +import { + TeacherOutputModel, + TeacherStatus, + TeacherType, +} from "../../../api/teacher/teacher.type.ts"; +import { + patchPublicList, + patchTeacherInList, +} from "../../../util/patchTeachersList.util.ts"; + +type Vars = { id: string; status: TeacherStatus }; +type Ctx = { + prevTeachersQueries: Array< + [readonly unknown[], TeacherOutputModel | undefined] + >; + prevTeacherDetail: TeacherType | undefined; +}; + +export function useChangeStatusMutation() { + const qc = useQueryClient(); + const success = useNotificationStore((s) => s.success); + const notifyError = useNotificationStore((s) => s.error); + + return useMutation({ + mutationFn: (vars) => + moderatorChangeTeacherStatusApi(vars).then(() => undefined), + onMutate: async ({ id, status }) => { + await qc.cancelQueries({ queryKey: queryKeys.teachers.all }); + + const prevPublicLists = qc.getQueriesData({ + queryKey: ["teachers", "list"], + }); + + const prevModeratorLists = qc + .getQueriesData({ queryKey: ["teachers"] }) + .filter( + ([key]) => + key.length === 2 && + key[0] === "teachers" && + typeof key[1] === "object", + ); + + const prevTeacherDetail = qc.getQueryData( + queryKeys.teacher(id), + ); + + prevPublicLists.forEach(([key, data]) => { + if (!data) { + return; + } + qc.setQueryData( + key, + patchPublicList(data, id, status), + ); + }); + + prevModeratorLists.forEach(([key, data]) => { + if (!data) { + return; + } + qc.setQueryData( + key, + patchTeacherInList(data, id, status), + ); + }); + + if (prevTeacherDetail) { + qc.setQueryData(queryKeys.teacher(id), { + ...prevTeacherDetail, + status, + }); + } + + return { + prevTeachersQueries: [...prevPublicLists, ...prevModeratorLists], + prevTeacherDetail, + }; + }, + onSuccess: async () => { + success("Status has been successfully changed"); + await qc.invalidateQueries({ + predicate: (q) => { + const key = q.queryKey; + return key[0] === "teachers" && key[1] === "list"; + }, + }); + }, + onError: (error, _vars, ctx) => { + const msg = getErrorMessage(error); + notifyError(msg); + if (!ctx) { + return; + } + ctx.prevTeachersQueries.forEach(([key, data]) => { + if (data === undefined) { + return; + } + qc.setQueryData(key, data); + }); + + if (ctx.prevTeacherDetail !== undefined) { + qc.setQueryData( + queryKeys.teacher(_vars.id), + ctx.prevTeacherDetail, + ); + } + }, + onSettled: (_data, _error, { id }) => { + qc.invalidateQueries({ queryKey: queryKeys.teacher(id) }); + }, + }); +} diff --git a/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx index 7cc84f63..230c0cf0 100644 --- a/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx +++ b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx @@ -17,6 +17,7 @@ export function useTeachersForModeratorQuery( retry: false, placeholderData: keepPreviousData, staleTime: 20 * 60 * 1000, + refetchOnMount: "always", }); useEffect(() => { diff --git a/client/src/features/teachers/query/useTeacherQuery.ts b/client/src/features/teachers/query/useTeacherQuery.ts index dd85cf5b..7abb5ed0 100644 --- a/client/src/features/teachers/query/useTeacherQuery.ts +++ b/client/src/features/teachers/query/useTeacherQuery.ts @@ -1,11 +1,21 @@ import { useQuery } from "@tanstack/react-query"; import { queryKeys } from "../../queryKeys"; -import { getTeacherByIdApi } from "../../../api/teacher/teacher.api"; +import { + getTeacherByIdApi, + getTeacherByIdForModeratorApi, +} from "../../../api/teacher/teacher.api"; +import { useAuthSessionStore } from "../../../store/authSession.store.ts"; export const useTeacherQuery = (teacherId: string) => { + const role = useAuthSessionStore((s) => s.user?.role); + + const isModerator = role === "moderator"; return useQuery({ queryKey: queryKeys.teacher(teacherId), - queryFn: () => getTeacherByIdApi(teacherId), + queryFn: () => + isModerator + ? getTeacherByIdForModeratorApi(teacherId) + : getTeacherByIdApi(teacherId), enabled: !!teacherId, }); }; diff --git a/client/src/features/teachers/query/useTeachersQuery.tsx b/client/src/features/teachers/query/useTeachersQuery.tsx index a3b47f0c..fc6da529 100644 --- a/client/src/features/teachers/query/useTeachersQuery.tsx +++ b/client/src/features/teachers/query/useTeachersQuery.tsx @@ -16,6 +16,7 @@ export function useTeachersQuery(params: TeachersQuery) { retry: false, placeholderData: keepPreviousData, staleTime: 20 * 60 * 1000, + refetchOnMount: "always", }); useEffect(() => { diff --git a/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx b/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx index 22f09073..c100c0d0 100644 --- a/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx +++ b/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx @@ -2,14 +2,20 @@ import { CardsList } from "../../components/cardsList/CardsList"; import { Pagination } from "../../components/ui/pagination/Pagination"; import { useRef, useState } from "react"; import { TeachersCardsSkeletonList } from "../../components/skeletons/TeachersCardsSkeletonList.tsx"; -import { SortByTeachersForModerator } from "../../api/teacher/teacher.type.ts"; +import { + SortByTeachersForModerator, + TeacherStatus, +} from "../../api/teacher/teacher.type.ts"; import { useTeachersForModeratorQuery } from "../../features/teachers/query/useTeacherForModeratorQuery.tsx"; +import { useChangeStatusMutation } from "../../features/moderator/mutation/useChangeStatus.ts"; export const ModeratorTeachersPage = () => { const [page, setPage] = useState(1); const [sortBy] = useState("status"); const listTopRef = useRef(null); + const { mutate, isPending } = useChangeStatusMutation(); + const handlePageChange = (page: number) => { setPage(page); @@ -20,6 +26,10 @@ export const ModeratorTeachersPage = () => { }); }; + const changeStatus = (id: string, status: TeacherStatus) => { + mutate({ id, status }); + }; + const { data, isFetching } = useTeachersForModeratorQuery({ pageNumber: page, pageSize: 10, @@ -43,7 +53,11 @@ export const ModeratorTeachersPage = () => { {isFetching ? ( ) : data?.items?.length ? ( - + ) : (
No teachers
)} diff --git a/client/src/pages/teacherDetail/teacherDetail.tsx b/client/src/pages/teacherDetail/teacherDetail.tsx index 1cc11f2f..a219f040 100644 --- a/client/src/pages/teacherDetail/teacherDetail.tsx +++ b/client/src/pages/teacherDetail/teacherDetail.tsx @@ -9,16 +9,22 @@ import { useState } from "react"; import { useTeacherQuery } from "../../features/teachers/query/useTeacherQuery"; import { TeacherCardSkeleton } from "../../components/skeletons/TeacherCardSkeleton"; import { ReviewsManager } from "../../components/teacherSection/Reviews/ReviewsManager"; - +import { useMatch } from "react-router-dom"; +import { useChangeStatusMutation } from "../../features/moderator/mutation/useChangeStatus.ts"; +import { TeacherStatus } from "../../api/teacher/teacher.type.ts"; type TabType = "about" | "subjects" | "schedule"; export const TeacherDetail = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const [activeTab, setActiveTab] = useState("subjects"); - + const isModeratorRoute = Boolean(useMatch("/moderator/*")); const { data: teacher, isLoading, error } = useTeacherQuery(id || ""); - + const { mutate: changeStatusMutation, isPending: isChangeStatusPrnding } = + useChangeStatusMutation(); + const changeStatus = (id: string, status: TeacherStatus) => { + changeStatusMutation({ id, status }); + }; const handleBack = () => { navigate(-1); }; @@ -82,7 +88,12 @@ export const TeacherDetail = () => { Back
- +
diff --git a/client/src/util/patchTeachersList.util.ts b/client/src/util/patchTeachersList.util.ts new file mode 100644 index 00000000..0b064a1a --- /dev/null +++ b/client/src/util/patchTeachersList.util.ts @@ -0,0 +1,24 @@ +import { + TeacherOutputModel, + TeacherStatus, +} from "../api/teacher/teacher.type.ts"; + +export const patchTeacherInList = ( + data: TeacherOutputModel, + id: string, + status: TeacherStatus, +): TeacherOutputModel => ({ + ...data, + items: data.items.map((t) => (t.id === id ? { ...t, status } : t)), +}); + +export const patchPublicList = ( + data: TeacherOutputModel, + id: string, + status: TeacherStatus, +): TeacherOutputModel => { + if (status !== "active") { + return { ...data, items: data.items.filter((t) => t.id !== id) }; + } + return patchTeacherInList(data, id, status); +}; diff --git a/server/src/controllers/teacher.controller.ts b/server/src/controllers/teacher.controller.ts index 4b663af0..744b637e 100644 --- a/server/src/controllers/teacher.controller.ts +++ b/server/src/controllers/teacher.controller.ts @@ -94,7 +94,7 @@ export class TeacherController { try { const teacher = await this.teacherQuery.getTeacherById(req.params.id); - if (!teacher) { + if (!teacher || teacher.status !== "active") { return res.status(404).json({ message: "Teacher not found" }); } @@ -104,6 +104,22 @@ export class TeacherController { } } + async getTeacherByIdForModerator( + req: RequestWithParams, + res: Response, + next: NextFunction, + ) { + const { id } = req.params; + + try { + const teacher = await this.teacherQuery.getTeacherById(id); + + return res.status(200).json(teacher); + } catch (err) { + return next(err); + } + } + async getMyWeeklyAvailability( req: Request, res: Response, diff --git a/server/src/routes/teacherRoute.ts b/server/src/routes/teacherRoute.ts index bab5129e..64fa7ec4 100644 --- a/server/src/routes/teacherRoute.ts +++ b/server/src/routes/teacherRoute.ts @@ -64,6 +64,13 @@ teacherRouter.get( teacherController.getTeacherById.bind(teacherController), ); +teacherRouter.get( + "/:id/moderator", + authMiddleware.handle, + requireRole("moderator"), + teacherController.getTeacherByIdForModerator.bind(teacherController), +); + teacherRouter.delete( "/:id", authMiddleware.handle, From d3845b1486b2bc1f852fd4a21c943adb8f08cac9 Mon Sep 17 00:00:00 2001 From: Dmytro Doronin <138795636+Dmytro-Doronin@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:26:33 +0100 Subject: [PATCH 4/4] fix: copilot suggestions --- .../moderator/mutation/useChangeStatus.ts | 63 ++++++++++--------- client/src/features/queryKeys.ts | 13 ++-- .../mutations/useUpdateMyProfileMutation.ts | 7 ++- .../query/useTeacherForModeratorQuery.tsx | 2 +- .../teachers/query/useTeacherQuery.ts | 9 ++- .../teachers/query/useTeachersQuery.tsx | 2 +- client/src/layouts/PrivateLayout.tsx | 3 +- ...hersPage.tsx => ModeratorTeachersPage.tsx} | 0 .../src/pages/teacherDetail/teacherDetail.tsx | 4 +- client/src/router/RoleIndexRedirect.tsx | 1 + .../moderatorPrivateRoutes.tsx | 2 +- server/src/controllers/teacher.controller.ts | 3 + 12 files changed, 64 insertions(+), 45 deletions(-) rename client/src/pages/moderatorTeachersPage/{ModeratorTeaschersPage.tsx => ModeratorTeachersPage.tsx} (100%) diff --git a/client/src/features/moderator/mutation/useChangeStatus.ts b/client/src/features/moderator/mutation/useChangeStatus.ts index dbc39db4..59fa6256 100644 --- a/client/src/features/moderator/mutation/useChangeStatus.ts +++ b/client/src/features/moderator/mutation/useChangeStatus.ts @@ -15,10 +15,11 @@ import { type Vars = { id: string; status: TeacherStatus }; type Ctx = { - prevTeachersQueries: Array< + prevPublicLists: Array<[readonly unknown[], TeacherOutputModel | undefined]>; + prevModeratorLists: Array< [readonly unknown[], TeacherOutputModel | undefined] >; - prevTeacherDetail: TeacherType | undefined; + prevTeacherModeratorDetail: TeacherType | undefined; }; export function useChangeStatusMutation() { @@ -33,20 +34,15 @@ export function useChangeStatusMutation() { await qc.cancelQueries({ queryKey: queryKeys.teachers.all }); const prevPublicLists = qc.getQueriesData({ - queryKey: ["teachers", "list"], + queryKey: ["teachers", "publicList"], }); - const prevModeratorLists = qc - .getQueriesData({ queryKey: ["teachers"] }) - .filter( - ([key]) => - key.length === 2 && - key[0] === "teachers" && - typeof key[1] === "object", - ); + const prevModeratorLists = qc.getQueriesData({ + queryKey: ["teachers", "moderatorList"], + }); - const prevTeacherDetail = qc.getQueryData( - queryKeys.teacher(id), + const prevTeacherModeratorDetail = qc.getQueryData( + queryKeys.teacherModerator(id), ); prevPublicLists.forEach(([key, data]) => { @@ -69,26 +65,22 @@ export function useChangeStatusMutation() { ); }); - if (prevTeacherDetail) { - qc.setQueryData(queryKeys.teacher(id), { - ...prevTeacherDetail, + if (prevTeacherModeratorDetail) { + qc.setQueryData(queryKeys.teacherModerator(id), { + ...prevTeacherModeratorDetail, status, }); } return { - prevTeachersQueries: [...prevPublicLists, ...prevModeratorLists], - prevTeacherDetail, + prevPublicLists, + prevModeratorLists, + prevTeacherModeratorDetail, }; }, onSuccess: async () => { success("Status has been successfully changed"); - await qc.invalidateQueries({ - predicate: (q) => { - const key = q.queryKey; - return key[0] === "teachers" && key[1] === "list"; - }, - }); + await qc.invalidateQueries({ queryKey: ["teachers", "publicList"] }); }, onError: (error, _vars, ctx) => { const msg = getErrorMessage(error); @@ -96,22 +88,31 @@ export function useChangeStatusMutation() { if (!ctx) { return; } - ctx.prevTeachersQueries.forEach(([key, data]) => { - if (data === undefined) { + ctx.prevPublicLists.forEach(([key, data]) => { + if (!data) { return; } qc.setQueryData(key, data); }); - if (ctx.prevTeacherDetail !== undefined) { + ctx.prevModeratorLists.forEach(([key, data]) => { + if (!data) { + return; + } + qc.setQueryData(key, data); + }); + + if (ctx.prevTeacherModeratorDetail) { qc.setQueryData( - queryKeys.teacher(_vars.id), - ctx.prevTeacherDetail, + _vars ? queryKeys.teacherModerator(_vars.id) : ["_"], + ctx.prevTeacherModeratorDetail, ); } }, - onSettled: (_data, _error, { id }) => { - qc.invalidateQueries({ queryKey: queryKeys.teacher(id) }); + onSettled: async (_d, _e, vars) => { + await qc.invalidateQueries({ + queryKey: queryKeys.teacherModerator(vars.id), + }); }, }); } diff --git a/client/src/features/queryKeys.ts b/client/src/features/queryKeys.ts index e386ac8e..6869a6da 100644 --- a/client/src/features/queryKeys.ts +++ b/client/src/features/queryKeys.ts @@ -13,11 +13,16 @@ export const queryKeys = { teachers: { all: ["teachers"] as const, myProfile: () => ["teachers", "me"] as const, - list: (params: TeachersQuery) => ["teachers", "list", params] as const, + publicList: (params: TeachersQuery) => + ["teachers", "publicList", params] as const, + + moderatorList: (params: TeachersForModeratorQuery) => + ["teachers", "moderatorList", params] as const, }, - teacher: (id: string) => ["teachers", id] as const, - teachersList: (params: TeachersQuery | TeachersForModeratorQuery) => - ["teachers", params] as const, + + teacherPublic: (id: string) => ["teachers", "publicDetail", id] as const, + teacherModerator: (id: string) => + ["teachers", "moderatorDetail", id] as const, appointments: ["appointments"] as const, teacherAppointments: (teacherId: string, page?: number, limit?: number) => diff --git a/client/src/features/teachers/mutations/useUpdateMyProfileMutation.ts b/client/src/features/teachers/mutations/useUpdateMyProfileMutation.ts index ed1e0dfe..7c5296d5 100644 --- a/client/src/features/teachers/mutations/useUpdateMyProfileMutation.ts +++ b/client/src/features/teachers/mutations/useUpdateMyProfileMutation.ts @@ -12,9 +12,14 @@ export const useUpdateMyProfileMutation = () => { queryClient.invalidateQueries({ queryKey: queryKeys.teachers.myProfile(), }); + queryClient.invalidateQueries({ queryKey: queryKeys.teachers.all }); + + queryClient.invalidateQueries({ + queryKey: queryKeys.teacherPublic(updatedTeacher.id), + }); queryClient.invalidateQueries({ - queryKey: queryKeys.teacher(updatedTeacher.id), + queryKey: queryKeys.teacherModerator(updatedTeacher.id), }); }, }); diff --git a/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx index 230c0cf0..0de563c5 100644 --- a/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx +++ b/client/src/features/teachers/query/useTeacherForModeratorQuery.tsx @@ -12,7 +12,7 @@ export function useTeachersForModeratorQuery( const notifyError = useNotificationStore((s) => s.error); const query = useQuery({ - queryKey: queryKeys.teachersList(params), + queryKey: queryKeys.teachers.moderatorList(params), queryFn: () => getAllTeachersForModeratorApi(params), retry: false, placeholderData: keepPreviousData, diff --git a/client/src/features/teachers/query/useTeacherQuery.ts b/client/src/features/teachers/query/useTeacherQuery.ts index 7abb5ed0..67b5e4f2 100644 --- a/client/src/features/teachers/query/useTeacherQuery.ts +++ b/client/src/features/teachers/query/useTeacherQuery.ts @@ -8,14 +8,17 @@ import { useAuthSessionStore } from "../../../store/authSession.store.ts"; export const useTeacherQuery = (teacherId: string) => { const role = useAuthSessionStore((s) => s.user?.role); - const isModerator = role === "moderator"; + return useQuery({ - queryKey: queryKeys.teacher(teacherId), + queryKey: isModerator + ? queryKeys.teacherModerator(teacherId) + : queryKeys.teacherPublic(teacherId), queryFn: () => isModerator ? getTeacherByIdForModeratorApi(teacherId) : getTeacherByIdApi(teacherId), - enabled: !!teacherId, + enabled: Boolean(teacherId), + retry: false, }); }; diff --git a/client/src/features/teachers/query/useTeachersQuery.tsx b/client/src/features/teachers/query/useTeachersQuery.tsx index fc6da529..bc029ed6 100644 --- a/client/src/features/teachers/query/useTeachersQuery.tsx +++ b/client/src/features/teachers/query/useTeachersQuery.tsx @@ -11,7 +11,7 @@ export function useTeachersQuery(params: TeachersQuery) { const notifyError = useNotificationStore((s) => s.error); const query = useQuery({ - queryKey: queryKeys.teachersList(params), + queryKey: queryKeys.teachers.publicList(params), queryFn: () => getAllTeachersApi(params), retry: false, placeholderData: keepPreviousData, diff --git a/client/src/layouts/PrivateLayout.tsx b/client/src/layouts/PrivateLayout.tsx index 4771340d..18322c75 100644 --- a/client/src/layouts/PrivateLayout.tsx +++ b/client/src/layouts/PrivateLayout.tsx @@ -9,13 +9,14 @@ import { TopBar } from "../components/headerPrivate/TopBar.tsx"; import { useAuthSessionStore } from "../store/authSession.store.ts"; import { useEffect } from "react"; import { useSocketStore } from "../store/socket.store.ts"; +import { usePresenceSubscribe } from "../hooks/usePresenceSubscribe.ts"; export const PrivateLayout = () => { const user = useAuthSessionStore((s) => s.user); const accessToken = useAuthSessionStore((s) => s.accessToken); const connect = useSocketStore((s) => s.connect); const disconnect = useSocketStore((s) => s.disconnect); - + usePresenceSubscribe(); let items; if (user?.role === "teacher") { diff --git a/client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx b/client/src/pages/moderatorTeachersPage/ModeratorTeachersPage.tsx similarity index 100% rename from client/src/pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx rename to client/src/pages/moderatorTeachersPage/ModeratorTeachersPage.tsx diff --git a/client/src/pages/teacherDetail/teacherDetail.tsx b/client/src/pages/teacherDetail/teacherDetail.tsx index a219f040..3b314d70 100644 --- a/client/src/pages/teacherDetail/teacherDetail.tsx +++ b/client/src/pages/teacherDetail/teacherDetail.tsx @@ -20,7 +20,7 @@ export const TeacherDetail = () => { const [activeTab, setActiveTab] = useState("subjects"); const isModeratorRoute = Boolean(useMatch("/moderator/*")); const { data: teacher, isLoading, error } = useTeacherQuery(id || ""); - const { mutate: changeStatusMutation, isPending: isChangeStatusPrnding } = + const { mutate: changeStatusMutation, isPending: isChangeStatusPending } = useChangeStatusMutation(); const changeStatus = (id: string, status: TeacherStatus) => { changeStatusMutation({ id, status }); @@ -92,7 +92,7 @@ export const TeacherDetail = () => { teacher={teacher} showBookButton={false} changeStatus={isModeratorRoute ? changeStatus : undefined} - isStatusPending={isModeratorRoute ? isChangeStatusPrnding : undefined} + isStatusPending={isModeratorRoute ? isChangeStatusPending : undefined} />
diff --git a/client/src/router/RoleIndexRedirect.tsx b/client/src/router/RoleIndexRedirect.tsx index d60d927e..b96c2621 100644 --- a/client/src/router/RoleIndexRedirect.tsx +++ b/client/src/router/RoleIndexRedirect.tsx @@ -29,4 +29,5 @@ export const RoleIndexRedirect = () => { /> ); } + return ; }; diff --git a/client/src/router/routesVariables/moderatorPrivateRoutes.tsx b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx index d509bd62..ebfa3bbb 100644 --- a/client/src/router/routesVariables/moderatorPrivateRoutes.tsx +++ b/client/src/router/routesVariables/moderatorPrivateRoutes.tsx @@ -6,7 +6,7 @@ import { import { ChatPage } from "../../pages/chat/chatPage/ChatPage.tsx"; import { EmptyChat } from "../../pages/chat/EmptyChat/EmptyChat.tsx"; import { ChatDialogPage } from "../../pages/chat/chatDialogPage/ChatDialogPage.tsx"; -import { ModeratorTeachersPage } from "../../pages/moderatorTeachersPage/ModeratorTeaschersPage.tsx"; +import { ModeratorTeachersPage } from "../../pages/moderatorTeachersPage/ModeratorTeachersPage.tsx"; import { TeacherDetail } from "../../pages/teacherDetail/teacherDetail.tsx"; export const moderatorPrivateRoutes: RouteObject[] = [ diff --git a/server/src/controllers/teacher.controller.ts b/server/src/controllers/teacher.controller.ts index 744b637e..6d57ec48 100644 --- a/server/src/controllers/teacher.controller.ts +++ b/server/src/controllers/teacher.controller.ts @@ -114,6 +114,9 @@ export class TeacherController { try { const teacher = await this.teacherQuery.getTeacherById(id); + if (!teacher) { + return res.status(404).json({ message: "Teacher not found" }); + } return res.status(200).json(teacher); } catch (err) { return next(err);