feat: create assignment process minimaliseren maar in aparte pagina houden
This commit is contained in:
		
							parent
							
								
									20173169b7
								
							
						
					
					
						commit
						5805294f4c
					
				
					 2 changed files with 386 additions and 422 deletions
				
			
		|  | @ -1,19 +1,16 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useI18n } from "vue-i18n"; | import { useI18n } from "vue-i18n"; | ||||||
| import { computed, onMounted, ref, watch } from "vue"; | import { computed, onMounted, ref, watch } from "vue"; | ||||||
|     import GroupSelector from "@/components/assignments/GroupSelector.vue"; | import { assignmentTitleRules, classRules, learningPathRules } from "@/utils/assignment-rules.ts"; | ||||||
|     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 auth from "@/services/auth/auth-service.ts"; | ||||||
| import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||||
|     import { useRouter } from "vue-router"; | import { useRouter, useRoute } from "vue-router"; | ||||||
| import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | ||||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; | import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
| import type { ClassesResponse } from "@/controllers/classes.ts"; | import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||||
| import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
| import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||||
|     import { useRoute } from "vue-router"; |  | ||||||
| 
 | 
 | ||||||
| const route = useRoute(); | const route = useRoute(); | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
|  | @ -22,12 +19,9 @@ | ||||||
| const username = ref<string>(""); | const username = ref<string>(""); | ||||||
| 
 | 
 | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|         // Redirect student |  | ||||||
|     if (role.value === "student") { |     if (role.value === "student") { | ||||||
|         await router.push("/user"); |         await router.push("/user"); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|         // Get the user's username |  | ||||||
|     const user = await auth.loadUser(); |     const user = await auth.loadUser(); | ||||||
|     username.value = user?.profile?.preferred_username ?? ""; |     username.value = user?.profile?.preferred_username ?? ""; | ||||||
| }); | }); | ||||||
|  | @ -35,33 +29,13 @@ | ||||||
| const language = computed(() => locale.value); | const language = computed(() => locale.value); | ||||||
| const form = ref(); | const form = ref(); | ||||||
| 
 | 
 | ||||||
|     //Fetch all learning paths |  | ||||||
| const learningPathsQueryResults = useGetAllLearningPaths(language); | const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||||
| 
 |  | ||||||
|     // Fetch and store all the teacher's classes |  | ||||||
| const classesQueryResults = useTeacherClassesQuery(username, true); | const classesQueryResults = useTeacherClassesQuery(username, true); | ||||||
| 
 | 
 | ||||||
| const selectedClass = ref(undefined); | const selectedClass = ref(undefined); | ||||||
| 
 |  | ||||||
| const assignmentTitle = ref(""); | const assignmentTitle = ref(""); | ||||||
| const selectedLearningPath = ref(route.query.hruid || undefined); | const selectedLearningPath = ref(route.query.hruid || undefined); | ||||||
| 
 |  | ||||||
|     // Disable combobox when learningPath prop is passed |  | ||||||
| const lpIsSelected = route.query.hruid !== undefined; | const lpIsSelected = route.query.hruid !== undefined; | ||||||
|     const deadline = ref(null); |  | ||||||
|     const description = ref(""); |  | ||||||
|     const groups = ref<string[][]>([]); |  | ||||||
| 
 |  | ||||||
|     // New group is added to the list |  | ||||||
|     function addGroupToList(students: string[]): void { |  | ||||||
|         if (students.length) { |  | ||||||
|             groups.value = [...groups.value, students]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     watch(selectedClass, () => { |  | ||||||
|         groups.value = []; |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
| const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||||
| 
 | 
 | ||||||
|  | @ -84,10 +58,11 @@ | ||||||
|         id: 0, |         id: 0, | ||||||
|         within: selectedClass.value?.id || "", |         within: selectedClass.value?.id || "", | ||||||
|         title: assignmentTitle.value, |         title: assignmentTitle.value, | ||||||
|             description: description.value, |         description: "", | ||||||
|         learningPath: lp || "", |         learningPath: lp || "", | ||||||
|  |         deadline: new Date(), | ||||||
|         language: language.value, |         language: language.value, | ||||||
|             groups: groups.value, |         groups: [], | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     mutate({ cid: assignmentDTO.within, data: assignmentDTO }); |     mutate({ cid: assignmentDTO.within, data: assignmentDTO }); | ||||||
|  | @ -96,112 +71,89 @@ | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="main-container"> |     <div class="main-container"> | ||||||
|         <h1 class="title">{{ t("new-assignment") }}</h1> |         <h1 class="h1">{{ t("new-assignment") }}</h1> | ||||||
|         <v-card class="form-card"> | 
 | ||||||
|             <v-form |         <v-card class="form-card elevation-2 pa-6"> | ||||||
|                 ref="form" |             <v-form ref="form" class="form-container" validate-on="submit lazy" @submit.prevent="submitFormHandler"> | ||||||
|                 class="form-container" |                 <v-container class="step-container pa-0"> | ||||||
|                 validate-on="submit lazy" | 
 | ||||||
|                 @submit.prevent="submitFormHandler" |                     <!-- Titel veld --> | ||||||
|             > |  | ||||||
|                 <v-container class="step-container"> |  | ||||||
|                     <v-card-text> |  | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                         v-model="assignmentTitle" |                         v-model="assignmentTitle" | ||||||
|                         :label="t('title')" |                         :label="t('title')" | ||||||
|                         :rules="assignmentTitleRules" |                         :rules="assignmentTitleRules" | ||||||
|                             density="compact" |                         density="comfortable" | ||||||
|                             variant="outlined" |                         variant="solo-filled" | ||||||
|  |                         prepend-inner-icon="mdi-format-title" | ||||||
|                         clearable |                         clearable | ||||||
|                         required |                         required | ||||||
|                         ></v-text-field> |                     /> | ||||||
|                     </v-card-text> |  | ||||||
| 
 | 
 | ||||||
|                     <using-query-result |                     <!-- Learning Path keuze --> | ||||||
|                         :query-result="learningPathsQueryResults" |                     <using-query-result :query-result="learningPathsQueryResults" v-slot="{ data }: { data: LearningPath[] }"> | ||||||
|                         v-slot="{ data }: { data: LearningPath[] }" |  | ||||||
|                     > |  | ||||||
|                         <v-card-text> |  | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedLearningPath" |                             v-model="selectedLearningPath" | ||||||
|                             :items="data" |                             :items="data" | ||||||
|                             :label="t('choose-lp')" |                             :label="t('choose-lp')" | ||||||
|                             :rules="learningPathRules" |                             :rules="learningPathRules" | ||||||
|                                 variant="outlined" |                             variant="solo-filled" | ||||||
|                             clearable |                             clearable | ||||||
|                                 hide-details |                             density="comfortable" | ||||||
|                                 density="compact" |                             chips | ||||||
|                                 append-inner-icon="mdi-magnify" |                             hide-no-data | ||||||
|  |                             hide-selected | ||||||
|                             item-title="title" |                             item-title="title" | ||||||
|                             item-value="hruid" |                             item-value="hruid" | ||||||
|                                 required |  | ||||||
|                             :disabled="lpIsSelected" |                             :disabled="lpIsSelected" | ||||||
|                                 :filter=" |                             :filter="(item, query: string) => item.title.toLowerCase().includes(query.toLowerCase())" | ||||||
|                                     (item, query: string) => item.title.toLowerCase().includes(query.toLowerCase()) |                             prepend-inner-icon="mdi-school" | ||||||
|                                 " |                         /> | ||||||
|                             ></v-combobox> |  | ||||||
|                         </v-card-text> |  | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
| 
 | 
 | ||||||
|                     <using-query-result |                     <!-- Klas keuze --> | ||||||
|                         :query-result="classesQueryResults" |                     <using-query-result :query-result="classesQueryResults" v-slot="{ data }: { data: ClassesResponse }"> | ||||||
|                         v-slot="{ data }: { data: ClassesResponse }" |  | ||||||
|                     > |  | ||||||
|                         <v-card-text> |  | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedClass" |                             v-model="selectedClass" | ||||||
|                             :items="data?.classes ?? []" |                             :items="data?.classes ?? []" | ||||||
|                             :label="t('pick-class')" |                             :label="t('pick-class')" | ||||||
|                             :rules="classRules" |                             :rules="classRules" | ||||||
|                                 variant="outlined" |                             variant="solo-filled" | ||||||
|                             clearable |                             clearable | ||||||
|                                 hide-details |                             density="comfortable" | ||||||
|                                 density="compact" |                             chips | ||||||
|                                 append-inner-icon="mdi-magnify" |                             hide-no-data | ||||||
|  |                             hide-selected | ||||||
|                             item-title="displayName" |                             item-title="displayName" | ||||||
|                             item-value="id" |                             item-value="id" | ||||||
|                                 required |                             prepend-inner-icon="mdi-account-multiple" | ||||||
|                             ></v-combobox> |                         /> | ||||||
|                         </v-card-text> |  | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
| 
 | 
 | ||||||
|                     <GroupSelector |                     <!-- Submit & Cancel --> | ||||||
|                         :classId="selectedClass?.id" |                     <v-divider class="my-6" /> | ||||||
|                         :groups="groups" |  | ||||||
|                         @groupCreated="addGroupToList" |  | ||||||
|                     /> |  | ||||||
| 
 | 
 | ||||||
|                     <!-- Counter for created groups --> |                     <div class="d-flex justify-end ga-2"> | ||||||
|                     <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-card-text> |  | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             class="mt-2" |                             color="primary" | ||||||
|                             color="secondary" |  | ||||||
|                             type="submit" |                             type="submit" | ||||||
|                             block |                             size="small" | ||||||
|                             >{{ t("submit") }} |                             prepend-icon="mdi-check-circle" | ||||||
|  |                             elevation="1" | ||||||
|  |                         > | ||||||
|  |                             {{ t("submit") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
|  | 
 | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             to="/user/assignment" |                             to="/user/assignment" | ||||||
|                             color="grey" |                             color="grey" | ||||||
|                             block |                             size="small" | ||||||
|                             >{{ t("cancel") }} |                             variant="text" | ||||||
|  |                             prepend-icon="mdi-close-circle" | ||||||
|  |                         > | ||||||
|  |                             {{ t("cancel") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
|                     </v-card-text> |                     </div> | ||||||
|  | 
 | ||||||
|                 </v-container> |                 </v-container> | ||||||
|             </v-form> |             </v-form> | ||||||
|         </v-card> |         </v-card> | ||||||
|  | @ -213,46 +165,61 @@ | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|         justify-content: center; |     justify-content: start; | ||||||
|  |     padding-top: 32px; | ||||||
|     text-align: center; |     text-align: center; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .form-card { | .form-card { | ||||||
|         display: flex; |     width: 100%; | ||||||
|         flex-direction: column; |     max-width: 720px; | ||||||
|         align-items: center; |     border-radius: 16px; | ||||||
|         justify-content: center; |  | ||||||
|         width: 55%; |  | ||||||
|         /*padding: 1%;*/ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .form-container { | .form-container { | ||||||
|         width: 100%; |  | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|  |     gap: 24px; | ||||||
|  |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .step-container { | .step-container { | ||||||
|     display: flex; |     display: flex; | ||||||
|         justify-content: center; |  | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|         min-height: 200px; |     gap: 24px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (max-width: 1000px) { | @media (max-width: 1000px) { | ||||||
|     .form-card { |     .form-card { | ||||||
|             width: 70%; |         width: 85%; | ||||||
|         padding: 1%; |         padding: 1%; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         .step-container { | } | ||||||
|             min-height: 300px; | 
 | ||||||
|  | @media (max-width: 600px) { | ||||||
|  | 
 | ||||||
|  |     h1 { | ||||||
|  |         font-size: 32px; | ||||||
|  |         text-align: center; | ||||||
|  |         margin-left: 0; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|     @media (max-width: 650px) { | @media (max-width: 400px) { | ||||||
|         .form-card { | 
 | ||||||
|             width: 95%; |     h1 { | ||||||
|  |         font-size: 24px; | ||||||
|  |         text-align: center; | ||||||
|  |         margin-left: 0; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .v-card { | ||||||
|  |     border: 2px solid #0e6942; | ||||||
|  |     border-radius: 12px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| </style> | </style> | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -6,15 +6,11 @@ import authState from "@/services/auth/auth-service.ts"; | ||||||
| import auth from "@/services/auth/auth-service.ts"; | import auth from "@/services/auth/auth-service.ts"; | ||||||
| import {useTeacherAssignmentsQuery, useTeacherClassesQuery} from "@/queries/teachers.ts"; | import {useTeacherAssignmentsQuery, useTeacherClassesQuery} from "@/queries/teachers.ts"; | ||||||
| import {useStudentAssignmentsQuery, useStudentClassesQuery} from "@/queries/students.ts"; | import {useStudentAssignmentsQuery, useStudentClassesQuery} from "@/queries/students.ts"; | ||||||
| import { ClassController, type ClassesResponse } from "@/controllers/classes.ts"; | import {useDeleteAssignmentMutation} from "@/queries/assignments.ts"; | ||||||
| import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |  | ||||||
| import { asyncComputed } from "@vueuse/core"; |  | ||||||
| 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"; | import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  | import {asyncComputed} from "@vueuse/core"; | ||||||
| 
 | 
 | ||||||
| const { t } = useI18n(); | const {t, locale} = useI18n(); | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const role = ref(auth.authState.activeRole); | const role = ref(auth.authState.activeRole); | ||||||
|  | @ -38,14 +34,47 @@ onMounted(async () => { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const isTeacher = computed(() => role.value === "teacher"); | const isTeacher = computed(() => role.value === "teacher"); | ||||||
| const assignmentsQuery = isTeacher ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); | const classesQueryResult = isTeacher.value ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); | ||||||
| const { mutate: assignmentMutation, isSuccess: assignmentIsSuccess } = useCreateAssignmentMutation(); |  | ||||||
| 
 | 
 | ||||||
| const classesQuery = isTeacher ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); | const assignmentsQueryResult = isTeacher.value ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); | ||||||
| const selectedClass = ref<ClassDTO | undefined>(undefined); |  | ||||||
| const isClassSelected = ref(false); |  | ||||||
| 
 | 
 | ||||||
| const assignmentTitle = ref<string>(""); | const allAssignments = asyncComputed( | ||||||
|  |     async () => { | ||||||
|  |         const assignments = assignmentsQueryResult.data.value?.assignments; | ||||||
|  |         if (!assignments) return []; | ||||||
|  | 
 | ||||||
|  |         const classes = classesQueryResult.data.value?.classes; | ||||||
|  |         if (!classes) return []; | ||||||
|  | 
 | ||||||
|  |         const result = assignments.map((a) => ({ | ||||||
|  |             id: a.id, | ||||||
|  |             class: classes.find((cls) => cls?.id === a.within) ?? undefined, | ||||||
|  |             title: a.title, | ||||||
|  |             description: a.description, | ||||||
|  |             learningPath: a.learningPath, | ||||||
|  |             language: a.language, | ||||||
|  |             deadline: a.deadline, | ||||||
|  |             groups: a.groups, | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         // Order the assignments by deadline | ||||||
|  |         return result.flat().sort((a, b) => { | ||||||
|  |             const now = Date.now(); | ||||||
|  |             const aTime = new Date(a.deadline).getTime(); | ||||||
|  |             const bTime = new Date(b.deadline).getTime(); | ||||||
|  | 
 | ||||||
|  |             const aIsPast = aTime < now; | ||||||
|  |             const bIsPast = bTime < now; | ||||||
|  | 
 | ||||||
|  |             if (aIsPast && !bIsPast) return 1; | ||||||
|  |             if (!aIsPast && bIsPast) return -1; | ||||||
|  | 
 | ||||||
|  |             return aTime - bTime; | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     [], | ||||||
|  |     {evaluating: true}, | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| async function goToCreateAssignment(): Promise<void> { | async function goToCreateAssignment(): Promise<void> { | ||||||
|     await router.push("/assignment/create"); |     await router.push("/assignment/create"); | ||||||
|  | @ -67,141 +96,154 @@ async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { | ||||||
|     mutate({cid: clsId, an: num}); |     mutate({cid: clsId, an: num}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function formatDate(date?: string | Date): string { | ||||||
|  |     if (!date) return "–"; | ||||||
|  |     const d = new Date(date); | ||||||
|  | 
 | ||||||
|  |     // Choose locale based on selected language | ||||||
|  |     const currentLocale = locale.value; | ||||||
|  | 
 | ||||||
|  |     return d.toLocaleDateString(currentLocale, { | ||||||
|  |         weekday: "short", | ||||||
|  |         day: "2-digit", | ||||||
|  |         month: "long", | ||||||
|  |         year: "numeric", | ||||||
|  |         hour: "numeric", | ||||||
|  |         minute: "2-digit", | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getDeadlineClass(deadline?: string | Date): string { | ||||||
|  |     if (!deadline) return ""; | ||||||
|  | 
 | ||||||
|  |     const date = new Date(deadline); | ||||||
|  |     const now = new Date(); | ||||||
|  |     const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000); | ||||||
|  | 
 | ||||||
|  |     if (date.getTime() < now.getTime()) return "deadline-passed"; | ||||||
|  |     if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours"; | ||||||
|  |     return "deadline-upcoming"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|     const user = await auth.loadUser(); |     const user = await auth.loadUser(); | ||||||
|     username.value = user?.profile?.preferred_username ?? ""; |     username.value = user?.profile?.preferred_username ?? ""; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| async function createAssignment(): Promise<void> { | onMounted(async () => { | ||||||
|     const cid = selectedClass.value!.id; |     const user = await auth.loadUser(); | ||||||
|     const assignmentData: Partial<AssignmentDTO> = { |     username.value = user?.profile?.preferred_username ?? ""; | ||||||
|         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> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |     <div class="assignments-container"> | ||||||
|         <h1>{{ t("assignments") }}</h1> |         <h1 class="h1">{{ t("assignments") }}</h1> | ||||||
|         <div class="loading-div" v-if="isLoading"> |  | ||||||
|             <v-progress-circular indeterminate></v-progress-circular> |  | ||||||
|         </div> |  | ||||||
|         <div v-if="isError"> |  | ||||||
|             <v-empty-state icon="mdi-alert-circle-outline" :text="errorMessage" |  | ||||||
|                 :title="t('error_title')"></v-empty-state> |  | ||||||
|         </div> |  | ||||||
|         <div v-else> |  | ||||||
|             <using-query-result :query-result="assignmentsQuery" |  | ||||||
|                 v-slot="assignmentsResponse: { data: AssignmentsResponse }"> |  | ||||||
|                 <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("assignment") }}</th> |  | ||||||
|                                         <th class="header"> |  | ||||||
|                                             {{ t("progress") }} |  | ||||||
|                                         </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="`/assignment/${a.within}/${a.id}`" variant="text"> |  | ||||||
|                                                 {{ a.title }} |  | ||||||
|                                                 <v-icon end> mdi-menu-right </v-icon> |  | ||||||
|                                             </v-btn> |  | ||||||
|                                         </td> |  | ||||||
|                                         <td> |  | ||||||
|                                             <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>Nov 9, 2025, 06:00 PM EST+1</td> |  | ||||||
|                                     </tr> |  | ||||||
|                                 </tbody> |  | ||||||
|                             </v-table> |  | ||||||
|                         </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"> |         <v-btn | ||||||
|                                     <p>{{ t("createClassInstructions") }}</p> |             v-if="isTeacher" | ||||||
|                                     <v-form @submit.prevent> |             color="primary" | ||||||
|                                         <v-text-field class="mt-4" :label="`${t('title')}`" v-model="assignmentTitle" |             class="mb-4 center-btn" | ||||||
|                                             :placeholder="`${t('EnterAssignmentTitle')}`" |             @click="goToCreateAssignment" | ||||||
|                                             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> |             {{ t("new-assignment") }} | ||||||
|                                             </v-card-text> |  | ||||||
|                                         </using-query-result> |  | ||||||
|                                         <v-btn class="mt-4" color="#f6faf2" type="submit" @click="createAssignment" block> |  | ||||||
|                                             {{ t("create")}} |  | ||||||
|         </v-btn> |         </v-btn> | ||||||
|                                     </v-form> | 
 | ||||||
|                                 </v-sheet> |         <using-query-result | ||||||
|  |             :query-result="assignmentsQueryResult" | ||||||
|  |         > | ||||||
|  |             <v-container> | ||||||
|  |                 <v-row> | ||||||
|  |                     <v-col | ||||||
|  |                         v-for="assignment in allAssignments" | ||||||
|  |                         :key="assignment.id" | ||||||
|  |                         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?.class?.displayName }} | ||||||
|  |                                 </span> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div | ||||||
|  |                                     class="assignment-deadline" | ||||||
|  |                                     :class="getDeadlineClass(assignment.deadline)" | ||||||
|  |                                 > | ||||||
|  |                                     {{ t("deadline") }}: | ||||||
|  |                                     <span>{{ formatDate(assignment.deadline) }}</span> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <div class="spacer"></div> | ||||||
|  | 
 | ||||||
|  |                             <div class="button-row"> | ||||||
|  |                                 <v-btn | ||||||
|  |                                     color="primary" | ||||||
|  |                                     variant="text" | ||||||
|  |                                     @click="goToAssignmentDetails(assignment.id, assignment?.class?.id)" | ||||||
|  |                                 > | ||||||
|  |                                     {{ t("view-assignment") }} | ||||||
|  |                                 </v-btn> | ||||||
|  |                                 <v-btn | ||||||
|  |                                     v-if="isTeacher" | ||||||
|  |                                     color="red" | ||||||
|  |                                     variant="text" | ||||||
|  |                                     @click="goToDeleteAssignment(assignment.id, assignment?.class?.id)" | ||||||
|  |                                 > | ||||||
|  |                                     {{ t("delete") }} | ||||||
|  |                                 </v-btn> | ||||||
|  |                             </div> | ||||||
|  |                         </v-card> | ||||||
|  |                     </v-col> | ||||||
|  |                 </v-row> | ||||||
|  |                 <v-row v-if="allAssignments.length === 0"> | ||||||
|  |                     <v-col cols="12"> | ||||||
|  |                         <div class="no-assignments"> | ||||||
|  |                             {{ t("no-assignments") }} | ||||||
|                         </div> |                         </div> | ||||||
|                     </v-col> |                     </v-col> | ||||||
|                 </v-row> |                 </v-row> | ||||||
|             </v-container> |             </v-container> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </div> |     </div> | ||||||
|     </main> |  | ||||||
| 
 |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .assignments-container { | .assignments-container { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     margin: 0 auto; |     margin: 0 auto; | ||||||
|     padding: 2% 4%; |  | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .center-btn { | .center-btn { | ||||||
|     display: block; |     display: block; | ||||||
|     margin-left: auto; |     margin: 0 auto 2rem auto; | ||||||
|     margin-right: auto; |     font-weight: 600; | ||||||
|  |     background-color: #10ad61; | ||||||
|  |     color: white; | ||||||
|  |     transition: background-color 0.2s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .center-btn:hover { | ||||||
|  |     background-color: #0e6942; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .assignment-card { | .assignment-card { | ||||||
|     padding: 1rem; |     padding: 1.25rem; | ||||||
|  |     border-radius: 16px; | ||||||
|  |     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | ||||||
|  |     background-color: white; | ||||||
|  |     transition: transform 0.2s, | ||||||
|  |     box-shadow 0.2s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assignment-card:hover { | ||||||
|  |     box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .top-content { | .top-content { | ||||||
|  | @ -209,6 +251,35 @@ async function createAssignment(): Promise<void> { | ||||||
|     word-break: break-word; |     word-break: break-word; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .assignment-title { | ||||||
|  |     font-weight: 700; | ||||||
|  |     font-size: 1.4rem; | ||||||
|  |     color: #0e6942; | ||||||
|  |     margin-bottom: 0.3rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assignment-class, | ||||||
|  | .assignment-deadline { | ||||||
|  |     font-size: 0.95rem; | ||||||
|  |     color: #444; | ||||||
|  |     margin-bottom: 0.2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .class-name { | ||||||
|  |     font-weight: 600; | ||||||
|  |     color: #097180; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assignment-deadline.deadline-passed { | ||||||
|  |     color: #d32f2f; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .assignment-deadline.deadline-in24hours { | ||||||
|  |     color: #f57c00; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .spacer { | .spacer { | ||||||
|     flex: 1; |     flex: 1; | ||||||
| } | } | ||||||
|  | @ -216,88 +287,14 @@ async function createAssignment(): Promise<void> { | ||||||
| .button-row { | .button-row { | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: flex-end; |     justify-content: flex-end; | ||||||
|     gap: 0.5rem; |     gap: 0.75rem; | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .assignment-title { | .no-assignments { | ||||||
|     font-weight: bold; |     text-align: center; | ||||||
|     font-size: 1.5rem; |     font-size: 1.2rem; | ||||||
|     margin-bottom: 0.1rem; |     color: #777; | ||||||
|     word-break: break-word; |     padding: 3rem 0; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .assignment-class { |  | ||||||
|     color: #666; |  | ||||||
|     font-size: 0.95rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .class-name { |  | ||||||
|     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> | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana