feat(frontend): leerkracht kan alle groepen binnen een assignment zien en leerling kan zijn group zien
This commit is contained in:
		
							parent
							
								
									83f01830e3
								
							
						
					
					
						commit
						20cf276faf
					
				
					 15 changed files with 397 additions and 341 deletions
				
			
		|  | @ -1,226 +0,0 @@ | |||
| <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 {AssignmentController} from "@/controllers/assignments.ts"; | ||||
| 
 | ||||
| /*** | ||||
|  TODO: when clicking the assign button from lp page pass the lp-object in a state: | ||||
|  */ | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| const {t, locale} = useI18n(); | ||||
| const role = ref(auth.authState.activeRole); | ||||
| const username = ref<string>(""); | ||||
| 
 | ||||
| async function submitForm(assignmentDTO: AssignmentDTO): Promise<void> { | ||||
|     //TODO: replace with query function | ||||
|     const controller: AssignmentController = new AssignmentController(assignmentDTO.class); | ||||
|     await controller.createAssignment(assignmentDTO); | ||||
| 
 | ||||
|     // Navigate back to all assignments | ||||
|     await router.push('/user/assignment'); | ||||
| } | ||||
| 
 | ||||
| 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<LearningPath | null>(props.learningPath ?? null); | ||||
| // Disable combobox when learningPath prop is passed | ||||
| const isLearningPathSelected = props.learningPath !== null; | ||||
| 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 = []; | ||||
| }); | ||||
| 
 | ||||
| async function submitFormHandler(): Promise<void> { | ||||
|     const {valid} = await form.value.validate(); | ||||
|     // Don't submit the form if all rules don't apply | ||||
|     if (!valid) return; | ||||
|     const assignmentDTO: AssignmentDTO = { | ||||
|         id: 0, | ||||
|         class: selectedClass.value?.id || "", | ||||
|         title: assignmentTitle.value, | ||||
|         description: description.value, | ||||
|         learningPath: selectedLearningPath.value?.hruid || "", | ||||
|         language: language.value | ||||
|     } | ||||
|     await submitForm(assignmentDTO); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <template> | ||||
|     <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="isLearningPathSelected" | ||||
|                                 :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-btn class="mt-2" color="secondary" type="submit" block>Submit</v-btn> | ||||
|                 </v-container> | ||||
|             </v-form> | ||||
|         </v-card> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <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,5 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
| import { ref, computed, defineEmits } from "vue"; | ||||
| import {ref, computed, defineEmits} from "vue"; | ||||
| import {deadlineRules} from "@/utils/assignment-rules.ts"; | ||||
| 
 | ||||
| const date = ref(""); | ||||
|  | @ -11,7 +11,7 @@ const formattedDeadline = computed(() => { | |||
|     return `${date.value} ${time.value}`; | ||||
| }); | ||||
| 
 | ||||
| const updateDeadline = () => { | ||||
| function updateDeadline(): void { | ||||
|     if (date.value && time.value) { | ||||
|         emit("update:deadline", formattedDeadline.value); | ||||
|     } | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ function filterStudents(data: StudentsResponse): { title: string, value: string | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| const createGroup = () => { | ||||
| function createGroup(): void { | ||||
|     if (selectedStudents.value.length) { | ||||
|         // Extract only usernames (student.value) | ||||
|         const usernames = selectedStudents.value.map(student => student.value); | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ | |||
|     "create-group": "Gruppe erstellen", | ||||
|     "class": "klasse", | ||||
|     "delete": "löschen", | ||||
|     "view-assignment": "Auftrag anzeigen" | ||||
|     "view-assignment": "Auftrag anzeigen", | ||||
|     "legendTeacherExclusive": "Information für Lehrkräfte", | ||||
|     "code": "code", | ||||
|     "class": "Klasse", | ||||
|  |  | |||
|  | @ -70,10 +70,8 @@ | |||
|     "create-group": "Create group", | ||||
|     "class": "class", | ||||
|     "delete": "delete", | ||||
|     "view-assignment": "View assignment" | ||||
|     "read-more": "Read more", | ||||
|     "view-assignment": "View assignment", | ||||
|     "code": "code", | ||||
|     "class": "class", | ||||
|     "invitations": "invitations", | ||||
|     "createClass": "create class", | ||||
|     "classname": "classname", | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ | |||
|     "create-group": "Créer un groupe", | ||||
|     "class": "classe", | ||||
|     "delete": "supprimer", | ||||
|     "view-assignment": "Voir le travail" | ||||
|     "view-assignment": "Voir le travail", | ||||
|     "read-more": "En savoir plus", | ||||
|     "code": "code", | ||||
|     "class": "classe", | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ | |||
|     "create-group": "Groep aanmaken", | ||||
|     "class": "klas", | ||||
|     "delete": "verwijderen", | ||||
|     "view-assignment": "Opdracht bekijken" | ||||
|     "view-assignment": "Opdracht bekijken", | ||||
|     "read-more": "Lees meer", | ||||
|     "code": "code", | ||||
|     "class": "klas", | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ export function useAssignmentsQuery( | |||
| export function useAssignmentQuery( | ||||
|     classid: MaybeRefOrGetter<string | undefined>, | ||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||
| ): UseQueryReturnType<AssignmentsResponse, Error> { | ||||
| ): UseQueryReturnType<AssignmentResponse, Error> { | ||||
|     const { cid, an } = toValues(classid, assignmentNumber, 1, true); | ||||
| 
 | ||||
|     return useQuery({ | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| import { computed, toValue } from "vue"; | ||||
| import {computed, type Ref, toValue} from "vue"; | ||||
| import type { MaybeRefOrGetter } from "vue"; | ||||
| import { | ||||
|     type QueryObserverResult, | ||||
|     useMutation, | ||||
|     type UseMutationReturnType, | ||||
|     type UseMutationReturnType, useQueries, | ||||
|     useQuery, | ||||
|     useQueryClient, | ||||
|     type UseQueryReturnType, | ||||
|  | @ -69,6 +70,20 @@ export function useStudentQuery( | |||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useStudentsByUsernamesQuery( | ||||
|     usernames: MaybeRefOrGetter<string[] | undefined> | ||||
| ): Ref<QueryObserverResult<StudentResponse>[]> { | ||||
|     const resolvedUsernames = toValue(usernames) ?? []; | ||||
| 
 | ||||
|     return useQueries({ | ||||
|         queries: resolvedUsernames?.map((username) => ({ | ||||
|             queryKey: computed(() => studentQueryKey(toValue(username))), | ||||
|             queryFn: async () => studentController.getByUsername(toValue(username)), | ||||
|             enabled: Boolean(toValue(username)), | ||||
|         })), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useStudentClassesQuery( | ||||
|     username: MaybeRefOrGetter<string | undefined>, | ||||
|     full: MaybeRefOrGetter<boolean> = true, | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ const router = createRouter({ | |||
|                     component: CreateAssignment, | ||||
|                 }, | ||||
|                 { | ||||
|                     path: ":id", | ||||
|                     path: ":classId/:id", | ||||
|                     name: "SingleAssigment", | ||||
|                     component: SingleAssignment, | ||||
|                 }, | ||||
|  |  | |||
|  | @ -1,10 +1,225 @@ | |||
| <script setup lang="ts"> | ||||
| import AssignmentForm from "@/components/assignments/AssignmentForm.vue"; | ||||
| 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 {AssignmentController} from "@/controllers/assignments.ts"; | ||||
| import {useCreateAssignmentMutation} from "@/queries/assignments.ts"; | ||||
| 
 | ||||
| /*** | ||||
|  TODO: when clicking the assign button from lp page pass the lp-object in a state: | ||||
|  */ | ||||
| 
 | ||||
| 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<LearningPath | null>(window.history.state?.learningPath ?? null); | ||||
| 
 | ||||
| // Disable combobox when learningPath prop is passed | ||||
| const lpIsSelected = window.history.state?.learningPath !== 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, isSuccess} = useCreateAssignmentMutation(); | ||||
| 
 | ||||
| async function submitFormHandler(): Promise<void> { | ||||
|     const {valid} = await form.value.validate(); | ||||
|     if (!valid) return; | ||||
| 
 | ||||
|     const assignmentDTO: AssignmentDTO = { | ||||
|         id: 0, | ||||
|         within: selectedClass.value?.id || "", | ||||
|         title: assignmentTitle.value, | ||||
|         description: description.value, | ||||
|         learningPath: selectedLearningPath.value?.hruid || "", | ||||
|         language: language.value, | ||||
|         groups: groups.value | ||||
|     }; | ||||
| 
 | ||||
|     mutate({cid: assignmentDTO.within, data: assignmentDTO}); | ||||
|     if (isSuccess) await router.push("/user/assignment"); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <template> | ||||
|     <AssignmentForm :learning-path="null"/> | ||||
|     <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-btn class="mt-2" color="secondary" type="submit" block>Submit</v-btn> | ||||
|                 </v-container> | ||||
|             </v-form> | ||||
|         </v-card> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <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> | ||||
|  |  | |||
|  | @ -4,27 +4,14 @@ import auth from "@/services/auth/auth-service.ts"; | |||
| import {computed, ref} from "vue"; | ||||
| import StudentAssignment from "@/views/assignments/StudentAssignment.vue"; | ||||
| import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue"; | ||||
| import {asyncComputed} from "@vueuse/core"; | ||||
| import {GroupController} from "@/controllers/groups.ts"; | ||||
| import {useRoute} from "vue-router"; | ||||
| 
 | ||||
| const role = auth.authState.activeRole; | ||||
| const isTeacher = computed(() => role === 'teacher'); | ||||
| 
 | ||||
| // Get the user's username/id | ||||
| const username = asyncComputed(async () => { | ||||
|     const user = await auth.loadUser(); | ||||
|     return user?.profile?.preferred_username ?? undefined | ||||
| }); | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| const classId = ref<string>(route.params.classId as string); | ||||
| const assignmentId = ref(Number(route.params.id)); | ||||
| const classId = window.history.state?.class_id; | ||||
| 
 | ||||
| const groupController = new GroupController(classId, assignmentId.value); | ||||
| 
 | ||||
| const groupDTOs = asyncComputed(async () => await groupController.getAll(true)); | ||||
| console.log(groupDTOs.value); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
|  | @ -32,14 +19,12 @@ console.log(groupDTOs.value); | |||
|     <TeacherAssignment | ||||
|         :class-id="classId" | ||||
|         :assignment-id="assignmentId" | ||||
|         :groups="groupDTOs" | ||||
|         v-if="isTeacher" | ||||
|     > | ||||
|     </TeacherAssignment> | ||||
|     <StudentAssignment | ||||
|         :class-id="classId" | ||||
|         :assignment-id="assignmentId" | ||||
|         :groups="groupDTOs" | ||||
|         v-else | ||||
|     > | ||||
|     </StudentAssignment> | ||||
|  |  | |||
|  | @ -5,16 +5,13 @@ 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 type {GroupDTO} from "@dwengo-1/common/interfaces/group"; | ||||
| import {asyncComputed} from "@vueuse/core"; | ||||
| import {useStudentsByUsernamesQuery} from "@/queries/students.ts"; | ||||
| import {AssignmentDTO} from "@dwengo-1/common/dist/interfaces/assignment.ts"; | ||||
| import {StudentDTO} from "@dwengo-1/common/dist/interfaces/student.ts"; | ||||
| import {useGroupsQuery} from "@/queries/groups.ts"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|     classId: string | ||||
|     assignmentId: number | ||||
|     groups: GroupDTO[] | undefined | ||||
| }>(); | ||||
| 
 | ||||
| const {t, locale} = useI18n(); | ||||
|  | @ -26,23 +23,19 @@ const username = asyncComputed(async () => { | |||
| }); | ||||
| 
 | ||||
| const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||
| const submitted = ref(false);//TODO: update by fetching submissions and check if group submitted | ||||
| 
 | ||||
| const submitted = ref(true);//TODO: update by fetching submissions and check group | ||||
| 
 | ||||
| const submitAssignment = async () => { | ||||
|     //TODO | ||||
| }; | ||||
| 
 | ||||
| const group = computed(() => { | ||||
|     return props?.groups?.find(group => | ||||
|         group.members.some(m => m.username === username.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) | ||||
|         ) | ||||
|     /** For testing | ||||
|     return {assignment: 1, | ||||
|         groupNumber: 1, | ||||
|         members: ["testleerling1"]} | ||||
|         */ | ||||
| }); | ||||
|      return {assignment: 1, | ||||
|      groupNumber: 1, | ||||
|      members: ["testleerling1"]} | ||||
|      */ | ||||
| ); | ||||
| 
 | ||||
| // Assuming group.value.members is a list of usernames TODO: case when it's StudentDTO's | ||||
| const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[]); | ||||
|  | @ -75,10 +68,10 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | |||
|                         {{ t("submitted") }} | ||||
|                     </v-chip> | ||||
|                 </div> | ||||
|                 <v-card-title class="text-h4">{{ data.title }}</v-card-title> | ||||
|                 <v-card-title class="text-h4">{{ data.assignment.title }}</v-card-title> | ||||
|                 <v-card-subtitle class="subtitle-section"> | ||||
|                     <v-btn | ||||
|                         :to="`/learningPath/${language}/${data.learningPath}`" | ||||
|                         :to="`/learningPath/${language}/${data.assignment.learningPath}`" | ||||
|                         variant="tonal" | ||||
|                         color="primary" | ||||
|                     > | ||||
|  | @ -87,30 +80,20 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | |||
|                 </v-card-subtitle> | ||||
| 
 | ||||
|                 <v-card-text class="description"> | ||||
|                     {{ data.description }} | ||||
|                     {{ data.assignment.description }} | ||||
|                 </v-card-text> | ||||
| 
 | ||||
|                 <v-card-text class="group-section"> | ||||
|                     <h3>{{ t("group") }}</h3> | ||||
|                     <div v-if="studentQueries"> | ||||
|                         <ul> | ||||
|                             <li v-for="student in studentQueries" :key="student.data?.student.id"> | ||||
|                                 {{ student.data?.student.firstName + ' ' + student.data?.student.lastName }} | ||||
|                             <li v-for="student in group?.members" :key="student.username"> | ||||
|                                 {{ student.firstName + ' ' + student.lastName }} | ||||
|                             </li> | ||||
|                         </ul> | ||||
|                     </div> | ||||
| 
 | ||||
|                 </v-card-text> | ||||
|                 <v-card-actions class="justify-end"> | ||||
|                     <v-btn | ||||
|                         size="large" | ||||
|                         color="success" | ||||
|                         variant="flat" | ||||
|                         @click="submitAssignment" | ||||
|                     > | ||||
|                         {{ t("submit") }} | ||||
|                     </v-btn> | ||||
|                 </v-card-actions> | ||||
| 
 | ||||
|             </v-card> | ||||
|         </using-query-result> | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| <script setup lang="ts"> | ||||
| import {computed, defineProps} from "vue"; | ||||
| import {computed, defineProps, ref} from "vue"; | ||||
| import {useI18n} from "vue-i18n"; | ||||
| import {AssignmentController, type AssignmentResponse} from "@/controllers/assignments.ts"; | ||||
| import {useAssignmentQuery} from "@/queries/assignments.ts"; | ||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
| import type {GroupDTO} from "@dwengo-1/common/interfaces/group"; | ||||
| import {useGroupsQuery} from "@/queries/groups.ts"; | ||||
| import {useGetLearningPathQuery} from "@/queries/learning-paths.ts"; | ||||
| import type {Language} from "@/data-objects/language.ts"; | ||||
| import type {LearningPath} from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|     classId: string | ||||
|     assignmentId: number | ||||
|     groups: GroupDTO[] | undefined | ||||
| }>(); | ||||
| 
 | ||||
| const {t, locale} = useI18n(); | ||||
|  | @ -17,10 +19,43 @@ const language = computed(() => locale.value); | |||
| const controller = new AssignmentController(props.classId); | ||||
| 
 | ||||
| const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||
| const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||
| 
 | ||||
| const deleteAssignment = async () => { | ||||
|     await controller.deleteAssignment(props.assignmentId.value); | ||||
| }; | ||||
| const lpQueryResult = useGetLearningPathQuery( | ||||
|     computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""), | ||||
|     computed(() => language.value as Language) | ||||
| ); | ||||
| 
 | ||||
| const allGroups = computed(() => { | ||||
|     const groups = groupsQueryResult.data.value?.groups; | ||||
|     if (!groups) return []; | ||||
| 
 | ||||
|     return groups.map(group => ({ | ||||
|         name: `${t('group')} ${group.groupNumber}`, | ||||
|         progress: 0, | ||||
|         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 = ref([ | ||||
|     {title: t('group'), align: 'start', key: 'name'}, | ||||
|     {title: t('progress'), align: 'center', key: 'progress'}, | ||||
|     {title: t('submission'), align: 'center', key: 'submission'} | ||||
| ]); | ||||
| 
 | ||||
| 
 | ||||
| async function deleteAssignment(): Promise<void> { | ||||
|     await controller.deleteAssignment(props.assignmentId); | ||||
| } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
|  | @ -50,45 +85,93 @@ const deleteAssignment = async () => { | |||
|                         <v-icon>mdi-delete</v-icon> | ||||
|                     </v-btn> | ||||
|                 </div> | ||||
|                 <v-card-title class="text-h4">{{ data.title }}</v-card-title> | ||||
|                 <v-card-title class="text-h4">{{ data.assignment.title }}</v-card-title> | ||||
|                 <v-card-subtitle class="subtitle-section"> | ||||
|                     <v-btn | ||||
|                         :to="`/learningPath/${language}/${data.learningPath}`" | ||||
|                         variant="tonal" | ||||
|                         color="primary" | ||||
|                     <using-query-result | ||||
|                         :query-result="lpQueryResult" | ||||
|                         v-slot="{ data: lpData }" | ||||
|                     > | ||||
|                         {{ t("learning-path") }} | ||||
|                     </v-btn> | ||||
|                         <v-btn v-if="lpData" | ||||
|                                :to="`/learningPath/${language}/${lpData.hruid}/${lpData.startNode.learningobjectHruid}`" | ||||
|                                variant="tonal" | ||||
|                                color="primary" | ||||
|                         > | ||||
|                             {{ t("learning-path") }} | ||||
|                         </v-btn> | ||||
|                     </using-query-result> | ||||
| 
 | ||||
|                 </v-card-subtitle> | ||||
| 
 | ||||
|                 <v-card-text class="description"> | ||||
|                     {{ data.description }} | ||||
|                     {{ data.assignment.description }} | ||||
|                 </v-card-text> | ||||
| 
 | ||||
|                 <v-card-text class="group-section"> | ||||
|                     <h3>{{ t("group") }}</h3> | ||||
|                     <h3>{{ t("groups") }}</h3> | ||||
|                     <div class="table-scroll"> | ||||
|                         <v-data-table | ||||
|                             :headers="headers" | ||||
|                             :items="allGroups" | ||||
|                             item-key="id" | ||||
|                             class="elevation-1" | ||||
|                         > | ||||
|                             <template v-slot:item.name="{ item }"> | ||||
|                                 <v-btn @click="openGroupDetails(item)" variant="text" color="primary"> | ||||
|                                     {{ item.name }} | ||||
|                                 </v-btn> | ||||
|                             </template> | ||||
| 
 | ||||
|                     <!-- Teacher view | ||||
|                     <div v-if="isTeacher"> | ||||
|                         <v-expansion-panels> | ||||
|                             <v-expansion-panel | ||||
|                                 v-for="(group, index) in assignment.groups" | ||||
|                                 :key="group.id" | ||||
|                             > | ||||
|                                 <v-expansion-panel-title> | ||||
|                                     {{ t("group") }} {{ index + 1 }} | ||||
|                                 </v-expansion-panel-title> | ||||
|                                 <v-expansion-panel-text> | ||||
|                                     <ul> | ||||
|                                         <li v-for="student in group.members" :key="student.username"> | ||||
|                                             {{ student.firstName + ' ' + student.lastName }} | ||||
|                                         </li> | ||||
|                                     </ul> | ||||
|                                 </v-expansion-panel-text> | ||||
|                             </v-expansion-panel> | ||||
|                         </v-expansion-panels> | ||||
|                     </div>--> | ||||
|                             <template v-slot: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 v-slot: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('not-submitted') }} | ||||
|                                 </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">Group 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" | ||||
|  | @ -98,6 +181,7 @@ const deleteAssignment = async () => { | |||
|                         {{ t("view-submissions") }} | ||||
|                     </v-btn> | ||||
|                 </v-card-actions> | ||||
|                 --> | ||||
|             </v-card> | ||||
|         </using-query-result> | ||||
|     </div> | ||||
|  | @ -105,5 +189,10 @@ const deleteAssignment = async () => { | |||
| 
 | ||||
| <style scoped> | ||||
| @import "@/assets/assignment.css"; | ||||
| 
 | ||||
| .table-scroll { | ||||
|     overflow-x: auto; | ||||
|     -webkit-overflow-scrolling: touch; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,25 +27,25 @@ if (isTeacher.value) { | |||
|     classesQueryResults = useStudentClassesQuery(username, true); | ||||
| } | ||||
| 
 | ||||
| //TODO: replace with query from classes | ||||
| //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) => { | ||||
|             //TODO: replace by class queries | ||||
|             const {assignments} = await classController.getAssignments(cls.id); | ||||
|             return assignments.map(a => ({ | ||||
|                 id: a.id, | ||||
|                 class: cls, // replace by the whole ClassDTO object | ||||
|                 class: cls, | ||||
|                 title: a.title, | ||||
|                 description: a.description, | ||||
|                 learningPath: a.learningPath, | ||||
|                 language: a.language, | ||||
|                 groups: [] | ||||
|                 groups: a.groups | ||||
|             })); | ||||
|         }) | ||||
|     ); | ||||
|  | @ -54,23 +54,20 @@ const assignments = asyncComputed(async () => { | |||
| }, []); | ||||
| 
 | ||||
| 
 | ||||
| const goToCreateAssignment = async () => { | ||||
| async function goToCreateAssignment(): Promise<void> { | ||||
|     await router.push('/assignment/create'); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| const goToAssignmentDetails = async (id: number, class_id: string) => { | ||||
|     await router.push({ | ||||
|         path: `/assignment/${id}`, | ||||
|         state: {class_id}, | ||||
|     }); | ||||
| }; | ||||
| async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { | ||||
|     await router.push(`/assignment/${clsId}/${id}`); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const goToDeleteAssignment = async (id: number, class_id: string) => { | ||||
| async function goToDeleteAssignment(id: number, clsId: string): Promise<void> { | ||||
|     //TODO: replace with query | ||||
|     const controller = new AssignmentController(class_id); | ||||
|     const controller = new AssignmentController(clsId); | ||||
|     await controller.deleteAssignment(id); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| onMounted(async () => { | ||||
|     const user = await auth.loadUser(); | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana