Merge branch 'dev' into feat/leerpad-vragen
This commit is contained in:
		
						commit
						8240059c2c
					
				
					 60 changed files with 10729 additions and 1042 deletions
				
			
		|  | @ -1,14 +1,258 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { computed, onMounted, ref, watch } from "vue"; | ||||
|     import GroupSelector from "@/components/assignments/GroupSelector.vue"; | ||||
|     import { assignmentTitleRules, classRules, descriptionRules, learningPathRules } from "@/utils/assignment-rules.ts"; | ||||
|     import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||
|     import { useRouter } from "vue-router"; | ||||
|     import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
|     import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||
|     import { useRoute } from "vue-router"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|     const { t, locale } = useI18n(); | ||||
|     const role = ref(auth.authState.activeRole); | ||||
|     const username = ref<string>(""); | ||||
| 
 | ||||
|     onMounted(async () => { | ||||
|         // Redirect student | ||||
|         if (role.value === "student") { | ||||
|             await router.push("/user"); | ||||
|         } | ||||
| 
 | ||||
|         // Get the user's username | ||||
|         const user = await auth.loadUser(); | ||||
|         username.value = user?.profile?.preferred_username ?? ""; | ||||
|     }); | ||||
| 
 | ||||
|     const language = computed(() => locale.value); | ||||
|     const form = ref(); | ||||
| 
 | ||||
|     //Fetch all learning paths | ||||
|     const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||
| 
 | ||||
|     // Fetch and store all the teacher's classes | ||||
|     const classesQueryResults = useTeacherClassesQuery(username, true); | ||||
| 
 | ||||
|     const selectedClass = ref(undefined); | ||||
| 
 | ||||
|     const assignmentTitle = ref(""); | ||||
|     const selectedLearningPath = ref(route.query.hruid || undefined); | ||||
| 
 | ||||
|     // Disable combobox when learningPath prop is passed | ||||
|     const lpIsSelected = route.query.hruid !== undefined; | ||||
|     const deadline = ref(null); | ||||
|     const description = ref(""); | ||||
|     const groups = ref<string[][]>([]); | ||||
| 
 | ||||
|     // New group is added to the list | ||||
|     function addGroupToList(students: string[]): void { | ||||
|         if (students.length) { | ||||
|             groups.value = [...groups.value, students]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     watch(selectedClass, () => { | ||||
|         groups.value = []; | ||||
|     }); | ||||
| 
 | ||||
|     const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||
| 
 | ||||
|     watch([isSuccess, data], async ([success, newData]) => { | ||||
|         if (success && newData?.assignment) { | ||||
|             await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     async function submitFormHandler(): Promise<void> { | ||||
|         const { valid } = await form.value.validate(); | ||||
|         if (!valid) return; | ||||
| 
 | ||||
|         let lp = selectedLearningPath.value; | ||||
|         if (!lpIsSelected) { | ||||
|             lp = selectedLearningPath.value?.hruid; | ||||
|         } | ||||
| 
 | ||||
|         const assignmentDTO: AssignmentDTO = { | ||||
|             id: 0, | ||||
|             within: selectedClass.value?.id || "", | ||||
|             title: assignmentTitle.value, | ||||
|             description: description.value, | ||||
|             learningPath: lp || "", | ||||
|             language: language.value, | ||||
|             groups: groups.value, | ||||
|         }; | ||||
| 
 | ||||
|         mutate({ cid: assignmentDTO.within, data: assignmentDTO }); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <main> | ||||
|         Hier zou de pagina staan om een assignment aan te maken voor de leerpad met hruid {{ route.query.hruid }} en | ||||
|         language {{ route.query.language }}. (Overschrijf dit) | ||||
|     </main> | ||||
|     <div class="main-container"> | ||||
|         <h1 class="title">{{ t("new-assignment") }}</h1> | ||||
|         <v-card class="form-card"> | ||||
|             <v-form | ||||
|                 ref="form" | ||||
|                 class="form-container" | ||||
|                 validate-on="submit lazy" | ||||
|                 @submit.prevent="submitFormHandler" | ||||
|             > | ||||
|                 <v-container class="step-container"> | ||||
|                     <v-card-text> | ||||
|                         <v-text-field | ||||
|                             v-model="assignmentTitle" | ||||
|                             :label="t('title')" | ||||
|                             :rules="assignmentTitleRules" | ||||
|                             density="compact" | ||||
|                             variant="outlined" | ||||
|                             clearable | ||||
|                             required | ||||
|                         ></v-text-field> | ||||
|                     </v-card-text> | ||||
| 
 | ||||
|                     <using-query-result | ||||
|                         :query-result="learningPathsQueryResults" | ||||
|                         v-slot="{ data }: { data: LearningPath[] }" | ||||
|                     > | ||||
|                         <v-card-text> | ||||
|                             <v-combobox | ||||
|                                 v-model="selectedLearningPath" | ||||
|                                 :items="data" | ||||
|                                 :label="t('choose-lp')" | ||||
|                                 :rules="learningPathRules" | ||||
|                                 variant="outlined" | ||||
|                                 clearable | ||||
|                                 hide-details | ||||
|                                 density="compact" | ||||
|                                 append-inner-icon="mdi-magnify" | ||||
|                                 item-title="title" | ||||
|                                 item-value="hruid" | ||||
|                                 required | ||||
|                                 :disabled="lpIsSelected" | ||||
|                                 :filter=" | ||||
|                                     (item, query: string) => item.title.toLowerCase().includes(query.toLowerCase()) | ||||
|                                 " | ||||
|                             ></v-combobox> | ||||
|                         </v-card-text> | ||||
|                     </using-query-result> | ||||
| 
 | ||||
|                     <using-query-result | ||||
|                         :query-result="classesQueryResults" | ||||
|                         v-slot="{ data }: { data: ClassesResponse }" | ||||
|                     > | ||||
|                         <v-card-text> | ||||
|                             <v-combobox | ||||
|                                 v-model="selectedClass" | ||||
|                                 :items="data?.classes ?? []" | ||||
|                                 :label="t('pick-class')" | ||||
|                                 :rules="classRules" | ||||
|                                 variant="outlined" | ||||
|                                 clearable | ||||
|                                 hide-details | ||||
|                                 density="compact" | ||||
|                                 append-inner-icon="mdi-magnify" | ||||
|                                 item-title="displayName" | ||||
|                                 item-value="id" | ||||
|                                 required | ||||
|                             ></v-combobox> | ||||
|                         </v-card-text> | ||||
|                     </using-query-result> | ||||
| 
 | ||||
|                     <GroupSelector | ||||
|                         :classId="selectedClass?.id" | ||||
|                         :groups="groups" | ||||
|                         @groupCreated="addGroupToList" | ||||
|                     /> | ||||
| 
 | ||||
|                     <!-- Counter for created groups --> | ||||
|                     <v-card-text v-if="groups.length"> | ||||
|                         <strong>Created Groups: {{ groups.length }}</strong> | ||||
|                     </v-card-text> | ||||
|                     <DeadlineSelector v-model:deadline="deadline" /> | ||||
|                     <v-card-text> | ||||
|                         <v-textarea | ||||
|                             v-model="description" | ||||
|                             :label="t('description')" | ||||
|                             variant="outlined" | ||||
|                             density="compact" | ||||
|                             auto-grow | ||||
|                             rows="3" | ||||
|                             :rules="descriptionRules" | ||||
|                         ></v-textarea> | ||||
|                     </v-card-text> | ||||
|                     <v-card-text> | ||||
|                         <v-btn | ||||
|                             class="mt-2" | ||||
|                             color="secondary" | ||||
|                             type="submit" | ||||
|                             block | ||||
|                             >{{ t("submit") }} | ||||
|                         </v-btn> | ||||
|                         <v-btn | ||||
|                             to="/user/assignment" | ||||
|                             color="grey" | ||||
|                             block | ||||
|                             >{{ t("cancel") }} | ||||
|                         </v-btn> | ||||
|                     </v-card-text> | ||||
|                 </v-container> | ||||
|             </v-form> | ||||
|         </v-card> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
| <style scoped> | ||||
|     .main-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     .form-card { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         width: 55%; | ||||
|         /*padding: 1%;*/ | ||||
|     } | ||||
| 
 | ||||
|     .form-container { | ||||
|         width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|     } | ||||
| 
 | ||||
|     .step-container { | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         flex-direction: column; | ||||
|         min-height: 200px; | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 1000px) { | ||||
|         .form-card { | ||||
|             width: 70%; | ||||
|             padding: 1%; | ||||
|         } | ||||
| 
 | ||||
|         .step-container { | ||||
|             min-height: 300px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 650px) { | ||||
|         .form-card { | ||||
|             width: 95%; | ||||
|         } | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,7 +1,75 @@ | |||
| <script setup lang="ts"></script> | ||||
| <script setup lang="ts"> | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { computed, type Ref, ref, watchEffect } from "vue"; | ||||
|     import StudentAssignment from "@/views/assignments/StudentAssignment.vue"; | ||||
|     import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import type { Language } from "@/data-objects/language.ts"; | ||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
| 
 | ||||
|     const role = auth.authState.activeRole; | ||||
|     const isTeacher = computed(() => role === "teacher"); | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const classId = ref<string>(route.params.classId as string); | ||||
|     const assignmentId = ref(Number(route.params.id)); | ||||
| 
 | ||||
|     function useGroupsWithProgress( | ||||
|         groups: Ref<GroupDTO[]>, | ||||
|         hruid: Ref<string>, | ||||
|         language: Ref<string>, | ||||
|     ): { groupProgressMap: Map<number, number> } { | ||||
|         const groupProgressMap: Map<number, number> = new Map<number, number>(); | ||||
| 
 | ||||
|         watchEffect(() => { | ||||
|             // Clear existing entries to avoid stale data | ||||
|             groupProgressMap.clear(); | ||||
| 
 | ||||
|             const lang = ref(language.value as Language); | ||||
| 
 | ||||
|             groups.value.forEach((group) => { | ||||
|                 const groupKey = group.groupNumber; | ||||
|                 const forGroup = ref({ | ||||
|                     forGroup: groupKey, | ||||
|                     assignmentNo: assignmentId, | ||||
|                     classId: classId, | ||||
|                 }); | ||||
| 
 | ||||
|                 const query = useGetLearningPathQuery(hruid.value, lang, forGroup); | ||||
| 
 | ||||
|                 const data = query.data.value; | ||||
| 
 | ||||
|                 groupProgressMap.set(groupKey, data ? calculateProgress(data) : 0); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         return { | ||||
|             groupProgressMap, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     function calculateProgress(lp: LearningPath): number { | ||||
|         return ((lp.amountOfNodes - lp.amountOfNodesLeft) / lp.amountOfNodes) * 100; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
|     <TeacherAssignment | ||||
|         :class-id="classId" | ||||
|         :assignment-id="assignmentId" | ||||
|         :use-groups-with-progress="useGroupsWithProgress" | ||||
|         v-if="isTeacher" | ||||
|     > | ||||
|     </TeacherAssignment> | ||||
|     <StudentAssignment | ||||
|         :class-id="classId" | ||||
|         :assignment-id="assignmentId" | ||||
|         :use-groups-with-progress="useGroupsWithProgress" | ||||
|         v-else | ||||
|     > | ||||
|     </StudentAssignment> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  |  | |||
							
								
								
									
										167
									
								
								frontend/src/views/assignments/StudentAssignment.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								frontend/src/views/assignments/StudentAssignment.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | |||
| <script setup lang="ts"> | ||||
|     import { ref, computed, type Ref } from "vue"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { useAssignmentQuery } from "@/queries/assignments.ts"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import type { AssignmentResponse } from "@/controllers/assignments.ts"; | ||||
|     import { asyncComputed } from "@vueuse/core"; | ||||
|     import { useStudentsByUsernamesQuery } from "@/queries/students.ts"; | ||||
|     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||
|     import type { Language } from "@/data-objects/language.ts"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         classId: string; | ||||
|         assignmentId: number; | ||||
|         useGroupsWithProgress: ( | ||||
|             groups: Ref<GroupDTO[]>, | ||||
|             hruid: Ref<string>, | ||||
|             language: Ref<Language>, | ||||
|         ) => { groupProgressMap: Map<number, number> }; | ||||
|     }>(); | ||||
| 
 | ||||
|     const { t, locale } = useI18n(); | ||||
|     const language = ref<Language>(locale.value as Language); | ||||
|     const learningPath = ref(); | ||||
|     // Get the user's username/id | ||||
|     const username = asyncComputed(async () => { | ||||
|         const user = await auth.loadUser(); | ||||
|         return user?.profile?.preferred_username ?? undefined; | ||||
|     }); | ||||
| 
 | ||||
|     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||
|     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||
| 
 | ||||
|     const submitted = ref(false); //TODO: update by fetching submissions and check if group submitted | ||||
| 
 | ||||
|     const lpQueryResult = useGetLearningPathQuery( | ||||
|         computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""), | ||||
|         computed(() => language.value), | ||||
|     ); | ||||
| 
 | ||||
|     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||
|     const group = computed(() => | ||||
|         groupsQueryResult?.data.value?.groups.find((group) => | ||||
|             group.members?.some((m) => m.username === username.value), | ||||
|         ), | ||||
|     ); | ||||
| 
 | ||||
|     const _groupArray = computed(() => (group.value ? [group.value] : [])); | ||||
|     const progressValue = ref(0); | ||||
|     /* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar | ||||
| Const {groupProgressMap} = props.useGroupsWithProgress( | ||||
| groupArray, | ||||
| learningPath, | ||||
| language | ||||
| ); | ||||
| */ | ||||
| 
 | ||||
|     // Assuming group.value.members is a list of usernames TODO: case when it's StudentDTO's | ||||
|     const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[]); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="container"> | ||||
|         <using-query-result | ||||
|             :query-result="assignmentQueryResult" | ||||
|             v-slot="{ data }: { data: AssignmentResponse }" | ||||
|         > | ||||
|             <v-card | ||||
|                 v-if="data" | ||||
|                 class="assignment-card" | ||||
|             > | ||||
|                 <div class="top-buttons"> | ||||
|                     <v-btn | ||||
|                         icon | ||||
|                         variant="text" | ||||
|                         class="back-btn" | ||||
|                         to="/user/assignment" | ||||
|                     > | ||||
|                         <v-icon>mdi-arrow-left</v-icon> | ||||
|                     </v-btn> | ||||
| 
 | ||||
|                     <v-chip | ||||
|                         v-if="submitted" | ||||
|                         class="ma-2 top-right-btn" | ||||
|                         label | ||||
|                         color="success" | ||||
|                     > | ||||
|                         {{ t("submitted") }} | ||||
|                     </v-chip> | ||||
|                 </div> | ||||
|                 <v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title> | ||||
| 
 | ||||
|                 <v-card-subtitle class="subtitle-section"> | ||||
|                     <using-query-result | ||||
|                         :query-result="lpQueryResult" | ||||
|                         v-slot="{ data: lpData }" | ||||
|                     > | ||||
|                         <v-btn | ||||
|                             v-if="lpData" | ||||
|                             :to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`" | ||||
|                             variant="tonal" | ||||
|                             color="primary" | ||||
|                         > | ||||
|                             {{ t("learning-path") }} | ||||
|                         </v-btn> | ||||
|                     </using-query-result> | ||||
|                 </v-card-subtitle> | ||||
| 
 | ||||
|                 <v-card-text class="description"> | ||||
|                     {{ data.assignment.description }} | ||||
|                 </v-card-text> | ||||
|                 <v-card-text> | ||||
|                     <v-row | ||||
|                         align="center" | ||||
|                         no-gutters | ||||
|                     > | ||||
|                         <v-col cols="auto"> | ||||
|                             <span class="progress-label">{{ t("progress") + ": " }}</span> | ||||
|                         </v-col> | ||||
|                         <v-col> | ||||
|                             <v-progress-linear | ||||
|                                 :model-value="progressValue" | ||||
|                                 color="primary" | ||||
|                                 height="20" | ||||
|                                 class="progress-bar" | ||||
|                             > | ||||
|                                 <template v-slot:default="{ value }"> | ||||
|                                     <strong>{{ Math.ceil(value) }}%</strong> | ||||
|                                 </template> | ||||
|                             </v-progress-linear> | ||||
|                         </v-col> | ||||
|                     </v-row> | ||||
|                 </v-card-text> | ||||
| 
 | ||||
|                 <v-card-text class="group-section"> | ||||
|                     <h3>{{ t("group") }}</h3> | ||||
|                     <div v-if="studentQueries"> | ||||
|                         <ul> | ||||
|                             <li | ||||
|                                 v-for="student in group?.members" | ||||
|                                 :key="student.username" | ||||
|                             > | ||||
|                                 {{ student.firstName + " " + student.lastName }} | ||||
|                             </li> | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                 </v-card-text> | ||||
|             </v-card> | ||||
|         </using-query-result> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     @import "@/assets/assignment.css"; | ||||
| 
 | ||||
|     .progress-label { | ||||
|         font-weight: bold; | ||||
|         margin-right: 5px; | ||||
|     } | ||||
| 
 | ||||
|     .progress-bar { | ||||
|         width: 40%; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,7 +0,0 @@ | |||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
							
								
								
									
										234
									
								
								frontend/src/views/assignments/TeacherAssignment.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								frontend/src/views/assignments/TeacherAssignment.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,234 @@ | |||
| <script setup lang="ts"> | ||||
|     import { computed, type Ref, ref } from "vue"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { useAssignmentQuery, useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||
|     import type { Language } from "@/data-objects/language.ts"; | ||||
|     import type { AssignmentResponse } from "@/controllers/assignments.ts"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         classId: string; | ||||
|         assignmentId: number; | ||||
|         useGroupsWithProgress: ( | ||||
|             groups: Ref<GroupDTO[]>, | ||||
|             hruid: Ref<string>, | ||||
|             language: Ref<Language>, | ||||
|         ) => { groupProgressMap: Map<number, number> }; | ||||
|     }>(); | ||||
| 
 | ||||
|     const { t, locale } = useI18n(); | ||||
|     const language = computed(() => locale.value); | ||||
|     const groups = ref(); | ||||
|     const learningPath = ref(); | ||||
| 
 | ||||
|     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||
|     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||
|     // Get learning path object | ||||
|     const lpQueryResult = useGetLearningPathQuery( | ||||
|         computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""), | ||||
|         computed(() => language.value as Language), | ||||
|     ); | ||||
| 
 | ||||
|     // Get all the groups withing the assignment | ||||
|     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||
|     groups.value = groupsQueryResult.data.value?.groups; | ||||
| 
 | ||||
|     /* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar | ||||
| Const {groupProgressMap} = props.useGroupsWithProgress( | ||||
|     groups, | ||||
|     learningPath, | ||||
|     language | ||||
| ); | ||||
| */ | ||||
| 
 | ||||
|     const allGroups = computed(() => { | ||||
|         const groups = groupsQueryResult.data.value?.groups; | ||||
|         if (!groups) return []; | ||||
| 
 | ||||
|         return groups.map((group) => ({ | ||||
|             name: `${t("group")} ${group.groupNumber}`, | ||||
|             progress: 0, //GroupProgressMap[group.groupNumber], | ||||
|             members: group.members, | ||||
|             submitted: false, //TODO: fetch from submission | ||||
|         })); | ||||
|     }); | ||||
| 
 | ||||
|     const dialog = ref(false); | ||||
|     const selectedGroup = ref({}); | ||||
| 
 | ||||
|     function openGroupDetails(group): void { | ||||
|         selectedGroup.value = group; | ||||
|         dialog.value = true; | ||||
|     } | ||||
| 
 | ||||
|     const headers = computed(() => [ | ||||
|         { title: t("group"), align: "start", key: "name" }, | ||||
|         { title: t("progress"), align: "center", key: "progress" }, | ||||
|         { title: t("submission"), align: "center", key: "submission" }, | ||||
|     ]); | ||||
| 
 | ||||
|     const { mutate } = useDeleteAssignmentMutation(); | ||||
| 
 | ||||
|     async function deleteAssignment(num: number, clsId: string): Promise<void> { | ||||
|         mutate( | ||||
|             { cid: clsId, an: num }, | ||||
|             { | ||||
|                 onSuccess: () => { | ||||
|                     window.location.href = "/user/assignment"; | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="container"> | ||||
|         <using-query-result | ||||
|             :query-result="assignmentQueryResult" | ||||
|             v-slot="{ data }: { data: AssignmentResponse }" | ||||
|         > | ||||
|             <v-card | ||||
|                 v-if="data" | ||||
|                 class="assignment-card" | ||||
|             > | ||||
|                 <div class="top-buttons"> | ||||
|                     <v-btn | ||||
|                         icon | ||||
|                         variant="text" | ||||
|                         class="back-btn" | ||||
|                         to="/user/assignment" | ||||
|                     > | ||||
|                         <v-icon>mdi-arrow-left</v-icon> | ||||
|                     </v-btn> | ||||
| 
 | ||||
|                     <v-btn | ||||
|                         icon | ||||
|                         variant="text" | ||||
|                         class="top-right-btn" | ||||
|                         @click="deleteAssignment(data.assignment.id, data.assignment.within)" | ||||
|                     > | ||||
|                         <v-icon>mdi-delete</v-icon> | ||||
|                     </v-btn> | ||||
|                 </div> | ||||
|                 <v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title> | ||||
|                 <v-card-subtitle class="subtitle-section"> | ||||
|                     <using-query-result | ||||
|                         :query-result="lpQueryResult" | ||||
|                         v-slot="{ data: lpData }" | ||||
|                     > | ||||
|                         <v-btn | ||||
|                             v-if="lpData" | ||||
|                             :to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`" | ||||
|                             variant="tonal" | ||||
|                             color="primary" | ||||
|                         > | ||||
|                             {{ t("learning-path") }} | ||||
|                         </v-btn> | ||||
|                     </using-query-result> | ||||
|                 </v-card-subtitle> | ||||
| 
 | ||||
|                 <v-card-text class="description"> | ||||
|                     {{ data.assignment.description }} | ||||
|                 </v-card-text> | ||||
| 
 | ||||
|                 <v-card-text class="group-section"> | ||||
|                     <h3>{{ t("groups") }}</h3> | ||||
|                     <div class="table-scroll"> | ||||
|                         <v-data-table | ||||
|                             :headers="headers" | ||||
|                             :items="allGroups" | ||||
|                             item-key="id" | ||||
|                             class="elevation-1" | ||||
|                         > | ||||
|                             <template #[`item.name`]="{ item }"> | ||||
|                                 <v-btn | ||||
|                                     @click="openGroupDetails(item)" | ||||
|                                     variant="text" | ||||
|                                     color="primary" | ||||
|                                 > | ||||
|                                     {{ item.name }} | ||||
|                                 </v-btn> | ||||
|                             </template> | ||||
| 
 | ||||
|                             <template #[`item.progress`]="{ item }"> | ||||
|                                 <v-progress-linear | ||||
|                                     :model-value="item.progress" | ||||
|                                     color="blue-grey" | ||||
|                                     height="25" | ||||
|                                 > | ||||
|                                     <template v-slot:default="{ value }"> | ||||
|                                         <strong>{{ Math.ceil(value) }}%</strong> | ||||
|                                     </template> | ||||
|                                 </v-progress-linear> | ||||
|                             </template> | ||||
| 
 | ||||
|                             <template #[`item.submission`]="{ item }"> | ||||
|                                 <v-btn | ||||
|                                     :to="item.submitted ? `${props.assignmentId}/submissions/` : undefined" | ||||
|                                     :color="item.submitted ? 'green' : 'red'" | ||||
|                                     variant="text" | ||||
|                                     class="text-capitalize" | ||||
|                                 > | ||||
|                                     {{ item.submitted ? t("see-submission") : t("no-submission") }} | ||||
|                                 </v-btn> | ||||
|                             </template> | ||||
|                         </v-data-table> | ||||
|                     </div> | ||||
|                 </v-card-text> | ||||
| 
 | ||||
|                 <v-dialog | ||||
|                     v-model="dialog" | ||||
|                     max-width="50%" | ||||
|                 > | ||||
|                     <v-card> | ||||
|                         <v-card-title class="headline">{{ t("members") }}</v-card-title> | ||||
|                         <v-card-text> | ||||
|                             <v-list> | ||||
|                                 <v-list-item | ||||
|                                     v-for="(member, index) in selectedGroup.members" | ||||
|                                     :key="index" | ||||
|                                 > | ||||
|                                     <v-list-item-content> | ||||
|                                         <v-list-item-title | ||||
|                                             >{{ member.firstName + " " + member.lastName }} | ||||
|                                         </v-list-item-title> | ||||
|                                     </v-list-item-content> | ||||
|                                 </v-list-item> | ||||
|                             </v-list> | ||||
|                         </v-card-text> | ||||
|                         <v-card-actions> | ||||
|                             <v-btn | ||||
|                                 color="primary" | ||||
|                                 @click="dialog = false" | ||||
|                                 >Close</v-btn | ||||
|                             > | ||||
|                         </v-card-actions> | ||||
|                     </v-card> | ||||
|                 </v-dialog> | ||||
|                 <!-- | ||||
|                 <v-card-actions class="justify-end"> | ||||
|                     <v-btn | ||||
|                         size="large" | ||||
|                         color="success" | ||||
|                         variant="text" | ||||
|                     > | ||||
|                         {{ t("view-submissions") }} | ||||
|                     </v-btn> | ||||
|                 </v-card-actions> | ||||
|                 --> | ||||
|             </v-card> | ||||
|         </using-query-result> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     @import "@/assets/assignment.css"; | ||||
| 
 | ||||
|     .table-scroll { | ||||
|         overflow-x: auto; | ||||
|         -webkit-overflow-scrolling: touch; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,7 +0,0 @@ | |||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
							
								
								
									
										188
									
								
								frontend/src/views/assignments/UserAssignments.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								frontend/src/views/assignments/UserAssignments.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,188 @@ | |||
| <script setup lang="ts"> | ||||
|     import { ref, computed, onMounted, watch } from "vue"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { useRouter } from "vue-router"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||
|     import { useStudentClassesQuery } from "@/queries/students.ts"; | ||||
|     import { ClassController } from "@/controllers/classes.ts"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { asyncComputed } from "@vueuse/core"; | ||||
|     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const router = useRouter(); | ||||
| 
 | ||||
|     const role = ref(auth.authState.activeRole); | ||||
|     const username = ref<string>(""); | ||||
| 
 | ||||
|     const isTeacher = computed(() => role.value === "teacher"); | ||||
| 
 | ||||
|     // Fetch and store all the teacher's classes | ||||
|     let classesQueryResults = undefined; | ||||
| 
 | ||||
|     if (isTeacher.value) { | ||||
|         classesQueryResults = useTeacherClassesQuery(username, true); | ||||
|     } else { | ||||
|         classesQueryResults = useStudentClassesQuery(username, true); | ||||
|     } | ||||
| 
 | ||||
|     //TODO: remove later | ||||
|     const classController = new ClassController(); | ||||
| 
 | ||||
|     //TODO: replace by query that fetches all user's assignment | ||||
|     const assignments = asyncComputed(async () => { | ||||
|         const classes = classesQueryResults?.data?.value?.classes; | ||||
|         if (!classes) return []; | ||||
|         const result = await Promise.all( | ||||
|             (classes as ClassDTO[]).map(async (cls) => { | ||||
|                 const { assignments } = await classController.getAssignments(cls.id); | ||||
|                 return assignments.map((a) => ({ | ||||
|                     id: a.id, | ||||
|                     class: cls, | ||||
|                     title: a.title, | ||||
|                     description: a.description, | ||||
|                     learningPath: a.learningPath, | ||||
|                     language: a.language, | ||||
|                     groups: a.groups, | ||||
|                 })); | ||||
|             }), | ||||
|         ); | ||||
| 
 | ||||
|         return result.flat(); | ||||
|     }, []); | ||||
| 
 | ||||
|     async function goToCreateAssignment(): Promise<void> { | ||||
|         await router.push("/assignment/create"); | ||||
|     } | ||||
| 
 | ||||
|     async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { | ||||
|         await router.push(`/assignment/${clsId}/${id}`); | ||||
|     } | ||||
| 
 | ||||
|     const { mutate, data, isSuccess } = useDeleteAssignmentMutation(); | ||||
| 
 | ||||
|     watch([isSuccess, data], async ([success, oldData]) => { | ||||
|         if (success && oldData?.assignment) { | ||||
|             window.location.reload(); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { | ||||
|         mutate({ cid: clsId, an: num }); | ||||
|     } | ||||
| 
 | ||||
|     onMounted(async () => { | ||||
|         const user = await auth.loadUser(); | ||||
|         username.value = user?.profile?.preferred_username ?? ""; | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="assignments-container"> | ||||
|         <h1>{{ t("assignments") }}</h1> | ||||
| 
 | ||||
|         <v-btn | ||||
|             v-if="isTeacher" | ||||
|             color="primary" | ||||
|             class="mb-4 center-btn" | ||||
|             @click="goToCreateAssignment" | ||||
|         > | ||||
|             {{ t("new-assignment") }} | ||||
|         </v-btn> | ||||
| 
 | ||||
|         <v-container> | ||||
|             <v-row> | ||||
|                 <v-col | ||||
|                     v-for="assignment in assignments" | ||||
|                     :key="assignment.id" | ||||
|                     cols="12" | ||||
|                 > | ||||
|                     <v-card class="assignment-card"> | ||||
|                         <div class="top-content"> | ||||
|                             <div class="assignment-title">{{ assignment.title }}</div> | ||||
|                             <div class="assignment-class"> | ||||
|                                 {{ t("class") }}: | ||||
|                                 <span class="class-name"> | ||||
|                                     {{ assignment.class.displayName }} | ||||
|                                 </span> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="spacer"></div> | ||||
| 
 | ||||
|                         <div class="button-row"> | ||||
|                             <v-btn | ||||
|                                 color="primary" | ||||
|                                 variant="text" | ||||
|                                 @click="goToAssignmentDetails(assignment.id, assignment.class.id)" | ||||
|                             > | ||||
|                                 {{ t("view-assignment") }} | ||||
|                             </v-btn> | ||||
|                             <v-btn | ||||
|                                 v-if="isTeacher" | ||||
|                                 color="red" | ||||
|                                 variant="text" | ||||
|                                 @click="goToDeleteAssignment(assignment.id, assignment.class.id)" | ||||
|                             > | ||||
|                                 {{ t("delete") }} | ||||
|                             </v-btn> | ||||
|                         </div> | ||||
|                     </v-card> | ||||
|                 </v-col> | ||||
|             </v-row> | ||||
|         </v-container> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .assignments-container { | ||||
|         width: 100%; | ||||
|         margin: 0 auto; | ||||
|         padding: 2% 4%; | ||||
|         box-sizing: border-box; | ||||
|     } | ||||
| 
 | ||||
|     .center-btn { | ||||
|         display: block; | ||||
|         margin-left: auto; | ||||
|         margin-right: auto; | ||||
|     } | ||||
| 
 | ||||
|     .assignment-card { | ||||
|         padding: 1rem; | ||||
|     } | ||||
| 
 | ||||
|     .top-content { | ||||
|         margin-bottom: 1rem; | ||||
|         word-break: break-word; | ||||
|     } | ||||
| 
 | ||||
|     .spacer { | ||||
|         flex: 1; | ||||
|     } | ||||
| 
 | ||||
|     .button-row { | ||||
|         display: flex; | ||||
|         justify-content: flex-end; | ||||
|         gap: 0.5rem; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
| 
 | ||||
|     .assignment-title { | ||||
|         font-weight: bold; | ||||
|         font-size: 1.5rem; | ||||
|         margin-bottom: 0.1rem; | ||||
|         word-break: break-word; | ||||
|     } | ||||
| 
 | ||||
|     .assignment-class { | ||||
|         color: #666; | ||||
|         font-size: 0.95rem; | ||||
|     } | ||||
| 
 | ||||
|     .class-name { | ||||
|         font-weight: 500; | ||||
|         color: #333; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,7 +0,0 @@ | |||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
		Reference in a new issue
	
	 Timo De Meyst
						Timo De Meyst