Merge pull request #206 from SELab-2/feat/class-functionality
Feat/class functionality
This commit is contained in:
		
						commit
						2fe87fbe57
					
				
					 21 changed files with 1008 additions and 494 deletions
				
			
		|  | @ -40,7 +40,7 @@ export async function updateInvitationHandler(req: Request, res: Response): Prom | |||
|     const sender = req.body.sender; | ||||
|     const receiver = req.body.receiver; | ||||
|     const classId = req.body.class; | ||||
|     req.body.accepted = req.body.accepted !== 'false'; | ||||
|     req.body.accepted = req.body.accepted !== false; | ||||
|     requireFields({ sender, receiver, classId }); | ||||
| 
 | ||||
|     const data = req.body as TeacherInvitationData; | ||||
|  |  | |||
|  | @ -34,6 +34,6 @@ router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); | |||
| router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); | ||||
| 
 | ||||
| // Invitations to other classes a teacher received
 | ||||
| router.get('/invitations', invitationRouter); | ||||
| router.use('/invitations', invitationRouter); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ export interface StudentResponse { | |||
|     student: StudentDTO; | ||||
| } | ||||
| export interface JoinRequestsResponse { | ||||
|     requests: ClassJoinRequestDTO[]; | ||||
|     joinRequests: ClassJoinRequestDTO[]; | ||||
| } | ||||
| export interface JoinRequestResponse { | ||||
|     request: ClassJoinRequestDTO; | ||||
|  |  | |||
|  | @ -11,10 +11,11 @@ export interface TeacherInvitationResponse { | |||
| 
 | ||||
| export class TeacherInvitationController extends BaseController { | ||||
|     constructor() { | ||||
|         super("teachers/invitations"); | ||||
|         super("teacher/invitations"); | ||||
|     } | ||||
| 
 | ||||
|     async getAll(username: string, sent: boolean): Promise<TeacherInvitationsResponse> { | ||||
|     async getAll(username: string, s: boolean): Promise<TeacherInvitationsResponse> { | ||||
|         const sent = s.toString(); | ||||
|         return this.get<TeacherInvitationsResponse>(`/${username}`, { sent }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,10 +54,9 @@ export class TeacherController extends BaseController { | |||
|         studentUsername: string, | ||||
|         accepted: boolean, | ||||
|     ): Promise<JoinRequestResponse> { | ||||
|         return this.put<JoinRequestResponse>( | ||||
|             `/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, | ||||
|         return this.put<JoinRequestResponse>(`/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, { | ||||
|             accepted, | ||||
|         ); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);}
 | ||||
|  |  | |||
|  | @ -75,7 +75,6 @@ | |||
|     "sent": "sent", | ||||
|     "failed": "fehlgeschlagen", | ||||
|     "wrong": "etwas ist schief gelaufen", | ||||
|     "created": "erstellt", | ||||
|     "submitSolution": "Lösung einreichen", | ||||
|     "submitNewSolution": "Neue Lösung einreichen", | ||||
|     "markAsDone": "Als fertig markieren", | ||||
|  | @ -86,5 +85,18 @@ | |||
|     "loadSubmission": "Einladen", | ||||
|     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", | ||||
|     "viewAsGroup": "Fortschritt ansehen von Gruppe...", | ||||
|     "assignLearningPath": "Als Aufgabe geben" | ||||
|     "assignLearningPath": "Als Aufgabe geben", | ||||
|     "created": "erstellt", | ||||
|     "remove": "entfernen", | ||||
|     "students": "Studenten", | ||||
|     "classJoinRequests": "Anfragen verbinden", | ||||
|     "reject": "zurückweisen", | ||||
|     "areusure": "Sind Sie sicher?", | ||||
|     "yes": "ja", | ||||
|     "teachers": "Lehrer", | ||||
|     "rejected": "abgelehnt", | ||||
|     "accepted": "akzeptiert", | ||||
|     "enterUsername": "Geben Sie den Benutzernamen der Lehrkraft ein, die Sie einladen möchten", | ||||
|     "username": "Nutzername", | ||||
|     "invite": "einladen" | ||||
| } | ||||
|  |  | |||
|  | @ -75,7 +75,6 @@ | |||
|     "sent": "sent", | ||||
|     "failed": "failed", | ||||
|     "wrong": "something went wrong", | ||||
|     "created": "created", | ||||
|     "submitSolution": "Submit solution", | ||||
|     "submitNewSolution": "Submit new solution", | ||||
|     "markAsDone": "Mark as completed", | ||||
|  | @ -86,5 +85,18 @@ | |||
|     "loadSubmission": "Load", | ||||
|     "noSubmissionsYet": "No submissions yet.", | ||||
|     "viewAsGroup": "View progress of group...", | ||||
|     "assignLearningPath": "assign" | ||||
|     "assignLearningPath": "assign", | ||||
|     "created": "created", | ||||
|     "remove": "remove", | ||||
|     "students": "students", | ||||
|     "classJoinRequests": "join requests", | ||||
|     "reject": "reject", | ||||
|     "areusure": "Are you sure?", | ||||
|     "yes": "yes", | ||||
|     "teachers": "teachers", | ||||
|     "accepted": "accepted", | ||||
|     "rejected": "rejected", | ||||
|     "enterUsername": "enter the username of the teacher you would like to invite", | ||||
|     "username": "username", | ||||
|     "invite": "invite" | ||||
| } | ||||
|  |  | |||
|  | @ -86,5 +86,17 @@ | |||
|     "loadSubmission": "Charger", | ||||
|     "noSubmissionsYet": "Pas encore de soumissions.", | ||||
|     "viewAsGroup": "Voir la progression du groupe...", | ||||
|     "assignLearningPath": "donner comme tâche" | ||||
|     "assignLearningPath": "donner comme tâche", | ||||
|     "remove": "supprimer", | ||||
|     "students": "étudiants", | ||||
|     "classJoinRequests": "demandes d'adhésion", | ||||
|     "reject": "rejeter", | ||||
|     "areusure": "Êtes-vous sûr?", | ||||
|     "yes": "oui", | ||||
|     "teachers": "enseignants", | ||||
|     "accepted": "acceptée", | ||||
|     "rejected": "rejetée", | ||||
|     "enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter", | ||||
|     "username": "Nom d'utilisateur", | ||||
|     "invite": "inviter" | ||||
| } | ||||
|  |  | |||
|  | @ -86,5 +86,17 @@ | |||
|     "loadSubmission": "Inladen", | ||||
|     "noSubmissionsYet": "Nog geen indieningen.", | ||||
|     "viewAsGroup": "Vooruitgang bekijken van groep...", | ||||
|     "assignLearningPath": "Als opdracht geven" | ||||
|     "assignLearningPath": "Als opdracht geven", | ||||
|     "remove": "verwijder", | ||||
|     "students": "studenten", | ||||
|     "classJoinRequests": "deelname verzoeken", | ||||
|     "reject": "weiger", | ||||
|     "areusure": "Bent u zeker?", | ||||
|     "yes": "ja", | ||||
|     "teachers": "leerkrachten", | ||||
|     "accepted": "geaccepteerd", | ||||
|     "rejected": "geweigerd", | ||||
|     "enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in", | ||||
|     "username": "gebruikersnaam", | ||||
|     "invite": "uitnodigen" | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,37 @@ | |||
| import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts"; | ||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | ||||
| import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||
| import { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||
| import { | ||||
|     useMutation, | ||||
|     type UseMutationReturnType, | ||||
|     useQuery, | ||||
|     type UseQueryReturnType, | ||||
|     useQueryClient, | ||||
| } from "@tanstack/vue-query"; | ||||
| import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts"; | ||||
| import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts"; | ||||
| import type { AnswerData } from "@dwengo-1/common/interfaces/answer"; | ||||
| import type { QuestionId } from "@dwengo-1/common/interfaces/question"; | ||||
| 
 | ||||
| // TODO caching
 | ||||
| /** 🔑 Query keys */ | ||||
| export function answersQueryKey( | ||||
|     questionId: QuestionId, | ||||
|     full: boolean, | ||||
| ): [string, string, number, string, number, boolean] { | ||||
|     const loId = questionId.learningObjectIdentifier; | ||||
|     return ["answers", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber, full]; | ||||
| } | ||||
| export function answerQueryKey( | ||||
|     questionId: QuestionId, | ||||
|     sequenceNumber: number, | ||||
| ): [string, string, number, string, number, number] { | ||||
|     const loId = questionId.learningObjectIdentifier; | ||||
|     return ["answer", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber, sequenceNumber]; | ||||
| } | ||||
| 
 | ||||
| export function useAnswersQuery( | ||||
|     questionId: MaybeRefOrGetter<QuestionId>, | ||||
|     full: MaybeRefOrGetter<boolean> = true, | ||||
| ): UseQueryReturnType<AnswersResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => answersQueryKey(toValue(questionId), toValue(full))), | ||||
|         queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)), | ||||
|         enabled: () => Boolean(toValue(questionId)), | ||||
|     }); | ||||
|  | @ -21,31 +42,68 @@ export function useAnswerQuery( | |||
|     sequenceNumber: MaybeRefOrGetter<number>, | ||||
| ): UseQueryReturnType<AnswerResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => answerQueryKey(toValue(questionId), toValue(sequenceNumber))), | ||||
|         queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)), | ||||
|         enabled: () => Boolean(toValue(questionId)), | ||||
|         enabled: () => Boolean(toValue(questionId)) && Boolean(toValue(sequenceNumber)), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useCreateAnswerMutation( | ||||
|     questionId: MaybeRefOrGetter<QuestionId>, | ||||
| ): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data), | ||||
|         onSuccess: async () => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), true), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), false), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useDeleteAnswerMutation( | ||||
|     questionId: MaybeRefOrGetter<QuestionId>, | ||||
| ): UseMutationReturnType<AnswerResponse, Error, number, unknown> { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq), | ||||
|         onSuccess: async () => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), true), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), false), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useUpdateAnswerMutation( | ||||
|     questionId: MaybeRefOrGetter<QuestionId>, | ||||
| ): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data), | ||||
|         mutationFn: async ({ answerData, seq }) => new AnswerController(toValue(questionId)).update(seq, answerData), | ||||
|         onSuccess: async (_, { seq }) => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answerQueryKey(toValue(questionId), seq), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), true), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), true), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: answersQueryKey(toValue(questionId), false), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ import { computed, toValue, type MaybeRefOrGetter } from "vue"; | |||
| import { invalidateAllAssignmentKeys } from "./assignments"; | ||||
| import { invalidateAllGroupKeys } from "./groups"; | ||||
| import { invalidateAllSubmissionKeys } from "./submissions"; | ||||
| import type { TeachersResponse } from "@/controllers/teachers"; | ||||
| import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||
| 
 | ||||
| const classController = new ClassController(); | ||||
| 
 | ||||
|  | @ -176,7 +178,7 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType< | |||
| export function useClassTeachersQuery( | ||||
|     id: MaybeRefOrGetter<string | undefined>, | ||||
|     full: MaybeRefOrGetter<boolean> = true, | ||||
| ): UseQueryReturnType<StudentsResponse, Error> { | ||||
| ): UseQueryReturnType<TeachersResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), | ||||
|         queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)), | ||||
|  | @ -223,7 +225,7 @@ export function useClassDeleteTeacherMutation(): UseMutationReturnType< | |||
| export function useClassTeacherInvitationsQuery( | ||||
|     id: MaybeRefOrGetter<string | undefined>, | ||||
|     full: MaybeRefOrGetter<boolean> = true, | ||||
| ): UseQueryReturnType<StudentsResponse, Error> { | ||||
| ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), | ||||
|         queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)), | ||||
|  |  | |||
|  | @ -14,12 +14,12 @@ export function questionsQueryKey( | |||
|     loId: LearningObjectIdentifierDTO, | ||||
|     full: boolean, | ||||
| ): [string, string, number, string, boolean] { | ||||
|     return ["questions", loId.hruid, loId.version, loId.language, full]; | ||||
|     return ["questions", loId.hruid, loId.version!, loId.language, full]; | ||||
| } | ||||
| 
 | ||||
| export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { | ||||
|     const loId = questionId.learningObjectIdentifier; | ||||
|     return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber]; | ||||
|     return ["question", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber]; | ||||
| } | ||||
| 
 | ||||
| export function useQuestionsQuery( | ||||
|  | @ -39,7 +39,7 @@ export function useQuestionQuery( | |||
|     const loId = toValue(questionId).learningObjectIdentifier; | ||||
|     const sequenceNumber = toValue(questionId).sequenceNumber; | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => questionQueryKey(loId, sequenceNumber)), | ||||
|         queryKey: computed(() => questionQueryKey(toValue(questionId))), | ||||
|         queryFn: async () => new QuestionController(loId).getBy(sequenceNumber), | ||||
|         enabled: () => Boolean(toValue(questionId)), | ||||
|     }); | ||||
|  | @ -55,6 +55,7 @@ export function useCreateQuestionMutation( | |||
|         onSuccess: async () => { | ||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: ["answers"] }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  | @ -88,6 +89,8 @@ export function useDeleteQuestionMutation( | |||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: ["answers"] }); | ||||
|             await queryClient.invalidateQueries({ queryKey: ["answer"] }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import type { GroupsResponse } from "@/controllers/groups.ts"; | |||
| import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||
| import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||
| import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
| import { teacherClassJoinRequests } from "@/queries/teachers.ts"; | ||||
| 
 | ||||
| const studentController = new StudentController(); | ||||
| 
 | ||||
|  | @ -174,13 +175,13 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType< | |||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), | ||||
|         onSuccess: async (newJoinRequest) => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(newJoinRequest.request.class) }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  | @ -200,6 +201,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType< | |||
|             const classId = deletedJoinRequest.request.class; | ||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(classId) }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,36 +1,56 @@ | |||
| import { useMutation, useQuery, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||
| import { computed, toValue } from "vue"; | ||||
| import { | ||||
|     useMutation, | ||||
|     useQuery, | ||||
|     useQueryClient, | ||||
|     type UseMutationReturnType, | ||||
|     type UseQueryReturnType, | ||||
| } from "@tanstack/vue-query"; | ||||
| import { toValue } from "vue"; | ||||
| import type { MaybeRefOrGetter } from "vue"; | ||||
| import { | ||||
|     TeacherInvitationController, | ||||
|     type TeacherInvitationResponse, | ||||
|     type TeacherInvitationsResponse, | ||||
| } from "@/controllers/teacher-invitations.ts"; | ||||
| } from "@/controllers/teacher-invitations"; | ||||
| import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||
| import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; | ||||
| 
 | ||||
| const controller = new TeacherInvitationController(); | ||||
| 
 | ||||
| /** 🔑 Query keys */ | ||||
| export function teacherInvitationsSentQueryKey(username: string): [string, string, string] { | ||||
|     return ["teacher-invitations", "sent", username]; | ||||
| } | ||||
| 
 | ||||
| export function teacherInvitationsReceivedQueryKey(username: string): [string, string, string] { | ||||
|     return ["teacher-invitations", "received", username]; | ||||
| } | ||||
| 
 | ||||
| export function teacherInvitationQueryKey(data: TeacherInvitationData): [string, string, string, string] { | ||||
|     return ["teacher-invitation", data.sender, data.receiver, data.class]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|     All the invitations the teacher sent | ||||
| **/ | ||||
|  * All the invitations the teacher sent | ||||
|  */ | ||||
| export function useTeacherInvitationsSentQuery( | ||||
|     username: MaybeRefOrGetter<string | undefined>, | ||||
| ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryFn: computed(async () => controller.getAll(toValue(username), true)), | ||||
|         queryKey: teacherInvitationsSentQueryKey(toValue(username)!), | ||||
|         queryFn: async () => controller.getAll(toValue(username)!, true), | ||||
|         enabled: () => Boolean(toValue(username)), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|     All the pending invitations sent to this teacher | ||||
|  * All the pending invitations sent to this teacher | ||||
|  */ | ||||
| export function useTeacherInvitationsReceivedQuery( | ||||
|     username: MaybeRefOrGetter<string | undefined>, | ||||
| ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryFn: computed(async () => controller.getAll(toValue(username), false)), | ||||
|         queryKey: teacherInvitationsReceivedQueryKey(toValue(username)!), | ||||
|         queryFn: async () => controller.getAll(toValue(username)!, false), | ||||
|         enabled: () => Boolean(toValue(username)), | ||||
|     }); | ||||
| } | ||||
|  | @ -39,7 +59,8 @@ export function useTeacherInvitationQuery( | |||
|     data: MaybeRefOrGetter<TeacherInvitationData | undefined>, | ||||
| ): UseQueryReturnType<TeacherInvitationResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryFn: computed(async () => controller.getBy(toValue(data))), | ||||
|         queryKey: teacherInvitationQueryKey(toValue(data)), | ||||
|         queryFn: async () => controller.getBy(toValue(data)), | ||||
|         enabled: () => Boolean(toValue(data)), | ||||
|     }); | ||||
| } | ||||
|  | @ -47,32 +68,68 @@ export function useTeacherInvitationQuery( | |||
| export function useCreateTeacherInvitationMutation(): UseMutationReturnType< | ||||
|     TeacherInvitationResponse, | ||||
|     Error, | ||||
|     TeacherDTO, | ||||
|     TeacherInvitationData, | ||||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (data: TeacherInvitationData) => controller.create(data), | ||||
|         mutationFn: async (data) => controller.create(data), | ||||
|         onSuccess: async (_, data) => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsSentQueryKey(data.sender), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsReceivedQueryKey(data.receiver), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useRespondTeacherInvitationMutation(): UseMutationReturnType< | ||||
|     TeacherInvitationResponse, | ||||
|     Error, | ||||
|     TeacherDTO, | ||||
|     TeacherInvitationData, | ||||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (data: TeacherInvitationData) => controller.respond(data), | ||||
|         mutationFn: async (data) => controller.respond(data), | ||||
|         onSuccess: async (_, data) => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsSentQueryKey(data.sender), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsReceivedQueryKey(data.receiver), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationQueryKey(data), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< | ||||
|     TeacherInvitationResponse, | ||||
|     Error, | ||||
|     TeacherDTO, | ||||
|     TeacherInvitationData, | ||||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async (data: TeacherInvitationData) => controller.remove(data), | ||||
|         mutationFn: async (data) => controller.remove(data), | ||||
|         onSuccess: async (_, data) => { | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsSentQueryKey(data.sender), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationsReceivedQueryKey(data.receiver), | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ | ||||
|                 queryKey: teacherInvitationQueryKey(data), | ||||
|             }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -37,6 +37,10 @@ function teacherQuestionsQueryKey(username: string, full: boolean): [string, str | |||
|     return ["teacher-questions", username, full]; | ||||
| } | ||||
| 
 | ||||
| export function teacherClassJoinRequests(classId: string): [string, string] { | ||||
|     return ["teacher-class-join-requests", classId]; | ||||
| } | ||||
| 
 | ||||
| export function useTeachersQuery(full: MaybeRefOrGetter<boolean> = false): UseQueryReturnType<TeachersResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => teachersQueryKey(toValue(full))), | ||||
|  | @ -92,7 +96,7 @@ export function useTeacherJoinRequestsQuery( | |||
|     classId: MaybeRefOrGetter<string | undefined>, | ||||
| ): UseQueryReturnType<JoinRequestsResponse, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => JOIN_REQUESTS_QUERY_KEY(toValue(username)!, toValue(classId)!)), | ||||
|         queryKey: computed(() => teacherClassJoinRequests(toValue(classId)!)), | ||||
|         queryFn: async () => teacherController.getStudentJoinRequests(toValue(username)!, toValue(classId)!), | ||||
|         enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), | ||||
|     }); | ||||
|  | @ -133,10 +137,11 @@ export function useUpdateJoinRequestMutation(): UseMutationReturnType< | |||
|         mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) => | ||||
|             teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted), | ||||
|         onSuccess: async (deletedJoinRequest) => { | ||||
|             const username = deletedJoinRequest.request.requester; | ||||
|             const username = deletedJoinRequest.request.requester.username; | ||||
|             const classId = deletedJoinRequest.request.class; | ||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(classId) }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | ||||
| import type { Theme } from "@dwengo-1/interfaces/theme"; | ||||
| import { getThemeController } from "@/controllers/controllers.ts"; | ||||
| import type { Theme } from "@dwengo-1/common/interfaces/theme"; | ||||
| 
 | ||||
| const themeController = getThemeController(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; | |||
| import SingleClass from "@/views/classes/SingleClass.vue"; | ||||
| import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; | ||||
| import NotFound from "@/components/errors/NotFound.vue"; | ||||
| import CreateClass from "@/views/classes/CreateClass.vue"; | ||||
| import CreateAssignment from "@/views/assignments/CreateAssignment.vue"; | ||||
| import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; | ||||
| import CallbackPage from "@/views/CallbackPage.vue"; | ||||
|  | @ -84,12 +83,6 @@ const router = createRouter({ | |||
|             component: SingleAssignment, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/class/create", | ||||
|             name: "CreateClass", | ||||
|             component: CreateClass, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/class/:id", | ||||
|             name: "SingleClass", | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -1,135 +1,358 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import { onMounted, ref } from "vue"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { onMounted, ref, watchEffect } from "vue"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import { ClassController, type ClassResponse } from "@/controllers/classes"; | ||||
|     import type { StudentsResponse } from "@/controllers/students"; | ||||
|     import type { ClassResponse } from "@/controllers/classes"; | ||||
|     import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students"; | ||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useTeacherJoinRequestsQuery, useUpdateJoinRequestMutation } from "@/queries/teachers"; | ||||
|     import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request"; | ||||
|     import { useClassDeleteStudentMutation, useClassQuery, useClassStudentsQuery } from "@/queries/classes"; | ||||
|     import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations"; | ||||
|     import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||
|     import { useDisplay } from "vuetify"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     // Username of logged in teacher | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const classController: ClassController = new ClassController(); | ||||
| 
 | ||||
|     // Find class id from route | ||||
|     const route = useRoute(); | ||||
|     const classId: string = route.params.id as string; | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
|     const usernameTeacher = ref<string | undefined>(undefined); | ||||
| 
 | ||||
|     const isLoading = ref(true); | ||||
|     const currentClass = ref<ClassDTO | undefined>(undefined); | ||||
|     const students = ref<StudentDTO[]>([]); | ||||
|     // Queries used to access the backend and catch loading or errors | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     // Gets the class a teacher wants to manage | ||||
|     const getClass = useClassQuery(classId); | ||||
|     // Get all students part of the class | ||||
|     const getStudents = useClassStudentsQuery(classId); | ||||
|     // Get all join requests for this class | ||||
|     const joinRequestsQuery = useTeacherJoinRequestsQuery(username, classId); | ||||
|     // Handle accepting or rejecting join requests | ||||
|     const { mutate } = useUpdateJoinRequestMutation(); | ||||
|     // Handle deletion of a student from the class | ||||
|     const { mutate: deleteStudentMutation } = useClassDeleteStudentMutation(); | ||||
|     // Handle creation of teacher invites | ||||
|     const { mutate: sentInviteMutation } = useCreateTeacherInvitationMutation(); | ||||
| 
 | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
| 
 | ||||
|         // Get class of which information should be shown | ||||
|         const classResponse: ClassResponse = await classController.getById(classId); | ||||
|         if (classResponse && classResponse.class) { | ||||
|             currentClass.value = classResponse.class; | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|             username.value = userObject!.profile.preferred_username; | ||||
|         } catch (error) { | ||||
|             isError.value = true; | ||||
|             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
| 
 | ||||
|         // Fetch all students of the class | ||||
|         const studentsResponse: StudentsResponse = await classController.getStudents(classId); | ||||
|         if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[]; | ||||
|     }); | ||||
| 
 | ||||
|     // TODO: Boolean that handles visibility for dialogs | ||||
|     // Popup to verify removing student | ||||
|     // Used to set the visibility of the dialog | ||||
|     const dialog = ref(false); | ||||
|     // Student selected for deletion | ||||
|     const selectedStudent = ref<StudentDTO | null>(null); | ||||
| 
 | ||||
|     // Let the teacher verify deletion of a student | ||||
|     function showPopup(s: StudentDTO): void { | ||||
|         selectedStudent.value = s; | ||||
|         dialog.value = true; | ||||
|     } | ||||
| 
 | ||||
|     // Remove student from class | ||||
|     function removeStudentFromclass(): void { | ||||
|         dialog.value = false; | ||||
|     async function removeStudentFromclass(): Promise<void> { | ||||
|         // Delete student from class | ||||
|         deleteStudentMutation( | ||||
|             { id: classId, username: selectedStudent.value!.username }, | ||||
|             { | ||||
|                 onSuccess: async () => { | ||||
|                     dialog.value = false; | ||||
|                     await getStudents.refetch(); | ||||
|                     showSnackbar(t("success"), "success"); | ||||
|                 }, | ||||
|                 onError: (e) => { | ||||
|                     dialog.value = false; | ||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean): void { | ||||
|         // Handle acception or rejection of a join request | ||||
|         mutate( | ||||
|             { | ||||
|                 teacherUsername: username.value!, | ||||
|                 studentUsername: c.requester.username, | ||||
|                 classId: c.class, | ||||
|                 accepted: accepted, | ||||
|             }, | ||||
|             { | ||||
|                 onSuccess: async () => { | ||||
|                     if (accepted) { | ||||
|                         await joinRequestsQuery.refetch(); | ||||
|                         await getStudents.refetch(); | ||||
| 
 | ||||
|                         showSnackbar(t("accepted"), "success"); | ||||
|                     } else { | ||||
|                         await joinRequestsQuery.refetch(); | ||||
|                         showSnackbar(t("rejected"), "success"); | ||||
|                     } | ||||
|                 }, | ||||
|                 onError: (e) => { | ||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     function sentInvite(): void { | ||||
|         if (!usernameTeacher.value) { | ||||
|             showSnackbar(t("please enter a valid username"), "error"); | ||||
|             return; | ||||
|         } | ||||
|         const data: TeacherInvitationData = { | ||||
|             sender: username.value!, | ||||
|             receiver: usernameTeacher.value, | ||||
|             class: classId, | ||||
|         }; | ||||
|         sentInviteMutation(data, { | ||||
|             onSuccess: () => { | ||||
|                 usernameTeacher.value = ""; | ||||
|             }, | ||||
|             onError: (e) => { | ||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|             }, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // Default of snackbar values | ||||
|     const snackbar = ref({ | ||||
|         visible: false, | ||||
|         message: "", | ||||
|         color: "success", | ||||
|     }); | ||||
| 
 | ||||
|     // Function to show snackbar on success or failure | ||||
|     function showSnackbar(message: string, color: string): void { | ||||
|         snackbar.value.message = message; | ||||
|         snackbar.value.color = color; | ||||
|         snackbar.value.visible = true; | ||||
|     } | ||||
| 
 | ||||
|     // Custom breakpoints | ||||
|     const customBreakpoints = { | ||||
|         xs: 0, | ||||
|         sm: 500, | ||||
|         md: 1370, | ||||
|         lg: 1400, | ||||
|         xl: 1600, | ||||
|     }; | ||||
| 
 | ||||
|     // Logic for small screens | ||||
|     const display = useDisplay(); | ||||
| 
 | ||||
|     // Reactive variables to hold custom logic based on breakpoints | ||||
|     const isSmAndDown = ref(false); | ||||
|     const isMdAndDown = ref(false); | ||||
| 
 | ||||
|     watchEffect(() => { | ||||
|         // Custom breakpoint logic | ||||
|         isSmAndDown.value = display.width.value < customBreakpoints.sm; | ||||
|         isMdAndDown.value = display.width.value < customBreakpoints.md; | ||||
|     }); | ||||
| </script> | ||||
| <template> | ||||
|     <main> | ||||
|         <div | ||||
|             class="loading-div" | ||||
|             v-if="isLoading" | ||||
|             class="text-center py-10" | ||||
|         > | ||||
|             <v-progress-circular | ||||
|                 indeterminate | ||||
|                 color="primary" | ||||
|             /> | ||||
|             <p>Loading...</p> | ||||
|             <v-progress-circular indeterminate></v-progress-circular> | ||||
|         </div> | ||||
|         <div v-else> | ||||
|             <h1 class="title">{{ currentClass!.displayName }}</h1> | ||||
|             <v-container | ||||
|                 fluid | ||||
|                 class="ma-4" | ||||
|             > | ||||
|                 <v-row | ||||
|                     no-gutters | ||||
|                     fluid | ||||
|         <div v-if="isError"> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-alert-circle-outline" | ||||
|                 :text="errorMessage" | ||||
|                 :title="t('error_title')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|         <using-query-result | ||||
|             :query-result="getClass" | ||||
|             v-slot="classResponse: { data: ClassResponse }" | ||||
|         > | ||||
|             <div> | ||||
|                 <h1 class="title">{{ classResponse.data.class.displayName }}</h1> | ||||
|                 <using-query-result | ||||
|                     :query-result="getStudents" | ||||
|                     v-slot="studentsResponse: { data: StudentsResponse }" | ||||
|                 > | ||||
|                     <v-col | ||||
|                         cols="12" | ||||
|                         sm="6" | ||||
|                         md="6" | ||||
|                     <v-container | ||||
|                         fluid | ||||
|                         class="ma-4" | ||||
|                     > | ||||
|                         <v-table class="table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th class="header">{{ t("students") }}</th> | ||||
|                                     <th class="header"></th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 <tr | ||||
|                                     v-for="s in students" | ||||
|                                     :key="s.id" | ||||
|                         <v-row | ||||
|                             no-gutters | ||||
|                             fluid | ||||
|                         > | ||||
|                             <v-col | ||||
|                                 cols="12" | ||||
|                                 sm="6" | ||||
|                                 md="6" | ||||
|                             > | ||||
|                                 <v-table class="table"> | ||||
|                                     <thead> | ||||
|                                         <tr> | ||||
|                                             <th class="header">{{ t("students") }}</th> | ||||
|                                             <th class="header"></th> | ||||
|                                         </tr> | ||||
|                                     </thead> | ||||
|                                     <tbody> | ||||
|                                         <tr | ||||
|                                             v-for="s in studentsResponse.data.students as StudentDTO[]" | ||||
|                                             :key="s.id" | ||||
|                                         > | ||||
|                                             <td> | ||||
|                                                 {{ s.firstName + " " + s.lastName }} | ||||
|                                             </td> | ||||
|                                             <td> | ||||
|                                                 <v-btn @click="showPopup(s)"> {{ t("remove") }} </v-btn> | ||||
|                                             </td> | ||||
|                                         </tr> | ||||
|                                     </tbody> | ||||
|                                 </v-table> | ||||
|                             </v-col> | ||||
|                             <using-query-result | ||||
|                                 :query-result="joinRequestsQuery" | ||||
|                                 v-slot="joinRequests: { data: JoinRequestsResponse }" | ||||
|                             > | ||||
|                                 <v-col | ||||
|                                     cols="12" | ||||
|                                     sm="6" | ||||
|                                     md="6" | ||||
|                                 > | ||||
|                                     <td> | ||||
|                                         {{ s.firstName + " " + s.lastName }} | ||||
|                                     </td> | ||||
|                                     <td> | ||||
|                                         <v-btn @click="showPopup"> {{ t("remove") }} </v-btn> | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </v-table> | ||||
|                     </v-col> | ||||
|                 </v-row> | ||||
|             </v-container> | ||||
|         </div> | ||||
|         <v-dialog | ||||
|             v-model="dialog" | ||||
|             max-width="400px" | ||||
|         > | ||||
|             <v-card> | ||||
|                 <v-card-title class="headline">{{ t("areusure") }}</v-card-title> | ||||
|                                     <v-table class="table"> | ||||
|                                         <thead> | ||||
|                                             <tr> | ||||
|                                                 <th class="header">{{ t("classJoinRequests") }}</th> | ||||
|                                                 <th class="header">{{ t("accept") + "/" + t("reject") }}</th> | ||||
|                                             </tr> | ||||
|                                         </thead> | ||||
|                                         <tbody> | ||||
|                                             <tr | ||||
|                                                 v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]" | ||||
|                                                 :key="(jr.class, jr.requester, jr.status)" | ||||
|                                             > | ||||
|                                                 <td> | ||||
|                                                     {{ jr.requester.firstName + " " + jr.requester.lastName }} | ||||
|                                                 </td> | ||||
|                                                 <td> | ||||
|                                                     <span v-if="!isSmAndDown && !isMdAndDown"> | ||||
|                                                         <v-btn | ||||
|                                                             @click="handleJoinRequest(jr, true)" | ||||
|                                                             class="mr-2" | ||||
|                                                             color="green" | ||||
|                                                         > | ||||
|                                                             {{ t("accept") }}</v-btn | ||||
|                                                         > | ||||
| 
 | ||||
|                 <v-card-actions> | ||||
|                     <v-spacer></v-spacer> | ||||
|                     <v-btn | ||||
|                         text | ||||
|                         @click="dialog = false" | ||||
|                                                         <v-btn | ||||
|                                                             @click="handleJoinRequest(jr, false)" | ||||
|                                                             class="mr-2" | ||||
|                                                             color="red" | ||||
|                                                         > | ||||
|                                                             {{ t("reject") }} | ||||
|                                                         </v-btn> | ||||
|                                                     </span> | ||||
|                                                     <span v-else> | ||||
|                                                         <v-btn | ||||
|                                                             @click="handleJoinRequest(jr, true)" | ||||
|                                                             icon="mdi-check-circle" | ||||
|                                                             class="mr-2" | ||||
|                                                             color="green" | ||||
|                                                             variant="text" | ||||
|                                                         ></v-btn> | ||||
|                                                         <v-btn | ||||
|                                                             @click="handleJoinRequest(jr, false)" | ||||
|                                                             icon="mdi-close-circle" | ||||
|                                                             class="mr-2" | ||||
|                                                             color="red" | ||||
|                                                             variant="text" | ||||
|                                                         ></v-btn> | ||||
|                                                     </span> | ||||
|                                                 </td> | ||||
|                                             </tr> | ||||
|                                         </tbody> | ||||
|                                     </v-table> | ||||
|                                 </v-col> | ||||
|                             </using-query-result> | ||||
|                         </v-row> | ||||
|                     </v-container> | ||||
|                 </using-query-result> | ||||
|             </div> | ||||
|             <div> | ||||
|                 <div class="join"> | ||||
|                     <h2>{{ t("invitations") }}</h2> | ||||
|                     <p>{{ t("enterUsername") }}</p> | ||||
| 
 | ||||
|                     <v-sheet | ||||
|                         class="pa-4 sheet" | ||||
|                         max-width="400" | ||||
|                     > | ||||
|                         {{ t("cancel") }} | ||||
|                     </v-btn> | ||||
|                     <v-btn | ||||
|                         text | ||||
|                         @click="removeStudentFromclass" | ||||
|                         >{{ t("yes") }}</v-btn | ||||
|                     > | ||||
|                 </v-card-actions> | ||||
|             </v-card> | ||||
|         </v-dialog> | ||||
|                         <v-form @submit.prevent> | ||||
|                             <v-text-field | ||||
|                                 :label="`${t('username')}`" | ||||
|                                 v-model="usernameTeacher" | ||||
|                                 :placeholder="`${t('username')}`" | ||||
|                                 variant="outlined" | ||||
|                             ></v-text-field> | ||||
|                             <v-btn | ||||
|                                 class="mt-4" | ||||
|                                 color="#f6faf2" | ||||
|                                 type="submit" | ||||
|                                 @click="sentInvite" | ||||
|                                 block | ||||
|                                 >{{ t("invite") }}</v-btn | ||||
|                             > | ||||
|                         </v-form> | ||||
|                     </v-sheet> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <v-dialog | ||||
|                 v-model="dialog" | ||||
|                 max-width="400px" | ||||
|             > | ||||
|                 <v-card> | ||||
|                     <v-card-title class="headline">{{ t("areusure") }}</v-card-title> | ||||
| 
 | ||||
|                     <v-card-actions> | ||||
|                         <v-spacer></v-spacer> | ||||
|                         <v-btn | ||||
|                             text | ||||
|                             @click="dialog = false" | ||||
|                         > | ||||
|                             {{ t("cancel") }} | ||||
|                         </v-btn> | ||||
|                         <v-btn | ||||
|                             text | ||||
|                             @click="removeStudentFromclass" | ||||
|                             >{{ t("yes") }}</v-btn | ||||
|                         > | ||||
|                     </v-card-actions> | ||||
|                 </v-card> | ||||
|             </v-dialog> | ||||
|             <v-snackbar | ||||
|                 v-model="snackbar.visible" | ||||
|                 :color="snackbar.color" | ||||
|                 timeout="3000" | ||||
|             > | ||||
|                 {{ snackbar.message }} | ||||
|             </v-snackbar> | ||||
|         </using-query-result> | ||||
|     </main> | ||||
| </template> | ||||
| <style scoped> | ||||
|  |  | |||
|  | @ -1,51 +1,51 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import { computed, onMounted, ref, type ComputedRef } from "vue"; | ||||
|     import { computed, onMounted, ref } from "vue"; | ||||
|     import { validate, version } from "uuid"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; | ||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|     import { StudentController } from "@/controllers/students"; | ||||
|     import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; | ||||
|     import { TeacherController } from "@/controllers/teachers"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes"; | ||||
|     import type { StudentsResponse } from "@/controllers/students"; | ||||
|     import type { TeachersResponse } from "@/controllers/teachers"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const studentController: StudentController = new StudentController(); | ||||
|     const teacherController: TeacherController = new TeacherController(); | ||||
| 
 | ||||
|     // Username of logged in student | ||||
|     const username = ref<string | undefined>(undefined); | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     onMounted(async () => { | ||||
|         const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all classes of the logged in student | ||||
|     const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username); | ||||
| 
 | ||||
|     // Empty list when classes are not yet loaded, else the list of classes of the user | ||||
|     const classes: ComputedRef<ClassDTO[]> = computed(() => { | ||||
|         // The classes are not yet fetched | ||||
|         if (!classesResponse.value) { | ||||
|             return []; | ||||
|         } | ||||
|         // The user has no classes | ||||
|         if (classesResponse.value.classes.length === 0) { | ||||
|             return []; | ||||
|         } | ||||
|         return classesResponse.value.classes as ClassDTO[]; | ||||
|     }); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
| 
 | ||||
|     // Students of selected class are shown when logged in student presses on the member count | ||||
|     const selectedClass = ref<ClassDTO | null>(null); | ||||
|     const students = ref<StudentDTO[]>([]); | ||||
|     const teachers = ref<TeacherDTO[]>([]); | ||||
|     const getStudents = ref(false); | ||||
| 
 | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|             username.value = userObject!.profile.preferred_username; | ||||
|         } catch (error) { | ||||
|             isError.value = true; | ||||
|             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all classes of the logged in student | ||||
|     const classesQuery = useStudentClassesQuery(username); | ||||
|     // Fetch all students of the class | ||||
|     const getStudentsQuery = useClassStudentsQuery(computed(() => selectedClass.value?.id)); | ||||
|     // Fetch all teachers of the class | ||||
|     const getTeachersQuery = useClassTeachersQuery(computed(() => selectedClass.value?.id)); | ||||
| 
 | ||||
|     // Boolean that handles visibility for dialogs | ||||
|     // Clicking on membercount will show a dialog with all members | ||||
|     const dialog = ref(false); | ||||
|  | @ -54,48 +54,19 @@ | |||
|     async function openStudentDialog(c: ClassDTO): Promise<void> { | ||||
|         selectedClass.value = c; | ||||
| 
 | ||||
|         // Clear previous value | ||||
|         // Let the component know it should show the students in a class | ||||
|         getStudents.value = true; | ||||
|         students.value = []; | ||||
|         await getStudentsQuery.refetch(); | ||||
|         dialog.value = true; | ||||
| 
 | ||||
|         // Fetch students from their usernames to display their full names | ||||
|         const studentDTOs: (StudentDTO | null)[] = await Promise.all( | ||||
|             c.students.map(async (uid) => { | ||||
|                 try { | ||||
|                     const res = await studentController.getByUsername(uid); | ||||
|                     return res.student; | ||||
|                 } catch (_) { | ||||
|                     return null; | ||||
|                 } | ||||
|             }), | ||||
|         ); | ||||
| 
 | ||||
|         // Only show students that are not fetched ass *null* | ||||
|         students.value = studentDTOs.filter(Boolean) as StudentDTO[]; | ||||
|     } | ||||
| 
 | ||||
|     async function openTeacherDialog(c: ClassDTO): Promise<void> { | ||||
|         selectedClass.value = c; | ||||
| 
 | ||||
|         // Clear previous value | ||||
|         // Let the component know it should show teachers of a class | ||||
|         getStudents.value = false; | ||||
|         teachers.value = []; | ||||
|         await getTeachersQuery.refetch(); | ||||
|         dialog.value = true; | ||||
| 
 | ||||
|         // Fetch names of teachers | ||||
|         const teacherDTOs: (TeacherDTO | null)[] = await Promise.all( | ||||
|             c.teachers.map(async (uid) => { | ||||
|                 try { | ||||
|                     const res = await teacherController.getByUsername(uid); | ||||
|                     return res.teacher; | ||||
|                 } catch (_) { | ||||
|                     return null; | ||||
|                 } | ||||
|             }), | ||||
|         ); | ||||
| 
 | ||||
|         teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[]; | ||||
|     } | ||||
| 
 | ||||
|     // Hold the code a student gives in to join a class | ||||
|  | @ -151,100 +122,111 @@ | |||
| <template> | ||||
|     <main> | ||||
|         <div | ||||
|             class="loading-div" | ||||
|             v-if="isLoading" | ||||
|             class="text-center py-10" | ||||
|         > | ||||
|             <v-progress-circular | ||||
|                 indeterminate | ||||
|                 color="primary" | ||||
|             /> | ||||
|             <p>Loading...</p> | ||||
|             <v-progress-circular indeterminate></v-progress-circular> | ||||
|         </div> | ||||
| 
 | ||||
|         <div | ||||
|             v-else-if="error" | ||||
|             class="text-center py-10 text-error" | ||||
|         > | ||||
|             <v-icon large>mdi-alert-circle</v-icon> | ||||
|             <p>Error loading: {{ error.message }}</p> | ||||
|         <div v-if="isError"> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-alert-circle-outline" | ||||
|                 :text="errorMessage" | ||||
|                 :title="t('error_title')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|         <div v-else> | ||||
|             <h1 class="title">{{ t("classes") }}</h1> | ||||
|             <v-container | ||||
|                 fluid | ||||
|                 class="ma-4" | ||||
|             <using-query-result | ||||
|                 :query-result="classesQuery" | ||||
|                 v-slot="classResponse: { data: ClassesResponse }" | ||||
|             > | ||||
|                 <v-row | ||||
|                     no-gutters | ||||
|                 <v-container | ||||
|                     fluid | ||||
|                     class="ma-4" | ||||
|                 > | ||||
|                     <v-col | ||||
|                         cols="12" | ||||
|                         sm="6" | ||||
|                         md="6" | ||||
|                     <v-row | ||||
|                         no-gutters | ||||
|                         fluid | ||||
|                     > | ||||
|                         <v-table class="table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th class="header">{{ t("classes") }}</th> | ||||
|                                     <th class="header">{{ t("teachers") }}</th> | ||||
|                                     <th class="header">{{ t("members") }}</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 <tr | ||||
|                                     v-for="c in classes" | ||||
|                                     :key="c.id" | ||||
|                                 > | ||||
|                                     <td>{{ c.displayName }}</td> | ||||
|                                     <td | ||||
|                                         class="link" | ||||
|                                         @click="openTeacherDialog(c)" | ||||
|                         <v-col | ||||
|                             cols="12" | ||||
|                             sm="6" | ||||
|                             md="6" | ||||
|                         > | ||||
|                             <v-table class="table"> | ||||
|                                 <thead> | ||||
|                                     <tr> | ||||
|                                         <th class="header">{{ t("classes") }}</th> | ||||
|                                         <th class="header">{{ t("teachers") }}</th> | ||||
|                                         <th class="header">{{ t("members") }}</th> | ||||
|                                     </tr> | ||||
|                                 </thead> | ||||
|                                 <tbody> | ||||
|                                     <tr | ||||
|                                         v-for="c in classResponse.data.classes as ClassDTO[]" | ||||
|                                         :key="c.id" | ||||
|                                     > | ||||
|                                         {{ c.teachers.length }} | ||||
|                                     </td> | ||||
|                                     <td | ||||
|                                         class="link" | ||||
|                                         @click="openStudentDialog(c)" | ||||
|                                     > | ||||
|                                         {{ c.students.length }} | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </v-table> | ||||
|                     </v-col> | ||||
|                 </v-row> | ||||
|             </v-container> | ||||
|                                         <td>{{ c.displayName }}</td> | ||||
|                                         <td | ||||
|                                             class="link" | ||||
|                                             @click="openTeacherDialog(c)" | ||||
|                                         > | ||||
|                                             {{ c.teachers.length }} | ||||
|                                         </td> | ||||
|                                         <td | ||||
|                                             class="link" | ||||
|                                             @click="openStudentDialog(c)" | ||||
|                                         > | ||||
|                                             {{ c.students.length }} | ||||
|                                         </td> | ||||
|                                     </tr> | ||||
|                                 </tbody> | ||||
|                             </v-table> | ||||
|                         </v-col> | ||||
|                     </v-row> | ||||
|                 </v-container> | ||||
|             </using-query-result> | ||||
| 
 | ||||
|             <v-dialog | ||||
|                 v-if="selectedClass" | ||||
|                 v-model="dialog" | ||||
|                 width="400" | ||||
|             > | ||||
|                 <v-card> | ||||
|                     <v-card-title> {{ selectedClass?.displayName }} </v-card-title> | ||||
|                     <v-card-title> {{ selectedClass!.displayName }} </v-card-title> | ||||
|                     <v-card-text> | ||||
|                         <ul v-if="getStudents"> | ||||
|                             <li | ||||
|                                 v-for="student in students" | ||||
|                                 :key="student.username" | ||||
|                             <using-query-result | ||||
|                                 :query-result="getStudentsQuery" | ||||
|                                 v-slot="studentsResponse: { data: StudentsResponse }" | ||||
|                             > | ||||
|                                 {{ student.firstName + " " + student.lastName }} | ||||
|                             </li> | ||||
|                                 <li | ||||
|                                     v-for="student in studentsResponse.data.students as StudentDTO[]" | ||||
|                                     :key="student.username" | ||||
|                                 > | ||||
|                                     {{ student.firstName + " " + student.lastName }} | ||||
|                                 </li> | ||||
|                             </using-query-result> | ||||
|                         </ul> | ||||
|                         <ul v-else> | ||||
|                             <li | ||||
|                                 v-for="teacher in teachers" | ||||
|                                 :key="teacher.username" | ||||
|                             <using-query-result | ||||
|                                 :query-result="getTeachersQuery" | ||||
|                                 v-slot="teachersResponse: { data: TeachersResponse }" | ||||
|                             > | ||||
|                                 {{ teacher.firstName + " " + teacher.lastName }} | ||||
|                             </li> | ||||
|                                 <li | ||||
|                                     v-for="teacher in teachersResponse.data.teachers as TeacherDTO[]" | ||||
|                                     :key="teacher.username" | ||||
|                                 > | ||||
|                                     {{ teacher.firstName + " " + teacher.lastName }} | ||||
|                                 </li> | ||||
|                             </using-query-result> | ||||
|                         </ul> | ||||
|                     </v-card-text> | ||||
|                     <v-card-actions> | ||||
|                         <v-btn | ||||
|                             color="primary" | ||||
|                             @click="dialog = false" | ||||
|                             >Close</v-btn | ||||
|                             >{{ t("close") }}</v-btn | ||||
|                         > | ||||
|                     </v-card-actions> | ||||
|                 </v-card> | ||||
|  |  | |||
|  | @ -1,41 +1,49 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import { computed, onMounted, ref, type ComputedRef } from "vue"; | ||||
|     import { onMounted, ref, watchEffect } from "vue"; | ||||
|     import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||
|     import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||
|     import { useTeacherClassesQuery } from "@/queries/teachers"; | ||||
|     import { ClassController, type ClassResponse } from "@/controllers/classes"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useClassesQuery, useCreateClassMutation } from "@/queries/classes"; | ||||
|     import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||
|     import { | ||||
|         useRespondTeacherInvitationMutation, | ||||
|         useTeacherInvitationsReceivedQuery, | ||||
|     } from "@/queries/teacher-invitations"; | ||||
|     import { useDisplay } from "vuetify"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const classController = new ClassController(); | ||||
| 
 | ||||
|     // Username of logged in teacher | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|             username.value = userObject!.profile.preferred_username; | ||||
|         } catch (error) { | ||||
|             isError.value = true; | ||||
|             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all classes of the logged in teacher | ||||
|     const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true); | ||||
| 
 | ||||
|     // Empty list when classes are not yet loaded, else the list of classes of the user | ||||
|     const classes: ComputedRef<ClassDTO[]> = computed(() => { | ||||
|         // The classes are not yet fetched | ||||
|         if (!classesResponse.value) { | ||||
|             return []; | ||||
|         } | ||||
|         // The user has no classes | ||||
|         if (classesResponse.value.classes.length === 0) { | ||||
|             return []; | ||||
|         } | ||||
|         return classesResponse.value.classes as ClassDTO[]; | ||||
|     }); | ||||
|     const classesQuery = useTeacherClassesQuery(username, true); | ||||
|     const allClassesQuery = useClassesQuery(); | ||||
|     const { mutate } = useCreateClassMutation(); | ||||
|     const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); | ||||
|     const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); | ||||
| 
 | ||||
|     // Boolean that handles visibility for dialogs | ||||
|     // Creating a class will generate a popup with the generated code | ||||
|  | @ -44,19 +52,26 @@ | |||
|     // Code generated when new class was created | ||||
|     const code = ref<string>(""); | ||||
| 
 | ||||
|     // TODO: waiting on frontend controllers | ||||
|     const invitations = ref<TeacherInvitationDTO[]>([]); | ||||
|     // Function to handle an invitation request | ||||
|     function handleInvitation(ti: TeacherInvitationDTO, accepted: boolean): void { | ||||
|         const data: TeacherInvitationData = { | ||||
|             sender: (ti.sender as TeacherDTO).id, | ||||
|             receiver: (ti.receiver as TeacherDTO).id, | ||||
|             class: ti.classId, | ||||
|             accepted: accepted, | ||||
|         }; | ||||
|         respondToInvitation(data, { | ||||
|             onSuccess: async () => { | ||||
|                 if (accepted) { | ||||
|                     await classesQuery.refetch(); | ||||
|                 } | ||||
| 
 | ||||
|     // Function to handle a accepted invitation request | ||||
|     function acceptRequest(): void { | ||||
|         //TODO: avoid linting issues when merging by filling the function | ||||
|         invitations.value = []; | ||||
|     } | ||||
| 
 | ||||
|     // Function to handle a denied invitation request | ||||
|     function denyRequest(): void { | ||||
|         //TODO: avoid linting issues when merging by filling the function | ||||
|         invitations.value = []; | ||||
|                 await getInvitationsQuery.refetch(); | ||||
|             }, | ||||
|             onError: (e) => { | ||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|             }, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // Teacher should be able to set a displayname when making a class | ||||
|  | @ -76,25 +91,25 @@ | |||
|     async function createClass(): Promise<void> { | ||||
|         // Check if the class name is valid | ||||
|         if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) { | ||||
|             try { | ||||
|                 const classDto: ClassDTO = { | ||||
|                     id: "", | ||||
|                     displayName: className.value, | ||||
|                     teachers: [username.value!], | ||||
|                     students: [], | ||||
|                     joinRequests: [], | ||||
|                 }; | ||||
|                 const classResponse: ClassResponse = await classController.createClass(classDto); | ||||
|                 const createdClass: ClassDTO = classResponse.class; | ||||
|                 code.value = createdClass.id; | ||||
|                 dialog.value = true; | ||||
|                 showSnackbar(t("created"), "success"); | ||||
|             const classDto: ClassDTO = { | ||||
|                 id: "", | ||||
|                 displayName: className.value, | ||||
|                 teachers: [username.value!], | ||||
|                 students: [], | ||||
|             }; | ||||
| 
 | ||||
|                 // Reload the table with classes so the new class appears | ||||
|                 await refetch(); | ||||
|             } catch (_) { | ||||
|                 showSnackbar(t("wrong"), "error"); | ||||
|             } | ||||
|             mutate(classDto, { | ||||
|                 onSuccess: async (classResponse) => { | ||||
|                     showSnackbar(t("classCreated"), "success"); | ||||
|                     const createdClass: ClassDTO = classResponse.class; | ||||
|                     code.value = createdClass.id; | ||||
|                     await classesQuery.refetch(); | ||||
|                 }, | ||||
|                 onError: (err) => { | ||||
|                     showSnackbar(t("creationFailed") + ": " + err.message, "error"); | ||||
|                 }, | ||||
|             }); | ||||
|             dialog.value = true; | ||||
|         } | ||||
|         if (!className.value || className.value === "") { | ||||
|             showSnackbar(t("name is mandatory"), "error"); | ||||
|  | @ -121,188 +136,272 @@ | |||
|         await navigator.clipboard.writeText(code.value); | ||||
|         copied.value = true; | ||||
|     } | ||||
| 
 | ||||
|     // Custom breakpoints | ||||
|     const customBreakpoints = { | ||||
|         xs: 0, | ||||
|         sm: 500, | ||||
|         md: 1370, | ||||
|         lg: 1400, | ||||
|         xl: 1600, | ||||
|     }; | ||||
| 
 | ||||
|     // Logic for small screens | ||||
|     const display = useDisplay(); | ||||
| 
 | ||||
|     // Reactive variables to hold custom logic based on breakpoints | ||||
|     const isMdAndDown = ref(false); | ||||
|     const isSmAndDown = ref(false); | ||||
| 
 | ||||
|     watchEffect(() => { | ||||
|         // Custom breakpoint logic | ||||
|         isMdAndDown.value = display.width.value < customBreakpoints.md; | ||||
|         isSmAndDown.value = display.width.value < customBreakpoints.sm; | ||||
|     }); | ||||
| 
 | ||||
|     // Code display dialog logic | ||||
|     const viewCodeDialog = ref(false); | ||||
|     const selectedCode = ref(""); | ||||
|     function openCodeDialog(codeToView: string): void { | ||||
|         selectedCode.value = codeToView; | ||||
|         viewCodeDialog.value = true; | ||||
|     } | ||||
| </script> | ||||
| <template> | ||||
|     <main> | ||||
|         <div | ||||
|             class="loading-div" | ||||
|             v-if="isLoading" | ||||
|             class="text-center py-10" | ||||
|         > | ||||
|             <v-progress-circular | ||||
|                 indeterminate | ||||
|                 color="primary" | ||||
|             /> | ||||
|             <p>Loading...</p> | ||||
|             <v-progress-circular indeterminate></v-progress-circular> | ||||
|         </div> | ||||
| 
 | ||||
|         <div | ||||
|             v-else-if="error" | ||||
|             class="text-center py-10 text-error" | ||||
|         > | ||||
|             <v-icon large>mdi-alert-circle</v-icon> | ||||
|             <p>Error loading: {{ error.message }}</p> | ||||
|         <div v-if="isError"> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-alert-circle-outline" | ||||
|                 :text="errorMessage" | ||||
|                 :title="t('error_title')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|         <div v-else> | ||||
|             <h1 class="title">{{ t("classes") }}</h1> | ||||
|             <v-container | ||||
|                 fluid | ||||
|                 class="ma-4" | ||||
|             <using-query-result | ||||
|                 :query-result="classesQuery" | ||||
|                 v-slot="classesResponse: { data: ClassesResponse }" | ||||
|             > | ||||
|                 <v-row | ||||
|                     no-gutters | ||||
|                 <v-container | ||||
|                     fluid | ||||
|                     class="ma-4" | ||||
|                 > | ||||
|                     <v-col | ||||
|                         cols="12" | ||||
|                         sm="6" | ||||
|                         md="6" | ||||
|                     <v-row | ||||
|                         no-gutters | ||||
|                         class="custom-breakpoint" | ||||
|                     > | ||||
|                         <v-table class="table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th class="header">{{ t("classes") }}</th> | ||||
|                                     <th class="header"> | ||||
|                                         {{ t("code") }} | ||||
|                                     </th> | ||||
|                                     <th class="header">{{ t("members") }}</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 <tr | ||||
|                                     v-for="c in classes" | ||||
|                                     :key="c.id" | ||||
|                                 > | ||||
|                                     <td> | ||||
|                                         <v-btn | ||||
|                                             :to="`/class/${c.id}`" | ||||
|                                             variant="text" | ||||
|                                         > | ||||
|                                             {{ c.displayName }} | ||||
|                                             <v-icon end> mdi-menu-right </v-icon> | ||||
|                                         </v-btn> | ||||
|                                     </td> | ||||
|                                     <td>{{ c.id }}</td> | ||||
|                                     <td>{{ c.students.length }}</td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </v-table> | ||||
|                     </v-col> | ||||
|                     <v-col | ||||
|                         cols="12" | ||||
|                         sm="6" | ||||
|                         md="6" | ||||
|                     > | ||||
|                         <div> | ||||
|                             <h2>{{ t("createClass") }}</h2> | ||||
| 
 | ||||
|                             <v-sheet | ||||
|                                 class="pa-4 sheet" | ||||
|                                 max-width="600px" | ||||
|                             > | ||||
|                                 <p>{{ t("createClassInstructions") }}</p> | ||||
|                                 <v-form @submit.prevent> | ||||
|                                     <v-text-field | ||||
|                                         class="mt-4" | ||||
|                                         :label="`${t('classname')}`" | ||||
|                                         v-model="className" | ||||
|                                         :placeholder="`${t('EnterNameOfClass')}`" | ||||
|                                         :rules="nameRules" | ||||
|                                         variant="outlined" | ||||
|                                     ></v-text-field> | ||||
|                                     <v-btn | ||||
|                                         class="mt-4" | ||||
|                                         color="#f6faf2" | ||||
|                                         type="submit" | ||||
|                                         @click="createClass" | ||||
|                                         block | ||||
|                                         >{{ t("create") }}</v-btn | ||||
|                         <v-col | ||||
|                             cols="12" | ||||
|                             sm="6" | ||||
|                             md="6" | ||||
|                             class="responsive-col" | ||||
|                         > | ||||
|                             <v-table class="table"> | ||||
|                                 <thead> | ||||
|                                     <tr> | ||||
|                                         <th class="header">{{ t("classes") }}</th> | ||||
|                                         <th class="header"> | ||||
|                                             {{ t("code") }} | ||||
|                                         </th> | ||||
|                                         <th class="header">{{ t("members") }}</th> | ||||
|                                     </tr> | ||||
|                                 </thead> | ||||
|                                 <tbody> | ||||
|                                     <tr | ||||
|                                         v-for="c in classesResponse.data.classes as ClassDTO[]" | ||||
|                                         :key="c.id" | ||||
|                                     > | ||||
|                                 </v-form> | ||||
|                             </v-sheet> | ||||
|                             <v-container> | ||||
|                                 <v-dialog | ||||
|                                     v-model="dialog" | ||||
|                                     max-width="400px" | ||||
|                                 > | ||||
|                                     <v-card> | ||||
|                                         <v-card-title class="headline">code</v-card-title> | ||||
|                                         <v-card-text> | ||||
|                                             <v-text-field | ||||
|                                                 v-model="code" | ||||
|                                                 readonly | ||||
|                                                 append-inner-icon="mdi-content-copy" | ||||
|                                                 @click:append-inner="copyToClipboard" | ||||
|                                             ></v-text-field> | ||||
|                                             <v-slide-y-transition> | ||||
|                                                 <div | ||||
|                                                     v-if="copied" | ||||
|                                                     class="text-center mt-2" | ||||
|                                                 > | ||||
|                                                     {{ t("copied") }} | ||||
|                                                 </div> | ||||
|                                             </v-slide-y-transition> | ||||
|                                         </v-card-text> | ||||
|                                         <v-card-actions> | ||||
|                                             <v-spacer></v-spacer> | ||||
|                                         <td> | ||||
|                                             <v-btn | ||||
|                                                 text | ||||
|                                                 @click=" | ||||
|                                                     dialog = false; | ||||
|                                                     copied = false; | ||||
|                                                 " | ||||
|                                                 :to="`/class/${c.id}`" | ||||
|                                                 variant="text" | ||||
|                                             > | ||||
|                                                 {{ t("close") }} | ||||
|                                                 {{ c.displayName }} | ||||
|                                                 <v-icon end> mdi-menu-right </v-icon> | ||||
|                                             </v-btn> | ||||
|                                         </v-card-actions> | ||||
|                                     </v-card> | ||||
|                                 </v-dialog> | ||||
|                             </v-container> | ||||
|                         </div> | ||||
|                     </v-col> | ||||
|                 </v-row> | ||||
|             </v-container> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                             <span v-if="!isMdAndDown">{{ c.id }}</span> | ||||
|                                             <span | ||||
|                                                 v-else | ||||
|                                                 style="cursor: pointer" | ||||
|                                                 @click="openCodeDialog(c.id)" | ||||
|                                                 ><v-icon icon="mdi-eye"></v-icon | ||||
|                                             ></span> | ||||
|                                         </td> | ||||
| 
 | ||||
|                                         <td>{{ c.students.length }}</td> | ||||
|                                     </tr> | ||||
|                                 </tbody> | ||||
|                             </v-table> | ||||
|                         </v-col> | ||||
|                         <v-col | ||||
|                             cols="12" | ||||
|                             sm="6" | ||||
|                             md="6" | ||||
|                             class="responsive-col" | ||||
|                         > | ||||
|                             <div> | ||||
|                                 <h2>{{ t("createClass") }}</h2> | ||||
| 
 | ||||
|                                 <v-sheet | ||||
|                                     class="pa-4 sheet" | ||||
|                                     max-width="600px" | ||||
|                                 > | ||||
|                                     <p>{{ t("createClassInstructions") }}</p> | ||||
|                                     <v-form @submit.prevent> | ||||
|                                         <v-text-field | ||||
|                                             class="mt-4" | ||||
|                                             :label="`${t('classname')}`" | ||||
|                                             v-model="className" | ||||
|                                             :placeholder="`${t('EnterNameOfClass')}`" | ||||
|                                             :rules="nameRules" | ||||
|                                             variant="outlined" | ||||
|                                         ></v-text-field> | ||||
|                                         <v-btn | ||||
|                                             class="mt-4" | ||||
|                                             color="#f6faf2" | ||||
|                                             type="submit" | ||||
|                                             @click="createClass" | ||||
|                                             block | ||||
|                                             >{{ t("create") }}</v-btn | ||||
|                                         > | ||||
|                                     </v-form> | ||||
|                                 </v-sheet> | ||||
|                                 <v-container> | ||||
|                                     <v-dialog | ||||
|                                         v-model="dialog" | ||||
|                                         max-width="400px" | ||||
|                                     > | ||||
|                                         <v-card> | ||||
|                                             <v-card-title class="headline">code</v-card-title> | ||||
|                                             <v-card-text> | ||||
|                                                 <v-text-field | ||||
|                                                     v-model="code" | ||||
|                                                     readonly | ||||
|                                                     append-inner-icon="mdi-content-copy" | ||||
|                                                     @click:append-inner="copyToClipboard" | ||||
|                                                 ></v-text-field> | ||||
|                                                 <v-slide-y-transition> | ||||
|                                                     <div | ||||
|                                                         v-if="copied" | ||||
|                                                         class="text-center mt-2" | ||||
|                                                     > | ||||
|                                                         {{ t("copied") }} | ||||
|                                                     </div> | ||||
|                                                 </v-slide-y-transition> | ||||
|                                             </v-card-text> | ||||
|                                             <v-card-actions> | ||||
|                                                 <v-spacer></v-spacer> | ||||
|                                                 <v-btn | ||||
|                                                     text | ||||
|                                                     @click=" | ||||
|                                                         dialog = false; | ||||
|                                                         copied = false; | ||||
|                                                     " | ||||
|                                                 > | ||||
|                                                     {{ t("close") }} | ||||
|                                                 </v-btn> | ||||
|                                             </v-card-actions> | ||||
|                                         </v-card> | ||||
|                                     </v-dialog> | ||||
|                                 </v-container> | ||||
|                             </div> | ||||
|                         </v-col> | ||||
|                     </v-row> | ||||
|                 </v-container> | ||||
|             </using-query-result> | ||||
| 
 | ||||
|             <h1 class="title"> | ||||
|                 {{ t("invitations") }} | ||||
|             </h1> | ||||
|             <v-table class="table"> | ||||
|                 <thead> | ||||
|                     <tr> | ||||
|                         <th class="header">{{ t("class") }}</th> | ||||
|                         <th class="header">{{ t("sender") }}</th> | ||||
|                         <th class="header"></th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                     <tr | ||||
|                         v-for="i in invitations" | ||||
|                         :key="i.classId" | ||||
|                     > | ||||
|                         <td> | ||||
|                             {{ i.classId }} | ||||
|                             <!-- TODO fetch display name via classId because db only returns classId field --> | ||||
|                         </td> | ||||
|                         <td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td> | ||||
|                         <td class="text-right"> | ||||
|                             <div> | ||||
|                                 <v-btn | ||||
|                                     color="green" | ||||
|                                     @click="acceptRequest" | ||||
|                                     class="mr-2" | ||||
|             <v-container | ||||
|                 fluid | ||||
|                 class="ma-4" | ||||
|             > | ||||
|                 <v-table class="table"> | ||||
|                     <thead> | ||||
|                         <tr> | ||||
|                             <th class="header">{{ t("class") }}</th> | ||||
|                             <th class="header">{{ t("sender") }}</th> | ||||
|                             <th class="header">{{ t("accept") + "/" + t("reject") }}</th> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                         <using-query-result | ||||
|                             :query-result="getInvitationsQuery" | ||||
|                             v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" | ||||
|                         > | ||||
|                             <using-query-result | ||||
|                                 :query-result="allClassesQuery" | ||||
|                                 v-slot="classesResponse: { data: ClassesResponse }" | ||||
|                             > | ||||
|                                 <tr | ||||
|                                     v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" | ||||
|                                     :key="i.classId" | ||||
|                                 > | ||||
|                                     {{ t("accept") }} | ||||
|                                 </v-btn> | ||||
|                                 <v-btn | ||||
|                                     color="red" | ||||
|                                     @click="denyRequest" | ||||
|                                 > | ||||
|                                     {{ t("deny") }} | ||||
|                                 </v-btn> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                     </tr> | ||||
|                 </tbody> | ||||
|             </v-table> | ||||
|                                     <td> | ||||
|                                         {{ | ||||
|                                             (classesResponse.data.classes as ClassDTO[]).filter( | ||||
|                                                 (c) => c.id == i.classId, | ||||
|                                             )[0].displayName | ||||
|                                         }} | ||||
|                                     </td> | ||||
|                                     <td> | ||||
|                                         {{ | ||||
|                                             (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName | ||||
|                                         }} | ||||
|                                     </td> | ||||
|                                     <td class="text-right"> | ||||
|                                         <span v-if="!isSmAndDown"> | ||||
|                                             <div> | ||||
|                                                 <v-btn | ||||
|                                                     color="green" | ||||
|                                                     @click="handleInvitation(i, true)" | ||||
|                                                     class="mr-2" | ||||
|                                                 > | ||||
|                                                     {{ t("accept") }} | ||||
|                                                 </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     color="red" | ||||
|                                                     @click="handleInvitation(i, false)" | ||||
|                                                 > | ||||
|                                                     {{ t("deny") }} | ||||
|                                                 </v-btn> | ||||
|                                             </div> | ||||
|                                         </span> | ||||
|                                         <span v-else> | ||||
|                                             <div> | ||||
|                                                 <v-btn | ||||
|                                                     @click="handleInvitation(i, true)" | ||||
|                                                     class="mr-2" | ||||
|                                                     icon="mdi-check-circle" | ||||
|                                                     color="green" | ||||
|                                                     variant="text" | ||||
|                                                 > | ||||
|                                                 </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     @click="handleInvitation(i, false)" | ||||
|                                                     class="mr-2" | ||||
|                                                     icon="mdi-close-circle" | ||||
|                                                     color="red" | ||||
|                                                     variant="text" | ||||
|                                                 > | ||||
|                                                 </v-btn></div | ||||
|                                         ></span> | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </using-query-result> | ||||
|                         </using-query-result> | ||||
|                     </tbody> | ||||
|                 </v-table> | ||||
|             </v-container> | ||||
|         </div> | ||||
|         <v-snackbar | ||||
|             v-model="snackbar.visible" | ||||
|  | @ -311,6 +410,42 @@ | |||
|         > | ||||
|             {{ snackbar.message }} | ||||
|         </v-snackbar> | ||||
|         <v-dialog | ||||
|             v-model="viewCodeDialog" | ||||
|             max-width="400px" | ||||
|         > | ||||
|             <v-card> | ||||
|                 <v-card-title class="headline">{{ t("code") }}</v-card-title> | ||||
|                 <v-card-text> | ||||
|                     <v-text-field | ||||
|                         v-model="selectedCode" | ||||
|                         readonly | ||||
|                         append-inner-icon="mdi-content-copy" | ||||
|                         @click:append-inner="copyToClipboard" | ||||
|                     ></v-text-field> | ||||
|                     <v-slide-y-transition> | ||||
|                         <div | ||||
|                             v-if="copied" | ||||
|                             class="text-center mt-2" | ||||
|                         > | ||||
|                             {{ t("copied") }} | ||||
|                         </div> | ||||
|                     </v-slide-y-transition> | ||||
|                 </v-card-text> | ||||
|                 <v-card-actions> | ||||
|                     <v-spacer></v-spacer> | ||||
|                     <v-btn | ||||
|                         text | ||||
|                         @click=" | ||||
|                             viewCodeDialog = false; | ||||
|                             copied = false; | ||||
|                         " | ||||
|                     > | ||||
|                         {{ t("close") }} | ||||
|                     </v-btn> | ||||
|                 </v-card-actions> | ||||
|             </v-card> | ||||
|         </v-dialog> | ||||
|     </main> | ||||
| </template> | ||||
| <style scoped> | ||||
|  | @ -378,7 +513,7 @@ | |||
|         margin-left: 30px; | ||||
|     } | ||||
| 
 | ||||
|     @media screen and (max-width: 800px) { | ||||
|     @media screen and (max-width: 850px) { | ||||
|         h1 { | ||||
|             text-align: center; | ||||
|             padding-left: 0; | ||||
|  | @ -401,5 +536,18 @@ | |||
|             justify-content: center; | ||||
|             margin: 5px; | ||||
|         } | ||||
| 
 | ||||
|         .custom-breakpoint { | ||||
|             flex-direction: column !important; | ||||
|         } | ||||
| 
 | ||||
|         .table { | ||||
|             width: 100%; | ||||
|         } | ||||
| 
 | ||||
|         .responsive-col { | ||||
|             max-width: 100% !important; | ||||
|             flex-basis: 100% !important; | ||||
|         } | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue
	
	 Laure Jablonski
						Laure Jablonski