Merge pull request #255 from SELab-2/feat/deadline
feat: Assignment deadline
This commit is contained in:
		
						commit
						8bad3b3dff
					
				
					 12 changed files with 213 additions and 5928 deletions
				
			
		|  | @ -1,49 +1,30 @@ | |||
| <script setup lang="ts"> | ||||
|     import { ref, computed } from "vue"; | ||||
|     import { ref, watch } from "vue"; | ||||
|     import { deadlineRules } from "@/utils/assignment-rules.ts"; | ||||
| 
 | ||||
|     const date = ref(""); | ||||
|     const time = ref("23:59"); | ||||
|     const emit = defineEmits(["update:deadline"]); | ||||
|     const emit = defineEmits<(e: "update:deadline", value: Date) => void>(); | ||||
| 
 | ||||
|     const formattedDeadline = computed(() => { | ||||
|         if (!date.value || !time.value) return ""; | ||||
|         return `${date.value} ${time.value}`; | ||||
|     }); | ||||
|     const datetime = ref(""); | ||||
| 
 | ||||
|     function updateDeadline(): void { | ||||
|         if (date.value && time.value) { | ||||
|             emit("update:deadline", formattedDeadline.value); | ||||
|     // Watch the datetime value and emit the update | ||||
|     watch(datetime, (val) => { | ||||
|         const newDate = new Date(val); | ||||
|         if (!isNaN(newDate.getTime())) { | ||||
|             emit("update:deadline", newDate); | ||||
|         } | ||||
|     } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div> | ||||
|         <v-card-text> | ||||
|             <v-text-field | ||||
|                 v-model="date" | ||||
|                 label="Select Deadline Date" | ||||
|                 type="date" | ||||
|                 variant="outlined" | ||||
|                 density="compact" | ||||
|                 :rules="deadlineRules" | ||||
|                 required | ||||
|                 @update:modelValue="updateDeadline" | ||||
|             ></v-text-field> | ||||
|         </v-card-text> | ||||
| 
 | ||||
|         <v-card-text> | ||||
|             <v-text-field | ||||
|                 v-model="time" | ||||
|                 label="Select Deadline Time" | ||||
|                 type="time" | ||||
|                 variant="outlined" | ||||
|                 density="compact" | ||||
|                 @update:modelValue="updateDeadline" | ||||
|             ></v-text-field> | ||||
|         </v-card-text> | ||||
|     </div> | ||||
|     <v-card-text> | ||||
|         <v-text-field | ||||
|             v-model="datetime" | ||||
|             type="datetime-local" | ||||
|             label="Select Deadline" | ||||
|             variant="outlined" | ||||
|             density="compact" | ||||
|             :rules="deadlineRules" | ||||
|             required | ||||
|         /> | ||||
|     </v-card-text> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  |  | |||
|  | @ -133,5 +133,7 @@ | |||
|     "see-submission": "Einsendung anzeigen", | ||||
|     "view-submissions": "Einsendungen anzeigen", | ||||
|     "valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein", | ||||
|     "creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut" | ||||
|     "creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut", | ||||
|     "no-assignments": "Derzeit gibt es keine Zuweisungen.", | ||||
|     "deadline": "deadline" | ||||
| } | ||||
|  |  | |||
|  | @ -133,5 +133,7 @@ | |||
|     "see-submission": "view submission", | ||||
|     "view-submissions": "view submissions", | ||||
|     "valid-username": "please enter a valid username", | ||||
|     "creationFailed": "creation failed, please try again" | ||||
|     "creationFailed": "creation failed, please try again", | ||||
|     "no-assignments": "There are currently no assignments.", | ||||
|     "deadline": "deadline" | ||||
| } | ||||
|  |  | |||
|  | @ -134,5 +134,7 @@ | |||
|     "see-submission": "voir la soumission", | ||||
|     "view-submissions": "voir les soumissions", | ||||
|     "valid-username": "veuillez entrer un nom d'utilisateur valide", | ||||
|     "creationFailed": "échec de la création, veuillez réessayer" | ||||
|     "creationFailed": "échec de la création, veuillez réessayer", | ||||
|     "no-assignments": "Il n'y a actuellement aucun travail.", | ||||
|     "deadline": "délai" | ||||
| } | ||||
|  |  | |||
|  | @ -133,5 +133,7 @@ | |||
|     "see-submission": "inzending bekijken", | ||||
|     "view-submissions": "inzendingen bekijken", | ||||
|     "valid-username": "voer een geldige gebruikersnaam in", | ||||
|     "creationFailed": "aanmaak mislukt, probeer het opnieuw" | ||||
|     "creationFailed": "aanmaak mislukt, probeer het opnieuw", | ||||
|     "no-assignments": "Er zijn momenteel geen opdrachten.", | ||||
|     "deadline": "deadline" | ||||
| } | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
| 
 | ||||
|     // Disable combobox when learningPath prop is passed | ||||
|     const lpIsSelected = route.query.hruid !== undefined; | ||||
|     const deadline = ref(null); | ||||
|     const deadline = ref(new Date()); | ||||
|     const description = ref(""); | ||||
|     const groups = ref<string[][]>([]); | ||||
| 
 | ||||
|  | @ -86,6 +86,7 @@ | |||
|             title: assignmentTitle.value, | ||||
|             description: description.value, | ||||
|             learningPath: lp || "", | ||||
|             deadline: deadline.value, | ||||
|             language: language.value, | ||||
|             groups: groups.value, | ||||
|         }; | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
|     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
|     import "../../assets/common.css"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const { t, locale } = useI18n(); | ||||
|     const router = useRouter(); | ||||
| 
 | ||||
|     const role = ref(auth.authState.activeRole); | ||||
|  | @ -28,30 +28,47 @@ | |||
|         classesQueryResults = useStudentClassesQuery(username, true); | ||||
|     } | ||||
| 
 | ||||
|     //TODO: remove later | ||||
|     const classController = new ClassController(); | ||||
| 
 | ||||
|     //TODO: replace by query that fetches all user's assignment | ||||
|     const assignments = asyncComputed(async () => { | ||||
|         const classes = classesQueryResults?.data?.value?.classes; | ||||
|         if (!classes) return []; | ||||
|         const result = await Promise.all( | ||||
|             (classes as ClassDTO[]).map(async (cls) => { | ||||
|                 const { assignments } = await classController.getAssignments(cls.id); | ||||
|                 return assignments.map((a) => ({ | ||||
|                     id: a.id, | ||||
|                     class: cls, | ||||
|                     title: a.title, | ||||
|                     description: a.description, | ||||
|                     learningPath: a.learningPath, | ||||
|                     language: a.language, | ||||
|                     groups: a.groups, | ||||
|                 })); | ||||
|             }), | ||||
|         ); | ||||
|     const assignments = asyncComputed( | ||||
|         async () => { | ||||
|             const classes = classesQueryResults?.data?.value?.classes; | ||||
|             if (!classes) return []; | ||||
| 
 | ||||
|         return result.flat(); | ||||
|     }, []); | ||||
|             const result = await Promise.all( | ||||
|                 (classes as ClassDTO[]).map(async (cls) => { | ||||
|                     const { assignments } = await classController.getAssignments(cls.id); | ||||
|                     return assignments.map((a) => ({ | ||||
|                         id: a.id, | ||||
|                         class: cls, | ||||
|                         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> { | ||||
|         await router.push("/assignment/create"); | ||||
|  | @ -73,6 +90,35 @@ | |||
|         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 () => { | ||||
|         const user = await auth.loadUser(); | ||||
|         username.value = user?.profile?.preferred_username ?? ""; | ||||
|  | @ -108,6 +154,13 @@ | |||
|                                     {{ 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> | ||||
|  | @ -132,6 +185,13 @@ | |||
|                     </v-card> | ||||
|                 </v-col> | ||||
|             </v-row> | ||||
|             <v-row v-if="assignments.length === 0"> | ||||
|                 <v-col cols="12"> | ||||
|                     <div class="no-assignments"> | ||||
|                         {{ t("no-assignments") }} | ||||
|                     </div> | ||||
|                 </v-col> | ||||
|             </v-row> | ||||
|         </v-container> | ||||
|     </div> | ||||
| </template> | ||||
|  | @ -145,12 +205,27 @@ | |||
| 
 | ||||
|     .center-btn { | ||||
|         display: block; | ||||
|         margin-left: auto; | ||||
|         margin-right: auto; | ||||
|         margin: 0 auto 2rem auto; | ||||
|         font-weight: 600; | ||||
|         background-color: #10ad61; | ||||
|         color: white; | ||||
|         transition: background-color 0.2s; | ||||
|     } | ||||
|     .center-btn:hover { | ||||
|         background-color: #0e6942; | ||||
|     } | ||||
| 
 | ||||
|     .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 { | ||||
|  | @ -158,6 +233,35 @@ | |||
|         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 { | ||||
|         flex: 1; | ||||
|     } | ||||
|  | @ -165,24 +269,14 @@ | |||
|     .button-row { | ||||
|         display: flex; | ||||
|         justify-content: flex-end; | ||||
|         gap: 0.5rem; | ||||
|         gap: 0.75rem; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
| 
 | ||||
|     .assignment-title { | ||||
|         font-weight: bold; | ||||
|         font-size: 1.5rem; | ||||
|         margin-bottom: 0.1rem; | ||||
|         word-break: break-word; | ||||
|     } | ||||
| 
 | ||||
|     .assignment-class { | ||||
|         color: #666; | ||||
|         font-size: 0.95rem; | ||||
|     } | ||||
| 
 | ||||
|     .class-name { | ||||
|         font-weight: 500; | ||||
|         color: #333; | ||||
|     .no-assignments { | ||||
|         text-align: center; | ||||
|         font-size: 1.2rem; | ||||
|         color: #777; | ||||
|         padding: 3rem 0; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana