feat: deadline is editeerbaar

This commit is contained in:
Joyelle Ndagijimana 2025-05-17 18:50:18 +02:00
parent 912111fce4
commit f67e3f5a1a
5 changed files with 163 additions and 145 deletions

View file

@ -2,15 +2,21 @@
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { deadlineRules } from "@/utils/assignment-rules.ts"; 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(""); const datetime = ref("");
datetime.value = props.deadline ? new Date(props.deadline).toISOString().slice(0, 16) : ""
// Watch the datetime value and emit the update // Watch the datetime value and emit the update
watch(datetime, (val) => { watch(datetime, (val) => {
const newDate = new Date(val); const newDate = new Date(val);
if (!isNaN(newDate.getTime())) { if (!isNaN(newDate.getTime())) {
emit("update:deadline", newDate); emit("update:deadline", newDate);
} else {
emit("update:deadline", null);
} }
}); });
</script> </script>

View file

@ -166,5 +166,11 @@
"pathContainsNonExistingLearningObjects": "At least one of the learning objects referenced in this path does not exist.", "pathContainsNonExistingLearningObjects": "At least one of the learning objects referenced in this path does not exist.",
"targetAgesMandatory": "Target ages must be specified.", "targetAgesMandatory": "Target ages must be specified.",
"hintRemoveIfUnconditionalTransition": "(remove this if this should be an unconditional transition)", "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."
} }

View file

@ -1,3 +1,7 @@
import {useI18n} from "vue-i18n";
const { t } = useI18n();
/** /**
* Validation rule for the assignment title. * Validation rule for the assignment title.
* *
@ -8,7 +12,7 @@ export const assignmentTitleRules = [
if (value?.length >= 1) { if (value?.length >= 1) {
return true; return true;
} // Title must not be empty } // Title must not be empty
return "Title cannot be empty."; return t("title-required");
}, },
]; ];
@ -23,7 +27,7 @@ export const classRules = [
if (value) { if (value) {
return true; return true;
} }
return "You must select at least one class."; return t("class-required");
}, },
]; ];
@ -34,30 +38,18 @@ export const classRules = [
*/ */
export const deadlineRules = [ export const deadlineRules = [
(value: string): string | boolean => { (value: string): string | boolean => {
if (!value) {
return "You must set a deadline.";
}
const selectedDateTime = new Date(value); const selectedDateTime = new Date(value);
const now = new Date(); const now = new Date();
if (isNaN(selectedDateTime.getTime())) { if (isNaN(selectedDateTime.getTime())) {
return "Invalid date or time."; return t("deadline-invalid");
} }
if (selectedDateTime <= now) { if (selectedDateTime <= now) {
return "The deadline must be in the future."; return t("deadline-past");
} }
return true; return true;
}, },
]; ];
export const descriptionRules = [
(value: string): string | boolean => {
if (!value || value.trim() === "") {
return "Description cannot be empty.";
}
return true;
},
];

View file

@ -1,91 +1,101 @@
<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 { assignmentTitleRules, classRules } from "@/utils/assignment-rules.ts"; import { assignmentTitleRules, classRules } from "@/utils/assignment-rules.ts";
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, useRoute } 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";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const role = ref(auth.authState.activeRole); const role = ref(auth.authState.activeRole);
const username = ref<string>(""); const username = ref<string>("");
onMounted(async () => { onMounted(async () => {
if (role.value === "student") { if (role.value === "student") {
await router.push("/user"); await router.push("/user");
} }
const user = await auth.loadUser(); const user = await auth.loadUser();
username.value = user?.profile?.preferred_username ?? ""; username.value = user?.profile?.preferred_username ?? "";
}); });
const language = computed(() => locale.value); const language = computed(() => locale.value);
const form = ref(); const form = ref();
const learningPathsQueryResults = useGetAllLearningPaths(language); const learningPathsQueryResults = useGetAllLearningPaths(language);
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?.toString() || undefined);
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]) => { const { mutate, data, isSuccess } = useCreateAssignmentMutation();
if (success && newData?.assignment) {
await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`);
}
});
async function submitFormHandler(): Promise<void> { watch([isSuccess, data], async ([success, newData]) => {
const { valid } = await form.value.validate(); if (success && newData?.assignment) {
if (!valid) return; await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`);
}
});
const lp = lpIsSelected async function submitFormHandler(): Promise<void> {
? route.query.hruid const { valid } = await form.value.validate();
: selectedLearningPath.value?.hruid; if (!valid) return;
if (!lp) { const lp = lpIsSelected.value
return; ? route.query.hruid
} : selectedLearningPath.value?.hruid;
console.log('Form values:', { if (!lp) {
title: assignmentTitle.value, return;
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 });
} }
const learningPathRules = [ const assignmentDTO: AssignmentDTO = {
(value: any) => { id: 0,
// Skip validation if LP is selected from query within: selectedClass.value?.id || "",
if (route.query.hruid) return true; title: assignmentTitle.value.toString(),
// Original validation logic description: "",
return Boolean(value) || 'Learning path is required'; 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> </script>
<template> <template>
@ -118,7 +128,6 @@
v-slot="{ data }: { data: LearningPath[] }" v-slot="{ data }: { data: LearningPath[] }"
> >
<v-combobox <v-combobox
v-model="selectedLearningPath"
:items="data" :items="data"
:label="t('choose-lp')" :label="t('choose-lp')"
:rules="lpIsSelected ? [] : learningPathRules" :rules="lpIsSelected ? [] : learningPathRules"
@ -185,59 +194,59 @@
</template> </template>
<style scoped> <style scoped>
.main-container { .main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: start; justify-content: start;
padding-top: 32px; padding-top: 32px;
text-align: center; 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 { .form-card {
width: 100%; width: 85%;
max-width: 720px; padding: 1%;
border-radius: 16px;
} }
}
.form-container { @media (max-width: 600px) {
display: flex; h1 {
flex-direction: column; font-size: 32px;
gap: 24px; text-align: center;
width: 100%; margin-left: 0;
} }
}
.step-container { @media (max-width: 400px) {
display: flex; h1 {
flex-direction: column; font-size: 24px;
gap: 24px; text-align: center;
margin-left: 0;
} }
}
@media (max-width: 1000px) { .v-card {
.form-card { border: 2px solid #0e6942;
width: 85%; border-radius: 12px;
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;
}
</style> </style>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <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 {useI18n} from "vue-i18n";
import { import {
useAssignmentQuery, useAssignmentQuery,
@ -12,11 +12,11 @@ import {useGetLearningPathQuery} from "@/queries/learning-paths.ts";
import type {Language} from "@/data-objects/language.ts"; import type {Language} from "@/data-objects/language.ts";
import type {AssignmentResponse} from "@/controllers/assignments.ts"; import type {AssignmentResponse} from "@/controllers/assignments.ts";
import type {GroupDTO, GroupDTOId} from "@dwengo-1/common/interfaces/group"; import type {GroupDTO, GroupDTOId} from "@dwengo-1/common/interfaces/group";
import {descriptionRules} from "@/utils/assignment-rules.ts";
import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue"; import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue";
import GroupProgressRow from "@/components/GroupProgressRow.vue"; import GroupProgressRow from "@/components/GroupProgressRow.vue";
import type {AssignmentDTO} from "@dwengo-1/common/dist/interfaces/assignment.ts"; import type {AssignmentDTO} from "@dwengo-1/common/dist/interfaces/assignment.ts";
import GroupSelector from "@/components/assignments/GroupSelector.vue"; import GroupSelector from "@/components/assignments/GroupSelector.vue";
import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue";
const props = defineProps<{ const props = defineProps<{
classId: string; classId: string;
@ -33,6 +33,7 @@ const form = ref();
const editingLearningPath = ref(learningPath); const editingLearningPath = ref(learningPath);
const description = ref(""); const description = ref("");
const deadline = ref<Date | null>(null);
const editGroups = ref(false); const editGroups = ref(false);
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
@ -51,6 +52,7 @@ watchEffect(() => {
if (assignment) { if (assignment) {
learningPath.value = assignment.learningPath; learningPath.value = assignment.learningPath;
lang.value = assignment.language as Language; lang.value = assignment.language as Language;
deadline.value = assignment.deadline ? new Date(assignment.deadline) : null;
if (lpQueryResult.data.value) { if (lpQueryResult.data.value) {
editingLearningPath.value = lpQueryResult.data.value; editingLearningPath.value = lpQueryResult.data.value;
@ -128,7 +130,7 @@ async function saveChanges(): Promise<void> {
const assignmentDTO: AssignmentDTO = { const assignmentDTO: AssignmentDTO = {
description: description.value, description: description.value,
//deadline: new Date(),TODO: deadline aanpassen deadline: deadline.value ?? null,
}; };
mutate({ mutate({
@ -244,7 +246,6 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
</div> </div>
</div> </div>
</div> </div>
<v-card-title class="text-h4 assignmentTopTitle" <v-card-title class="text-h4 assignmentTopTitle"
>{{ assignmentResponse.data.assignment.title }} >{{ assignmentResponse.data.assignment.title }}
</v-card-title> </v-card-title>
@ -258,6 +259,7 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
:to="goToLearningPathLink()" :to="goToLearningPathLink()"
variant="tonal" variant="tonal"
color="primary" color="primary"
:disabled="isEditing"
> >
{{ t("learning-path") }} {{ t("learning-path") }}
</v-btn> </v-btn>
@ -269,7 +271,11 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
</v-alert> </v-alert>
</using-query-result> </using-query-result>
</v-card-subtitle> </v-card-subtitle>
<v-card-text v-if="isEditing">
<deadline-selector
v-model:deadline="deadline"
/>
</v-card-text>
<v-card-text <v-card-text
v-if="!isEditing" v-if="!isEditing"
class="description" class="description"
@ -284,7 +290,6 @@ async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
density="compact" density="compact"
auto-grow auto-grow
rows="3" rows="3"
:rules="descriptionRules"
></v-textarea> ></v-textarea>
</v-card-text> </v-card-text>
</v-card> </v-card>