feat(frontend): query component gebruiken bij CreateAssignment
This commit is contained in:
		
							parent
							
								
									3758e7455f
								
							
						
					
					
						commit
						5d69ea9aa4
					
				
					 10 changed files with 370 additions and 339 deletions
				
			
		|  | @ -7,5 +7,5 @@ export interface AssignmentDTO { | ||||||
|     description: string; |     description: string; | ||||||
|     learningPath: string; |     learningPath: string; | ||||||
|     language: string; |     language: string; | ||||||
|     groups?: GroupDTO[] | string[]; // TODO
 |     groups?: GroupDTO[] | string[][]; // TODO
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,65 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
| import { ref, defineProps, defineEmits, computed } from 'vue'; |  | ||||||
| import { useI18n } from 'vue-i18n'; |  | ||||||
| 
 |  | ||||||
| const props = defineProps({ |  | ||||||
|     students: Array, // All students |  | ||||||
|     availableClass: Object, // Selected class |  | ||||||
|     groups: Array, // All groups |  | ||||||
| }); |  | ||||||
| const emit = defineEmits(['groupCreated']); |  | ||||||
| const { t } = useI18n(); |  | ||||||
| 
 |  | ||||||
| const selectedStudents = ref([]); |  | ||||||
| 
 |  | ||||||
| // Filter students based on the selected class and exclude students already in a group |  | ||||||
| const filteredStudents = computed(() => { |  | ||||||
|     if (props.availableClass) { |  | ||||||
|         const studentsInClass = props.availableClass.students.map(st => ({ |  | ||||||
|             title: `${st.firstName} ${st.lastName}`, |  | ||||||
|             value: st.username, |  | ||||||
|         })); |  | ||||||
| 
 |  | ||||||
|         const studentsInGroups = props.groups.flat(); |  | ||||||
| 
 |  | ||||||
|         return studentsInClass.filter(student => !studentsInGroups.includes(student.value)); |  | ||||||
|     } |  | ||||||
|     return []; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| const createGroup = () => { |  | ||||||
|     if (selectedStudents.value.length) { |  | ||||||
|         // Extract only usernames (student.value) |  | ||||||
|         const usernames = selectedStudents.value.map(student => student.value); |  | ||||||
|         emit('groupCreated', usernames); |  | ||||||
|         selectedStudents.value = []; // Reset selection after creating group |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|     <v-card-text> |  | ||||||
|         <v-combobox |  | ||||||
|             v-model="selectedStudents" |  | ||||||
|             :items="filteredStudents" |  | ||||||
|             item-title="title" |  | ||||||
|             item-value="value" |  | ||||||
|             :label="t('choose-students')" |  | ||||||
|             variant="outlined" |  | ||||||
|             clearable |  | ||||||
|             multiple |  | ||||||
|             hide-details |  | ||||||
|             density="compact" |  | ||||||
|             chips |  | ||||||
|             append-inner-icon="mdi-magnify" |  | ||||||
|         ></v-combobox> |  | ||||||
| 
 |  | ||||||
|         <v-btn @click="createGroup" color="primary" class="mt-2" size="small">{{ t('create-group') }}</v-btn> |  | ||||||
|     </v-card-text> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
							
								
								
									
										250
									
								
								frontend/src/components/assignments/AssignmentForm.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								frontend/src/components/assignments/AssignmentForm.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,250 @@ | ||||||
|  | <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 like this: | ||||||
|  |  router.push({ | ||||||
|  |  path: '/assignment/create', | ||||||
|  |  query: { learningPath: 'learningPathObject' } | ||||||
|  |  }); | ||||||
|  |  */ | ||||||
|  | const props = defineProps<{ | ||||||
|  |     learningPath?: LearningPath | null;  // Optional learningPath prop | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const router = useRouter(); | ||||||
|  | const {t, locale} = useI18n(); | ||||||
|  | const role = ref(auth.authState.activeRole); | ||||||
|  | const username = ref<string | null>(null); | ||||||
|  | 
 | ||||||
|  | interface FormData { | ||||||
|  |     assignmentTitle: string; | ||||||
|  |     selectedLearningPath: string; | ||||||
|  |     selectedClass: string; | ||||||
|  |     groups: string[][]; | ||||||
|  |     deadline: string; | ||||||
|  |     description: string; | ||||||
|  |     currentLanguage: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function submitForm(assignmentTitle: string, | ||||||
|  |                           selectedLearningPath: string, | ||||||
|  |                           selectedClass: string, | ||||||
|  |                           groups: string[][], | ||||||
|  |                           deadline: string, | ||||||
|  |                           description: string, | ||||||
|  |                           currentLanguage: string): Promise<void> { | ||||||
|  |     const assignmentDTO: AssignmentDTO = { | ||||||
|  |         id: 0, | ||||||
|  |         class: selectedClass, | ||||||
|  |         title: assignmentTitle, | ||||||
|  |         description: description, | ||||||
|  |         learningPath: selectedLearningPath, | ||||||
|  |         language: currentLanguage, | ||||||
|  |         groups: groups, | ||||||
|  |         //deadline: deadline, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     //TODO: replace with query function | ||||||
|  |     const controller: AssignmentController = new AssignmentController(selectedClass); | ||||||
|  |     await controller.createAssignment(assignmentDTO); | ||||||
|  |     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 ?? null; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | const addGroupToList = (students: string[]) => { | ||||||
|  |     if (students.length) { | ||||||
|  |         groups.value = [...groups.value, students]; | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | watch(selectedClass, () => { | ||||||
|  |     groups.value = []; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const submitFormHandler = async () => { | ||||||
|  |     const {valid} = await form.value.validate(); | ||||||
|  |     // Don't submit the form if all rules don't apply | ||||||
|  |     if (!valid) return; | ||||||
|  |     await submitForm(assignmentTitle.value, selectedLearningPath.value?.hruid, selectedClass.value.id, groups.value, deadline.value, description.value, locale.value); | ||||||
|  | }; | ||||||
|  | </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,6 +1,6 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, computed, defineEmits } from "vue"; | import { ref, computed, defineEmits } from "vue"; | ||||||
| import {deadlineRules} from "@/utils/assignmentForm.ts"; | import {deadlineRules} from "@/utils/assignment-rules.ts"; | ||||||
| 
 | 
 | ||||||
| const date = ref(""); | const date = ref(""); | ||||||
| const time = ref("23:59"); | const time = ref("23:59"); | ||||||
							
								
								
									
										76
									
								
								frontend/src/components/assignments/GroupSelector.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								frontend/src/components/assignments/GroupSelector.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import {ref, defineProps, defineEmits} from 'vue'; | ||||||
|  | import {useI18n} from 'vue-i18n'; | ||||||
|  | import {useClassStudentsQuery} from "@/queries/classes.ts"; | ||||||
|  | import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  | import type {StudentsResponse} from "@/controllers/students.ts"; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ | ||||||
|  |     classId: string | undefined | ||||||
|  |     groups: string[][], // All groups | ||||||
|  | }>(); | ||||||
|  | const emit = defineEmits(['groupCreated']); | ||||||
|  | const {t} = useI18n(); | ||||||
|  | 
 | ||||||
|  | const selectedStudents = ref([]); | ||||||
|  | 
 | ||||||
|  | const studentQueryResult = useClassStudentsQuery(() => props.classId, true); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | function filterStudents(data: StudentsResponse): { title: string, value: string }[] { | ||||||
|  |     const students = data.students; | ||||||
|  |     const studentsInGroups = props.groups.flat(); | ||||||
|  | 
 | ||||||
|  |     return students | ||||||
|  |         ?.map(st => ({ | ||||||
|  |             title: `${st.firstName} ${st.lastName}`, | ||||||
|  |             value: st.username, | ||||||
|  |         })) | ||||||
|  |         .filter(student => !studentsInGroups.includes(student.value)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | const createGroup = () => { | ||||||
|  |     if (selectedStudents.value.length) { | ||||||
|  |         // Extract only usernames (student.value) | ||||||
|  |         const usernames = selectedStudents.value.map(student => student.value); | ||||||
|  |         emit('groupCreated', usernames); | ||||||
|  |         selectedStudents.value = []; // Reset selection after creating group | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <using-query-result | ||||||
|  |         :query-result="studentQueryResult" | ||||||
|  |         v-slot="{ data }: { data: StudentsResponse }" | ||||||
|  |     > | ||||||
|  |         <h3>{{ t('create-groups') }}</h3> | ||||||
|  |         <v-card-text> | ||||||
|  |             <v-combobox | ||||||
|  |                 v-model="selectedStudents" | ||||||
|  |                 :items="filterStudents(data)" | ||||||
|  |                 item-title="title" | ||||||
|  |                 item-value="value" | ||||||
|  |                 :label="t('choose-students')" | ||||||
|  |                 variant="outlined" | ||||||
|  |                 clearable | ||||||
|  |                 multiple | ||||||
|  |                 hide-details | ||||||
|  |                 density="compact" | ||||||
|  |                 chips | ||||||
|  |                 append-inner-icon="mdi-magnify" | ||||||
|  |             ></v-combobox> | ||||||
|  | 
 | ||||||
|  |             <v-btn @click="createGroup" color="primary" class="mt-2" size="small"> | ||||||
|  |                 {{ t('create-group') }} | ||||||
|  |             </v-btn> | ||||||
|  |         </v-card-text> | ||||||
|  |     </using-query-result> | ||||||
|  | 
 | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { ThemeController } from "@/controllers/themes.ts"; | import { ThemeController } from "@/controllers/themes.ts"; | ||||||
| import { LearningObjectController } from "@/controllers/learning-objects.ts"; | import { LearningObjectController } from "@/controllers/learning-objects.ts"; | ||||||
| import { LearningPathController } from "@/controllers/learning-paths.ts"; | import { LearningPathController } from "@/controllers/learning-paths.ts"; | ||||||
|  | import {ClassController} from "@/controllers/classes.ts"; | ||||||
| 
 | 
 | ||||||
| export function controllerGetter<T>(factory: new () => T): () => T { | export function controllerGetter<T>(factory: new () => T): () => T { | ||||||
|     let instance: T | undefined; |     let instance: T | undefined; | ||||||
|  | @ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T { | ||||||
| export const getThemeController = controllerGetter(ThemeController); | export const getThemeController = controllerGetter(ThemeController); | ||||||
| export const getLearningObjectController = controllerGetter(LearningObjectController); | export const getLearningObjectController = controllerGetter(LearningObjectController); | ||||||
| export const getLearningPathController = controllerGetter(LearningPathController); | export const getLearningPathController = controllerGetter(LearningPathController); | ||||||
|  | export const getClassController = controllerGetter(ClassController); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | import {useMutation, useQueryClient, type UseMutationReturnType} from "@tanstack/vue-query"; | ||||||
|  | import {AssignmentController, type AssignmentResponse} from "@/controllers/assignments.ts"; | ||||||
|  | import type {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; | ||||||
|  | 
 | ||||||
|  | export function useCreateAssignmentMutation(classId: string): UseMutationReturnType<AssignmentResponse, Error, AssignmentDTO, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     const assignmentController = new AssignmentController(classId); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data: AssignmentDTO) => assignmentController.createAssignment(data), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({queryKey: ["assignments"]}); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								frontend/src/queries/classes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/queries/classes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | import {useQuery, type UseQueryReturnType} from "@tanstack/vue-query"; | ||||||
|  | import {computed, type MaybeRefOrGetter, toValue} from "vue"; | ||||||
|  | import type {StudentsResponse} from "@/controllers/students.ts"; | ||||||
|  | import {getClassController} from "@/controllers/controllers.ts"; | ||||||
|  | 
 | ||||||
|  | const classController = getClassController(); | ||||||
|  | 
 | ||||||
|  | function classStudentsQueryKey(classId: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["class-students", classId, full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export function useClassStudentsQuery( | ||||||
|  |     classId: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<StudentsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => classStudentsQueryKey(toValue(classId)!, toValue(full))), | ||||||
|  |         queryFn: async () => classController.getStudents(toValue(classId)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(classId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | @ -1,47 +1,3 @@ | ||||||
| /** |  | ||||||
|  * Submits the form data to the backend. |  | ||||||
|  * |  | ||||||
|  * @param assignmentTitle - The title of the assignment. |  | ||||||
|  * @param selectedLearningPath - The selected learning path, containing hruid and title. |  | ||||||
|  * @param selectedClass - The selected classes, an array of class objects. |  | ||||||
|  * @param groups - An array of groups, each containing student IDs. |  | ||||||
|  * @param deadline - The deadline of the assignment in ISO format. |  | ||||||
|  * @param description - The description of the assignment |  | ||||||
|  * Sends a POST request to the backend with the form data. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| import {AssignmentController} from "@/controllers/assignments.ts"; |  | ||||||
| import type {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; |  | ||||||
| 
 |  | ||||||
| export const submitForm = async ( |  | ||||||
|     assignmentTitle: string, |  | ||||||
|     selectedLearningPath: string, |  | ||||||
|     selectedClass: string, |  | ||||||
|     groups: string[], |  | ||||||
|     deadline: string, |  | ||||||
|     description: string, |  | ||||||
|     currentLanguage: string |  | ||||||
| ) => { |  | ||||||
|     const formData: AssignmentDTO = { |  | ||||||
|         id: 4, |  | ||||||
|         class: selectedClass, |  | ||||||
|         title: assignmentTitle, |  | ||||||
|         description: description, |  | ||||||
|         learningPath: selectedLearningPath, |  | ||||||
|         language: currentLanguage |  | ||||||
|         //groups: [],
 |  | ||||||
|         //deadline: deadline,
 |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     console.log(formData); |  | ||||||
| 
 |  | ||||||
|     const controller: AssignmentController = new AssignmentController(selectedClass); |  | ||||||
| 
 |  | ||||||
|     const response = await controller.createAssignment(formData); |  | ||||||
|     console.log(response); |  | ||||||
| 
 |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Validation rule for the assignment title. |  * Validation rule for the assignment title. | ||||||
|  * |  * | ||||||
|  | @ -1,236 +1,10 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import {useI18n} from "vue-i18n"; | import AssignmentForm from "@/components/assignments/AssignmentForm.vue"; | ||||||
|     import {computed, onMounted, ref, watch} from "vue"; |  | ||||||
|     import GroupSelector from "@/components/GroupSelector.vue"; |  | ||||||
|     import { |  | ||||||
|         assignmentTitleRules, |  | ||||||
|         classRules, |  | ||||||
|         descriptionRules, |  | ||||||
|         learningPathRules, |  | ||||||
|         submitForm |  | ||||||
|     } from "@/utils/assignmentForm.ts"; |  | ||||||
|     import DeadlineSelector from "@/components/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"; |  | ||||||
| 
 |  | ||||||
|     const router = useRouter(); |  | ||||||
|     const {t, locale} = useI18n(); |  | ||||||
|     const role = ref(auth.authState.activeRole); |  | ||||||
|     const username = ref<string | null>(null); |  | ||||||
| 
 |  | ||||||
|     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 ?? null; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     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 { data: classes, isLoading, error, refetch } = useTeacherClassesQuery(username, true); |  | ||||||
|     const allClasses = computed(() => { |  | ||||||
|         if (isLoading.value) { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|         if (error.value) { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|         return classes.value?.classes || []; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const selectedClass = ref(null); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     const assignmentTitle = ref(''); |  | ||||||
|     const deadline = ref(null); |  | ||||||
|     const description = ref(''); |  | ||||||
|     const selectedLearningPath = ref(null); |  | ||||||
|     const groups = ref<string[][]>([]); |  | ||||||
| 
 |  | ||||||
|     const availableClass = computed(() => { |  | ||||||
|         //TODO: replace by real data |  | ||||||
|         return classes.value?.classes.find(cl => selectedClass.value?.value === cl.id) || null; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const allStudents = computed(() => { |  | ||||||
|         //TODO: replace by real data |  | ||||||
|         /*if (!selectedClass.value) return []; |  | ||||||
|         const cl = classes.find(c => c.id === selectedClass.value.value); |  | ||||||
|         return cl ? cl.students.map(st => ({ |  | ||||||
|             title: `${st.firstName} ${st.lastName}`, |  | ||||||
|             value: st.username, |  | ||||||
|             classes: cl |  | ||||||
|         })) : [];*/ |  | ||||||
|         return []; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     // New group is added to the list |  | ||||||
|     const addGroupToList = (students: string[]) => { |  | ||||||
|         if (students.length) { |  | ||||||
|             groups.value = [...groups.value, students]; |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     watch(selectedClass, () => { |  | ||||||
|         groups.value = []; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const submitFormHandler = async () => { |  | ||||||
|         const { valid } = await form.value.validate(); |  | ||||||
|         // Don't submit the form if all rules don't apply |  | ||||||
|         if (!valid) return; |  | ||||||
|         submitForm(assignmentTitle.value, selectedLearningPath.value?.hruid, selectedClass.value.value, groups.value, deadline.value, description.value, locale.value); |  | ||||||
|     }; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| <template> | <template> | ||||||
|     <div class="main-container"> |     <AssignmentForm :learning-path="null"/> | ||||||
|         <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="value" |  | ||||||
|                                 required |  | ||||||
|                                 :filter="(item, query: string) => item.title.toLowerCase().includes(query.toLowerCase())" |  | ||||||
|                             ></v-combobox> |  | ||||||
|                         </v-card-text> |  | ||||||
|                     </using-query-result> |  | ||||||
| 
 |  | ||||||
|                     <v-card-text> |  | ||||||
|                         <v-combobox |  | ||||||
|                             v-model="selectedClass" |  | ||||||
|                             :items="allClasses" |  | ||||||
|                             :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> |  | ||||||
| 
 |  | ||||||
|                     <DeadlineSelector v-model:deadline="deadline" /> |  | ||||||
| 
 |  | ||||||
|                     <h3>{{ t('create-groups') }}</h3> |  | ||||||
| 
 |  | ||||||
|                     <GroupSelector |  | ||||||
|                         :students="allStudents" |  | ||||||
|                         :availableClass="availableClass" |  | ||||||
|                         :groups="groups" |  | ||||||
|                         @groupCreated="addGroupToList" |  | ||||||
|                     /> |  | ||||||
| 
 |  | ||||||
|                     <!-- Counter for created groups --> |  | ||||||
|                     <v-card-text v-if="groups.length"> |  | ||||||
|                         <strong>Created Groups: {{ groups.length }}</strong> |  | ||||||
|                     </v-card-text> |  | ||||||
| 
 |  | ||||||
|                     <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> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <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> | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana