Merge remote-tracking branch 'origin/dev' into fix/verschillende-authenticatieproblemen
# Conflicts: # frontend/src/i18n/locale/de.json # frontend/src/i18n/locale/en.json
This commit is contained in:
		
						commit
						af0f40f63e
					
				
					 21 changed files with 1006 additions and 492 deletions
				
			
		|  | @ -40,7 +40,7 @@ export async function updateInvitationHandler(req: Request, res: Response): Prom | ||||||
|     const sender = req.body.sender; |     const sender = req.body.sender; | ||||||
|     const receiver = req.body.receiver; |     const receiver = req.body.receiver; | ||||||
|     const classId = req.body.class; |     const classId = req.body.class; | ||||||
|     req.body.accepted = req.body.accepted !== 'false'; |     req.body.accepted = req.body.accepted !== false; | ||||||
|     requireFields({ sender, receiver, classId }); |     requireFields({ sender, receiver, classId }); | ||||||
| 
 | 
 | ||||||
|     const data = req.body as TeacherInvitationData; |     const data = req.body as TeacherInvitationData; | ||||||
|  |  | ||||||
|  | @ -34,6 +34,6 @@ router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); | ||||||
| router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); | router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); | ||||||
| 
 | 
 | ||||||
| // Invitations to other classes a teacher received
 | // Invitations to other classes a teacher received
 | ||||||
| router.get('/invitations', invitationRouter); | router.use('/invitations', invitationRouter); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ export interface StudentResponse { | ||||||
|     student: StudentDTO; |     student: StudentDTO; | ||||||
| } | } | ||||||
| export interface JoinRequestsResponse { | export interface JoinRequestsResponse { | ||||||
|     requests: ClassJoinRequestDTO[]; |     joinRequests: ClassJoinRequestDTO[]; | ||||||
| } | } | ||||||
| export interface JoinRequestResponse { | export interface JoinRequestResponse { | ||||||
|     request: ClassJoinRequestDTO; |     request: ClassJoinRequestDTO; | ||||||
|  |  | ||||||
|  | @ -11,10 +11,11 @@ export interface TeacherInvitationResponse { | ||||||
| 
 | 
 | ||||||
| export class TeacherInvitationController extends BaseController { | export class TeacherInvitationController extends BaseController { | ||||||
|     constructor() { |     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 }); |         return this.get<TeacherInvitationsResponse>(`/${username}`, { sent }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,10 +54,9 @@ export class TeacherController extends BaseController { | ||||||
|         studentUsername: string, |         studentUsername: string, | ||||||
|         accepted: boolean, |         accepted: boolean, | ||||||
|     ): Promise<JoinRequestResponse> { |     ): Promise<JoinRequestResponse> { | ||||||
|         return this.put<JoinRequestResponse>( |         return this.put<JoinRequestResponse>(`/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, { | ||||||
|             `/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, |  | ||||||
|             accepted, |             accepted, | ||||||
|         ); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);}
 |     // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);}
 | ||||||
|  |  | ||||||
|  | @ -88,5 +88,17 @@ | ||||||
|     "loadSubmission": "Einladen", |     "loadSubmission": "Einladen", | ||||||
|     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", |     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", | ||||||
|     "viewAsGroup": "Fortschritt ansehen von Gruppe...", |     "viewAsGroup": "Fortschritt ansehen von Gruppe...", | ||||||
|     "assignLearningPath": "Als Aufgabe geben" |     "assignLearningPath": "Als Aufgabe geben", | ||||||
|  |     "remove": "entfernen", | ||||||
|  |     "students": "Studenten", | ||||||
|  |     "classJoinRequests": "Beitrittsanfragen", | ||||||
|  |     "reject": "ablehnen", | ||||||
|  |     "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" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -88,5 +88,17 @@ | ||||||
|     "loadSubmission": "Load", |     "loadSubmission": "Load", | ||||||
|     "noSubmissionsYet": "No submissions yet.", |     "noSubmissionsYet": "No submissions yet.", | ||||||
|     "viewAsGroup": "View progress of group...", |     "viewAsGroup": "View progress of group...", | ||||||
|     "assignLearningPath": "assign" |     "assignLearningPath": "assign", | ||||||
|  |     "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" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -88,5 +88,17 @@ | ||||||
|     "loadSubmission": "Charger", |     "loadSubmission": "Charger", | ||||||
|     "noSubmissionsYet": "Pas encore de soumissions.", |     "noSubmissionsYet": "Pas encore de soumissions.", | ||||||
|     "viewAsGroup": "Voir la progression du groupe...", |     "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" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -88,5 +88,17 @@ | ||||||
|     "loadSubmission": "Inladen", |     "loadSubmission": "Inladen", | ||||||
|     "noSubmissionsYet": "Nog geen indieningen.", |     "noSubmissionsYet": "Nog geen indieningen.", | ||||||
|     "viewAsGroup": "Vooruitgang bekijken van groep...", |     "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 { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | import { | ||||||
| import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; |     useMutation, | ||||||
|  |     type UseMutationReturnType, | ||||||
|  |     useQuery, | ||||||
|  |     type UseQueryReturnType, | ||||||
|  |     useQueryClient, | ||||||
|  | } from "@tanstack/vue-query"; | ||||||
| import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts"; | 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( | export function useAnswersQuery( | ||||||
|     questionId: MaybeRefOrGetter<QuestionId>, |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|     full: MaybeRefOrGetter<boolean> = true, |     full: MaybeRefOrGetter<boolean> = true, | ||||||
| ): UseQueryReturnType<AnswersResponse, Error> { | ): UseQueryReturnType<AnswersResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|  |         queryKey: computed(() => answersQueryKey(toValue(questionId), toValue(full))), | ||||||
|         queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)), |         queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)), | ||||||
|         enabled: () => Boolean(toValue(questionId)), |         enabled: () => Boolean(toValue(questionId)), | ||||||
|     }); |     }); | ||||||
|  | @ -21,31 +42,68 @@ export function useAnswerQuery( | ||||||
|     sequenceNumber: MaybeRefOrGetter<number>, |     sequenceNumber: MaybeRefOrGetter<number>, | ||||||
| ): UseQueryReturnType<AnswerResponse, Error> { | ): UseQueryReturnType<AnswerResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|  |         queryKey: computed(() => answerQueryKey(toValue(questionId), toValue(sequenceNumber))), | ||||||
|         queryFn: async () => new AnswerController(toValue(questionId)).getBy(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( | export function useCreateAnswerMutation( | ||||||
|     questionId: MaybeRefOrGetter<QuestionId>, |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
| ): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> { | ): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     return useMutation({ | ||||||
|         mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data), |         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( | export function useDeleteAnswerMutation( | ||||||
|     questionId: MaybeRefOrGetter<QuestionId>, |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
| ): UseMutationReturnType<AnswerResponse, Error, number, unknown> { | ): UseMutationReturnType<AnswerResponse, Error, number, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     return useMutation({ | ||||||
|         mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq), |         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( | export function useUpdateAnswerMutation( | ||||||
|     questionId: MaybeRefOrGetter<QuestionId>, |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
| ): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> { | ): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     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 { invalidateAllAssignmentKeys } from "./assignments"; | ||||||
| import { invalidateAllGroupKeys } from "./groups"; | import { invalidateAllGroupKeys } from "./groups"; | ||||||
| import { invalidateAllSubmissionKeys } from "./submissions"; | import { invalidateAllSubmissionKeys } from "./submissions"; | ||||||
|  | import type { TeachersResponse } from "@/controllers/teachers"; | ||||||
|  | import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||||
| 
 | 
 | ||||||
| const classController = new ClassController(); | const classController = new ClassController(); | ||||||
| 
 | 
 | ||||||
|  | @ -176,7 +178,7 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType< | ||||||
| export function useClassTeachersQuery( | export function useClassTeachersQuery( | ||||||
|     id: MaybeRefOrGetter<string | undefined>, |     id: MaybeRefOrGetter<string | undefined>, | ||||||
|     full: MaybeRefOrGetter<boolean> = true, |     full: MaybeRefOrGetter<boolean> = true, | ||||||
| ): UseQueryReturnType<StudentsResponse, Error> { | ): UseQueryReturnType<TeachersResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), |         queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), | ||||||
|         queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)), |         queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)), | ||||||
|  | @ -223,7 +225,7 @@ export function useClassDeleteTeacherMutation(): UseMutationReturnType< | ||||||
| export function useClassTeacherInvitationsQuery( | export function useClassTeacherInvitationsQuery( | ||||||
|     id: MaybeRefOrGetter<string | undefined>, |     id: MaybeRefOrGetter<string | undefined>, | ||||||
|     full: MaybeRefOrGetter<boolean> = true, |     full: MaybeRefOrGetter<boolean> = true, | ||||||
| ): UseQueryReturnType<StudentsResponse, Error> { | ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), |         queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), | ||||||
|         queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)), |         queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)), | ||||||
|  |  | ||||||
|  | @ -14,12 +14,12 @@ export function questionsQueryKey( | ||||||
|     loId: LearningObjectIdentifierDTO, |     loId: LearningObjectIdentifierDTO, | ||||||
|     full: boolean, |     full: boolean, | ||||||
| ): [string, string, number, string, 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] { | export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { | ||||||
|     const loId = questionId.learningObjectIdentifier; |     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( | export function useQuestionsQuery( | ||||||
|  | @ -39,7 +39,7 @@ export function useQuestionQuery( | ||||||
|     const loId = toValue(questionId).learningObjectIdentifier; |     const loId = toValue(questionId).learningObjectIdentifier; | ||||||
|     const sequenceNumber = toValue(questionId).sequenceNumber; |     const sequenceNumber = toValue(questionId).sequenceNumber; | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => questionQueryKey(loId, sequenceNumber)), |         queryKey: computed(() => questionQueryKey(toValue(questionId))), | ||||||
|         queryFn: async () => new QuestionController(loId).getBy(sequenceNumber), |         queryFn: async () => new QuestionController(loId).getBy(sequenceNumber), | ||||||
|         enabled: () => Boolean(toValue(questionId)), |         enabled: () => Boolean(toValue(questionId)), | ||||||
|     }); |     }); | ||||||
|  | @ -55,6 +55,7 @@ export function useCreateQuestionMutation( | ||||||
|         onSuccess: async () => { |         onSuccess: async () => { | ||||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); |             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), true) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); |             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 { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
| import type { QuestionsResponse } from "@/controllers/questions.ts"; | import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
| import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
|  | import { teacherClassJoinRequests } from "@/queries/teachers.ts"; | ||||||
| 
 | 
 | ||||||
| const studentController = new StudentController(); | const studentController = new StudentController(); | ||||||
| 
 | 
 | ||||||
|  | @ -174,13 +175,13 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType< | ||||||
|     unknown |     unknown | ||||||
| > { | > { | ||||||
|     const queryClient = useQueryClient(); |     const queryClient = useQueryClient(); | ||||||
| 
 |  | ||||||
|     return useMutation({ |     return useMutation({ | ||||||
|         mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), |         mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), | ||||||
|         onSuccess: async (newJoinRequest) => { |         onSuccess: async (newJoinRequest) => { | ||||||
|             await queryClient.invalidateQueries({ |             await queryClient.invalidateQueries({ | ||||||
|                 queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username), |                 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; |             const classId = deletedJoinRequest.request.class; | ||||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); |             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 { | ||||||
| import { computed, toValue } from "vue"; |     useMutation, | ||||||
|  |     useQuery, | ||||||
|  |     useQueryClient, | ||||||
|  |     type UseMutationReturnType, | ||||||
|  |     type UseQueryReturnType, | ||||||
|  | } from "@tanstack/vue-query"; | ||||||
|  | import { toValue } from "vue"; | ||||||
| import type { MaybeRefOrGetter } from "vue"; | import type { MaybeRefOrGetter } from "vue"; | ||||||
| import { | import { | ||||||
|     TeacherInvitationController, |     TeacherInvitationController, | ||||||
|     type TeacherInvitationResponse, |     type TeacherInvitationResponse, | ||||||
|     type TeacherInvitationsResponse, |     type TeacherInvitationsResponse, | ||||||
| } from "@/controllers/teacher-invitations.ts"; | } from "@/controllers/teacher-invitations"; | ||||||
| import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; | import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||||
| import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; |  | ||||||
| 
 | 
 | ||||||
| const controller = new TeacherInvitationController(); | 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( | export function useTeacherInvitationsSentQuery( | ||||||
|     username: MaybeRefOrGetter<string | undefined>, |     username: MaybeRefOrGetter<string | undefined>, | ||||||
| ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||||
|     return useQuery({ |     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)), |         enabled: () => Boolean(toValue(username)), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|     All the pending invitations sent to this teacher |  * All the pending invitations sent to this teacher | ||||||
|  */ |  */ | ||||||
| export function useTeacherInvitationsReceivedQuery( | export function useTeacherInvitationsReceivedQuery( | ||||||
|     username: MaybeRefOrGetter<string | undefined>, |     username: MaybeRefOrGetter<string | undefined>, | ||||||
| ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ): UseQueryReturnType<TeacherInvitationsResponse, Error> { | ||||||
|     return useQuery({ |     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)), |         enabled: () => Boolean(toValue(username)), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | @ -39,7 +59,8 @@ export function useTeacherInvitationQuery( | ||||||
|     data: MaybeRefOrGetter<TeacherInvitationData | undefined>, |     data: MaybeRefOrGetter<TeacherInvitationData | undefined>, | ||||||
| ): UseQueryReturnType<TeacherInvitationResponse, Error> { | ): UseQueryReturnType<TeacherInvitationResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryFn: computed(async () => controller.getBy(toValue(data))), |         queryKey: teacherInvitationQueryKey(toValue(data)), | ||||||
|  |         queryFn: async () => controller.getBy(toValue(data)), | ||||||
|         enabled: () => Boolean(toValue(data)), |         enabled: () => Boolean(toValue(data)), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | @ -47,32 +68,68 @@ export function useTeacherInvitationQuery( | ||||||
| export function useCreateTeacherInvitationMutation(): UseMutationReturnType< | export function useCreateTeacherInvitationMutation(): UseMutationReturnType< | ||||||
|     TeacherInvitationResponse, |     TeacherInvitationResponse, | ||||||
|     Error, |     Error, | ||||||
|     TeacherDTO, |     TeacherInvitationData, | ||||||
|     unknown |     unknown | ||||||
| > { | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     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< | export function useRespondTeacherInvitationMutation(): UseMutationReturnType< | ||||||
|     TeacherInvitationResponse, |     TeacherInvitationResponse, | ||||||
|     Error, |     Error, | ||||||
|     TeacherDTO, |     TeacherInvitationData, | ||||||
|     unknown |     unknown | ||||||
| > { | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     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< | export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< | ||||||
|     TeacherInvitationResponse, |     TeacherInvitationResponse, | ||||||
|     Error, |     Error, | ||||||
|     TeacherDTO, |     TeacherInvitationData, | ||||||
|     unknown |     unknown | ||||||
| > { | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|     return useMutation({ |     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]; |     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> { | export function useTeachersQuery(full: MaybeRefOrGetter<boolean> = false): UseQueryReturnType<TeachersResponse, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => teachersQueryKey(toValue(full))), |         queryKey: computed(() => teachersQueryKey(toValue(full))), | ||||||
|  | @ -92,7 +96,7 @@ export function useTeacherJoinRequestsQuery( | ||||||
|     classId: MaybeRefOrGetter<string | undefined>, |     classId: MaybeRefOrGetter<string | undefined>, | ||||||
| ): UseQueryReturnType<JoinRequestsResponse, Error> { | ): UseQueryReturnType<JoinRequestsResponse, Error> { | ||||||
|     return useQuery({ |     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)!), |         queryFn: async () => teacherController.getStudentJoinRequests(toValue(username)!, toValue(classId)!), | ||||||
|         enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), |         enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), | ||||||
|     }); |     }); | ||||||
|  | @ -133,10 +137,11 @@ export function useUpdateJoinRequestMutation(): UseMutationReturnType< | ||||||
|         mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) => |         mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) => | ||||||
|             teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted), |             teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted), | ||||||
|         onSuccess: async (deletedJoinRequest) => { |         onSuccess: async (deletedJoinRequest) => { | ||||||
|             const username = deletedJoinRequest.request.requester; |             const username = deletedJoinRequest.request.requester.username; | ||||||
|             const classId = deletedJoinRequest.request.class; |             const classId = deletedJoinRequest.request.class; | ||||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); |             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 { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | import { type MaybeRefOrGetter, toValue } from "vue"; | ||||||
| import type { Theme } from "@dwengo-1/interfaces/theme"; |  | ||||||
| import { getThemeController } from "@/controllers/controllers.ts"; | import { getThemeController } from "@/controllers/controllers.ts"; | ||||||
|  | import type { Theme } from "@dwengo-1/common/interfaces/theme"; | ||||||
| 
 | 
 | ||||||
| const themeController = getThemeController(); | const themeController = getThemeController(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,6 @@ import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; | ||||||
| import SingleClass from "@/views/classes/SingleClass.vue"; | import SingleClass from "@/views/classes/SingleClass.vue"; | ||||||
| import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; | import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; | ||||||
| import NotFound from "@/components/errors/NotFound.vue"; | import NotFound from "@/components/errors/NotFound.vue"; | ||||||
| import CreateClass from "@/views/classes/CreateClass.vue"; |  | ||||||
| import CreateAssignment from "@/views/assignments/CreateAssignment.vue"; | import CreateAssignment from "@/views/assignments/CreateAssignment.vue"; | ||||||
| import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; | import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; | ||||||
| import CallbackPage from "@/views/CallbackPage.vue"; | import CallbackPage from "@/views/CallbackPage.vue"; | ||||||
|  | @ -84,12 +83,6 @@ const router = createRouter({ | ||||||
|             component: SingleAssignment, |             component: SingleAssignment, | ||||||
|             meta: { requiresAuth: true }, |             meta: { requiresAuth: true }, | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|             path: "/class/create", |  | ||||||
|             name: "CreateClass", |  | ||||||
|             component: CreateClass, |  | ||||||
|             meta: { requiresAuth: true }, |  | ||||||
|         }, |  | ||||||
|         { |         { | ||||||
|             path: "/class/:id", |             path: "/class/:id", | ||||||
|             name: "SingleClass", |             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"> | <script setup lang="ts"> | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     import authState from "@/services/auth/auth-service.ts"; | ||||||
|     import { onMounted, ref } from "vue"; |     import { onMounted, ref, watchEffect } from "vue"; | ||||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |  | ||||||
|     import { useRoute } from "vue-router"; |     import { useRoute } from "vue-router"; | ||||||
|     import { ClassController, type ClassResponse } from "@/controllers/classes"; |     import type { ClassResponse } from "@/controllers/classes"; | ||||||
|     import type { StudentsResponse } from "@/controllers/students"; |     import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students"; | ||||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; |     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(); |     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 route = useRoute(); | ||||||
|     const classId: string = route.params.id as string; |     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); |     // Queries used to access the backend and catch loading or errors | ||||||
|     const currentClass = ref<ClassDTO | undefined>(undefined); |  | ||||||
|     const students = ref<StudentDTO[]>([]); |  | ||||||
| 
 | 
 | ||||||
|     // Find the username of the logged in user so it can be used to fetch other information |     // Gets the class a teacher wants to manage | ||||||
|     // When loading the page |     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 () => { |     onMounted(async () => { | ||||||
|         const userObject = await authState.loadUser(); |         isLoading.value = true; | ||||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; |         try { | ||||||
| 
 |             const userObject = await authState.loadUser(); | ||||||
|         // Get class of which information should be shown |             username.value = userObject!.profile.preferred_username; | ||||||
|         const classResponse: ClassResponse = await classController.getById(classId); |         } catch (error) { | ||||||
|         if (classResponse && classResponse.class) { |             isError.value = true; | ||||||
|             currentClass.value = classResponse.class; |             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||||
|  |         } finally { | ||||||
|             isLoading.value = false; |             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 |     // Used to set the visibility of the dialog | ||||||
|     // Popup to verify removing student |  | ||||||
|     const dialog = ref(false); |     const dialog = ref(false); | ||||||
|  |     // Student selected for deletion | ||||||
|     const selectedStudent = ref<StudentDTO | null>(null); |     const selectedStudent = ref<StudentDTO | null>(null); | ||||||
| 
 | 
 | ||||||
|  |     // Let the teacher verify deletion of a student | ||||||
|     function showPopup(s: StudentDTO): void { |     function showPopup(s: StudentDTO): void { | ||||||
|         selectedStudent.value = s; |         selectedStudent.value = s; | ||||||
|         dialog.value = true; |         dialog.value = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Remove student from class |     async function removeStudentFromclass(): Promise<void> { | ||||||
|     function removeStudentFromclass(): void { |         // Delete student from class | ||||||
|         dialog.value = false; |         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> | </script> | ||||||
| <template> | <template> | ||||||
|     <main> |     <main> | ||||||
|         <div |         <div | ||||||
|  |             class="loading-div" | ||||||
|             v-if="isLoading" |             v-if="isLoading" | ||||||
|             class="text-center py-10" |  | ||||||
|         > |         > | ||||||
|             <v-progress-circular |             <v-progress-circular indeterminate></v-progress-circular> | ||||||
|                 indeterminate |  | ||||||
|                 color="primary" |  | ||||||
|             /> |  | ||||||
|             <p>Loading...</p> |  | ||||||
|         </div> |         </div> | ||||||
|         <div v-else> |         <div v-if="isError"> | ||||||
|             <h1 class="title">{{ currentClass!.displayName }}</h1> |             <v-empty-state | ||||||
|             <v-container |                 icon="mdi-alert-circle-outline" | ||||||
|                 fluid |                 :text="errorMessage" | ||||||
|                 class="ma-4" |                 :title="t('error_title')" | ||||||
|             > |             ></v-empty-state> | ||||||
|                 <v-row |         </div> | ||||||
|                     no-gutters |         <using-query-result | ||||||
|                     fluid |             :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 |                     <v-container | ||||||
|                         cols="12" |                         fluid | ||||||
|                         sm="6" |                         class="ma-4" | ||||||
|                         md="6" |  | ||||||
|                     > |                     > | ||||||
|                         <v-table class="table"> |                         <v-row | ||||||
|                             <thead> |                             no-gutters | ||||||
|                                 <tr> |                             fluid | ||||||
|                                     <th class="header">{{ t("students") }}</th> |                         > | ||||||
|                                     <th class="header"></th> |                             <v-col | ||||||
|                                 </tr> |                                 cols="12" | ||||||
|                             </thead> |                                 sm="6" | ||||||
|                             <tbody> |                                 md="6" | ||||||
|                                 <tr |                             > | ||||||
|                                     v-for="s in students" |                                 <v-table class="table"> | ||||||
|                                     :key="s.id" |                                     <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> |                                     <v-table class="table"> | ||||||
|                                         {{ s.firstName + " " + s.lastName }} |                                         <thead> | ||||||
|                                     </td> |                                             <tr> | ||||||
|                                     <td> |                                                 <th class="header">{{ t("classJoinRequests") }}</th> | ||||||
|                                         <v-btn @click="showPopup"> {{ t("remove") }} </v-btn> |                                                 <th class="header">{{ t("accept") + "/" + t("reject") }}</th> | ||||||
|                                     </td> |                                             </tr> | ||||||
|                                 </tr> |                                         </thead> | ||||||
|                             </tbody> |                                         <tbody> | ||||||
|                         </v-table> |                                             <tr | ||||||
|                     </v-col> |                                                 v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]" | ||||||
|                 </v-row> |                                                 :key="(jr.class, jr.requester, jr.status)" | ||||||
|             </v-container> |                                             > | ||||||
|         </div> |                                                 <td> | ||||||
|         <v-dialog |                                                     {{ jr.requester.firstName + " " + jr.requester.lastName }} | ||||||
|             v-model="dialog" |                                                 </td> | ||||||
|             max-width="400px" |                                                 <td> | ||||||
|         > |                                                     <span v-if="!isSmAndDown && !isMdAndDown"> | ||||||
|             <v-card> |                                                         <v-btn | ||||||
|                 <v-card-title class="headline">{{ t("areusure") }}</v-card-title> |                                                             @click="handleJoinRequest(jr, true)" | ||||||
|  |                                                             class="mr-2" | ||||||
|  |                                                             color="green" | ||||||
|  |                                                         > | ||||||
|  |                                                             {{ t("accept") }}</v-btn | ||||||
|  |                                                         > | ||||||
| 
 | 
 | ||||||
|                 <v-card-actions> |                                                         <v-btn | ||||||
|                     <v-spacer></v-spacer> |                                                             @click="handleJoinRequest(jr, false)" | ||||||
|                     <v-btn |                                                             class="mr-2" | ||||||
|                         text |                                                             color="red" | ||||||
|                         @click="dialog = false" |                                                         > | ||||||
|  |                                                             {{ 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-form @submit.prevent> | ||||||
|                     </v-btn> |                             <v-text-field | ||||||
|                     <v-btn |                                 :label="`${t('username')}`" | ||||||
|                         text |                                 v-model="usernameTeacher" | ||||||
|                         @click="removeStudentFromclass" |                                 :placeholder="`${t('username')}`" | ||||||
|                         >{{ t("yes") }}</v-btn |                                 variant="outlined" | ||||||
|                     > |                             ></v-text-field> | ||||||
|                 </v-card-actions> |                             <v-btn | ||||||
|             </v-card> |                                 class="mt-4" | ||||||
|         </v-dialog> |                                 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> |     </main> | ||||||
| </template> | </template> | ||||||
| <style scoped> | <style scoped> | ||||||
|  |  | ||||||
|  | @ -1,51 +1,51 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     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 { validate, version } from "uuid"; | ||||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||||
|     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; |     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; | ||||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; |     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
|     import { StudentController } from "@/controllers/students"; |  | ||||||
|     import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; |     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 { t } = useI18n(); | ||||||
|     const studentController: StudentController = new StudentController(); |  | ||||||
|     const teacherController: TeacherController = new TeacherController(); |  | ||||||
| 
 | 
 | ||||||
|     // Username of logged in student |     // Username of logged in student | ||||||
|     const username = ref<string | undefined>(undefined); |     const username = ref<string | undefined>(undefined); | ||||||
| 
 |     const isLoading = ref(false); | ||||||
|     // Find the username of the logged in user so it can be used to fetch other information |     const isError = ref(false); | ||||||
|     // When loading the page |     const errorMessage = ref<string>(""); | ||||||
|     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[]; |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     // Students of selected class are shown when logged in student presses on the member count |     // Students of selected class are shown when logged in student presses on the member count | ||||||
|     const selectedClass = ref<ClassDTO | null>(null); |     const selectedClass = ref<ClassDTO | null>(null); | ||||||
|     const students = ref<StudentDTO[]>([]); |  | ||||||
|     const teachers = ref<TeacherDTO[]>([]); |  | ||||||
|     const getStudents = ref(false); |     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 |     // Boolean that handles visibility for dialogs | ||||||
|     // Clicking on membercount will show a dialog with all members |     // Clicking on membercount will show a dialog with all members | ||||||
|     const dialog = ref(false); |     const dialog = ref(false); | ||||||
|  | @ -54,48 +54,19 @@ | ||||||
|     async function openStudentDialog(c: ClassDTO): Promise<void> { |     async function openStudentDialog(c: ClassDTO): Promise<void> { | ||||||
|         selectedClass.value = c; |         selectedClass.value = c; | ||||||
| 
 | 
 | ||||||
|         // Clear previous value |         // Let the component know it should show the students in a class | ||||||
|         getStudents.value = true; |         getStudents.value = true; | ||||||
|         students.value = []; |         await getStudentsQuery.refetch(); | ||||||
|         dialog.value = true; |         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> { |     async function openTeacherDialog(c: ClassDTO): Promise<void> { | ||||||
|         selectedClass.value = c; |         selectedClass.value = c; | ||||||
| 
 | 
 | ||||||
|         // Clear previous value |         // Let the component know it should show teachers of a class | ||||||
|         getStudents.value = false; |         getStudents.value = false; | ||||||
|         teachers.value = []; |         await getTeachersQuery.refetch(); | ||||||
|         dialog.value = true; |         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 |     // Hold the code a student gives in to join a class | ||||||
|  | @ -151,100 +122,111 @@ | ||||||
| <template> | <template> | ||||||
|     <main> |     <main> | ||||||
|         <div |         <div | ||||||
|  |             class="loading-div" | ||||||
|             v-if="isLoading" |             v-if="isLoading" | ||||||
|             class="text-center py-10" |  | ||||||
|         > |         > | ||||||
|             <v-progress-circular |             <v-progress-circular indeterminate></v-progress-circular> | ||||||
|                 indeterminate |  | ||||||
|                 color="primary" |  | ||||||
|             /> |  | ||||||
|             <p>Loading...</p> |  | ||||||
|         </div> |         </div> | ||||||
| 
 |         <div v-if="isError"> | ||||||
|         <div |             <v-empty-state | ||||||
|             v-else-if="error" |                 icon="mdi-alert-circle-outline" | ||||||
|             class="text-center py-10 text-error" |                 :text="errorMessage" | ||||||
|         > |                 :title="t('error_title')" | ||||||
|             <v-icon large>mdi-alert-circle</v-icon> |             ></v-empty-state> | ||||||
|             <p>Error loading: {{ error.message }}</p> |  | ||||||
|         </div> |         </div> | ||||||
|         <div v-else> |         <div v-else> | ||||||
|             <h1 class="title">{{ t("classes") }}</h1> |             <h1 class="title">{{ t("classes") }}</h1> | ||||||
|             <v-container |             <using-query-result | ||||||
|                 fluid |                 :query-result="classesQuery" | ||||||
|                 class="ma-4" |                 v-slot="classResponse: { data: ClassesResponse }" | ||||||
|             > |             > | ||||||
|                 <v-row |                 <v-container | ||||||
|                     no-gutters |  | ||||||
|                     fluid |                     fluid | ||||||
|  |                     class="ma-4" | ||||||
|                 > |                 > | ||||||
|                     <v-col |                     <v-row | ||||||
|                         cols="12" |                         no-gutters | ||||||
|                         sm="6" |                         fluid | ||||||
|                         md="6" |  | ||||||
|                     > |                     > | ||||||
|                         <v-table class="table"> |                         <v-col | ||||||
|                             <thead> |                             cols="12" | ||||||
|                                 <tr> |                             sm="6" | ||||||
|                                     <th class="header">{{ t("classes") }}</th> |                             md="6" | ||||||
|                                     <th class="header">{{ t("teachers") }}</th> |                         > | ||||||
|                                     <th class="header">{{ t("members") }}</th> |                             <v-table class="table"> | ||||||
|                                 </tr> |                                 <thead> | ||||||
|                             </thead> |                                     <tr> | ||||||
|                             <tbody> |                                         <th class="header">{{ t("classes") }}</th> | ||||||
|                                 <tr |                                         <th class="header">{{ t("teachers") }}</th> | ||||||
|                                     v-for="c in classes" |                                         <th class="header">{{ t("members") }}</th> | ||||||
|                                     :key="c.id" |                                     </tr> | ||||||
|                                 > |                                 </thead> | ||||||
|                                     <td>{{ c.displayName }}</td> |                                 <tbody> | ||||||
|                                     <td |                                     <tr | ||||||
|                                         class="link" |                                         v-for="c in classResponse.data.classes as ClassDTO[]" | ||||||
|                                         @click="openTeacherDialog(c)" |                                         :key="c.id" | ||||||
|                                     > |                                     > | ||||||
|                                         {{ c.teachers.length }} |                                         <td>{{ c.displayName }}</td> | ||||||
|                                     </td> |                                         <td | ||||||
|                                     <td |                                             class="link" | ||||||
|                                         class="link" |                                             @click="openTeacherDialog(c)" | ||||||
|                                         @click="openStudentDialog(c)" |                                         > | ||||||
|                                     > |                                             {{ c.teachers.length }} | ||||||
|                                         {{ c.students.length }} |                                         </td> | ||||||
|                                     </td> |                                         <td | ||||||
|                                 </tr> |                                             class="link" | ||||||
|                             </tbody> |                                             @click="openStudentDialog(c)" | ||||||
|                         </v-table> |                                         > | ||||||
|                     </v-col> |                                             {{ c.students.length }} | ||||||
|                 </v-row> |                                         </td> | ||||||
|             </v-container> |                                     </tr> | ||||||
|  |                                 </tbody> | ||||||
|  |                             </v-table> | ||||||
|  |                         </v-col> | ||||||
|  |                     </v-row> | ||||||
|  |                 </v-container> | ||||||
|  |             </using-query-result> | ||||||
| 
 | 
 | ||||||
|             <v-dialog |             <v-dialog | ||||||
|  |                 v-if="selectedClass" | ||||||
|                 v-model="dialog" |                 v-model="dialog" | ||||||
|                 width="400" |                 width="400" | ||||||
|             > |             > | ||||||
|                 <v-card> |                 <v-card> | ||||||
|                     <v-card-title> {{ selectedClass?.displayName }} </v-card-title> |                     <v-card-title> {{ selectedClass!.displayName }} </v-card-title> | ||||||
|                     <v-card-text> |                     <v-card-text> | ||||||
|                         <ul v-if="getStudents"> |                         <ul v-if="getStudents"> | ||||||
|                             <li |                             <using-query-result | ||||||
|                                 v-for="student in students" |                                 :query-result="getStudentsQuery" | ||||||
|                                 :key="student.username" |                                 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> | ||||||
|                         <ul v-else> |                         <ul v-else> | ||||||
|                             <li |                             <using-query-result | ||||||
|                                 v-for="teacher in teachers" |                                 :query-result="getTeachersQuery" | ||||||
|                                 :key="teacher.username" |                                 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> |                         </ul> | ||||||
|                     </v-card-text> |                     </v-card-text> | ||||||
|                     <v-card-actions> |                     <v-card-actions> | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             color="primary" |                             color="primary" | ||||||
|                             @click="dialog = false" |                             @click="dialog = false" | ||||||
|                             >Close</v-btn |                             >{{ t("close") }}</v-btn | ||||||
|                         > |                         > | ||||||
|                     </v-card-actions> |                     </v-card-actions> | ||||||
|                 </v-card> |                 </v-card> | ||||||
|  |  | ||||||
|  | @ -1,41 +1,49 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     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 { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; | ||||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |     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 { 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 { t } = useI18n(); | ||||||
|     const classController = new ClassController(); |  | ||||||
| 
 | 
 | ||||||
|     // Username of logged in teacher |     // Username of logged in teacher | ||||||
|     const username = ref<string | undefined>(undefined); |     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 |     // Load current user before rendering the page | ||||||
|     // When loading the page |  | ||||||
|     onMounted(async () => { |     onMounted(async () => { | ||||||
|         const userObject = await authState.loadUser(); |         isLoading.value = true; | ||||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; |         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 |     // Fetch all classes of the logged in teacher | ||||||
|     const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true); |     const classesQuery = useTeacherClassesQuery(username, true); | ||||||
| 
 |     const allClassesQuery = useClassesQuery(); | ||||||
|     // Empty list when classes are not yet loaded, else the list of classes of the user |     const { mutate } = useCreateClassMutation(); | ||||||
|     const classes: ComputedRef<ClassDTO[]> = computed(() => { |     const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); | ||||||
|         // The classes are not yet fetched |     const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); | ||||||
|         if (!classesResponse.value) { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|         // The user has no classes |  | ||||||
|         if (classesResponse.value.classes.length === 0) { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|         return classesResponse.value.classes as ClassDTO[]; |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     // Boolean that handles visibility for dialogs |     // Boolean that handles visibility for dialogs | ||||||
|     // Creating a class will generate a popup with the generated code |     // Creating a class will generate a popup with the generated code | ||||||
|  | @ -44,19 +52,26 @@ | ||||||
|     // Code generated when new class was created |     // Code generated when new class was created | ||||||
|     const code = ref<string>(""); |     const code = ref<string>(""); | ||||||
| 
 | 
 | ||||||
|     // TODO: waiting on frontend controllers |     // Function to handle an invitation request | ||||||
|     const invitations = ref<TeacherInvitationDTO[]>([]); |     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 |                 await getInvitationsQuery.refetch(); | ||||||
|     function acceptRequest(): void { |             }, | ||||||
|         //TODO: avoid linting issues when merging by filling the function |             onError: (e) => { | ||||||
|         invitations.value = []; |                 showSnackbar(t("failed") + ": " + e.message, "error"); | ||||||
|     } |             }, | ||||||
| 
 |         }); | ||||||
|     // Function to handle a denied invitation request |  | ||||||
|     function denyRequest(): void { |  | ||||||
|         //TODO: avoid linting issues when merging by filling the function |  | ||||||
|         invitations.value = []; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Teacher should be able to set a displayname when making a class |     // Teacher should be able to set a displayname when making a class | ||||||
|  | @ -76,25 +91,25 @@ | ||||||
|     async function createClass(): Promise<void> { |     async function createClass(): Promise<void> { | ||||||
|         // Check if the class name is valid |         // Check if the class name is valid | ||||||
|         if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) { |         if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) { | ||||||
|             try { |             const classDto: ClassDTO = { | ||||||
|                 const classDto: ClassDTO = { |                 id: "", | ||||||
|                     id: "", |                 displayName: className.value, | ||||||
|                     displayName: className.value, |                 teachers: [username.value!], | ||||||
|                     teachers: [username.value!], |                 students: [], | ||||||
|                     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"); |  | ||||||
| 
 | 
 | ||||||
|                 // Reload the table with classes so the new class appears |             mutate(classDto, { | ||||||
|                 await refetch(); |                 onSuccess: async (classResponse) => { | ||||||
|             } catch (_) { |                     showSnackbar(t("classCreated"), "success"); | ||||||
|                 showSnackbar(t("wrong"), "error"); |                     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 === "") { |         if (!className.value || className.value === "") { | ||||||
|             showSnackbar(t("name is mandatory"), "error"); |             showSnackbar(t("name is mandatory"), "error"); | ||||||
|  | @ -121,188 +136,272 @@ | ||||||
|         await navigator.clipboard.writeText(code.value); |         await navigator.clipboard.writeText(code.value); | ||||||
|         copied.value = true; |         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> | </script> | ||||||
| <template> | <template> | ||||||
|     <main> |     <main> | ||||||
|         <div |         <div | ||||||
|  |             class="loading-div" | ||||||
|             v-if="isLoading" |             v-if="isLoading" | ||||||
|             class="text-center py-10" |  | ||||||
|         > |         > | ||||||
|             <v-progress-circular |             <v-progress-circular indeterminate></v-progress-circular> | ||||||
|                 indeterminate |  | ||||||
|                 color="primary" |  | ||||||
|             /> |  | ||||||
|             <p>Loading...</p> |  | ||||||
|         </div> |         </div> | ||||||
| 
 |         <div v-if="isError"> | ||||||
|         <div |             <v-empty-state | ||||||
|             v-else-if="error" |                 icon="mdi-alert-circle-outline" | ||||||
|             class="text-center py-10 text-error" |                 :text="errorMessage" | ||||||
|         > |                 :title="t('error_title')" | ||||||
|             <v-icon large>mdi-alert-circle</v-icon> |             ></v-empty-state> | ||||||
|             <p>Error loading: {{ error.message }}</p> |  | ||||||
|         </div> |         </div> | ||||||
|         <div v-else> |         <div v-else> | ||||||
|             <h1 class="title">{{ t("classes") }}</h1> |             <h1 class="title">{{ t("classes") }}</h1> | ||||||
|             <v-container |             <using-query-result | ||||||
|                 fluid |                 :query-result="classesQuery" | ||||||
|                 class="ma-4" |                 v-slot="classesResponse: { data: ClassesResponse }" | ||||||
|             > |             > | ||||||
|                 <v-row |                 <v-container | ||||||
|                     no-gutters |  | ||||||
|                     fluid |                     fluid | ||||||
|  |                     class="ma-4" | ||||||
|                 > |                 > | ||||||
|                     <v-col |                     <v-row | ||||||
|                         cols="12" |                         no-gutters | ||||||
|                         sm="6" |                         class="custom-breakpoint" | ||||||
|                         md="6" |  | ||||||
|                     > |                     > | ||||||
|                         <v-table class="table"> |                         <v-col | ||||||
|                             <thead> |                             cols="12" | ||||||
|                                 <tr> |                             sm="6" | ||||||
|                                     <th class="header">{{ t("classes") }}</th> |                             md="6" | ||||||
|                                     <th class="header"> |                             class="responsive-col" | ||||||
|                                         {{ t("code") }} |                         > | ||||||
|                                     </th> |                             <v-table class="table"> | ||||||
|                                     <th class="header">{{ t("members") }}</th> |                                 <thead> | ||||||
|                                 </tr> |                                     <tr> | ||||||
|                             </thead> |                                         <th class="header">{{ t("classes") }}</th> | ||||||
|                             <tbody> |                                         <th class="header"> | ||||||
|                                 <tr |                                             {{ t("code") }} | ||||||
|                                     v-for="c in classes" |                                         </th> | ||||||
|                                     :key="c.id" |                                         <th class="header">{{ t("members") }}</th> | ||||||
|                                 > |                                     </tr> | ||||||
|                                     <td> |                                 </thead> | ||||||
|                                         <v-btn |                                 <tbody> | ||||||
|                                             :to="`/class/${c.id}`" |                                     <tr | ||||||
|                                             variant="text" |                                         v-for="c in classesResponse.data.classes as ClassDTO[]" | ||||||
|                                         > |                                         :key="c.id" | ||||||
|                                             {{ 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-form> |                                         <td> | ||||||
|                             </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 |                                             <v-btn | ||||||
|                                                 text |                                                 :to="`/class/${c.id}`" | ||||||
|                                                 @click=" |                                                 variant="text" | ||||||
|                                                     dialog = false; |  | ||||||
|                                                     copied = false; |  | ||||||
|                                                 " |  | ||||||
|                                             > |                                             > | ||||||
|                                                 {{ t("close") }} |                                                 {{ c.displayName }} | ||||||
|  |                                                 <v-icon end> mdi-menu-right </v-icon> | ||||||
|                                             </v-btn> |                                             </v-btn> | ||||||
|                                         </v-card-actions> |                                         </td> | ||||||
|                                     </v-card> |                                         <td> | ||||||
|                                 </v-dialog> |                                             <span v-if="!isMdAndDown">{{ c.id }}</span> | ||||||
|                             </v-container> |                                             <span | ||||||
|                         </div> |                                                 v-else | ||||||
|                     </v-col> |                                                 style="cursor: pointer" | ||||||
|                 </v-row> |                                                 @click="openCodeDialog(c.id)" | ||||||
|             </v-container> |                                                 ><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"> |             <h1 class="title"> | ||||||
|                 {{ t("invitations") }} |                 {{ t("invitations") }} | ||||||
|             </h1> |             </h1> | ||||||
|             <v-table class="table"> |             <v-container | ||||||
|                 <thead> |                 fluid | ||||||
|                     <tr> |                 class="ma-4" | ||||||
|                         <th class="header">{{ t("class") }}</th> |             > | ||||||
|                         <th class="header">{{ t("sender") }}</th> |                 <v-table class="table"> | ||||||
|                         <th class="header"></th> |                     <thead> | ||||||
|                     </tr> |                         <tr> | ||||||
|                 </thead> |                             <th class="header">{{ t("class") }}</th> | ||||||
|                 <tbody> |                             <th class="header">{{ t("sender") }}</th> | ||||||
|                     <tr |                             <th class="header">{{ t("accept") + "/" + t("reject") }}</th> | ||||||
|                         v-for="i in invitations" |                         </tr> | ||||||
|                         :key="i.classId" |                     </thead> | ||||||
|                     > |                     <tbody> | ||||||
|                         <td> |                         <using-query-result | ||||||
|                             {{ i.classId }} |                             :query-result="getInvitationsQuery" | ||||||
|                             <!-- TODO fetch display name via classId because db only returns classId field --> |                             v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" | ||||||
|                         </td> |                         > | ||||||
|                         <td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td> |                             <using-query-result | ||||||
|                         <td class="text-right"> |                                 :query-result="allClassesQuery" | ||||||
|                             <div> |                                 v-slot="classesResponse: { data: ClassesResponse }" | ||||||
|                                 <v-btn |                             > | ||||||
|                                     color="green" |                                 <tr | ||||||
|                                     @click="acceptRequest" |                                     v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" | ||||||
|                                     class="mr-2" |                                     :key="i.classId" | ||||||
|                                 > |                                 > | ||||||
|                                     {{ t("accept") }} |                                     <td> | ||||||
|                                 </v-btn> |                                         {{ | ||||||
|                                 <v-btn |                                             (classesResponse.data.classes as ClassDTO[]).filter( | ||||||
|                                     color="red" |                                                 (c) => c.id == i.classId, | ||||||
|                                     @click="denyRequest" |                                             )[0].displayName | ||||||
|                                 > |                                         }} | ||||||
|                                     {{ t("deny") }} |                                     </td> | ||||||
|                                 </v-btn> |                                     <td> | ||||||
|                             </div> |                                         {{ | ||||||
|                         </td> |                                             (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName | ||||||
|                     </tr> |                                         }} | ||||||
|                 </tbody> |                                     </td> | ||||||
|             </v-table> |                                     <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> |         </div> | ||||||
|         <v-snackbar |         <v-snackbar | ||||||
|             v-model="snackbar.visible" |             v-model="snackbar.visible" | ||||||
|  | @ -311,6 +410,42 @@ | ||||||
|         > |         > | ||||||
|             {{ snackbar.message }} |             {{ snackbar.message }} | ||||||
|         </v-snackbar> |         </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> |     </main> | ||||||
| </template> | </template> | ||||||
| <style scoped> | <style scoped> | ||||||
|  | @ -378,7 +513,7 @@ | ||||||
|         margin-left: 30px; |         margin-left: 30px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @media screen and (max-width: 800px) { |     @media screen and (max-width: 850px) { | ||||||
|         h1 { |         h1 { | ||||||
|             text-align: center; |             text-align: center; | ||||||
|             padding-left: 0; |             padding-left: 0; | ||||||
|  | @ -401,5 +536,18 @@ | ||||||
|             justify-content: center; |             justify-content: center; | ||||||
|             margin: 5px; |             margin: 5px; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         .custom-breakpoint { | ||||||
|  |             flex-direction: column !important; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .table { | ||||||
|  |             width: 100%; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .responsive-col { | ||||||
|  |             max-width: 100% !important; | ||||||
|  |             flex-basis: 100% !important; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger