Merge remote-tracking branch 'origin/feat/class-functionality' into feat/class-functionality-fix-bugs
This commit is contained in:
		
						commit
						6c408d516c
					
				
					 9 changed files with 293 additions and 213 deletions
				
			
		|  | @ -82,5 +82,7 @@ | |||
|     "reject": "zurückweisen", | ||||
|     "areusure": "Sind Sie sicher?", | ||||
|     "yes": "ja", | ||||
|     "teachers": "Lehrer" | ||||
|     "teachers": "Lehrer", | ||||
|     "rejected": "abgelehnt", | ||||
|     "accepted": "akzeptiert" | ||||
| } | ||||
|  |  | |||
|  | @ -82,5 +82,7 @@ | |||
|     "reject": "reject", | ||||
|     "areusure": "Are you sure?", | ||||
|     "yes": "yes", | ||||
|     "teachers": "teachers" | ||||
|     "teachers": "teachers", | ||||
|     "accepted": "accepted", | ||||
|     "rejected": "rejected" | ||||
| } | ||||
|  |  | |||
|  | @ -82,5 +82,7 @@ | |||
|     "reject": "rejeter", | ||||
|     "areusure": "Êtes-vous sûr?", | ||||
|     "yes": "oui", | ||||
|     "teachers": "enseignants" | ||||
|     "teachers": "enseignants", | ||||
|     "accepted": "acceptée", | ||||
|     "rejected": "rejetée" | ||||
| } | ||||
|  |  | |||
|  | @ -82,5 +82,7 @@ | |||
|     "reject": "weiger", | ||||
|     "areusure": "Bent u zeker?", | ||||
|     "yes": "ja", | ||||
|     "teachers": "leerkrachten" | ||||
|     "teachers": "leerkrachten", | ||||
|     "accepted": "geaccepteerd", | ||||
|     "rejected": "geweigerd" | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; | |||
| import SingleClass from "@/views/classes/SingleClass.vue"; | ||||
| import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; | ||||
| import NotFound from "@/components/errors/NotFound.vue"; | ||||
| import CreateClass from "@/views/classes/CreateClass.vue"; | ||||
| import CreateAssignment from "@/views/assignments/CreateAssignment.vue"; | ||||
| import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; | ||||
| import CallbackPage from "@/views/CallbackPage.vue"; | ||||
|  | @ -84,12 +83,6 @@ const router = createRouter({ | |||
|             component: SingleAssignment, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/class/create", | ||||
|             name: "CreateClass", | ||||
|             component: CreateClass, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/class/:id", | ||||
|             name: "SingleClass", | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -2,72 +2,82 @@ | |||
|     import { useI18n } from "vue-i18n"; | ||||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import { onMounted, ref } from "vue"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import { ClassController, type ClassResponse } from "@/controllers/classes"; | ||||
|     import type { ClassResponse } from "@/controllers/classes"; | ||||
|     import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students"; | ||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useTeacherJoinRequestsQuery, useUpdateJoinRequestMutation } from "@/queries/teachers"; | ||||
|     import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request"; | ||||
|     import { useClassDeleteStudentMutation, useClassQuery, useClassStudentsQuery } from "@/queries/classes"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     // Username of logged in teacher | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const classController: ClassController = new ClassController(); | ||||
| 
 | ||||
|     // Find class id from route | ||||
|     const route = useRoute(); | ||||
|     const classId: string = route.params.id as string; | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
| 
 | ||||
|     const isLoading = ref(true); | ||||
|     const currentClass = ref<ClassDTO | undefined>(undefined); | ||||
|     const students = ref<StudentDTO[]>([]); | ||||
|     // Queries used to access the backend and catch loading or errors | ||||
| 
 | ||||
|     // Gets the class a teacher wants to manage | ||||
|     const getClass = useClassQuery(classId); | ||||
|     // Get all students part of the class | ||||
|     const getStudents = useClassStudentsQuery(classId); | ||||
|     // Get all join requests for this class | ||||
|     const joinRequestsQuery = useTeacherJoinRequestsQuery(username, classId); | ||||
|     // Handle accepting or rejecting join requests | ||||
|     const { mutate } = useUpdateJoinRequestMutation(); | ||||
|     // Handle deletion of a student from the class | ||||
|     const { mutate: deleteStudentMutation } = useClassDeleteStudentMutation(); | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
| 
 | ||||
|         // Get class of which information should be shown | ||||
|         const classResponse: ClassResponse = await classController.getById(classId); | ||||
|         if (classResponse && classResponse.class) { | ||||
|             currentClass.value = classResponse.class; | ||||
|             username.value = userObject!.profile!.preferred_username; | ||||
|         } catch (error) { | ||||
|             isError.value = true; | ||||
|             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
| 
 | ||||
|         // Fetch all students of the class | ||||
|         const studentsResponse: StudentsResponse = await classController.getStudents(classId); | ||||
|         if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[]; | ||||
|     }); | ||||
| 
 | ||||
|     // TODO: Boolean that handles visibility for dialogs | ||||
|     // Popup to verify removing student | ||||
|     // Used to set the visibility of the dialog | ||||
|     const dialog = ref(false); | ||||
|     // Student selected for deletion | ||||
|     const selectedStudent = ref<StudentDTO | null>(null); | ||||
| 
 | ||||
|     // Let the teacher verify deletion of a student | ||||
|     function showPopup(s: StudentDTO): void { | ||||
|         selectedStudent.value = s; | ||||
|         dialog.value = true; | ||||
|     } | ||||
| 
 | ||||
|     // Remove student from class | ||||
|     async function removeStudentFromclass(): Promise<void> { | ||||
|         // TODO: replace by query | ||||
|         if (selectedStudent.value) await classController.deleteStudent(classId, selectedStudent.value.username); | ||||
|         // Delete student from class | ||||
|         deleteStudentMutation( | ||||
|             { id: classId, username: selectedStudent.value!.username }, | ||||
|             { | ||||
|                 onSuccess: async () => { | ||||
|                     dialog.value = false; | ||||
| 
 | ||||
|         selectedStudent.value = null; | ||||
|         //TODO when query; reload table so student not longer in table | ||||
|                     await getStudents.refetch(); | ||||
|                     showSnackbar(t("success"), "success"); | ||||
|                 }, | ||||
|                 onError: (e) => { | ||||
|                     dialog.value = false; | ||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     // TODO: query + relaoding | ||||
|     function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean) : void { | ||||
|     function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean): void { | ||||
|         // Handle acception or rejection of a join request | ||||
|         mutate( | ||||
|             { | ||||
|                 teacherUsername: username.value!, | ||||
|  | @ -76,22 +86,32 @@ | |||
|                 accepted: accepted, | ||||
|             }, | ||||
|             { | ||||
|                 onSuccess: () => { | ||||
|                     showSnackbar(t("sent"), "success"); | ||||
|                 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"); | ||||
|                     throw 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; | ||||
|  | @ -101,17 +121,28 @@ | |||
| <template> | ||||
|     <main> | ||||
|         <div | ||||
|             class="loading-div" | ||||
|             v-if="isLoading" | ||||
|             class="text-center py-10" | ||||
|         > | ||||
|             <v-progress-circular | ||||
|                 indeterminate | ||||
|                 color="primary" | ||||
|             /> | ||||
|             <p>Loading...</p> | ||||
|             <v-progress-circular indeterminate></v-progress-circular> | ||||
|         </div> | ||||
|         <div v-else> | ||||
|             <h1 class="title">{{ currentClass!.displayName }}</h1> | ||||
|         <div v-if="isError"> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-alert-circle-outline" | ||||
|                 :text="errorMessage" | ||||
|                 :title="t('error_title')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|         <using-query-result | ||||
|             :query-result="getClass" | ||||
|             v-slot="classResponse: { data: ClassResponse }" | ||||
|         > | ||||
|             <div> | ||||
|                 <h1 class="title">{{ classResponse.data.class.displayName }}</h1> | ||||
|                 <using-query-result | ||||
|                     :query-result="getStudents" | ||||
|                     v-slot="studentsResponse: { data: StudentsResponse }" | ||||
|                 > | ||||
|                     <v-container | ||||
|                         fluid | ||||
|                         class="ma-4" | ||||
|  | @ -134,7 +165,7 @@ | |||
|                                     </thead> | ||||
|                                     <tbody> | ||||
|                                         <tr | ||||
|                                     v-for="s in students" | ||||
|                                             v-for="s in studentsResponse.data.students as StudentDTO[]" | ||||
|                                             :key="s.id" | ||||
|                                         > | ||||
|                                             <td> | ||||
|  | @ -195,6 +226,7 @@ | |||
|                             </using-query-result> | ||||
|                         </v-row> | ||||
|                     </v-container> | ||||
|                 </using-query-result> | ||||
|             </div> | ||||
|             <v-dialog | ||||
|                 v-model="dialog" | ||||
|  | @ -226,6 +258,7 @@ | |||
|             > | ||||
|                 {{ snackbar.message }} | ||||
|             </v-snackbar> | ||||
|         </using-query-result> | ||||
|     </main> | ||||
| </template> | ||||
| <style scoped> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
|     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; | ||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|     import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; | ||||
|     import { type ClassesResponse } from "@/controllers/classes"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes"; | ||||
|     import type { StudentsResponse } from "@/controllers/students"; | ||||
|  | @ -17,16 +17,26 @@ | |||
| 
 | ||||
|     // Username of logged in student | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
| 
 | ||||
|     // Students of selected class are shown when logged in student presses on the member count | ||||
|     const selectedClass = ref<ClassDTO | null>(null); | ||||
|     const getStudents = ref(false); | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
|             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 | ||||
|  | @ -44,7 +54,7 @@ | |||
|     async function openStudentDialog(c: ClassDTO): Promise<void> { | ||||
|         selectedClass.value = c; | ||||
| 
 | ||||
|         // let the component know it should show the students in a class | ||||
|         // Let the component know it should show the students in a class | ||||
|         getStudents.value = true; | ||||
|         await getStudentsQuery.refetch(); | ||||
|         dialog.value = true; | ||||
|  | @ -53,7 +63,7 @@ | |||
|     async function openTeacherDialog(c: ClassDTO): Promise<void> { | ||||
|         selectedClass.value = c; | ||||
| 
 | ||||
|         // let the component know it should show teachers of a class | ||||
|         // Let the component know it should show teachers of a class | ||||
|         getStudents.value = false; | ||||
|         await getTeachersQuery.refetch(); | ||||
|         dialog.value = true; | ||||
|  | @ -111,7 +121,20 @@ | |||
| </script> | ||||
| <template> | ||||
|     <main> | ||||
|         <div> | ||||
|         <div | ||||
|             class="loading-div" | ||||
|             v-if="isLoading" | ||||
|         > | ||||
|             <v-progress-circular indeterminate></v-progress-circular> | ||||
|         </div> | ||||
|         <div v-if="isError"> | ||||
|             <v-empty-state | ||||
|                 icon="mdi-alert-circle-outline" | ||||
|                 :text="errorMessage" | ||||
|                 :title="t('error_title')" | ||||
|             ></v-empty-state> | ||||
|         </div> | ||||
|         <div v-else> | ||||
|             <h1 class="title">{{ t("classes") }}</h1> | ||||
|             <using-query-result | ||||
|                 :query-result="classesQuery" | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; | ||||
|     import { useTeacherClassesQuery } from "@/queries/teachers"; | ||||
|     import { type ClassesResponse, type ClassResponse } from "@/controllers/classes"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useClassesQuery, useClassTeacherInvitationsQuery, useCreateClassMutation } from "@/queries/classes"; | ||||
|     import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||
|  | @ -15,19 +15,29 @@ | |||
| 
 | ||||
|     // Username of logged in teacher | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|     const isLoading = ref(false); | ||||
|     const isError = ref(false); | ||||
|     const errorMessage = ref<string>(""); | ||||
| 
 | ||||
|     // Find the username of the logged in user so it can be used to fetch other information | ||||
|     // When loading the page | ||||
|     // Load current user before rendering the page | ||||
|     onMounted(async () => { | ||||
|         isLoading.value = true; | ||||
|         try { | ||||
|             const userObject = await authState.loadUser(); | ||||
|         username.value = userObject?.profile?.preferred_username ?? undefined; | ||||
|             username.value = userObject!.profile!.preferred_username; | ||||
|         } catch (error) { | ||||
|             isError.value = true; | ||||
|             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all classes of the logged in teacher | ||||
|     const classesQuery = useTeacherClassesQuery(username, true); | ||||
|     const allClassesQuery = useClassesQuery(); | ||||
|     const { mutate } = useCreateClassMutation(); | ||||
|     const getInvitationsQuery = useClassTeacherInvitationsQuery(username); | ||||
|     const getInvitationsQuery = useClassTeacherInvitationsQuery(username); // TODO: use useTeacherInvitationsReceivedQuery | ||||
| 
 | ||||
|     // Boolean that handles visibility for dialogs | ||||
|     // Creating a class will generate a popup with the generated code | ||||
|  | @ -111,6 +121,19 @@ | |||
| </script> | ||||
| <template> | ||||
|     <main> | ||||
|         <div | ||||
|         class="loading-div" | ||||
|         v-if="isLoading" | ||||
|     > | ||||
|         <v-progress-circular indeterminate></v-progress-circular> | ||||
|     </div> | ||||
|     <div v-if="isError"> | ||||
|         <v-empty-state | ||||
|             icon="mdi-alert-circle-outline" | ||||
|             :text="errorMessage" | ||||
|             :title="t('error_title')" | ||||
|         ></v-empty-state> | ||||
|     </div v-else> | ||||
|         <div> | ||||
|             <h1 class="title">{{ t("classes") }}</h1> | ||||
|             <using-query-result | ||||
|  | @ -252,15 +275,22 @@ | |||
|                         :query-result="getInvitationsQuery" | ||||
|                         v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" | ||||
|                     > | ||||
|                     <using-query-result :query-result="allClassesQuery" v-slot="classesResponse: {data: ClassesResponse}"> | ||||
|                         <using-query-result | ||||
|                             :query-result="allClassesQuery" | ||||
|                             v-slot="classesResponse: { data: ClassesResponse }" | ||||
|                         > | ||||
|                             <tr | ||||
|                                 v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" | ||||
|                                 :key="i.classId" | ||||
|                             > | ||||
|                                 <td> | ||||
|                                 {{ (classesResponse.data.classes as ClassDTO[]).filter((c) => c.id == i.classId)[0] }} | ||||
|                                     {{ | ||||
|                                         (classesResponse.data.classes as ClassDTO[]).filter((c) => c.id == i.classId)[0] | ||||
|                                     }} | ||||
|                                 </td> | ||||
|                                 <td> | ||||
|                                     {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} | ||||
|                                 </td> | ||||
|                             <td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td> | ||||
|                                 <td class="text-right"> | ||||
|                                     <div> | ||||
|                                         <v-btn | ||||
|  |  | |||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl