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; | ||||
|     learningPath: 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"> | ||||
| import { ref, computed, defineEmits } from "vue"; | ||||
| import {deadlineRules} from "@/utils/assignmentForm.ts"; | ||||
| import {deadlineRules} from "@/utils/assignment-rules.ts"; | ||||
| 
 | ||||
| const date = ref(""); | ||||
| 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 { LearningObjectController } from "@/controllers/learning-objects.ts"; | ||||
| import { LearningPathController } from "@/controllers/learning-paths.ts"; | ||||
| import {ClassController} from "@/controllers/classes.ts"; | ||||
| 
 | ||||
| export function controllerGetter<T>(factory: new () => T): () => T { | ||||
|     let instance: T | undefined; | ||||
|  | @ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T { | |||
| export const getThemeController = controllerGetter(ThemeController); | ||||
| export const getLearningObjectController = controllerGetter(LearningObjectController); | ||||
| 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. | ||||
|  * | ||||
|  | @ -1,236 +1,10 @@ | |||
| <script setup lang="ts"> | ||||
|     import {useI18n} from "vue-i18n"; | ||||
|     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); | ||||
|     }; | ||||
| import AssignmentForm from "@/components/assignments/AssignmentForm.vue"; | ||||
| </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="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> | ||||
|     <AssignmentForm :learning-path="null"/> | ||||
| </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> | ||||
|  |  | |||
		Reference in a new issue