feat: create assignment form is klaar
This commit is contained in:
		
							parent
							
								
									05fa69f0c7
								
							
						
					
					
						commit
						db7c5409fc
					
				
					 8 changed files with 273 additions and 69 deletions
				
			
		|  | @ -1,37 +1,81 @@ | |||
| <script setup lang="ts"> | ||||
|     import { ref, defineProps, defineEmits } from 'vue'; | ||||
|     import { useI18n } from 'vue-i18n'; | ||||
| import { ref, defineProps, defineEmits, computed } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| 
 | ||||
|     const props = defineProps({ | ||||
|         students: Array, | ||||
|     }); | ||||
|     const emit = defineEmits(['groupCreated']); | ||||
|     const { t } = useI18n(); | ||||
|     const selectedStudents = ref([]); | ||||
| const props = defineProps({ | ||||
|     students: Array, // All students | ||||
|     availableClasses: Array, // Selected classes | ||||
|     groups: Array, // All groups | ||||
| }); | ||||
| const emit = defineEmits(['groupCreated']); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
|     const createGroup = () => { | ||||
|         if (selectedStudents.value.length) { | ||||
|             emit('groupCreated', selectedStudents.value); | ||||
|             selectedStudents.value = []; // Reset selection after creating group | ||||
| const selectedClass = ref(null); | ||||
| const selectedStudents = ref([]); | ||||
| 
 | ||||
| // Filter students based on the selected class and exclude students already in a group | ||||
| const filteredStudents = computed(() => { | ||||
|     if (selectedClass.value) { | ||||
|         // Find the class based on selectedClass id | ||||
|         const selected = props.availableClasses.find(cl => cl.id === selectedClass.value.id); | ||||
|         if (selected) { | ||||
|             // Get all students from the selected class | ||||
|             const studentsInClass = selected.students.map(st => ({ | ||||
|                 title: `${st.firstName} ${st.lastName}`, | ||||
|                 value: st.username, | ||||
|             })); | ||||
| 
 | ||||
|             // Get the list of students already in any group | ||||
|             const studentsInGroups = props.groups.flat(); | ||||
| 
 | ||||
|             // Filter out students that are already in a group | ||||
|             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="selectedClass" | ||||
|             :items="props.availableClasses" | ||||
|             item-title="displayName" | ||||
|             item-value="id" | ||||
|             :label="t('pick-class')" | ||||
|             variant="solo" | ||||
|             clearable | ||||
|             hide-details | ||||
|             density="compact" | ||||
|             class="mb-4" | ||||
|         ></v-combobox> | ||||
| 
 | ||||
|         <v-combobox | ||||
|             v-model="selectedStudents" | ||||
|             :items="props.students" | ||||
|             :label="t('choose-groups')" | ||||
|             :items="filteredStudents" | ||||
|             item-title="title" | ||||
|             item-value="value" | ||||
|             :label="t('choose-students')" | ||||
|             variant="solo" | ||||
|             clearable | ||||
|             multiple | ||||
|             hide-details | ||||
|             density="compact" | ||||
|             chips | ||||
|             append-inner-icon="mdi-magnify" | ||||
|             item-title="title" | ||||
|             item-value="value" | ||||
|         ></v-combobox> | ||||
| 
 | ||||
|         <v-btn @click="createGroup" color="primary" class="mt-2" size="small">{{ t('create-group') }}</v-btn> | ||||
|     </v-card-text> | ||||
| </template> | ||||
|  |  | |||
|  | @ -46,5 +46,10 @@ | |||
|     "groups": "Gruppen", | ||||
|     "learning-path": "Lernpfad", | ||||
|     "choose-lp": "Einen lernpfad auswählen", | ||||
|     "choose-classes": "Klassen wählen" | ||||
|     "choose-classes": "Klassen wählen", | ||||
|     "create-groups": "Gruppen erstellen", | ||||
|     "title": "Titel", | ||||
|     "pick-class": "Wählen Sie eine klasse", | ||||
|     "choose-students": "Studenten auswählen", | ||||
|     "create-group": "Gruppe erstellen" | ||||
| } | ||||
|  |  | |||
|  | @ -46,5 +46,10 @@ | |||
|     "groups": "Groups", | ||||
|     "learning-path": "Learning path", | ||||
|     "choose-lp": "Select a learning path", | ||||
|     "choose-classes": "Select classes" | ||||
|     "choose-classes": "Select classes", | ||||
|     "create-groups": "Create groups", | ||||
|     "title": "Title", | ||||
|     "pick-class": "Pick a class", | ||||
|     "choose-students": "Select students", | ||||
|     "create-group": "Create group" | ||||
| } | ||||
|  |  | |||
|  | @ -45,6 +45,11 @@ | |||
|     "previous": "précédent", | ||||
|     "groups": "Groupes", | ||||
|     "learning-path": "Parcours d'apprentissage", | ||||
|     "choose-lp": "Choisis un parcours d'apprentissage", | ||||
|     "choose-classes": "Choisis des classes" | ||||
|     "choose-lp": "Choisissez un parcours d'apprentissage", | ||||
|     "choose-classes": "Choisissez des classes", | ||||
|     "create-groups": "Crée des groupes", | ||||
|     "title": "Titre", | ||||
|     "pick-class": "Choisissez une classe", | ||||
|     "choose-students": "Sélectionnez des élèves", | ||||
|     "create-group": "Créer un groupe" | ||||
| } | ||||
|  |  | |||
|  | @ -46,5 +46,10 @@ | |||
|     "groups": "Groepen", | ||||
|     "learning-path": "Leerpad", | ||||
|     "choose-lp": "Kies een leerpad", | ||||
|     "choose-classes": "Kies klassen" | ||||
|     "choose-classes": "Kies klassen", | ||||
|     "create-groups": "Groepen maken", | ||||
|     "title": "Titel", | ||||
|     "pick-class": "Kies een klas", | ||||
|     "choose-students": "Studenten selecteren", | ||||
|     "create-group": "Groep aanmaken" | ||||
| } | ||||
|  |  | |||
							
								
								
									
										73
									
								
								frontend/src/utils/assignmentForm.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/src/utils/assignmentForm.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | |||
| /** | ||||
|  * 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 selectedClasses - The selected classes, an array of class objects. | ||||
|  * @param groups - An array of groups, each containing student IDs. | ||||
|  * | ||||
|  * Sends a POST request to the backend with the form data. | ||||
|  */ | ||||
| export const submitForm = async ( | ||||
|     assignmentTitle: string, | ||||
|     selectedLearningPath: any, | ||||
|     selectedClasses: any[], | ||||
|     groups: string[][] | ||||
| ) => { | ||||
|     const formData = { | ||||
|         title: assignmentTitle, | ||||
|         hruid: selectedLearningPath?.hruid, | ||||
|         classes: selectedClasses.map(cl => cl.value), | ||||
|         groups: groups | ||||
|     }; | ||||
| 
 | ||||
|     try { | ||||
|         const response = await fetch(/*"http://localhost:3000/api/assignment"*/"", { | ||||
|             method: "POST", | ||||
|             headers: { "Content-Type": "application/json" }, | ||||
|             body: JSON.stringify(formData) | ||||
|         }); | ||||
|         const data = await response.json(); | ||||
|         console.log("Form submitted successfully:", data); | ||||
|     } catch (error) { | ||||
|         console.error("Error submitting form:", error); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Validation rule for the assignment title. | ||||
|  * | ||||
|  * Ensures that the title is not empty. | ||||
|  */ | ||||
| export const assignmentTitleRules = [ | ||||
|     (value: string) => { | ||||
|         if (value?.length >= 1) return true;  // Title must not be empty
 | ||||
|         return 'Title cannot be empty.'; | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| /** | ||||
|  * Validation rule for the learning path selection. | ||||
|  * | ||||
|  * Ensures that a valid learning path is selected. | ||||
|  */ | ||||
| export const learningPathRules = [ | ||||
|     (value: { hruid: string; title: string }) => { | ||||
|         if (value && value.hruid) { | ||||
|             return true;  // Valid if hruid is present
 | ||||
|         } | ||||
|         return 'You must select a learning path.'; | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| /** | ||||
|  * Validation rule for the classes selection. | ||||
|  * | ||||
|  * Ensures that at least one class is selected. | ||||
|  */ | ||||
| export const classesRules = [ | ||||
|     (value: any[]) => { | ||||
|         if (value?.length >= 1) return true; | ||||
|         return 'You must select at least one class.'; | ||||
|     }, | ||||
| ]; | ||||
							
								
								
									
										58
									
								
								frontend/src/utils/tempData.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								frontend/src/utils/tempData.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| // TODO : temp data until frontend controllers are ready
 | ||||
| type Teacher = { | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|     classes: Array<Class>; | ||||
| }; | ||||
| 
 | ||||
| type Student = { | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|     classes: Array<Class>; | ||||
| }; | ||||
| 
 | ||||
| type Class = { | ||||
|     id: string; | ||||
|     displayName: string; | ||||
|     teachers: Array<Teacher>; | ||||
|     students: Array<Student>; | ||||
| }; | ||||
| 
 | ||||
| const student01: Student = { username: "id01", firstName: "Mark", lastName: "Knopfler", classes: [] }; | ||||
| const student02: Student = { username: "id02", firstName: "John", lastName: "Hiat", classes: [] }; | ||||
| const student03: Student = { username: "id03", firstName: "Aaron", lastName: "Lewis", classes: [] }; | ||||
| 
 | ||||
| const teacher01: Student = { username: "id11", firstName: "Mark", lastName: "Knopfler", classes: [] }; | ||||
| const teacher02: Student = { username: "id12", firstName: "John", lastName: "Hiat", classes: [] }; | ||||
| const teacher03: Student = { username: "id13", firstName: "Aaron", lastName: "Lewis", classes: [] }; | ||||
| 
 | ||||
| const class01: Class = { | ||||
|     id: "class01", | ||||
|     displayName: "class 01", | ||||
|     teachers: [teacher01], | ||||
|     students: [student01, student02], | ||||
| }; | ||||
| const class02: Class = { | ||||
|     id: "class02", | ||||
|     displayName: "class 02", | ||||
|     teachers: [teacher02], | ||||
|     students: [student01, student03], | ||||
| }; | ||||
| const class03: Class = { | ||||
|     id: "class03", | ||||
|     displayName: "class 03", | ||||
|     teachers: [teacher03], | ||||
|     students: [student02, student03], | ||||
| }; | ||||
| 
 | ||||
| student01.classes = [class01, class02]; | ||||
| student02.classes = [class01, class03]; | ||||
| student03.classes = [class02, class03]; | ||||
| 
 | ||||
| teacher01.classes = [class01]; | ||||
| teacher02.classes = [class02]; | ||||
| teacher03.classes = [class03]; | ||||
| 
 | ||||
| export const classes: Array<Class> = [class01, class02, class03]; | ||||
|  | @ -2,49 +2,66 @@ | |||
|     import {useI18n} from "vue-i18n"; | ||||
|     import {computed, onMounted, ref, watch} from "vue"; | ||||
|     import GroupSelector from "@/components/GroupSelector.vue"; | ||||
|     import {classes} from "@/utils/tempData.ts"; | ||||
|     import {assignmentTitleRules, classesRules, learningPathRules, submitForm} from "@/utils/assignmentForm.ts";  // Assuming your tempData.ts has the required classes | ||||
| 
 | ||||
|     const {t, locale} = useI18n(); | ||||
| 
 | ||||
|     const language = ref(locale.value); | ||||
| 
 | ||||
|     const searchQuery = ref(""); | ||||
|     const searchQuery = ref(''); | ||||
| 
 | ||||
|     const assignmentTitle = ref(''); | ||||
|     const allLearningPaths = ref([]); | ||||
|     const filteredLearningPaths = ref([]); | ||||
|     const selectedLearningPath = ref(null); | ||||
|     const allClasses = ref(["f", "r"]); | ||||
|     const allClasses = ref([...classes.map(cl => ({title: cl.displayName, value: cl.id}))]); | ||||
|     const selectedClasses = ref([]); | ||||
|     const allStudents = ref([]); // Fetched students from each selected class | ||||
|     const groups = ref<string[][]>([]);  // Each group is a list of student {names, id's} | ||||
|     const groups = ref<string[][]>([]); | ||||
| 
 | ||||
|     const availableClasses = computed(() => { | ||||
|         //TODO: replace by real data | ||||
|         return classes.filter(cl => selectedClasses.value.some(c => c.value === cl.id)); | ||||
|     }); | ||||
| 
 | ||||
|     const allStudents = computed(() => { | ||||
|         //TODO: replace by real data | ||||
|         return classes | ||||
|             .filter(cl => selectedClasses.value.some(c => c.value === cl.id)) | ||||
|             .flatMap(cl => cl.students.map(st => ({ | ||||
|                 title: `${st.firstName} ${st.lastName}`, | ||||
|                 value: st.username, | ||||
|                 classes: cl | ||||
|             }))); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     // New group is added to the list | ||||
|     const addGroupToList = (students: string[]) => { | ||||
|         if (students.length) { | ||||
|             groups.value = [...groups.value, students]; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     // Fetch all learning paths initially | ||||
|     async function fetchAllLearningPaths() { | ||||
|         //TODO: replace by function from controller | ||||
|         try { | ||||
|             //TODO: replace by function from controller | ||||
|             const response = await fetch(`http://localhost:3000/api/learningPath?language=${language.value}`); | ||||
| 
 | ||||
|             // Error | ||||
|             if (!response.ok) throw new Error("Failed to fetch learning paths"); | ||||
| 
 | ||||
|             // Collect all the learning paths and store them in a list by hruid and title | ||||
|             const data = await response.json(); | ||||
|             allLearningPaths.value = data.map((lp: { hruid: string; title: string }) => ({ | ||||
|                 hruid: lp.hruid, | ||||
|                 title: lp.title | ||||
|             })); | ||||
| 
 | ||||
|             // Get all the learning paths in the filtered list | ||||
|             filteredLearningPaths.value = [...allLearningPaths.value]; | ||||
|         } catch (error) { | ||||
|             console.error(error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Re-fetch the learning paths when the language changes | ||||
|     watch( | ||||
|         () => locale.value, | ||||
|         (newLocale) => { | ||||
|             // Check if the language is valid | ||||
|             if (!["nl", "en"].includes(newLocale)) { | ||||
|                 language.value = "en"; | ||||
|             } | ||||
|  | @ -59,34 +76,31 @@ | |||
|         ); | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all learning paths on mount | ||||
|     onMounted(fetchAllLearningPaths); | ||||
| 
 | ||||
|     // All students that aren't already in a group | ||||
|     const availableStudents = computed(() => { | ||||
|         const groupedStudents = new Set(groups.value.flat()); | ||||
|         return allStudents.value.filter(student => !groupedStudents.has(student)); | ||||
|     }); | ||||
| 
 | ||||
|     const addGroupToList = (students: string[]) => { | ||||
|         if (students.length) { | ||||
|             groups.value.push(students); | ||||
|         } | ||||
|     const submitFormHandler = () => { | ||||
|         submitForm(assignmentTitle.value, selectedLearningPath.value, selectedClasses.value, groups.value); | ||||
|     }; | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <template> | ||||
|     <div class="main-container"> | ||||
|         <h1 class="title">{{ t("new-assignment") }}</h1> | ||||
|         <v-card class="form-card"> | ||||
|             <v-form class="form-container"> | ||||
|             <v-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="solo" clearable required></v-text-field> | ||||
|                     </v-card-text> | ||||
| 
 | ||||
|                     <v-card-text> | ||||
|                         <v-combobox | ||||
|                             v-model="selectedLearningPath" | ||||
|                             :items="searchResults" | ||||
|                             :label="t('choose-lp')" | ||||
|                             :rules="learningPathRules" | ||||
|                             variant="solo" | ||||
|                             clearable | ||||
|                             hide-details | ||||
|  | @ -94,6 +108,7 @@ | |||
|                             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> | ||||
|  | @ -103,39 +118,33 @@ | |||
|                             v-model="selectedClasses" | ||||
|                             :items="allClasses" | ||||
|                             :label="t('choose-classes')" | ||||
|                             :rules="classesRules" | ||||
|                             variant="solo" | ||||
|                             clearable | ||||
|                             multiple | ||||
|                             hide-details | ||||
|                             density="compact" | ||||
|                             chips | ||||
|                             append-inner-icon="mdi-magnify" | ||||
|                             item-title="title" | ||||
|                             item-value="value" | ||||
|                             required | ||||
|                         ></v-combobox> | ||||
|                     </v-card-text> | ||||
| 
 | ||||
|                     <v-container> | ||||
|                         <h3>{{ t('create-groups') }}</h3> | ||||
|                     <h3>{{ t('create-groups') }}</h3> | ||||
| 
 | ||||
|                         <GroupSelector | ||||
|                             :students="availableStudents" | ||||
|                             @groupCreated="addGroupToList" | ||||
|                         /> | ||||
|                     <GroupSelector | ||||
|                         :students="allStudents" | ||||
|                         :availableClasses="availableClasses" | ||||
|                         :groups="groups" | ||||
|                         @groupCreated="addGroupToList" | ||||
|                     /> | ||||
| 
 | ||||
|                         <!-- Counter for created groups --> | ||||
|                         <v-card-text v-if="groups.length"> | ||||
|                             <strong>{{ t('created-groups') }}: {{ groups.length }}</strong> | ||||
|                         </v-card-text> | ||||
| 
 | ||||
|                         <!-- Display created groups --> | ||||
|                         <v-card-text v-if="groups.length"> | ||||
|                             <ul> | ||||
|                                 <li v-for="(group, index) in groups" :key="index"> | ||||
|                                     {{ group.join(', ') }} | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </v-card-text> | ||||
|                     </v-container> | ||||
|                     <!-- Counter for created groups --> | ||||
|                     <v-card-text v-if="groups.length"> | ||||
|                         <strong>Created Groups: {{ groups.length }}</strong> | ||||
|                     </v-card-text> | ||||
| 
 | ||||
|                 </v-container> | ||||
|                 <v-btn class="mt-2" type="submit" block>Submit</v-btn> | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana