feat: bezig met ui/ux assignment, bug in testdata
This commit is contained in:
		
							parent
							
								
									7e4e179121
								
							
						
					
					
						commit
						509db70c73
					
				
					 6 changed files with 227 additions and 79 deletions
				
			
		|  | @ -13,6 +13,7 @@ import { requireFields } from './error-helper.js'; | |||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||
| import { EntityDTO } from '@mikro-orm/core'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| 
 | ||||
| function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } { | ||||
|     const classid = req.params.classid; | ||||
|  | @ -38,14 +39,19 @@ export async function getAllAssignmentsHandler(req: Request, res: Response): Pro | |||
| 
 | ||||
| export async function createAssignmentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const classid = req.params.classid; | ||||
|     const description = req.body.description; | ||||
|     const language = req.body.language; | ||||
|     const learningPath = req.body.learningPath; | ||||
|     const description = req.body.description || ""; | ||||
|     const language = req.body.language || FALLBACK_LANG; | ||||
|     const learningPath = req.body.learningPath || ""; | ||||
|     const title = req.body.title; | ||||
| 
 | ||||
|     requireFields({ description, language, learningPath, title }); | ||||
|     requireFields({ title }); | ||||
| 
 | ||||
|     const assignmentData = req.body as AssignmentDTO; | ||||
|     const assignmentData = { | ||||
|         description: description, | ||||
|         language: language, | ||||
|         learningPath: learningPath, | ||||
|         title: title, | ||||
|     } as AssignmentDTO; | ||||
|     const assignment = await createAssignment(classid, assignmentData); | ||||
| 
 | ||||
|     res.json({ assignment }); | ||||
|  |  | |||
							
								
								
									
										49
									
								
								frontend/src/components/DwengoTable.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								frontend/src/components/DwengoTable.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| <template> | ||||
|     <v-table class="table"> | ||||
|         <thead> | ||||
|             <tr v-for="name in columns" :key="column"> | ||||
|                 <th class="header">{{ name }}</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr | ||||
|                 v-for="([item1, item2, item3], index) in listItems" | ||||
|                 :key="index" | ||||
|             > | ||||
|                 <td></td> | ||||
|                 <td> | ||||
|                     <v-btn | ||||
|                         :to="`/class/${c.id}`" | ||||
|                         variant="text" | ||||
|                     > | ||||
|                         {{ c.displayName }} | ||||
|                         <v-icon end> mdi-menu-right </v-icon> | ||||
|                     </v-btn> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <span v-if="!isMdAndDown">{{ c.id }}</span> | ||||
|                     <span | ||||
|                         v-else | ||||
|                         style="cursor: pointer" | ||||
|                         @click="openCodeDialog(c.id)" | ||||
|                         ><v-icon icon="mdi-eye"></v-icon | ||||
|                     ></span> | ||||
|                 </td> | ||||
| 
 | ||||
|                 <td>{{ c.students.length }}</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </v-table> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'columnList', | ||||
|   props: { | ||||
|     items: { | ||||
|       type: Array, | ||||
|       required: true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										0
									
								
								frontend/src/components/assignments/AssignmentCard.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/components/assignments/AssignmentCard.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -25,7 +25,7 @@ export class AssignmentController extends BaseController { | |||
|         return this.get<AssignmentResponse>(`/${num}`); | ||||
|     } | ||||
| 
 | ||||
|     async createAssignment(data: AssignmentDTO): Promise<AssignmentResponse> { | ||||
|     async createAssignment(data: Partial<AssignmentDTO>): Promise<AssignmentResponse> { | ||||
|         return this.post<AssignmentResponse>(`/`, data); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -117,7 +117,7 @@ export function useAssignmentQuery( | |||
| export function useCreateAssignmentMutation(): UseMutationReturnType< | ||||
|     AssignmentResponse, | ||||
|     Error, | ||||
|     { cid: string; data: AssignmentDTO }, | ||||
|     { cid: string; data: Partial<AssignmentDTO> }, | ||||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
|  |  | |||
|  | @ -6,10 +6,10 @@ import authState from "@/services/auth/auth-service.ts"; | |||
| import auth from "@/services/auth/auth-service.ts"; | ||||
| import { useTeacherAssignmentsQuery, useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||
| import { useStudentAssignmentsQuery, useStudentClassesQuery } from "@/queries/students.ts"; | ||||
| import { ClassController } from "@/controllers/classes.ts"; | ||||
| import { ClassController, type ClassesResponse } from "@/controllers/classes.ts"; | ||||
| import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
| import { asyncComputed } from "@vueuse/core"; | ||||
| import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
| import { useCreateAssignmentMutation, useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
| import type { AssignmentsResponse } from "@/controllers/assignments"; | ||||
| import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|  | @ -38,8 +38,14 @@ onMounted(async () => { | |||
| }); | ||||
| 
 | ||||
| const isTeacher = computed(() => role.value === "teacher"); | ||||
| 
 | ||||
| const assignmentsQuery = isTeacher ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); | ||||
| const { mutate: assignmentMutation, isSuccess: assignmentIsSuccess } = useCreateAssignmentMutation(); | ||||
| 
 | ||||
| const classesQuery = isTeacher ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); | ||||
| const selectedClass = ref<ClassDTO | undefined>(undefined); | ||||
| const isClassSelected = ref(false); | ||||
| 
 | ||||
| const assignmentTitle = ref<string>(""); | ||||
| 
 | ||||
| async function goToCreateAssignment(): Promise<void> { | ||||
|     await router.push("/assignment/create"); | ||||
|  | @ -65,6 +71,27 @@ onMounted(async () => { | |||
|     const user = await auth.loadUser(); | ||||
|     username.value = user?.profile?.preferred_username ?? ""; | ||||
| }); | ||||
| 
 | ||||
| async function createAssignment(): Promise<void> { | ||||
|     const cid = selectedClass.value!.id; | ||||
|     const assignmentData: Partial<AssignmentDTO> = { | ||||
|         within: cid, | ||||
|         title: assignmentTitle.value!, | ||||
|     }; | ||||
| 
 | ||||
|     assignmentMutation({ cid: cid, data: assignmentData}, { | ||||
|         onSuccess: async (classResponse) => { | ||||
|             // showSnackbar(t("classCreated"), "success"); | ||||
|             // const createdClass: ClassDTO = classResponse.class; | ||||
|             // code.value = createdClass.id; | ||||
|             await assignmentsQuery.refetch(); | ||||
|         }, | ||||
|         onError: (err) => { | ||||
|             console.log(err); | ||||
|             // showSnackbar(t("creationFailed") + ": " + err.message, "error"); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  | @ -80,74 +107,76 @@ onMounted(async () => { | |||
|         <div v-else> | ||||
|             <using-query-result :query-result="assignmentsQuery" | ||||
|                 v-slot="assignmentsResponse: { data: AssignmentsResponse }"> | ||||
|                 <v-btn v-if="isTeacher" color="primary" class="mb-4 center-btn" @click="goToCreateAssignment"> | ||||
|                     {{ t("new-assignment") }} | ||||
|                 </v-btn> | ||||
|                 <v-container> | ||||
|                 <v-container fluid class="ma-4"> | ||||
|                     <v-row no-gutters class="custom-breakpoint"> | ||||
|                         <v-col cols="12" sm="6" md="6" class="responsive-col"> | ||||
|                             <v-table class="table"> | ||||
|                                 <thead> | ||||
|                                     <tr> | ||||
|                                 <th class="header">{{ t("assignments") }}</th> | ||||
|                                         <th class="header">{{ t("assignment") }}</th> | ||||
|                                         <th class="header"> | ||||
|                                     {{ t("class") }} | ||||
|                                             {{ t("progress") }} | ||||
|                                         </th> | ||||
|                                 <th class="header">{{ t("groups") }}</th> | ||||
|                                         <th class="header">{{ t("deadline") }}</th> | ||||
|                                     </tr> | ||||
|                                 </thead> | ||||
|                                 <tbody> | ||||
|                                     <tr v-for="a in assignmentsResponse.data.assignments as AssignmentDTO[]" | ||||
|                                         :key="a.id + a.within"> | ||||
|                                         <td> | ||||
|                                     <v-btn :to="`/class/${a.within}`" variant="text"> | ||||
|                                             <v-btn :to="`/assignment/${a.within}/${a.id}`" variant="text"> | ||||
|                                                 {{ a.title }} | ||||
|                                                 <v-icon end> mdi-menu-right </v-icon> | ||||
|                                             </v-btn> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                     <span>{{ a.within }}</span> | ||||
|                                     <!-- <span v-if="!isMdAndDown">{{ c.id }}</span> | ||||
|                                     <span v-else style="cursor: pointer" @click="openCodeDialog(c.id)"><v-icon | ||||
|                                             icon="mdi-eye"></v-icon></span> --> | ||||
|                                             <v-progress-linear :model-value="0" color="blue-grey" height="25"> | ||||
|                                                 <template v-slot:default="{ value }"> | ||||
|                                                     <strong>{{ Math.ceil(value) }}%</strong> | ||||
|                                                 </template> | ||||
|                                             </v-progress-linear> | ||||
|                                         </td> | ||||
| 
 | ||||
|                                 <td>{{ a.groups.length }}</td> | ||||
|                                         <td>Nov 9, 2025, 06:00 PM EST+1</td> | ||||
|                                     </tr> | ||||
|                                 </tbody> | ||||
|                             </v-table> | ||||
|                 </v-container> | ||||
|                         </v-col> | ||||
|                         <v-col cols="12" sm="6" md="6" class="responsive-col"> | ||||
|                             <div> | ||||
|                                 <h2>{{ t("createAssignment") }}</h2> | ||||
| 
 | ||||
|                                 <v-sheet class="pa-4 sheet" max-width="600px"> | ||||
|                                     <p>{{ t("createClassInstructions") }}</p> | ||||
|                                     <v-form @submit.prevent> | ||||
|                                         <v-text-field class="mt-4" :label="`${t('title')}`" v-model="assignmentTitle" | ||||
|                                             :placeholder="`${t('EnterAssignmentTitle')}`" | ||||
|                                             variant="outlined"></v-text-field> | ||||
|                                         <using-query-result :query-result="classesQuery" | ||||
|                                             v-slot="{ data }: { data: ClassesResponse }"> | ||||
|                                             <v-card-text class="mt-4"> | ||||
|                                                 <v-combobox class="mt-4" v-model="selectedClass"  | ||||
|                                                     :items="data.classes" | ||||
|                                                     :label="t('choose-class')" | ||||
|                                                     variant="outlined"  | ||||
|                                                     clearable  | ||||
|                                                     hide-details  | ||||
|                                                     density="compact" | ||||
|                                                     append-inner-icon="mdi-magnify"  | ||||
|                                                     item-title="displayName" | ||||
|                                                     item-value="id"  | ||||
|                                                     required  | ||||
|                                                     :disabled="isClassSelected"  | ||||
|                                                     :filter="(item: ClassDTO, query: string) => item.displayName.toLowerCase().includes(query.toLowerCase())" | ||||
|                                                 > | ||||
|                                                 </v-combobox> | ||||
|                                             </v-card-text> | ||||
|                                         </using-query-result> | ||||
|         </div> | ||||
|             <div class="assignments-container"> | ||||
|                 <using-query-result :query-result="assignmentsQuery" | ||||
|                     v-slot="assignmentsResponse: { data: AssignmentsResponse }"> | ||||
|                     <v-container> | ||||
|                         <v-row> | ||||
|                             <v-col v-for="assignment in assignmentsResponse.data.assignments as AssignmentDTO[]" | ||||
|                                 :key="assignment.id + assignment.within" cols="12"> | ||||
|                                 <v-card class="assignment-card"> | ||||
|                                     <div class="top-content"> | ||||
|                                         <div class="assignment-title">{{ assignment.title }}</div> | ||||
|                                         <div class="assignment-class"> | ||||
|                                             {{ t("class") }}: | ||||
|                                             <span class="class-name"> | ||||
|                                                 {{ assignment.within }} | ||||
|                                             </span> | ||||
|                                         </div> | ||||
|                                     </div> | ||||
| 
 | ||||
|                                     <div class="spacer"></div> | ||||
| 
 | ||||
|                                     <div class="button-row"> | ||||
|                                         <v-btn color="primary" variant="text" | ||||
|                                             @click="goToAssignmentDetails(assignment.id, assignment.within)"> | ||||
|                                             {{ t("view-assignment") }} | ||||
|                                         </v-btn> | ||||
|                                         <v-btn v-if="isTeacher" color="red" variant="text" | ||||
|                                             @click="goToDeleteAssignment(assignment.id, assignment.within)"> | ||||
|                                             {{ t("delete") }} | ||||
|                                         <v-btn class="mt-4" color="#f6faf2" type="submit" @click="createAssignment" block> | ||||
|                                             {{ t("create")}} | ||||
|                                         </v-btn> | ||||
|                                     </v-form> | ||||
|                                 </v-sheet> | ||||
|                             </div> | ||||
|                                 </v-card> | ||||
|                         </v-col> | ||||
|                     </v-row> | ||||
|                 </v-container> | ||||
|  | @ -207,4 +236,68 @@ onMounted(async () => { | |||
|     font-weight: 500; | ||||
|     color: #333; | ||||
| } | ||||
| 
 | ||||
| .header { | ||||
|     font-weight: bold !important; | ||||
|     background-color: #0e6942; | ||||
|     color: white; | ||||
|     padding: 10px; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|     color: #0e6942; | ||||
|     text-transform: uppercase; | ||||
|     font-weight: bolder; | ||||
|     padding-top: 2%; | ||||
|     font-size: 50px; | ||||
| } | ||||
| 
 | ||||
| h2 { | ||||
|     color: #0e6942; | ||||
|     font-size: 30px; | ||||
| } | ||||
| 
 | ||||
| .join { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 20px; | ||||
|     margin-top: 50px; | ||||
| } | ||||
| 
 | ||||
| .link { | ||||
|     color: #0b75bb; | ||||
|     text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|     margin-left: 30px; | ||||
| } | ||||
| 
 | ||||
| td, | ||||
| th { | ||||
|     border-bottom: 1px solid #0e6942; | ||||
|     border-top: 1px solid #0e6942; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|     width: 90%; | ||||
|     padding-top: 10px; | ||||
|     border-collapse: collapse; | ||||
| } | ||||
| 
 | ||||
| table thead th:first-child { | ||||
|     border-top-left-radius: 10px; | ||||
| } | ||||
| 
 | ||||
| .table thead th:last-child { | ||||
|     border-top-right-radius: 10px; | ||||
| } | ||||
| 
 | ||||
| .table tbody tr:nth-child(odd) { | ||||
|     background-color: white; | ||||
| } | ||||
| 
 | ||||
| .table tbody tr:nth-child(even) { | ||||
|     background-color: #f6faf2; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue
	
	 Adriaan Jacquet
						Adriaan Jacquet