feat: deadline is editeerbaar
This commit is contained in:
		
							parent
							
								
									912111fce4
								
							
						
					
					
						commit
						f67e3f5a1a
					
				
					 5 changed files with 163 additions and 145 deletions
				
			
		|  | @ -2,15 +2,21 @@ | |||
|     import { ref, watch } from "vue"; | ||||
|     import { deadlineRules } from "@/utils/assignment-rules.ts"; | ||||
| 
 | ||||
|     const emit = defineEmits<(e: "update:deadline", value: Date) => void>(); | ||||
|     const emit = defineEmits<(e: "update:deadline", value: Date | null) => void>(); | ||||
|     const props = defineProps<{ deadline: Date | null }>(); | ||||
| 
 | ||||
|     const datetime = ref(""); | ||||
| 
 | ||||
|     datetime.value = props.deadline ? new Date(props.deadline).toISOString().slice(0, 16) : "" | ||||
| 
 | ||||
| 
 | ||||
|     // Watch the datetime value and emit the update | ||||
|     watch(datetime, (val) => { | ||||
|         const newDate = new Date(val); | ||||
|         if (!isNaN(newDate.getTime())) { | ||||
|             emit("update:deadline", newDate); | ||||
|         } else { | ||||
|             emit("update:deadline", null); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -166,5 +166,11 @@ | |||
|     "pathContainsNonExistingLearningObjects": "At least one of the learning objects referenced in this path does not exist.", | ||||
|     "targetAgesMandatory": "Target ages must be specified.", | ||||
|     "hintRemoveIfUnconditionalTransition": "(remove this if this should be an unconditional transition)", | ||||
|     "hintKeywordsSeparatedBySpaces": "Keywords separated by spaces" | ||||
|     "hintKeywordsSeparatedBySpaces": "Keywords separated by spaces", | ||||
|     "title-required": "Title cannot be empty.", | ||||
|     "class-required": "You must select at least one class.", | ||||
|     "deadline-invalid": "Invalid date or time.", | ||||
|     "deadline-past": "The deadline must be in the future.", | ||||
|     "lp-required": "You must select a learning path.", | ||||
|     "lp-invalied": "The selected learning path doesn't exist." | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,7 @@ | |||
| import {useI18n} from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| /** | ||||
|  * Validation rule for the assignment title. | ||||
|  * | ||||
|  | @ -8,7 +12,7 @@ export const assignmentTitleRules = [ | |||
|         if (value?.length >= 1) { | ||||
|             return true; | ||||
|         } // Title must not be empty
 | ||||
|         return "Title cannot be empty."; | ||||
|         return t("title-required"); | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
|  | @ -23,7 +27,7 @@ export const classRules = [ | |||
|         if (value) { | ||||
|             return true; | ||||
|         } | ||||
|         return "You must select at least one class."; | ||||
|         return t("class-required"); | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
|  | @ -34,30 +38,18 @@ export const classRules = [ | |||
|  */ | ||||
| export const deadlineRules = [ | ||||
|     (value: string): string | boolean => { | ||||
|         if (!value) { | ||||
|             return "You must set a deadline."; | ||||
|         } | ||||
| 
 | ||||
|         const selectedDateTime = new Date(value); | ||||
|         const now = new Date(); | ||||
| 
 | ||||
|         if (isNaN(selectedDateTime.getTime())) { | ||||
|             return "Invalid date or time."; | ||||
|             return t("deadline-invalid"); | ||||
|         } | ||||
| 
 | ||||
|         if (selectedDateTime <= now) { | ||||
|             return "The deadline must be in the future."; | ||||
|             return t("deadline-past"); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| export const descriptionRules = [ | ||||
|     (value: string): string | boolean => { | ||||
|         if (!value || value.trim() === "") { | ||||
|             return "Description cannot be empty."; | ||||
|         } | ||||
|         return true; | ||||
|     }, | ||||
| ]; | ||||
|  |  | |||
|  | @ -1,91 +1,101 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { computed, onMounted, ref, watch } from "vue"; | ||||
|     import { assignmentTitleRules, classRules } from "@/utils/assignment-rules.ts"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||
|     import { useRouter, useRoute } from "vue-router"; | ||||
|     import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
|     import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { computed, onMounted, ref, watch } from "vue"; | ||||
| import { assignmentTitleRules, classRules } from "@/utils/assignment-rules.ts"; | ||||
| import auth from "@/services/auth/auth-service.ts"; | ||||
| import { useTeacherClassesQuery } from "@/queries/teachers.ts"; | ||||
| import { useRouter, useRoute } from "vue-router"; | ||||
| import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | ||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||
| import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
| import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|     const { t, locale } = useI18n(); | ||||
|     const role = ref(auth.authState.activeRole); | ||||
|     const username = ref<string>(""); | ||||
| const route = useRoute(); | ||||
| const router = useRouter(); | ||||
| const { t, locale } = useI18n(); | ||||
| const role = ref(auth.authState.activeRole); | ||||
| const username = ref<string>(""); | ||||
| 
 | ||||
|     onMounted(async () => { | ||||
|         if (role.value === "student") { | ||||
|             await router.push("/user"); | ||||
|         } | ||||
|         const user = await auth.loadUser(); | ||||
|         username.value = user?.profile?.preferred_username ?? ""; | ||||
|     }); | ||||
| onMounted(async () => { | ||||
|     if (role.value === "student") { | ||||
|         await router.push("/user"); | ||||
|     } | ||||
|     const user = await auth.loadUser(); | ||||
|     username.value = user?.profile?.preferred_username ?? ""; | ||||
| }); | ||||
| 
 | ||||
|     const language = computed(() => locale.value); | ||||
|     const form = ref(); | ||||
| const language = computed(() => locale.value); | ||||
| const form = ref(); | ||||
| 
 | ||||
|     const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||
|     const classesQueryResults = useTeacherClassesQuery(username, true); | ||||
| const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||
| const classesQueryResults = useTeacherClassesQuery(username, true); | ||||
| 
 | ||||
|     const selectedClass = ref(undefined); | ||||
|     const assignmentTitle = ref(""); | ||||
|     const selectedLearningPath = ref(route.query.hruid?.toString() || undefined); | ||||
| const selectedClass = ref(undefined); | ||||
| const assignmentTitle = ref(""); | ||||
| 
 | ||||
|     const lpIsSelected = route.query.hruid !== undefined; | ||||
| const selectedLearningPath = ref<LearningPath | undefined>(undefined); | ||||
| const lpIsSelected = ref(false); | ||||
| watch(learningPathsQueryResults.data, (data) => { | ||||
|     const hruidFromRoute = route.query.hruid?.toString(); | ||||
|     if (!hruidFromRoute || !data) return; | ||||
| 
 | ||||
|     const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||
|     // Verify if the hruid given in the url is valid before accepting it | ||||
|     const matchedLP = data.find(lp => lp.hruid === hruidFromRoute); | ||||
|     if (matchedLP) { | ||||
|         selectedLearningPath.value = matchedLP; | ||||
|         lpIsSelected.value = true; | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
|     watch([isSuccess, data], async ([success, newData]) => { | ||||
|         if (success && newData?.assignment) { | ||||
|             await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); | ||||
|         } | ||||
|     }); | ||||
| const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||
| 
 | ||||
|     async function submitFormHandler(): Promise<void> { | ||||
|         const { valid } = await form.value.validate(); | ||||
|         if (!valid) return; | ||||
| watch([isSuccess, data], async ([success, newData]) => { | ||||
|     if (success && newData?.assignment) { | ||||
|         await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
|         const lp = lpIsSelected | ||||
|             ? route.query.hruid | ||||
|             : selectedLearningPath.value?.hruid; | ||||
| async function submitFormHandler(): Promise<void> { | ||||
|     const { valid } = await form.value.validate(); | ||||
|     if (!valid) return; | ||||
| 
 | ||||
|         if (!lp) { | ||||
|             return; | ||||
|         } | ||||
|     const lp = lpIsSelected.value | ||||
|         ? route.query.hruid | ||||
|         : selectedLearningPath.value?.hruid; | ||||
| 
 | ||||
|         console.log('Form values:', { | ||||
|             title: assignmentTitle.value, | ||||
|             class: selectedClass.value, | ||||
|             lp: selectedLearningPath.value | ||||
|         }); | ||||
| 
 | ||||
|         const assignmentDTO: AssignmentDTO = { | ||||
|             id: 0, | ||||
|             within: selectedClass.value?.id || "", | ||||
|             title: assignmentTitle.value.toString(), | ||||
|             description: "", | ||||
|             learningPath: lp.toString(), | ||||
|             language: language.value.toString(), | ||||
|             deadline: null, | ||||
|             groups: [], | ||||
|         }; | ||||
| 
 | ||||
|         mutate({ cid: assignmentDTO.within, data: assignmentDTO }); | ||||
|     if (!lp) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const learningPathRules = [ | ||||
|         (value: any) => { | ||||
|             // Skip validation if LP is selected from query | ||||
|             if (route.query.hruid) return true; | ||||
|             // Original validation logic | ||||
|             return Boolean(value) || 'Learning path is required'; | ||||
|         } | ||||
|     ]; | ||||
|     const assignmentDTO: AssignmentDTO = { | ||||
|         id: 0, | ||||
|         within: selectedClass.value?.id || "", | ||||
|         title: assignmentTitle.value.toString(), | ||||
|         description: "", | ||||
|         learningPath: lp.toString(), | ||||
|         language: language.value.toString(), | ||||
|         deadline: null, | ||||
|         groups: [], | ||||
|     }; | ||||
| 
 | ||||
|     mutate({ cid: assignmentDTO.within, data: assignmentDTO }); | ||||
| } | ||||
| 
 | ||||
| const learningPathRules = [ | ||||
|     (value: any) => { | ||||
| 
 | ||||
|         if(lpIsSelected.value) return; | ||||
| 
 | ||||
|         if (!value) return t("lp-required"); | ||||
| 
 | ||||
|         const allLPs = learningPathsQueryResults.data.value ?? []; | ||||
|         const valid = allLPs.some(lp => lp.hruid === value?.hruid); | ||||
|         return valid || t("lp-invalid"); | ||||
|     } | ||||
| ]; | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  | @ -118,7 +128,6 @@ | |||
|                         v-slot="{ data }: { data: LearningPath[] }" | ||||
|                     > | ||||
|                         <v-combobox | ||||
|                             v-model="selectedLearningPath" | ||||
|                             :items="data" | ||||
|                             :label="t('choose-lp')" | ||||
|                             :rules="lpIsSelected ? [] : learningPathRules" | ||||
|  | @ -185,59 +194,59 @@ | |||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|     .main-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         justify-content: start; | ||||
|         padding-top: 32px; | ||||
|         text-align: center; | ||||
|     } | ||||
| .main-container { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: start; | ||||
|     padding-top: 32px; | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| .form-card { | ||||
|     width: 100%; | ||||
|     max-width: 720px; | ||||
|     border-radius: 16px; | ||||
| } | ||||
| 
 | ||||
| .form-container { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 24px; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| .step-container { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 24px; | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 1000px) { | ||||
|     .form-card { | ||||
|         width: 100%; | ||||
|         max-width: 720px; | ||||
|         border-radius: 16px; | ||||
|         width: 85%; | ||||
|         padding: 1%; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     .form-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: 24px; | ||||
|         width: 100%; | ||||
| @media (max-width: 600px) { | ||||
|     h1 { | ||||
|         font-size: 32px; | ||||
|         text-align: center; | ||||
|         margin-left: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     .step-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: 24px; | ||||
| @media (max-width: 400px) { | ||||
|     h1 { | ||||
|         font-size: 24px; | ||||
|         text-align: center; | ||||
|         margin-left: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     @media (max-width: 1000px) { | ||||
|         .form-card { | ||||
|             width: 85%; | ||||
|             padding: 1%; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 600px) { | ||||
|         h1 { | ||||
|             font-size: 32px; | ||||
|             text-align: center; | ||||
|             margin-left: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 400px) { | ||||
|         h1 { | ||||
|             font-size: 24px; | ||||
|             text-align: center; | ||||
|             margin-left: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .v-card { | ||||
|         border: 2px solid #0e6942; | ||||
|         border-radius: 12px; | ||||
|     } | ||||
| .v-card { | ||||
|     border: 2px solid #0e6942; | ||||
|     border-radius: 12px; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script setup lang="ts"> | ||||
| import {computed, type Ref, ref, watch, watchEffect} from "vue"; | ||||
| import {computed, ref, watch, watchEffect} from "vue"; | ||||
| import {useI18n} from "vue-i18n"; | ||||
| import { | ||||
|     useAssignmentQuery, | ||||
|  | @ -12,11 +12,11 @@ import {useGetLearningPathQuery} from "@/queries/learning-paths.ts"; | |||
| import type {Language} from "@/data-objects/language.ts"; | ||||
| import type {AssignmentResponse} from "@/controllers/assignments.ts"; | ||||
| import type {GroupDTO, GroupDTOId} from "@dwengo-1/common/interfaces/group"; | ||||
| import {descriptionRules} from "@/utils/assignment-rules.ts"; | ||||
| import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue"; | ||||
| import GroupProgressRow from "@/components/GroupProgressRow.vue"; | ||||
| import type {AssignmentDTO} from "@dwengo-1/common/dist/interfaces/assignment.ts"; | ||||
| import GroupSelector from "@/components/assignments/GroupSelector.vue"; | ||||
| import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|     classId: string; | ||||
|  | @ -33,6 +33,7 @@ const form = ref(); | |||
| 
 | ||||
| const editingLearningPath = ref(learningPath); | ||||
| const description = ref(""); | ||||
| const deadline = ref<Date | null>(null); | ||||
| const editGroups = ref(false); | ||||
| 
 | ||||
| const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||
|  | @ -51,6 +52,7 @@ watchEffect(() => { | |||
|     if (assignment) { | ||||
|         learningPath.value = assignment.learningPath; | ||||
|         lang.value = assignment.language as Language; | ||||
|         deadline.value = assignment.deadline ? new Date(assignment.deadline) : null; | ||||
| 
 | ||||
|         if (lpQueryResult.data.value) { | ||||
|             editingLearningPath.value = lpQueryResult.data.value; | ||||
|  | @ -128,7 +130,7 @@ async function saveChanges(): Promise<void> { | |||
| 
 | ||||
|     const assignmentDTO: AssignmentDTO = { | ||||
|         description: description.value, | ||||
|         //deadline: new Date(),TODO: deadline aanpassen | ||||
|         deadline: deadline.value ?? null, | ||||
|     }; | ||||
| 
 | ||||
|     mutate({ | ||||
|  | @ -244,7 +246,6 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { | |||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
| 
 | ||||
|                                 <v-card-title class="text-h4 assignmentTopTitle" | ||||
|                                 >{{ assignmentResponse.data.assignment.title }} | ||||
|                                 </v-card-title> | ||||
|  | @ -258,6 +259,7 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { | |||
|                                             :to="goToLearningPathLink()" | ||||
|                                             variant="tonal" | ||||
|                                             color="primary" | ||||
|                                             :disabled="isEditing" | ||||
|                                         > | ||||
|                                             {{ t("learning-path") }} | ||||
|                                         </v-btn> | ||||
|  | @ -269,7 +271,11 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { | |||
|                                         </v-alert> | ||||
|                                     </using-query-result> | ||||
|                                 </v-card-subtitle> | ||||
| 
 | ||||
|                                 <v-card-text v-if="isEditing"> | ||||
|                                     <deadline-selector | ||||
|                                         v-model:deadline="deadline" | ||||
|                                     /> | ||||
|                                 </v-card-text> | ||||
|                                 <v-card-text | ||||
|                                     v-if="!isEditing" | ||||
|                                     class="description" | ||||
|  | @ -284,7 +290,6 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { | |||
|                                         density="compact" | ||||
|                                         auto-grow | ||||
|                                         rows="3" | ||||
|                                         :rules="descriptionRules" | ||||
|                                     ></v-textarea> | ||||
|                                 </v-card-text> | ||||
|                             </v-card> | ||||
|  |  | |||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana