feat: drag and drop en random selection voor groepen
This commit is contained in:
		
							parent
							
								
									936a34b709
								
							
						
					
					
						commit
						a3185ed1c1
					
				
					 10 changed files with 4347 additions and 1249 deletions
				
			
		|  | @ -30,10 +30,9 @@ | ||||||
|     display: flex; |     display: flex; | ||||||
|     gap: 0.5rem; |     gap: 0.5rem; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     color: #0e6942 |     color: #0e6942; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| .group-section { | .group-section { | ||||||
|     margin-top: 2rem; |     margin-top: 2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| <template> | <template> | ||||||
|     <v-table class="table"> |     <v-table class="table"> | ||||||
|         <thead> |         <thead> | ||||||
|             <tr v-for="name in columns" :key="column"> |             <tr | ||||||
|  |                 v-for="name in columns" | ||||||
|  |                 :key="column" | ||||||
|  |             > | ||||||
|                 <th class="header">{{ name }}</th> |                 <th class="header">{{ name }}</th> | ||||||
|             </tr> |             </tr> | ||||||
|         </thead> |         </thead> | ||||||
|  | @ -37,13 +40,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| export default { |     export default { | ||||||
|   name: 'columnList', |         name: "columnList", | ||||||
|   props: { |         props: { | ||||||
|     items: { |             items: { | ||||||
|       type: Array, |                 type: Array, | ||||||
|       required: true |                 required: true, | ||||||
|     } |             }, | ||||||
|   } |         }, | ||||||
| } |     }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,37 +1,37 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
| import { computed } from "vue"; |     import { computed } from "vue"; | ||||||
| import type { Language } from "@/data-objects/language.ts"; |     import type { Language } from "@/data-objects/language.ts"; | ||||||
| import { calculateProgress } from "@/utils/assignment-utils.ts"; |     import { calculateProgress } from "@/utils/assignment-utils.ts"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |     const props = defineProps<{ | ||||||
|     groupNumber: number; |         groupNumber: number; | ||||||
|     learningPath: string; |         learningPath: string; | ||||||
|     language: Language; |         language: Language; | ||||||
|     assignmentId: number; |         assignmentId: number; | ||||||
|     classId: string; |         classId: string; | ||||||
| }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
| const query = useGetLearningPathQuery( |     const query = useGetLearningPathQuery( | ||||||
|     () => props.learningPath, |         () => props.learningPath, | ||||||
|     () => props.language, |         () => props.language, | ||||||
|     () => ({ |         () => ({ | ||||||
|         forGroup: props.groupNumber, |             forGroup: props.groupNumber, | ||||||
|         assignmentNo: props.assignmentId, |             assignmentNo: props.assignmentId, | ||||||
|         classId: props.classId, |             classId: props.classId, | ||||||
|     }), |         }), | ||||||
| ); |     ); | ||||||
| 
 | 
 | ||||||
| const progress = computed(() => { |     const progress = computed(() => { | ||||||
|     if (!query.data.value) return 0; |         if (!query.data.value) return 0; | ||||||
|     return calculateProgress(query.data.value); |         return calculateProgress(query.data.value); | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const progressColor = computed(() => { |     const progressColor = computed(() => { | ||||||
|     if (progress.value < 50) return "error"; |         if (progress.value < 50) return "error"; | ||||||
|     if (progress.value < 80) return "warning"; |         if (progress.value < 80) return "warning"; | ||||||
|     return "success"; |         return "success"; | ||||||
| }); |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |  | ||||||
|  | @ -1,23 +1,23 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
| import { useAssignmentSubmissionsQuery } from "@/queries/assignments.ts"; |     import { useAssignmentSubmissionsQuery } from "@/queries/assignments.ts"; | ||||||
| import type { SubmissionsResponse } from "@/controllers/submissions.ts"; |     import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |     const props = defineProps<{ | ||||||
|     group: object; |         group: object; | ||||||
|     assignmentId: number; |         assignmentId: number; | ||||||
|     classId: string; |         classId: string; | ||||||
|     goToGroupSubmissionLink: (groupNo: number) => void; |         goToGroupSubmissionLink: (groupNo: number) => void; | ||||||
| }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
| const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| const submissionsQuery = useAssignmentSubmissionsQuery( |     const submissionsQuery = useAssignmentSubmissionsQuery( | ||||||
|     () => props.classId, |         () => props.classId, | ||||||
|     () => props.assignmentId, |         () => props.assignmentId, | ||||||
|     () => props.group.originalGroupNo, |         () => props.group.originalGroupNo, | ||||||
|     () => true, |         () => true, | ||||||
| ); |     ); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |  | ||||||
|  | @ -1,114 +1,438 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, } from "vue"; |     import { computed, ref, watch } from "vue"; | ||||||
| import draggable from "vuedraggable"; |     import draggable from "vuedraggable"; | ||||||
| import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { useClassStudentsQuery } from "@/queries/classes"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |     const props = defineProps<{ | ||||||
|     classId: string | undefined; |         classId: string | undefined; | ||||||
|     groups: string[][]; |         groups: object[]; | ||||||
| }>(); |     }>(); | ||||||
| const emit = defineEmits(["done", "groupsUpdated"]); |     const emit = defineEmits(["close", "groupsUpdated", "done"]); | ||||||
| const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
| const groupList = ref(props.groups.map(g => [...g])); // deep copy |     interface StudentItem { | ||||||
| const unassigned = ref<string[]>([]); // voor vrije studenten |         username: string; | ||||||
|  |         fullName: string; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| function addNewGroup() { |     const { data: studentsData } = useClassStudentsQuery(() => props.classId, true); | ||||||
|     groupList.value.push([]); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function removeGroup(index: number) { |     // Dialog states | ||||||
|     unassigned.value.push(...groupList.value[index]); |     const activeDialog = ref<"random" | "dragdrop" | null>(null); | ||||||
|     groupList.value.splice(index, 1); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function saveChanges() { |     // Drag state | ||||||
|     emit("groupsUpdated", groupList.value); |     const draggedItem = ref<{groupIndex: number, studentIndex: number} | null>(null); | ||||||
|     emit("done"); | 
 | ||||||
| } |     const currentGroups = ref<StudentItem[][]>([]); | ||||||
|  |     const unassignedStudents = ref<StudentItem[]>([]); | ||||||
|  |     const allStudents = ref<StudentItem[]>([]); | ||||||
|  | 
 | ||||||
|  |     // Random groups state | ||||||
|  |     const groupSize = ref(1); | ||||||
|  |     const randomGroupsPreview = ref<StudentItem[][]>([]); | ||||||
|  | 
 | ||||||
|  |     // Initialize data | ||||||
|  |     watch( | ||||||
|  |         () => [studentsData.value, props.groups], | ||||||
|  |         ([studentsVal, existingGroups]) => { | ||||||
|  |             if (!studentsVal) return; | ||||||
|  | 
 | ||||||
|  |             // Initialize all students | ||||||
|  |             allStudents.value = studentsVal.students.map((s) => ({ | ||||||
|  |                 username: s.username, | ||||||
|  |                 fullName: `${s.firstName} ${s.lastName}`, | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |             // Initialize groups if they exist | ||||||
|  |             if (existingGroups && existingGroups.length > 0) { | ||||||
|  |                 currentGroups.value = existingGroups.map((g) => [...g.members]); | ||||||
|  |                 const assignedUsernames = new Set( | ||||||
|  |                     existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)), | ||||||
|  |                 ); | ||||||
|  |                 unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username)); | ||||||
|  |             } else { | ||||||
|  |                 // Default to all students unassigned | ||||||
|  |                 currentGroups.value = []; | ||||||
|  |                 unassignedStudents.value = [...allStudents.value]; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Initialize random preview with current groups | ||||||
|  |             randomGroupsPreview.value = [...currentGroups.value]; | ||||||
|  |         }, | ||||||
|  |         { immediate: true }, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // Random groups functions | ||||||
|  |     function generateRandomGroups() { | ||||||
|  |         if (groupSize.value < 1) return; | ||||||
|  | 
 | ||||||
|  |         // Shuffle students | ||||||
|  |         const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5); | ||||||
|  | 
 | ||||||
|  |         // Create new groups | ||||||
|  |         const newGroups: StudentItem[][] = []; | ||||||
|  |         const groupCount = Math.ceil(shuffled.length / groupSize.value); | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < groupCount; i++) { | ||||||
|  |             newGroups.push([]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Distribute students | ||||||
|  |         shuffled.forEach((student, index) => { | ||||||
|  |             const groupIndex = index % groupCount; | ||||||
|  |             newGroups[groupIndex].push(student); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         randomGroupsPreview.value = newGroups; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function saveRandomGroups() { | ||||||
|  |         if (randomGroupsPreview.value.length === 0) { | ||||||
|  |             alert(t("please-generate-groups-first")); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         emit( | ||||||
|  |             "groupsUpdated", | ||||||
|  |             randomGroupsPreview.value.map((g) => g.map((s) => s.username)), | ||||||
|  |         ); | ||||||
|  |         activeDialog.value = null; | ||||||
|  |         emit("done"); | ||||||
|  |         emit("close"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Drag and drop functions | ||||||
|  |     function addNewGroup() { | ||||||
|  |         currentGroups.value.push([]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function removeGroup(index: number) { | ||||||
|  |         // Move students back to unassigned | ||||||
|  |         unassignedStudents.value.push(...currentGroups.value[index]); | ||||||
|  |         currentGroups.value.splice(index, 1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Native Drag & Drop Handlers | ||||||
|  |     function handleDragStart(groupIndex: number, studentIndex: number) { | ||||||
|  |         draggedItem.value = { groupIndex, studentIndex }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function handleDragOver(e: DragEvent, _: number) { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.dataTransfer!.dropEffect = "move"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number) { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         if (!draggedItem.value) return; | ||||||
|  | 
 | ||||||
|  |         const { groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex } = draggedItem.value; | ||||||
|  |         const isSameGroup = sourceGroupIndex === targetGroupIndex; | ||||||
|  | 
 | ||||||
|  |         let sourceArray, targetArray; | ||||||
|  | 
 | ||||||
|  |         // Determine source and target arrays | ||||||
|  |         if (sourceGroupIndex === -1) { | ||||||
|  |             sourceArray = unassignedStudents.value; | ||||||
|  |         } else { | ||||||
|  |             sourceArray = currentGroups.value[sourceGroupIndex]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (targetGroupIndex === -1) { | ||||||
|  |             targetArray = unassignedStudents.value; | ||||||
|  |         } else { | ||||||
|  |             targetArray = currentGroups.value[targetGroupIndex]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove from source | ||||||
|  |         const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1); | ||||||
|  | 
 | ||||||
|  |         // Add to target | ||||||
|  |         if (targetStudentIndex !== undefined) { | ||||||
|  |             targetArray.splice(targetStudentIndex, 0, movedStudent); | ||||||
|  |         } else { | ||||||
|  |             targetArray.push(movedStudent); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         draggedItem.value = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     function saveDragDrop() { | ||||||
|  |         if (unassignedStudents.value.length > 0) { | ||||||
|  |             alert(t("please-assign-all-students")); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         emit( | ||||||
|  |             "groupsUpdated", | ||||||
|  |             currentGroups.value.map((g) => g.map((s) => s.username)), | ||||||
|  |         ); | ||||||
|  |         activeDialog.value = null; | ||||||
|  |         emit("done"); | ||||||
|  |         emit("close"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Preview current groups in the main view | ||||||
|  |     const showGroupsPreview = computed(() => { | ||||||
|  |         return currentGroups.value.length > 0 || unassignedStudents.value.length > 0; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function removeStudent(groupIndex, student) { | ||||||
|  |         const group = currentGroups.value[groupIndex]; | ||||||
|  |         currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username); | ||||||
|  |         unassignedStudents.value.push(student); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <v-card> |     <v-card class="pa-4"> | ||||||
|         <v-card-title>{{ t("edit-groups") }}</v-card-title> |         <!-- Current Groups Preview --> | ||||||
|         <v-card-text> |         <div | ||||||
|             <v-row> |             v-if="showGroupsPreview" | ||||||
|                 <!-- Ongegroepeerde studenten --> |             class="mb-6" | ||||||
|                 <v-col cols="12" sm="4"> |         > | ||||||
|                     <h4>{{ t("unassigned") }}</h4> |             <h3 class="mb-2">{{ t("current-groups") }}</h3> | ||||||
|                     <draggable |             <div | ||||||
|                         v-model="unassigned" |                 v-for="(group, index) in currentGroups" | ||||||
|                         group="students" |                 :key="'preview-' + index" | ||||||
|                         item-key="username" |                 class="mb-3" | ||||||
|                         class="group-box" |             > | ||||||
|  |                 <div class="d-flex align-center"> | ||||||
|  |                     <strong class="mr-2">{{ t("group") }} {{ index + 1 }}:</strong> | ||||||
|  |                     <span class="text-caption">({{ group.length }} {{ t("members") }})</span> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="d-flex flex-wrap"> | ||||||
|  |                     <v-chip | ||||||
|  |                         v-for="student in group" | ||||||
|  |                         :key="student.username" | ||||||
|  |                         class="ma-1" | ||||||
|                     > |                     > | ||||||
|                         <template #item="{ element }"> |                         {{ student.fullName }} | ||||||
|                             <v-chip>{{ element }}</v-chip> |                     </v-chip> | ||||||
|                         </template> |                 </div> | ||||||
|                     </draggable> |             </div> | ||||||
|                 </v-col> |  | ||||||
| 
 | 
 | ||||||
|                 <!-- Bestaande groepen --> |             <div | ||||||
|                 <v-col |                 v-if="unassignedStudents.length > 0" | ||||||
|                     v-for="(group, i) in groupList" |                 class="mt-3" | ||||||
|                     :key="i" |             > | ||||||
|                     cols="12" |                 <strong>{{ t("unassigned") }}:</strong> | ||||||
|                     sm="4" |                 <div class="d-flex flex-wrap"> | ||||||
|                 > |                     <label>{{unassignedStudents.length}}</label> | ||||||
|                     <h4>{{ t("group") }} {{ i + 1 }}</h4> |                 </div> | ||||||
|                     <draggable |             </div> | ||||||
|                         v-model="groupList[i]" |         </div> | ||||||
|                         group="students" |  | ||||||
|                         item-key="username" |  | ||||||
|                         class="group-box" |  | ||||||
|                     > |  | ||||||
|                         <template #item="{ element }"> |  | ||||||
|                             <v-chip>{{ element }}</v-chip> |  | ||||||
|                         </template> |  | ||||||
|                     </draggable> |  | ||||||
| 
 |  | ||||||
|                     <v-btn |  | ||||||
|                         color="error" |  | ||||||
|                         size="x-small" |  | ||||||
|                         @click="removeGroup(i)" |  | ||||||
|                         class="mt-2" |  | ||||||
|                     > |  | ||||||
|                         {{ t("remove-group") }} |  | ||||||
|                     </v-btn> |  | ||||||
|                 </v-col> |  | ||||||
|             </v-row> |  | ||||||
| 
 | 
 | ||||||
|  |         <!-- Action Buttons --> | ||||||
|  |         <v-row | ||||||
|  |             justify="center" | ||||||
|  |             class="mb-4" | ||||||
|  |         > | ||||||
|             <v-btn |             <v-btn | ||||||
|                 color="primary" |                 color="primary" | ||||||
|                 class="mt-4" |                 @click="activeDialog = 'random'" | ||||||
|                 @click="addNewGroup" |  | ||||||
|             > |             > | ||||||
|                 {{ t("add-group") }} |                 {{ t("randomly-create-groups") }} | ||||||
|             </v-btn> |  | ||||||
|         </v-card-text> |  | ||||||
|         <v-card-actions> |  | ||||||
|             <v-btn |  | ||||||
|                 color="success" |  | ||||||
|                 @click="saveChanges" |  | ||||||
|             > |  | ||||||
|                 {{ t("save") }} |  | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <v-btn |             <v-btn | ||||||
|                 @click="$emit('done')" |                 color="secondary" | ||||||
|                 variant="text" |                 class="ml-4" | ||||||
|  |                 @click="activeDialog = 'dragdrop'" | ||||||
|             > |             > | ||||||
|                 {{ t("cancel") }} |                 {{ t("drag-and-drop") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|         </v-card-actions> |         </v-row> | ||||||
|  | 
 | ||||||
|  |         <!-- Random Groups Dialog --> | ||||||
|  |         <v-dialog | ||||||
|  |             :model-value="activeDialog === 'random'" | ||||||
|  |             @update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))" | ||||||
|  |             max-width="600" | ||||||
|  |         > | ||||||
|  |             <v-card> | ||||||
|  |                 <v-card-title>{{ t("randomly-create-groups") }}</v-card-title> | ||||||
|  |                 <v-card-text> | ||||||
|  |                     <v-row align="center"> | ||||||
|  |                         <v-col cols="6"> | ||||||
|  |                             <v-text-field | ||||||
|  |                                 v-model.number="groupSize" | ||||||
|  |                                 type="number" | ||||||
|  |                                 min="1" | ||||||
|  |                                 :max="allStudents.length" | ||||||
|  |                                 :label="t('group-size-label')" | ||||||
|  |                                 dense | ||||||
|  |                             /> | ||||||
|  |                         </v-col> | ||||||
|  |                         <v-col cols="6"> | ||||||
|  |                             <v-btn | ||||||
|  |                                 color="primary" | ||||||
|  |                                 @click="generateRandomGroups" | ||||||
|  |                                 :disabled="groupSize < 1" | ||||||
|  |                                 block | ||||||
|  |                             > | ||||||
|  |                                 {{ t("generate-groups") }} | ||||||
|  |                             </v-btn> | ||||||
|  |                         </v-col> | ||||||
|  |                     </v-row> | ||||||
|  | 
 | ||||||
|  |                     <div class="mt-4"> | ||||||
|  |                         <div class="d-flex justify-space-between align-center mb-2"> | ||||||
|  |                             <strong>{{ t("preview") }}</strong> | ||||||
|  |                             <span class="text-caption"> {{ randomGroupsPreview.length }} {{ t("groups") }} </span> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <v-expansion-panels> | ||||||
|  |                             <v-expansion-panel | ||||||
|  |                                 v-for="(group, index) in randomGroupsPreview" | ||||||
|  |                                 :key="'random-preview-' + index" | ||||||
|  |                             > | ||||||
|  |                                 <v-expansion-panel-title> | ||||||
|  |                                     {{ t("group") }} {{ index + 1 }} ({{ group.length }} {{ t("members") }}) | ||||||
|  |                                 </v-expansion-panel-title> | ||||||
|  |                                 <v-expansion-panel-text> | ||||||
|  |                                     <v-chip | ||||||
|  |                                         v-for="student in group" | ||||||
|  |                                         :key="student.username" | ||||||
|  |                                         class="ma-1" | ||||||
|  |                                     > | ||||||
|  |                                         {{ student.fullName }} | ||||||
|  |                                     </v-chip> | ||||||
|  |                                 </v-expansion-panel-text> | ||||||
|  |                             </v-expansion-panel> | ||||||
|  |                         </v-expansion-panels> | ||||||
|  |                     </div> | ||||||
|  |                 </v-card-text> | ||||||
|  | 
 | ||||||
|  |                 <v-card-actions> | ||||||
|  |                     <v-spacer /> | ||||||
|  |                     <v-btn | ||||||
|  |                         text | ||||||
|  |                         @click="activeDialog = null" | ||||||
|  |                         >{{ t("cancel") }}</v-btn | ||||||
|  |                     > | ||||||
|  |                     <v-btn | ||||||
|  |                         color="success" | ||||||
|  |                         @click="saveRandomGroups" | ||||||
|  |                         :disabled="randomGroupsPreview.length === 0" | ||||||
|  |                     > | ||||||
|  |                         {{ t("save") }} | ||||||
|  |                     </v-btn> | ||||||
|  |                 </v-card-actions> | ||||||
|  |             </v-card> | ||||||
|  |         </v-dialog> | ||||||
|  | 
 | ||||||
|  |         <!-- Drag and Drop Dialog --> | ||||||
|  |         <v-dialog | ||||||
|  |             :model-value="activeDialog === 'dragdrop'" | ||||||
|  |             @update:model-value="(val) => (val ? (activeDialog = 'dragdrop') : (activeDialog = null))" | ||||||
|  |             max-width="900" | ||||||
|  |         > | ||||||
|  |             <v-card> | ||||||
|  |                 <v-card-title class="d-flex justify-space-between align-center"> | ||||||
|  |                     <div>{{ t("drag-and-drop") }}</div> | ||||||
|  |                     <v-btn color="primary" small @click="addNewGroup">+</v-btn> | ||||||
|  |                 </v-card-title> | ||||||
|  | 
 | ||||||
|  |                 <v-card-text> | ||||||
|  |                     <v-row> | ||||||
|  |                         <!-- Groups Column --> | ||||||
|  |                         <v-col cols="12" md="8"> | ||||||
|  |                             <div v-if="currentGroups.length === 0" class="text-center py-4"> | ||||||
|  |                                 <v-alert type="info">{{ t("no-groups-yet") }}</v-alert> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <template v-else> | ||||||
|  |                                 <div | ||||||
|  |                                     v-for="(group, groupIndex) in currentGroups" | ||||||
|  |                                     :key="groupIndex" | ||||||
|  |                                     class="mb-4" | ||||||
|  |                                     @dragover.prevent="handleDragOver($event, groupIndex)" | ||||||
|  |                                     @drop="handleDrop($event, groupIndex)" | ||||||
|  |                                 > | ||||||
|  |                                     <div class="d-flex justify-space-between align-center mb-2"> | ||||||
|  |                                         <strong>{{ t("group") }} {{ groupIndex + 1 }}</strong> | ||||||
|  |                                         <v-btn icon small color="error" @click="removeGroup(groupIndex)"> | ||||||
|  |                                             <v-icon>mdi-delete</v-icon> | ||||||
|  |                                         </v-btn> | ||||||
|  |                                     </div> | ||||||
|  | 
 | ||||||
|  |                                     <div class="group-box pa-2"> | ||||||
|  |                                         <div | ||||||
|  |                                             v-for="(student, studentIndex) in group" | ||||||
|  |                                             :key="student.username" | ||||||
|  |                                             class="draggable-item ma-1" | ||||||
|  |                                             draggable="true" | ||||||
|  |                                             @dragstart="handleDragStart(groupIndex, studentIndex)" | ||||||
|  |                                             @dragover.prevent="handleDragOver($event, groupIndex)" | ||||||
|  |                                             @drop="handleDrop($event, groupIndex, studentIndex)" | ||||||
|  |                                         > | ||||||
|  |                                             <v-chip close @click:close="removeStudent(groupIndex, student)"> | ||||||
|  |                                                 {{ student.fullName }} | ||||||
|  |                                             </v-chip> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </template> | ||||||
|  |                         </v-col> | ||||||
|  | 
 | ||||||
|  |                         <!-- Unassigned Students Column --> | ||||||
|  |                         <v-col | ||||||
|  |                             cols="12" | ||||||
|  |                             md="4" | ||||||
|  |                             @dragover.prevent="handleDragOver($event, -1)" | ||||||
|  |                             @drop="handleDrop($event, -1)" | ||||||
|  |                         > | ||||||
|  |                             <div class="mb-2"> | ||||||
|  |                                 <strong>{{ t("unassigned") }}</strong> | ||||||
|  |                                 <span class="text-caption ml-2">({{ unassignedStudents.length }})</span> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <div class="group-box pa-2"> | ||||||
|  |                                 <div | ||||||
|  |                                     v-for="(student, studentIndex) in unassignedStudents" | ||||||
|  |                                     :key="student.username" | ||||||
|  |                                     class="draggable-item ma-1" | ||||||
|  |                                     draggable="true" | ||||||
|  |                                     @dragstart="handleDragStart(-1, studentIndex)" | ||||||
|  |                                     @dragover.prevent="handleDragOver($event, -1)" | ||||||
|  |                                     @drop="handleDrop($event, -1, studentIndex)" | ||||||
|  |                                 > | ||||||
|  |                                     <v-chip>{{ student.fullName }}</v-chip> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </v-col> | ||||||
|  |                     </v-row> | ||||||
|  |                 </v-card-text> | ||||||
|  | 
 | ||||||
|  |                 <v-card-actions> | ||||||
|  |                     <v-spacer /> | ||||||
|  |                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> | ||||||
|  |                     <v-btn | ||||||
|  |                         color="primary" | ||||||
|  |                         @click="saveDragDrop" | ||||||
|  |                         :disabled="unassignedStudents.length > 0" | ||||||
|  |                     > | ||||||
|  |                         {{ t("save") }} | ||||||
|  |                     </v-btn> | ||||||
|  |                 </v-card-actions> | ||||||
|  |             </v-card> | ||||||
|  |         </v-dialog> | ||||||
|     </v-card> |     </v-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .group-box { |     .group-box { | ||||||
|     min-height: 100px; |         min-height: 150px; | ||||||
|     border: 1px dashed #ccc; |         max-height: 300px; | ||||||
|     padding: 8px; |         overflow-y: auto; | ||||||
|     margin-bottom: 16px; |         background-color: #fafafa; | ||||||
|     background-color: #fafafa; |         border-radius: 4px; | ||||||
| } |     } | ||||||
|  | 
 | ||||||
|  |     .v-expansion-panel-text { | ||||||
|  |         max-height: 200px; | ||||||
|  |         overflow-y: auto; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,72 +1,72 @@ | ||||||
| <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, learningPathRules } from "@/utils/assignment-rules.ts"; |     import { assignmentTitleRules, classRules, learningPathRules } 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(); | ||||||
|  |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const language = computed(() => locale.value); | ||||||
|  |     const form = ref(); | ||||||
|  | 
 | ||||||
|  |     const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||||
|  |     const classesQueryResults = useTeacherClassesQuery(username, true); | ||||||
|  | 
 | ||||||
|  |     const selectedClass = ref(undefined); | ||||||
|  |     const assignmentTitle = ref(""); | ||||||
|  |     const selectedLearningPath = ref(route.query.hruid || undefined); | ||||||
|  |     const lpIsSelected = route.query.hruid !== undefined; | ||||||
|  | 
 | ||||||
|  |     const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||||
|  | 
 | ||||||
|  |     watch([isSuccess, data], async ([success, newData]) => { | ||||||
|  |         if (success && newData?.assignment) { | ||||||
|  |             await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     async function submitFormHandler(): Promise<void> { | ||||||
|  |         const { valid } = await form.value.validate(); | ||||||
|  |         if (!valid) return; | ||||||
|  | 
 | ||||||
|  |         let lp = selectedLearningPath.value; | ||||||
|  |         if (!lpIsSelected) { | ||||||
|  |             lp = selectedLearningPath.value?.hruid; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const assignmentDTO: AssignmentDTO = { | ||||||
|  |             id: 0, | ||||||
|  |             within: selectedClass.value?.id || "", | ||||||
|  |             title: assignmentTitle.value, | ||||||
|  |             description: "", | ||||||
|  |             learningPath: lp || "", | ||||||
|  |             deadline: new Date(), | ||||||
|  |             language: language.value, | ||||||
|  |             groups: [], | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         mutate({ cid: assignmentDTO.within, data: assignmentDTO }); | ||||||
|     } |     } | ||||||
|     const user = await auth.loadUser(); |  | ||||||
|     username.value = user?.profile?.preferred_username ?? ""; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const language = computed(() => locale.value); |  | ||||||
| const form = ref(); |  | ||||||
| 
 |  | ||||||
| const learningPathsQueryResults = useGetAllLearningPaths(language); |  | ||||||
| const classesQueryResults = useTeacherClassesQuery(username, true); |  | ||||||
| 
 |  | ||||||
| const selectedClass = ref(undefined); |  | ||||||
| const assignmentTitle = ref(""); |  | ||||||
| const selectedLearningPath = ref(route.query.hruid || undefined); |  | ||||||
| const lpIsSelected = route.query.hruid !== undefined; |  | ||||||
| 
 |  | ||||||
| const { mutate, data, isSuccess } = useCreateAssignmentMutation(); |  | ||||||
| 
 |  | ||||||
| watch([isSuccess, data], async ([success, newData]) => { |  | ||||||
|     if (success && newData?.assignment) { |  | ||||||
|         await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| async function submitFormHandler(): Promise<void> { |  | ||||||
|     const { valid } = await form.value.validate(); |  | ||||||
|     if (!valid) return; |  | ||||||
| 
 |  | ||||||
|     let lp = selectedLearningPath.value; |  | ||||||
|     if (!lpIsSelected) { |  | ||||||
|         lp = selectedLearningPath.value?.hruid; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const assignmentDTO: AssignmentDTO = { |  | ||||||
|         id: 0, |  | ||||||
|         within: selectedClass.value?.id || "", |  | ||||||
|         title: assignmentTitle.value, |  | ||||||
|         description: "", |  | ||||||
|         learningPath: lp || "", |  | ||||||
|         deadline: new Date(), |  | ||||||
|         language: language.value, |  | ||||||
|         groups: [], |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     mutate({ cid: assignmentDTO.within, data: assignmentDTO }); |  | ||||||
| } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -74,9 +74,13 @@ async function submitFormHandler(): Promise<void> { | ||||||
|         <h1 class="h1">{{ t("new-assignment") }}</h1> |         <h1 class="h1">{{ t("new-assignment") }}</h1> | ||||||
| 
 | 
 | ||||||
|         <v-card class="form-card elevation-2 pa-6"> |         <v-card class="form-card elevation-2 pa-6"> | ||||||
|             <v-form ref="form" class="form-container" validate-on="submit lazy" @submit.prevent="submitFormHandler"> |             <v-form | ||||||
|  |                 ref="form" | ||||||
|  |                 class="form-container" | ||||||
|  |                 validate-on="submit lazy" | ||||||
|  |                 @submit.prevent="submitFormHandler" | ||||||
|  |             > | ||||||
|                 <v-container class="step-container pa-0"> |                 <v-container class="step-container pa-0"> | ||||||
| 
 |  | ||||||
|                     <!-- Titel veld --> |                     <!-- Titel veld --> | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                         v-model="assignmentTitle" |                         v-model="assignmentTitle" | ||||||
|  | @ -90,7 +94,10 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                     /> |                     /> | ||||||
| 
 | 
 | ||||||
|                     <!-- Learning Path keuze --> |                     <!-- Learning Path keuze --> | ||||||
|                     <using-query-result :query-result="learningPathsQueryResults" v-slot="{ data }: { data: LearningPath[] }"> |                     <using-query-result | ||||||
|  |                         :query-result="learningPathsQueryResults" | ||||||
|  |                         v-slot="{ data }: { data: LearningPath[] }" | ||||||
|  |                     > | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedLearningPath" |                             v-model="selectedLearningPath" | ||||||
|                             :items="data" |                             :items="data" | ||||||
|  | @ -111,7 +118,10 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
| 
 | 
 | ||||||
|                     <!-- Klas keuze --> |                     <!-- Klas keuze --> | ||||||
|                     <using-query-result :query-result="classesQueryResults" v-slot="{ data }: { data: ClassesResponse }"> |                     <using-query-result | ||||||
|  |                         :query-result="classesQueryResults" | ||||||
|  |                         v-slot="{ data }: { data: ClassesResponse }" | ||||||
|  |                     > | ||||||
|                         <v-combobox |                         <v-combobox | ||||||
|                             v-model="selectedClass" |                             v-model="selectedClass" | ||||||
|                             :items="data?.classes ?? []" |                             :items="data?.classes ?? []" | ||||||
|  | @ -153,7 +163,6 @@ async function submitFormHandler(): Promise<void> { | ||||||
|                             {{ t("cancel") }} |                             {{ t("cancel") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
|                     </div> |                     </div> | ||||||
| 
 |  | ||||||
|                 </v-container> |                 </v-container> | ||||||
|             </v-form> |             </v-form> | ||||||
|         </v-card> |         </v-card> | ||||||
|  | @ -161,65 +170,59 @@ async function submitFormHandler(): Promise<void> { | ||||||
| </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: 85%; |         width: 100%; | ||||||
|         padding: 1%; |         max-width: 720px; | ||||||
|  |         border-radius: 16px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } |     .form-container { | ||||||
| 
 |         display: flex; | ||||||
| @media (max-width: 600px) { |         flex-direction: column; | ||||||
| 
 |         gap: 24px; | ||||||
|     h1 { |         width: 100%; | ||||||
|         font-size: 32px; |  | ||||||
|         text-align: center; |  | ||||||
|         margin-left: 0; |  | ||||||
|     } |     } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| @media (max-width: 400px) { |     .step-container { | ||||||
| 
 |         display: flex; | ||||||
|     h1 { |         flex-direction: column; | ||||||
|         font-size: 24px; |         gap: 24px; | ||||||
|         text-align: center; |  | ||||||
|         margin-left: 0; |  | ||||||
|     } |     } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .v-card { |     @media (max-width: 1000px) { | ||||||
|     border: 2px solid #0e6942; |         .form-card { | ||||||
|     border-radius: 12px; |             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; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,90 +1,87 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import {ref, computed, watchEffect} from "vue"; |     import { ref, computed, watchEffect } from "vue"; | ||||||
| import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
| import {useI18n} from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| import {useAssignmentQuery} from "@/queries/assignments.ts"; |     import { useAssignmentQuery } from "@/queries/assignments.ts"; | ||||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
| import type {AssignmentResponse} from "@/controllers/assignments.ts"; |     import type { AssignmentResponse } from "@/controllers/assignments.ts"; | ||||||
| import {asyncComputed} from "@vueuse/core"; |     import { asyncComputed } from "@vueuse/core"; | ||||||
| import {useStudentsByUsernamesQuery} from "@/queries/students.ts"; |     import { useStudentsByUsernamesQuery } from "@/queries/students.ts"; | ||||||
| import {useGroupsQuery} from "@/queries/groups.ts"; |     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||||
| import {useGetLearningPathQuery} from "@/queries/learning-paths.ts"; |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
| import type {Language} from "@/data-objects/language.ts"; |     import type { Language } from "@/data-objects/language.ts"; | ||||||
| import {calculateProgress} from "@/utils/assignment-utils.ts"; |     import { calculateProgress } from "@/utils/assignment-utils.ts"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |     const props = defineProps<{ | ||||||
|     classId: string; |         classId: string; | ||||||
|     assignmentId: number; |         assignmentId: number; | ||||||
| }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
| const {t} = useI18n(); |     const { t } = useI18n(); | ||||||
| const lang = ref(); |     const lang = ref(); | ||||||
| const learningPath = ref(); |     const learningPath = ref(); | ||||||
| // Get the user's username/id |     // Get the user's username/id | ||||||
| const username = asyncComputed(async () => { |     const username = asyncComputed(async () => { | ||||||
|     const user = await auth.loadUser(); |         const user = await auth.loadUser(); | ||||||
|     return user?.profile?.preferred_username ?? undefined; |         return user?.profile?.preferred_username ?? undefined; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); |     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||||
| learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); |  | ||||||
| const group = computed(() => { |  | ||||||
|     const groups = groupsQueryResult.data.value?.groups; |  | ||||||
| 
 |  | ||||||
|     if (!groups) return undefined; |  | ||||||
| 
 |  | ||||||
|     // Sort by original groupNumber |  | ||||||
|     const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber); |  | ||||||
| 
 |  | ||||||
|     return sortedGroups |  | ||||||
|         .map((group, index) => ({ |  | ||||||
|             ...group, |  | ||||||
|             groupNo: index + 1, // Renumbered index |  | ||||||
|         })) |  | ||||||
|         .find((group) => group.members?.some((m) => m.username === username.value)); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| watchEffect(() => { |  | ||||||
|     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|     lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| const learningPathParams = computed(() => { |     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||||
|     if (!group.value || !learningPath.value || !lang.value) return undefined; |     const group = computed(() => { | ||||||
|  |         const groups = groupsQueryResult.data.value?.groups; | ||||||
| 
 | 
 | ||||||
|     return { |         if (!groups) return undefined; | ||||||
|         forGroup: group.value.groupNumber, |  | ||||||
|         assignmentNo: props.assignmentId, |  | ||||||
|         classId: props.classId, |  | ||||||
|     }; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| const lpQueryResult = useGetLearningPathQuery( |         // Sort by original groupNumber | ||||||
|     () => learningPath.value, |         const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber); | ||||||
|     () => lang.value, |  | ||||||
|     () => learningPathParams.value |  | ||||||
| ); |  | ||||||
| 
 | 
 | ||||||
|  |         return sortedGroups | ||||||
|  |             .map((group, index) => ({ | ||||||
|  |                 ...group, | ||||||
|  |                 groupNo: index + 1, // Renumbered index | ||||||
|  |             })) | ||||||
|  |             .find((group) => group.members?.some((m) => m.username === username.value)); | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
| const progressColor = computed(() => { |     watchEffect(() => { | ||||||
|     const progress = calculateProgress(lpQueryResult.data.value); |         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|     if (progress >= 100) return "success"; |         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; | ||||||
|     if (progress >= 50) return "warning"; |     }); | ||||||
|     return "error"; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[] ?? undefined); |     const learningPathParams = computed(() => { | ||||||
|  |         if (!group.value || !learningPath.value || !lang.value) return undefined; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             forGroup: group.value.groupNumber, | ||||||
|  |             assignmentNo: props.assignmentId, | ||||||
|  |             classId: props.classId, | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const lpQueryResult = useGetLearningPathQuery( | ||||||
|  |         () => learningPath.value, | ||||||
|  |         () => lang.value, | ||||||
|  |         () => learningPathParams.value, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const progressColor = computed(() => { | ||||||
|  |         const progress = calculateProgress(lpQueryResult.data.value); | ||||||
|  |         if (progress >= 100) return "success"; | ||||||
|  |         if (progress >= 50) return "warning"; | ||||||
|  |         return "error"; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const studentQueries = useStudentsByUsernamesQuery(() => (group.value?.members as string[]) ?? undefined); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|         <using-query-result |         <using-query-result | ||||||
|             :query-result="assignmentQueryResult" |             :query-result="assignmentQueryResult" | ||||||
|             v-slot="assignmentResponse : { data: AssignmentResponse }" |             v-slot="assignmentResponse: { data: AssignmentResponse }" | ||||||
|         > |         > | ||||||
|             <v-card |             <v-card | ||||||
|                 v-if="assignmentResponse" |                 v-if="assignmentResponse" | ||||||
|  | @ -100,9 +97,8 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                         <v-icon>mdi-arrow-left</v-icon> |                         <v-icon>mdi-arrow-left</v-icon> | ||||||
|                     </v-btn> |                     </v-btn> | ||||||
|                 </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> | ||||||
| 
 | 
 | ||||||
|                 <v-card-subtitle class="subtitle-section"> |                 <v-card-subtitle class="subtitle-section"> | ||||||
|  | @ -112,14 +108,17 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                     > |                     > | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             v-if="lpData" |                             v-if="lpData" | ||||||
|                             :to="group ? `/learningPath/${lpData.hruid}/${assignmentResponse.data.assignment?.language}/${lpData.startNode.learningobjectHruid}?forGroup=${0}&assignmentNo=${assignmentId}&classId=${classId}` : undefined" |                             :to=" | ||||||
|  |                                 group | ||||||
|  |                                     ? `/learningPath/${lpData.hruid}/${assignmentResponse.data.assignment?.language}/${lpData.startNode.learningobjectHruid}?forGroup=${0}&assignmentNo=${assignmentId}&classId=${classId}` | ||||||
|  |                                     : undefined | ||||||
|  |                             " | ||||||
|                             :disabled="!group" |                             :disabled="!group" | ||||||
|                             variant="tonal" |                             variant="tonal" | ||||||
|                             color="primary" |                             color="primary" | ||||||
|                         > |                         > | ||||||
|                             {{ t("learning-path") }} |                             {{ t("learning-path") }} | ||||||
|                         </v-btn> |                         </v-btn> | ||||||
| 
 |  | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
|                 </v-card-subtitle> |                 </v-card-subtitle> | ||||||
| 
 | 
 | ||||||
|  | @ -163,21 +162,23 @@ const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as | ||||||
|                     </div> |                     </div> | ||||||
| 
 | 
 | ||||||
|                     <div v-else> |                     <div v-else> | ||||||
|                         <v-alert type="info" variant="text"> |                         <v-alert | ||||||
|  |                             type="info" | ||||||
|  |                             variant="text" | ||||||
|  |                         > | ||||||
|                             {{ t("not-in-group-message") }} |                             {{ t("not-in-group-message") }} | ||||||
|                         </v-alert> |                         </v-alert> | ||||||
|                     </div> |                     </div> | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
| 
 |  | ||||||
|             </v-card> |             </v-card> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| @import "@/assets/assignment.css"; |     @import "@/assets/assignment.css"; | ||||||
| 
 | 
 | ||||||
| .progress-bar { |     .progress-bar { | ||||||
|     width: 40%; |         width: 40%; | ||||||
| } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,150 +1,146 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import {computed, type Ref, ref, watch, watchEffect} from "vue"; |     import { computed, type Ref, ref, watch, watchEffect } from "vue"; | ||||||
| import {useI18n} from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| import { |     import { | ||||||
|     useAssignmentQuery, |         useAssignmentQuery, | ||||||
|     useDeleteAssignmentMutation, |         useDeleteAssignmentMutation, | ||||||
|     useUpdateAssignmentMutation |         useUpdateAssignmentMutation, | ||||||
| } from "@/queries/assignments.ts"; |     } from "@/queries/assignments.ts"; | ||||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
| import {useGroupsQuery} from "@/queries/groups.ts"; |     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||||
| import {useGetAllLearningPaths, useGetLearningPathQuery} from "@/queries/learning-paths.ts"; |     import { useGetAllLearningPaths, 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 type {LearningPath} from "@/data-objects/learning-paths/learning-path"; |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||||
| import {descriptionRules, learningPathRules} from "@/utils/assignment-rules.ts"; |     import { descriptionRules, learningPathRules } 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"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |     const props = defineProps<{ | ||||||
|     classId: string; |         classId: string; | ||||||
|     assignmentId: number; |         assignmentId: number; | ||||||
|     useGroupsWithProgress: ( |         useGroupsWithProgress: ( | ||||||
|         groups: Ref<GroupDTO[]>, |             groups: Ref<GroupDTO[]>, | ||||||
|         hruid: Ref<string>, |             hruid: Ref<string>, | ||||||
|         language: Ref<Language>, |             language: Ref<Language>, | ||||||
|     ) => { groupProgressMap: Map<number, number> }; |         ) => { groupProgressMap: Map<number, number> }; | ||||||
| }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
| const isEditing = ref(false); |     const isEditing = ref(false); | ||||||
| 
 | 
 | ||||||
| const {t} = useI18n(); |     const { t } = useI18n(); | ||||||
| const lang = ref(); |     const lang = ref(); | ||||||
| const groups = ref<GroupDTO[] | GroupDTOId[]>([]); |     const groups = ref<GroupDTO[] | GroupDTOId[]>([]); | ||||||
| const learningPath = ref(); |     const learningPath = ref(); | ||||||
| const form = ref(); |     const form = ref(); | ||||||
| 
 | 
 | ||||||
|  |     const editingLearningPath = ref(learningPath); | ||||||
|  |     const description = ref(""); | ||||||
|  |     const editGroups = ref(false); | ||||||
| 
 | 
 | ||||||
| const editingLearningPath = ref(learningPath); |     const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); | ||||||
| const description = ref(""); |     // Get learning path object | ||||||
| const editGroups = ref(false); |     const lpQueryResult = useGetLearningPathQuery( | ||||||
| 
 |         computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""), | ||||||
| 
 |         computed(() => assignmentQueryResult.data.value?.assignment?.language as Language), | ||||||
| const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); |  | ||||||
| // Get learning path object |  | ||||||
| const lpQueryResult = useGetLearningPathQuery( |  | ||||||
|     computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""), |  | ||||||
|     computed(() => assignmentQueryResult.data.value?.assignment?.language as Language), |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| // Get all the groups withing the assignment |  | ||||||
| const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); |  | ||||||
| groups.value = groupsQueryResult.data.value?.groups ?? []; |  | ||||||
| 
 |  | ||||||
| watchEffect(() => { |  | ||||||
|     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |  | ||||||
|     lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const allGroups = computed(() => { |  | ||||||
|     const groups = groupsQueryResult.data.value?.groups; |  | ||||||
| 
 |  | ||||||
|     if (!groups) return []; |  | ||||||
| 
 |  | ||||||
|     // Sort by original groupNumber |  | ||||||
|     const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber); |  | ||||||
| 
 |  | ||||||
|     // Assign new sequential numbers starting from 1 |  | ||||||
|     return sortedGroups.map((group, index) => ({ |  | ||||||
|         groupNo: index + 1, // New group number that will be used |  | ||||||
|         name: `${t("group")} ${index + 1}`, |  | ||||||
|         members: group.members, |  | ||||||
|         originalGroupNo: group.groupNumber, // Keep original number if needed |  | ||||||
|     })); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const dialog = ref(false); |  | ||||||
| const selectedGroup = ref({}); |  | ||||||
| 
 |  | ||||||
| function openGroupDetails(group): void { |  | ||||||
|     selectedGroup.value = group; |  | ||||||
|     dialog.value = true; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function deleteAssignment(num: number, clsId: string): Promise<void> { |  | ||||||
|     const {mutate} = useDeleteAssignmentMutation(); |  | ||||||
|     mutate( |  | ||||||
|         {cid: clsId, an: num}, |  | ||||||
|         { |  | ||||||
|             onSuccess: () => { |  | ||||||
|                 window.location.href = "/user/assignment"; |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|     ); |     ); | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function goToLearningPathLink(): string | undefined { |     // Get all the groups withing the assignment | ||||||
|     const assignment = assignmentQueryResult.data.value?.assignment; |     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||||
|     const lp = lpQueryResult.data.value; |     groups.value = groupsQueryResult.data.value?.groups ?? []; | ||||||
| 
 | 
 | ||||||
|     if (!assignment || !lp) return undefined; |     watchEffect(() => { | ||||||
| 
 |         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|     return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`; |         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function goToGroupSubmissionLink(groupNo: number): string | undefined { |  | ||||||
|     const lp = lpQueryResult.data.value; |  | ||||||
|     if (!lp) return undefined; |  | ||||||
| 
 |  | ||||||
|     return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const learningPathsQueryResults = useGetAllLearningPaths(lang); |  | ||||||
| 
 |  | ||||||
| const {mutate, data, isSuccess} = useUpdateAssignmentMutation(); |  | ||||||
| 
 |  | ||||||
| watch([isSuccess, data], ([success, newData]) => { |  | ||||||
|     if (success && newData?.assignment) { |  | ||||||
|         window.location.reload(); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| async function saveChanges(): Promise<void> { |  | ||||||
|     const {valid} = await form.value.validate(); |  | ||||||
|     if (!valid) return; |  | ||||||
| 
 |  | ||||||
|     isEditing.value = false; |  | ||||||
| 
 |  | ||||||
|     const lp = learningPath.value; |  | ||||||
| 
 |  | ||||||
|     const assignmentDTO: AssignmentDTO = { |  | ||||||
|         id: assignmentQueryResult.data.value?.assignment.id, |  | ||||||
|         description: description.value, |  | ||||||
|         learningPath: lp || "", |  | ||||||
|         deadline: new Date(), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     mutate({ |  | ||||||
|         cid: assignmentQueryResult.data.value?.assignment.within, |  | ||||||
|         an: assignmentQueryResult.data.value?.assignment.id, |  | ||||||
|         data: assignmentDTO |  | ||||||
|     }); |     }); | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
|  |     const allGroups = computed(() => { | ||||||
|  |         const groups = groupsQueryResult.data.value?.groups; | ||||||
| 
 | 
 | ||||||
|  |         if (!groups) return []; | ||||||
|  | 
 | ||||||
|  |         // Sort by original groupNumber | ||||||
|  |         const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber); | ||||||
|  | 
 | ||||||
|  |         // Assign new sequential numbers starting from 1 | ||||||
|  |         return sortedGroups.map((group, index) => ({ | ||||||
|  |             groupNo: index + 1, // New group number that will be used | ||||||
|  |             name: `${t("group")} ${index + 1}`, | ||||||
|  |             members: group.members, | ||||||
|  |             originalGroupNo: group.groupNumber, // Keep original number if needed | ||||||
|  |         })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const dialog = ref(false); | ||||||
|  |     const selectedGroup = ref({}); | ||||||
|  | 
 | ||||||
|  |     function openGroupDetails(group): void { | ||||||
|  |         selectedGroup.value = group; | ||||||
|  |         dialog.value = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function deleteAssignment(num: number, clsId: string): Promise<void> { | ||||||
|  |         const { mutate } = useDeleteAssignmentMutation(); | ||||||
|  |         mutate( | ||||||
|  |             { cid: clsId, an: num }, | ||||||
|  |             { | ||||||
|  |                 onSuccess: () => { | ||||||
|  |                     window.location.href = "/user/assignment"; | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function goToLearningPathLink(): string | undefined { | ||||||
|  |         const assignment = assignmentQueryResult.data.value?.assignment; | ||||||
|  |         const lp = lpQueryResult.data.value; | ||||||
|  | 
 | ||||||
|  |         if (!assignment || !lp) return undefined; | ||||||
|  | 
 | ||||||
|  |         return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function goToGroupSubmissionLink(groupNo: number): string | undefined { | ||||||
|  |         const lp = lpQueryResult.data.value; | ||||||
|  |         if (!lp) return undefined; | ||||||
|  | 
 | ||||||
|  |         return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const learningPathsQueryResults = useGetAllLearningPaths(lang); | ||||||
|  | 
 | ||||||
|  |     const { mutate, data, isSuccess } = useUpdateAssignmentMutation(); | ||||||
|  | 
 | ||||||
|  |     watch([isSuccess, data], ([success, newData]) => { | ||||||
|  |         if (success && newData?.assignment) { | ||||||
|  |             window.location.reload(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     async function saveChanges(): Promise<void> { | ||||||
|  |         const { valid } = await form.value.validate(); | ||||||
|  |         if (!valid) return; | ||||||
|  | 
 | ||||||
|  |         isEditing.value = false; | ||||||
|  | 
 | ||||||
|  |         const lp = learningPath.value; | ||||||
|  | 
 | ||||||
|  |         const assignmentDTO: AssignmentDTO = { | ||||||
|  |             id: assignmentQueryResult.data.value?.assignment.id, | ||||||
|  |             description: description.value, | ||||||
|  |             learningPath: lp || "", | ||||||
|  |             deadline: new Date(), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         mutate({ | ||||||
|  |             cid: assignmentQueryResult.data.value?.assignment.within, | ||||||
|  |             an: assignmentQueryResult.data.value?.assignment.id, | ||||||
|  |             data: assignmentDTO, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -167,7 +163,11 @@ async function saveChanges(): Promise<void> { | ||||||
|                         md="6" |                         md="6" | ||||||
|                         class="responsive-col" |                         class="responsive-col" | ||||||
|                     > |                     > | ||||||
|                         <v-form ref="form" validate-on="submit lazy" @submit.prevent="saveChanges"> |                         <v-form | ||||||
|  |                             ref="form" | ||||||
|  |                             validate-on="submit lazy" | ||||||
|  |                             @submit.prevent="saveChanges" | ||||||
|  |                         > | ||||||
|                             <v-card |                             <v-card | ||||||
|                                 v-if="assignmentResponse" |                                 v-if="assignmentResponse" | ||||||
|                                 class="assignment-card" |                                 class="assignment-card" | ||||||
|  | @ -189,11 +189,11 @@ async function saveChanges(): Promise<void> { | ||||||
|                                                 variant="text" |                                                 variant="text" | ||||||
|                                                 class="top_next_to_right_button" |                                                 class="top_next_to_right_button" | ||||||
|                                                 @click=" |                                                 @click=" | ||||||
|                                                 () => { |                                                     () => { | ||||||
|                                                     isEditing = true; |                                                         isEditing = true; | ||||||
|                                                     description = assignmentResponse.data.assignment.description; |                                                         description = assignmentResponse.data.assignment.description; | ||||||
|                                                 } |                                                     } | ||||||
|                                             " |                                                 " | ||||||
|                                             > |                                             > | ||||||
|                                                 <v-icon>mdi-pencil</v-icon> |                                                 <v-icon>mdi-pencil</v-icon> | ||||||
|                                             </v-btn> |                                             </v-btn> | ||||||
|  | @ -201,10 +201,14 @@ async function saveChanges(): Promise<void> { | ||||||
|                                                 v-else |                                                 v-else | ||||||
|                                                 variant="text" |                                                 variant="text" | ||||||
|                                                 class="top-right-btn" |                                                 class="top-right-btn" | ||||||
|                                                 @click="() => {isEditing = false; editingLearningPath=learningPath}" |                                                 @click=" | ||||||
|                                             >{{ t("cancel") }} |                                                     () => { | ||||||
|                                             </v-btn |                                                         isEditing = false; | ||||||
|                                             > |                                                         editingLearningPath = learningPath; | ||||||
|  |                                                     } | ||||||
|  |                                                 " | ||||||
|  |                                                 >{{ t("cancel") }} | ||||||
|  |                                             </v-btn> | ||||||
| 
 | 
 | ||||||
|                                             <v-btn |                                             <v-btn | ||||||
|                                                 v-if="!isEditing" |                                                 v-if="!isEditing" | ||||||
|  | @ -212,11 +216,11 @@ async function saveChanges(): Promise<void> { | ||||||
|                                                 variant="text" |                                                 variant="text" | ||||||
|                                                 class="top-right-btn" |                                                 class="top-right-btn" | ||||||
|                                                 @click=" |                                                 @click=" | ||||||
|                                                 deleteAssignment( |                                                     deleteAssignment( | ||||||
|                                                     assignmentResponse.data.assignment.id, |                                                         assignmentResponse.data.assignment.id, | ||||||
|                                                     assignmentResponse.data.assignment.within, |                                                         assignmentResponse.data.assignment.within, | ||||||
|                                                 ) |                                                     ) | ||||||
|                                             " |                                                 " | ||||||
|                                             > |                                             > | ||||||
|                                                 <v-icon>mdi-delete</v-icon> |                                                 <v-icon>mdi-delete</v-icon> | ||||||
|                                             </v-btn> |                                             </v-btn> | ||||||
|  | @ -233,9 +237,8 @@ async function saveChanges(): Promise<void> { | ||||||
|                                     </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> | ||||||
|                                 <v-card-subtitle |                                 <v-card-subtitle | ||||||
|                                     v-if="!isEditing" |                                     v-if="!isEditing" | ||||||
|  | @ -275,9 +278,9 @@ async function saveChanges(): Promise<void> { | ||||||
|                                             item-value="hruid" |                                             item-value="hruid" | ||||||
|                                             required |                                             required | ||||||
|                                             :filter=" |                                             :filter=" | ||||||
|                                             (item, query: string) => |                                                 (item, query: string) => | ||||||
|                                                 item.title.toLowerCase().includes(query.toLowerCase()) |                                                     item.title.toLowerCase().includes(query.toLowerCase()) | ||||||
|                                         " |                                             " | ||||||
|                                         ></v-combobox> |                                         ></v-combobox> | ||||||
|                                     </v-card-text> |                                     </v-card-text> | ||||||
|                                 </using-query-result> |                                 </using-query-result> | ||||||
|  | @ -317,7 +320,7 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         > |                                         > | ||||||
|                                             <v-list-item-content> |                                             <v-list-item-content> | ||||||
|                                                 <v-list-item-title |                                                 <v-list-item-title | ||||||
|                                                 >{{ member.firstName + " " + member.lastName }} |                                                     >{{ member.firstName + " " + member.lastName }} | ||||||
|                                                 </v-list-item-title> |                                                 </v-list-item-title> | ||||||
|                                             </v-list-item-content> |                                             </v-list-item-content> | ||||||
|                                         </v-list-item> |                                         </v-list-item> | ||||||
|  | @ -327,24 +330,22 @@ async function saveChanges(): Promise<void> { | ||||||
|                                     <v-btn |                                     <v-btn | ||||||
|                                         color="primary" |                                         color="primary" | ||||||
|                                         @click="dialog = false" |                                         @click="dialog = false" | ||||||
|                                     >Close |                                         >Close | ||||||
|                                     </v-btn |                                     </v-btn> | ||||||
|                                     > |  | ||||||
|                                 </v-card-actions> |                                 </v-card-actions> | ||||||
|                             </v-card> |                             </v-card> | ||||||
|                         </v-dialog> |                         </v-dialog> | ||||||
|                     </v-col> |                     </v-col> | ||||||
| 
 | 
 | ||||||
|                     <!-- The second column of the screen --> |                     <!-- The second column of the screen --> | ||||||
|                     <template v-if="!editGroups"> |                     <v-col | ||||||
|                         <v-col |                         cols="12" | ||||||
|                             cols="12" |                         sm="6" | ||||||
|                             sm="6" |                         md="6" | ||||||
|                             md="6" |                         class="responsive-col" | ||||||
|                             class="responsive-col" |                     > | ||||||
|                         > |                         <v-table class="table"> | ||||||
|                             <v-table class="table"> |                             <thead> | ||||||
|                                 <thead> |  | ||||||
|                                 <tr> |                                 <tr> | ||||||
|                                     <th class="header">{{ t("group") }}</th> |                                     <th class="header">{{ t("group") }}</th> | ||||||
|                                     <th class="header">{{ t("progress") }}</th> |                                     <th class="header">{{ t("progress") }}</th> | ||||||
|  | @ -356,11 +357,10 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         > |                                         > | ||||||
|                                             <v-icon>mdi-pencil</v-icon> |                                             <v-icon>mdi-pencil</v-icon> | ||||||
|                                         </v-btn> |                                         </v-btn> | ||||||
| 
 |  | ||||||
|                                     </th> |                                     </th> | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                                 </thead> |                             </thead> | ||||||
|                                 <tbody> |                             <tbody> | ||||||
|                                 <tr |                                 <tr | ||||||
|                                     v-for="g in allGroups" |                                     v-for="g in allGroups" | ||||||
|                                     :key="g.originalGroupNo" |                                     :key="g.originalGroupNo" | ||||||
|  | @ -403,133 +403,138 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         > |                                         > | ||||||
|                                             <v-icon color="red">mdi-delete</v-icon> |                                             <v-icon color="red">mdi-delete</v-icon> | ||||||
|                                         </v-btn> |                                         </v-btn> | ||||||
| 
 |  | ||||||
|                                     </td> |                                     </td> | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                                 </tbody> |                             </tbody> | ||||||
|                             </v-table> |                         </v-table> | ||||||
|                         </v-col> |                     </v-col> | ||||||
|                     </template> |                 </v-row> | ||||||
|                     <template v-else> |                 <v-dialog | ||||||
|  |                     v-model="editGroups" | ||||||
|  |                     max-width="800" | ||||||
|  |                     persistent | ||||||
|  |                 > | ||||||
|  |                     <v-card-text> | ||||||
|                         <GroupSelector |                         <GroupSelector | ||||||
|                             :groups="allGroups" |                             :groups="allGroups" | ||||||
|                             :class-id="classId" |                             :class-id="props.classId" | ||||||
|                             @groupsUpdated="handleUpdatedGroups" |                             :assignment-id="props.assignmentId" | ||||||
|  |                             @close="editGroups = false" | ||||||
|                         /> |                         /> | ||||||
|                     </template> |                     </v-card-text> | ||||||
|                 </v-row> |                 </v-dialog> | ||||||
|             </v-container> |             </v-container> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| @import "@/assets/assignment.css"; |     @import "@/assets/assignment.css"; | ||||||
| 
 | 
 | ||||||
| .table-scroll { |     .table-scroll { | ||||||
|     overflow-x: auto; |         overflow-x: auto; | ||||||
|     -webkit-overflow-scrolling: touch; |         -webkit-overflow-scrolling: touch; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header { |  | ||||||
|     font-weight: bold !important; |  | ||||||
|     background-color: #0e6942; |  | ||||||
|     color: white; |  | ||||||
|     padding: 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| td, |  | ||||||
| th { |  | ||||||
|     border-bottom: 1px solid #0e6942; |  | ||||||
|     border-top: 1px solid #0e6942; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .table { |  | ||||||
|     width: 90%; |  | ||||||
|     padding-top: 10px; |  | ||||||
|     border-collapse: collapse; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media screen and (max-width: 850px) { |  | ||||||
|     h1 { |  | ||||||
|         text-align: center; |  | ||||||
|         padding-left: 0; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .join { |     .header { | ||||||
|         text-align: center; |         font-weight: bold !important; | ||||||
|         align-items: center; |         background-color: #0e6942; | ||||||
|         margin-left: 0; |         color: white; | ||||||
|  |         padding: 10px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .sheet { |     table thead th:first-child { | ||||||
|         width: 100%; |         border-top-left-radius: 10px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     main { |     .table thead th:last-child { | ||||||
|         display: flex; |         border-top-right-radius: 10px; | ||||||
|         flex-direction: column; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         margin: 5px; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .custom-breakpoint { |     .table tbody tr:nth-child(odd) { | ||||||
|         flex-direction: column !important; |         background-color: white; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .table tbody tr:nth-child(even) { | ||||||
|  |         background-color: #f6faf2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     td, | ||||||
|  |     th { | ||||||
|  |         border-bottom: 1px solid #0e6942; | ||||||
|  |         border-top: 1px solid #0e6942; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .table { |     .table { | ||||||
|         width: 100%; |         width: 90%; | ||||||
|  |         padding-top: 10px; | ||||||
|  |         border-collapse: collapse; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .responsive-col { |     h1 { | ||||||
|         max-width: 100% !important; |         color: #0e6942; | ||||||
|         flex-basis: 100% !important; |         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; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @media screen and (max-width: 850px) { | ||||||
|  |         h1 { | ||||||
|  |             text-align: center; | ||||||
|  |             padding-left: 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .join { | ||||||
|  |             text-align: center; | ||||||
|  |             align-items: center; | ||||||
|  |             margin-left: 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .sheet { | ||||||
|  |             width: 100%; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         main { | ||||||
|  |             display: flex; | ||||||
|  |             flex-direction: column; | ||||||
|  |             align-items: center; | ||||||
|  |             justify-content: center; | ||||||
|  |             margin: 5px; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .custom-breakpoint { | ||||||
|  |             flex-direction: column !important; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .table { | ||||||
|  |             width: 100%; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .responsive-col { | ||||||
|  |             max-width: 100% !important; | ||||||
|  |             flex-basis: 100% !important; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,141 +1,143 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import {ref, computed, onMounted, watch} from "vue"; |     import { ref, computed, onMounted, watch } from "vue"; | ||||||
| import {useI18n} from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| import {useRouter} from "vue-router"; |     import { useRouter } from "vue-router"; | ||||||
| import authState from "@/services/auth/auth-service.ts"; |     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 {useDeleteAssignmentMutation} from "@/queries/assignments.ts"; |     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||||
| import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
| import {asyncComputed} from "@vueuse/core"; |     import { asyncComputed } from "@vueuse/core"; | ||||||
| 
 | 
 | ||||||
| const {t, locale} = useI18n(); |     const { t, locale } = useI18n(); | ||||||
| const router = useRouter(); |     const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const role = ref(auth.authState.activeRole); |     const role = ref(auth.authState.activeRole); | ||||||
| const username = ref<string | undefined>(undefined); |     const username = ref<string | undefined>(undefined); | ||||||
| const isLoading = ref(false); |     const isLoading = ref(false); | ||||||
| const isError = ref(false); |     const isError = ref(false); | ||||||
| const errorMessage = ref<string>(""); |     const errorMessage = ref<string>(""); | ||||||
| 
 | 
 | ||||||
| // Load current user before rendering the page |     // Load current user before rendering the page | ||||||
| onMounted(async () => { |     onMounted(async () => { | ||||||
|     isLoading.value = true; |         isLoading.value = true; | ||||||
|     try { |         try { | ||||||
|         const userObject = await authState.loadUser(); |             const userObject = await authState.loadUser(); | ||||||
|         username.value = userObject!.profile.preferred_username; |             username.value = userObject!.profile.preferred_username; | ||||||
|     } catch (error) { |         } catch (error) { | ||||||
|         isError.value = true; |             isError.value = true; | ||||||
|         errorMessage.value = error instanceof Error ? error.message : String(error); |             errorMessage.value = error instanceof Error ? error.message : String(error); | ||||||
|     } finally { |         } finally { | ||||||
|         isLoading.value = false; |             isLoading.value = false; | ||||||
|     } |         } | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const isTeacher = computed(() => role.value === "teacher"); |  | ||||||
| const classesQueryResult = isTeacher.value ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); |  | ||||||
| 
 |  | ||||||
| const assignmentsQueryResult = isTeacher.value ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); |  | ||||||
| 
 |  | ||||||
| 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> { |  | ||||||
|     await router.push("/assignment/create"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { |  | ||||||
|     await router.push(`/assignment/${clsId}/${id}`); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const {mutate, data, isSuccess} = useDeleteAssignmentMutation(); |  | ||||||
| 
 |  | ||||||
| watch([isSuccess, data], async ([success, oldData]) => { |  | ||||||
|     if (success && oldData?.assignment) { |  | ||||||
|         window.location.reload(); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { |  | ||||||
|     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 { |     const isTeacher = computed(() => role.value === "teacher"); | ||||||
|     if (!deadline) return ""; |     const classesQueryResult = isTeacher.value | ||||||
|  |         ? useTeacherClassesQuery(username, true) | ||||||
|  |         : useStudentClassesQuery(username, true); | ||||||
| 
 | 
 | ||||||
|     const date = new Date(deadline); |     const assignmentsQueryResult = isTeacher.value | ||||||
|     const now = new Date(); |         ? useTeacherAssignmentsQuery(username, true) | ||||||
|     const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000); |         : useStudentAssignmentsQuery(username, true); | ||||||
| 
 | 
 | ||||||
|     if (date.getTime() < now.getTime()) return "deadline-passed"; |     const allAssignments = asyncComputed( | ||||||
|     if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours"; |         async () => { | ||||||
|     return "deadline-upcoming"; |             const assignments = assignmentsQueryResult.data.value?.assignments; | ||||||
| } |             if (!assignments) return []; | ||||||
| 
 | 
 | ||||||
| onMounted(async () => { |             const classes = classesQueryResult.data.value?.classes; | ||||||
|     const user = await auth.loadUser(); |             if (!classes) return []; | ||||||
|     username.value = user?.profile?.preferred_username ?? ""; |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| onMounted(async () => { |             const result = assignments.map((a) => ({ | ||||||
|     const user = await auth.loadUser(); |                 id: a.id, | ||||||
|     username.value = user?.profile?.preferred_username ?? ""; |                 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> { | ||||||
|  |         await router.push("/assignment/create"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { | ||||||
|  |         await router.push(`/assignment/${clsId}/${id}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const { mutate, data, isSuccess } = useDeleteAssignmentMutation(); | ||||||
|  | 
 | ||||||
|  |     watch([isSuccess, data], async ([success, oldData]) => { | ||||||
|  |         if (success && oldData?.assignment) { | ||||||
|  |             window.location.reload(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { | ||||||
|  |         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 ?? ""; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     onMounted(async () => { | ||||||
|  |         const user = await auth.loadUser(); | ||||||
|  |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
|  |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -151,9 +153,7 @@ onMounted(async () => { | ||||||
|             {{ t("new-assignment") }} |             {{ t("new-assignment") }} | ||||||
|         </v-btn> |         </v-btn> | ||||||
| 
 | 
 | ||||||
|         <using-query-result |         <using-query-result :query-result="assignmentsQueryResult"> | ||||||
|             :query-result="assignmentsQueryResult" |  | ||||||
|         > |  | ||||||
|             <v-container> |             <v-container> | ||||||
|                 <v-row> |                 <v-row> | ||||||
|                     <v-col |                     <v-col | ||||||
|  | @ -167,8 +167,8 @@ onMounted(async () => { | ||||||
|                                 <div class="assignment-class"> |                                 <div class="assignment-class"> | ||||||
|                                     {{ t("class") }}: |                                     {{ t("class") }}: | ||||||
|                                     <span class="class-name"> |                                     <span class="class-name"> | ||||||
|                                     {{ assignment?.class?.displayName }} |                                         {{ assignment?.class?.displayName }} | ||||||
|                                 </span> |                                     </span> | ||||||
|                                 </div> |                                 </div> | ||||||
|                                 <div |                                 <div | ||||||
|                                     class="assignment-deadline" |                                     class="assignment-deadline" | ||||||
|  | @ -214,87 +214,88 @@ onMounted(async () => { | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .assignments-container { |     .assignments-container { | ||||||
|     width: 100%; |         width: 100%; | ||||||
|     margin: 0 auto; |         margin: 0 auto; | ||||||
|     box-sizing: border-box; |         box-sizing: border-box; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .center-btn { |     .center-btn { | ||||||
|     display: block; |         display: block; | ||||||
|     margin: 0 auto 2rem auto; |         margin: 0 auto 2rem auto; | ||||||
|     font-weight: 600; |         font-weight: 600; | ||||||
|     background-color: #10ad61; |         background-color: #10ad61; | ||||||
|     color: white; |         color: white; | ||||||
|     transition: background-color 0.2s; |         transition: background-color 0.2s; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .center-btn:hover { |     .center-btn:hover { | ||||||
|     background-color: #0e6942; |         background-color: #0e6942; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .assignment-card { |     .assignment-card { | ||||||
|     padding: 1.25rem; |         padding: 1.25rem; | ||||||
|     border-radius: 16px; |         border-radius: 16px; | ||||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |         box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | ||||||
|     background-color: white; |         background-color: white; | ||||||
|     transition: transform 0.2s, |         transition: | ||||||
|     box-shadow 0.2s; |             transform 0.2s, | ||||||
| } |             box-shadow 0.2s; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| .assignment-card:hover { |     .assignment-card:hover { | ||||||
|     box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); |         box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .top-content { |     .top-content { | ||||||
|     margin-bottom: 1rem; |         margin-bottom: 1rem; | ||||||
|     word-break: break-word; |         word-break: break-word; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .assignment-title { |     .assignment-title { | ||||||
|     font-weight: 700; |         font-weight: 700; | ||||||
|     font-size: 1.4rem; |         font-size: 1.4rem; | ||||||
|     color: #0e6942; |         color: #0e6942; | ||||||
|     margin-bottom: 0.3rem; |         margin-bottom: 0.3rem; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .assignment-class, |     .assignment-class, | ||||||
| .assignment-deadline { |     .assignment-deadline { | ||||||
|     font-size: 0.95rem; |         font-size: 0.95rem; | ||||||
|     color: #444; |         color: #444; | ||||||
|     margin-bottom: 0.2rem; |         margin-bottom: 0.2rem; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .class-name { |     .class-name { | ||||||
|     font-weight: 600; |         font-weight: 600; | ||||||
|     color: #097180; |         color: #097180; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .assignment-deadline.deadline-passed { |     .assignment-deadline.deadline-passed { | ||||||
|     color: #d32f2f; |         color: #d32f2f; | ||||||
|     font-weight: bold; |         font-weight: bold; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .assignment-deadline.deadline-in24hours { |     .assignment-deadline.deadline-in24hours { | ||||||
|     color: #f57c00; |         color: #f57c00; | ||||||
|     font-weight: bold; |         font-weight: bold; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .spacer { |     .spacer { | ||||||
|     flex: 1; |         flex: 1; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .button-row { |     .button-row { | ||||||
|     display: flex; |         display: flex; | ||||||
|     justify-content: flex-end; |         justify-content: flex-end; | ||||||
|     gap: 0.75rem; |         gap: 0.75rem; | ||||||
|     flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .no-assignments { |     .no-assignments { | ||||||
|     text-align: center; |         text-align: center; | ||||||
|     font-size: 1.2rem; |         font-size: 1.2rem; | ||||||
|     color: #777; |         color: #777; | ||||||
|     padding: 3rem 0; |         padding: 3rem 0; | ||||||
| } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										3606
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3606
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana