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,18 +1,18 @@ | ||||||
| <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, | ||||||
|         () => ({ |         () => ({ | ||||||
|  | @ -20,18 +20,18 @@ const query = useGetLearningPathQuery( | ||||||
|             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); | ||||||
|  | 
 | ||||||
|  |     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("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> |  | ||||||
|                     <draggable |  | ||||||
|                         v-model="unassigned" |  | ||||||
|                         group="students" |  | ||||||
|                         item-key="username" |  | ||||||
|                         class="group-box" |  | ||||||
|         > |         > | ||||||
|                         <template #item="{ element }"> |             <h3 class="mb-2">{{ t("current-groups") }}</h3> | ||||||
|                             <v-chip>{{ element }}</v-chip> |             <div | ||||||
|                         </template> |                 v-for="(group, index) in currentGroups" | ||||||
|                     </draggable> |                 :key="'preview-' + index" | ||||||
|                 </v-col> |                 class="mb-3" | ||||||
|  |             > | ||||||
|  |                 <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" | ||||||
|  |                     > | ||||||
|  |                         {{ student.fullName }} | ||||||
|  |                     </v-chip> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
| 
 | 
 | ||||||
|                 <!-- Bestaande groepen --> |             <div | ||||||
|                 <v-col |                 v-if="unassignedStudents.length > 0" | ||||||
|                     v-for="(group, i) in groupList" |                 class="mt-3" | ||||||
|                     :key="i" |  | ||||||
|                     cols="12" |  | ||||||
|                     sm="4" |  | ||||||
|             > |             > | ||||||
|                     <h4>{{ t("group") }} {{ i + 1 }}</h4> |                 <strong>{{ t("unassigned") }}:</strong> | ||||||
|                     <draggable |                 <div class="d-flex flex-wrap"> | ||||||
|                         v-model="groupList[i]" |                     <label>{{unassignedStudents.length}}</label> | ||||||
|                         group="students" |                 </div> | ||||||
|                         item-key="username" |             </div> | ||||||
|                         class="group-box" |         </div> | ||||||
|                     > |  | ||||||
|                         <template #item="{ element }"> |  | ||||||
|                             <v-chip>{{ element }}</v-chip> |  | ||||||
|                         </template> |  | ||||||
|                     </draggable> |  | ||||||
| 
 | 
 | ||||||
|  |         <!-- Action Buttons --> | ||||||
|  |         <v-row | ||||||
|  |             justify="center" | ||||||
|  |             class="mb-4" | ||||||
|  |         > | ||||||
|             <v-btn |             <v-btn | ||||||
|                         color="error" |                 color="primary" | ||||||
|                         size="x-small" |                 @click="activeDialog = 'random'" | ||||||
|                         @click="removeGroup(i)" |  | ||||||
|                         class="mt-2" |  | ||||||
|             > |             > | ||||||
|                         {{ t("remove-group") }} |                 {{ t("randomly-create-groups") }} | ||||||
|  |             </v-btn> | ||||||
|  |             <v-btn | ||||||
|  |                 color="secondary" | ||||||
|  |                 class="ml-4" | ||||||
|  |                 @click="activeDialog = 'dragdrop'" | ||||||
|  |             > | ||||||
|  |                 {{ t("drag-and-drop") }} | ||||||
|  |             </v-btn> | ||||||
|  |         </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-btn> | ||||||
|                         </v-col> |                         </v-col> | ||||||
|                     </v-row> |                     </v-row> | ||||||
| 
 | 
 | ||||||
|             <v-btn |                     <div class="mt-4"> | ||||||
|                 color="primary" |                         <div class="d-flex justify-space-between align-center mb-2"> | ||||||
|                 class="mt-4" |                             <strong>{{ t("preview") }}</strong> | ||||||
|                 @click="addNewGroup" |                             <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" | ||||||
|                             > |                             > | ||||||
|                 {{ t("add-group") }} |                                 <v-expansion-panel-title> | ||||||
|             </v-btn> |                                     {{ 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-text> | ||||||
|  | 
 | ||||||
|                 <v-card-actions> |                 <v-card-actions> | ||||||
|  |                     <v-spacer /> | ||||||
|  |                     <v-btn | ||||||
|  |                         text | ||||||
|  |                         @click="activeDialog = null" | ||||||
|  |                         >{{ t("cancel") }}</v-btn | ||||||
|  |                     > | ||||||
|                     <v-btn |                     <v-btn | ||||||
|                         color="success" |                         color="success" | ||||||
|                 @click="saveChanges" |                         @click="saveRandomGroups" | ||||||
|  |                         :disabled="randomGroupsPreview.length === 0" | ||||||
|                     > |                     > | ||||||
|                         {{ t("save") }} |                         {{ t("save") }} | ||||||
|                     </v-btn> |                     </v-btn> | ||||||
|             <v-btn |                 </v-card-actions> | ||||||
|                 @click="$emit('done')" |             </v-card> | ||||||
|                 variant="text" |         </v-dialog> | ||||||
|  | 
 | ||||||
|  |         <!-- Drag and Drop Dialog --> | ||||||
|  |         <v-dialog | ||||||
|  |             :model-value="activeDialog === 'dragdrop'" | ||||||
|  |             @update:model-value="(val) => (val ? (activeDialog = 'dragdrop') : (activeDialog = null))" | ||||||
|  |             max-width="900" | ||||||
|         > |         > | ||||||
|                 {{ t("cancel") }} |             <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-btn> | ||||||
|                 </v-card-actions> |                 </v-card-actions> | ||||||
|             </v-card> |             </v-card> | ||||||
|  |         </v-dialog> | ||||||
|  |     </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,51 +1,51 @@ | ||||||
| <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(); |         const user = await auth.loadUser(); | ||||||
|         username.value = user?.profile?.preferred_username ?? ""; |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const language = computed(() => locale.value); |     const language = computed(() => locale.value); | ||||||
| const form = ref(); |     const form = ref(); | ||||||
| 
 | 
 | ||||||
| const learningPathsQueryResults = useGetAllLearningPaths(language); |     const learningPathsQueryResults = useGetAllLearningPaths(language); | ||||||
| const classesQueryResults = useTeacherClassesQuery(username, true); |     const classesQueryResults = useTeacherClassesQuery(username, true); | ||||||
| 
 | 
 | ||||||
| const selectedClass = ref(undefined); |     const selectedClass = ref(undefined); | ||||||
| const assignmentTitle = ref(""); |     const assignmentTitle = ref(""); | ||||||
| const selectedLearningPath = ref(route.query.hruid || undefined); |     const selectedLearningPath = ref(route.query.hruid || undefined); | ||||||
| const lpIsSelected = route.query.hruid !== undefined; |     const lpIsSelected = route.query.hruid !== undefined; | ||||||
| 
 | 
 | ||||||
| const { mutate, data, isSuccess } = useCreateAssignmentMutation(); |     const { mutate, data, isSuccess } = useCreateAssignmentMutation(); | ||||||
| 
 | 
 | ||||||
| watch([isSuccess, data], async ([success, newData]) => { |     watch([isSuccess, data], async ([success, newData]) => { | ||||||
|         if (success && newData?.assignment) { |         if (success && newData?.assignment) { | ||||||
|             await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); |             await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`); | ||||||
|         } |         } | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| async function submitFormHandler(): Promise<void> { |     async function submitFormHandler(): Promise<void> { | ||||||
|         const { valid } = await form.value.validate(); |         const { valid } = await form.value.validate(); | ||||||
|         if (!valid) return; |         if (!valid) return; | ||||||
| 
 | 
 | ||||||
|  | @ -66,7 +66,7 @@ async function submitFormHandler(): Promise<void> { | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         mutate({ cid: assignmentDTO.within, data: assignmentDTO }); |         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 { |     .form-card { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         max-width: 720px; |         max-width: 720px; | ||||||
|         border-radius: 16px; |         border-radius: 16px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .form-container { |     .form-container { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: 24px; |         gap: 24px; | ||||||
|         width: 100%; |         width: 100%; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .step-container { |     .step-container { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: 24px; |         gap: 24px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| @media (max-width: 1000px) { |     @media (max-width: 1000px) { | ||||||
|         .form-card { |         .form-card { | ||||||
|             width: 85%; |             width: 85%; | ||||||
|             padding: 1%; |             padding: 1%; | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| } |     @media (max-width: 600px) { | ||||||
| 
 |  | ||||||
| @media (max-width: 600px) { |  | ||||||
| 
 |  | ||||||
|         h1 { |         h1 { | ||||||
|             font-size: 32px; |             font-size: 32px; | ||||||
|             text-align: center; |             text-align: center; | ||||||
|             margin-left: 0; |             margin-left: 0; | ||||||
|         } |         } | ||||||
| } |     } | ||||||
| 
 |  | ||||||
| @media (max-width: 400px) { |  | ||||||
| 
 | 
 | ||||||
|  |     @media (max-width: 400px) { | ||||||
|         h1 { |         h1 { | ||||||
|             font-size: 24px; |             font-size: 24px; | ||||||
|             text-align: center; |             text-align: center; | ||||||
|             margin-left: 0; |             margin-left: 0; | ||||||
|         } |         } | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .v-card { |     .v-card { | ||||||
|         border: 2px solid #0e6942; |         border: 2px solid #0e6942; | ||||||
|         border-radius: 12px; |         border-radius: 12px; | ||||||
| } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </style> | </style> | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,37 +1,36 @@ | ||||||
| <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; |     learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
| 
 | 
 | ||||||
| 
 |     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||||
| const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); |     const group = computed(() => { | ||||||
| const group = computed(() => { |  | ||||||
|         const groups = groupsQueryResult.data.value?.groups; |         const groups = groupsQueryResult.data.value?.groups; | ||||||
| 
 | 
 | ||||||
|         if (!groups) return undefined; |         if (!groups) return undefined; | ||||||
|  | @ -45,15 +44,14 @@ const group = computed(() => { | ||||||
|                 groupNo: index + 1, // Renumbered index |                 groupNo: index + 1, // Renumbered index | ||||||
|             })) |             })) | ||||||
|             .find((group) => group.members?.some((m) => m.username === username.value)); |             .find((group) => group.members?.some((m) => m.username === username.value)); | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 |     watchEffect(() => { | ||||||
| watchEffect(() => { |  | ||||||
|         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; |         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const learningPathParams = computed(() => { |     const learningPathParams = computed(() => { | ||||||
|         if (!group.value || !learningPath.value || !lang.value) return undefined; |         if (!group.value || !learningPath.value || !lang.value) return undefined; | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|  | @ -61,30 +59,29 @@ const learningPathParams = computed(() => { | ||||||
|             assignmentNo: props.assignmentId, |             assignmentNo: props.assignmentId, | ||||||
|             classId: props.classId, |             classId: props.classId, | ||||||
|         }; |         }; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const lpQueryResult = useGetLearningPathQuery( |     const lpQueryResult = useGetLearningPathQuery( | ||||||
|         () => learningPath.value, |         () => learningPath.value, | ||||||
|         () => lang.value, |         () => lang.value, | ||||||
|     () => learningPathParams.value |         () => learningPathParams.value, | ||||||
| ); |     ); | ||||||
| 
 | 
 | ||||||
| 
 |     const progressColor = computed(() => { | ||||||
| const progressColor = computed(() => { |  | ||||||
|         const progress = calculateProgress(lpQueryResult.data.value); |         const progress = calculateProgress(lpQueryResult.data.value); | ||||||
|         if (progress >= 100) return "success"; |         if (progress >= 100) return "success"; | ||||||
|         if (progress >= 50) return "warning"; |         if (progress >= 50) return "warning"; | ||||||
|         return "error"; |         return "error"; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[] ?? undefined); |     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,25 +1,25 @@ | ||||||
| <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: ( | ||||||
|  | @ -27,39 +27,37 @@ const props = defineProps<{ | ||||||
|             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( | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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?.learningPath ?? ""), | ||||||
|         computed(() => assignmentQueryResult.data.value?.assignment?.language as Language), |         computed(() => assignmentQueryResult.data.value?.assignment?.language as Language), | ||||||
| ); |     ); | ||||||
| 
 | 
 | ||||||
| // Get all the groups withing the assignment |     // Get all the groups withing the assignment | ||||||
| const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); |     const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true); | ||||||
| groups.value = groupsQueryResult.data.value?.groups ?? []; |     groups.value = groupsQueryResult.data.value?.groups ?? []; | ||||||
| 
 | 
 | ||||||
| watchEffect(() => { |     watchEffect(() => { | ||||||
|         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; |         learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath; | ||||||
|         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; |         lang.value = assignmentQueryResult.data.value?.assignment?.language as Language; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const allGroups = computed(() => { |     const allGroups = computed(() => { | ||||||
|         const groups = groupsQueryResult.data.value?.groups; |         const groups = groupsQueryResult.data.value?.groups; | ||||||
| 
 | 
 | ||||||
|         if (!groups) return []; |         if (!groups) return []; | ||||||
|  | @ -74,56 +72,56 @@ const allGroups = computed(() => { | ||||||
|             members: group.members, |             members: group.members, | ||||||
|             originalGroupNo: group.groupNumber, // Keep original number if needed |             originalGroupNo: group.groupNumber, // Keep original number if needed | ||||||
|         })); |         })); | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const dialog = ref(false); |     const dialog = ref(false); | ||||||
| const selectedGroup = ref({}); |     const selectedGroup = ref({}); | ||||||
| 
 | 
 | ||||||
| function openGroupDetails(group): void { |     function openGroupDetails(group): void { | ||||||
|         selectedGroup.value = group; |         selectedGroup.value = group; | ||||||
|         dialog.value = true; |         dialog.value = true; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| async function deleteAssignment(num: number, clsId: string): Promise<void> { |     async function deleteAssignment(num: number, clsId: string): Promise<void> { | ||||||
|     const {mutate} = useDeleteAssignmentMutation(); |         const { mutate } = useDeleteAssignmentMutation(); | ||||||
|         mutate( |         mutate( | ||||||
|         {cid: clsId, an: num}, |             { cid: clsId, an: num }, | ||||||
|             { |             { | ||||||
|                 onSuccess: () => { |                 onSuccess: () => { | ||||||
|                     window.location.href = "/user/assignment"; |                     window.location.href = "/user/assignment"; | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ); |         ); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function goToLearningPathLink(): string | undefined { |     function goToLearningPathLink(): string | undefined { | ||||||
|         const assignment = assignmentQueryResult.data.value?.assignment; |         const assignment = assignmentQueryResult.data.value?.assignment; | ||||||
|         const lp = lpQueryResult.data.value; |         const lp = lpQueryResult.data.value; | ||||||
| 
 | 
 | ||||||
|         if (!assignment || !lp) return undefined; |         if (!assignment || !lp) return undefined; | ||||||
| 
 | 
 | ||||||
|         return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`; |         return `/learningPath/${lp.hruid}/${assignment.language}/${lp.startNode.learningobjectHruid}?assignmentNo=${props.assignmentId}&classId=${props.classId}`; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function goToGroupSubmissionLink(groupNo: number): string | undefined { |     function goToGroupSubmissionLink(groupNo: number): string | undefined { | ||||||
|         const lp = lpQueryResult.data.value; |         const lp = lpQueryResult.data.value; | ||||||
|         if (!lp) return undefined; |         if (!lp) return undefined; | ||||||
| 
 | 
 | ||||||
|         return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; |         return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| const learningPathsQueryResults = useGetAllLearningPaths(lang); |     const learningPathsQueryResults = useGetAllLearningPaths(lang); | ||||||
| 
 | 
 | ||||||
| const {mutate, data, isSuccess} = useUpdateAssignmentMutation(); |     const { mutate, data, isSuccess } = useUpdateAssignmentMutation(); | ||||||
| 
 | 
 | ||||||
| watch([isSuccess, data], ([success, newData]) => { |     watch([isSuccess, data], ([success, newData]) => { | ||||||
|         if (success && newData?.assignment) { |         if (success && newData?.assignment) { | ||||||
|             window.location.reload(); |             window.location.reload(); | ||||||
|         } |         } | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| async function saveChanges(): Promise<void> { |     async function saveChanges(): Promise<void> { | ||||||
|     const {valid} = await form.value.validate(); |         const { valid } = await form.value.validate(); | ||||||
|         if (!valid) return; |         if (!valid) return; | ||||||
| 
 | 
 | ||||||
|         isEditing.value = false; |         isEditing.value = false; | ||||||
|  | @ -140,11 +138,9 @@ async function saveChanges(): Promise<void> { | ||||||
|         mutate({ |         mutate({ | ||||||
|             cid: assignmentQueryResult.data.value?.assignment.within, |             cid: assignmentQueryResult.data.value?.assignment.within, | ||||||
|             an: assignmentQueryResult.data.value?.assignment.id, |             an: assignmentQueryResult.data.value?.assignment.id, | ||||||
|         data: assignmentDTO |             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" | ||||||
|  | @ -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=" | ||||||
|  |                                                     () => { | ||||||
|  |                                                         isEditing = false; | ||||||
|  |                                                         editingLearningPath = learningPath; | ||||||
|  |                                                     } | ||||||
|  |                                                 " | ||||||
|                                                 >{{ t("cancel") }} |                                                 >{{ t("cancel") }} | ||||||
|                                             </v-btn |                                             </v-btn> | ||||||
|                                             > |  | ||||||
| 
 | 
 | ||||||
|                                             <v-btn |                                             <v-btn | ||||||
|                                                 v-if="!isEditing" |                                                 v-if="!isEditing" | ||||||
|  | @ -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" | ||||||
|  | @ -328,15 +331,13 @@ async function saveChanges(): Promise<void> { | ||||||
|                                         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" | ||||||
|  | @ -356,7 +357,6 @@ 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> | ||||||
|  | @ -403,99 +403,104 @@ 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 { |     .header { | ||||||
|         font-weight: bold !important; |         font-weight: bold !important; | ||||||
|         background-color: #0e6942; |         background-color: #0e6942; | ||||||
|         color: white; |         color: white; | ||||||
|         padding: 10px; |         padding: 10px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| table thead th:first-child { |     table thead th:first-child { | ||||||
|         border-top-left-radius: 10px; |         border-top-left-radius: 10px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .table thead th:last-child { |     .table thead th:last-child { | ||||||
|         border-top-right-radius: 10px; |         border-top-right-radius: 10px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .table tbody tr:nth-child(odd) { |     .table tbody tr:nth-child(odd) { | ||||||
|         background-color: white; |         background-color: white; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .table tbody tr:nth-child(even) { |     .table tbody tr:nth-child(even) { | ||||||
|         background-color: #f6faf2; |         background-color: #f6faf2; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| td, |     td, | ||||||
| th { |     th { | ||||||
|         border-bottom: 1px solid #0e6942; |         border-bottom: 1px solid #0e6942; | ||||||
|         border-top: 1px solid #0e6942; |         border-top: 1px solid #0e6942; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .table { |     .table { | ||||||
|         width: 90%; |         width: 90%; | ||||||
|         padding-top: 10px; |         padding-top: 10px; | ||||||
|         border-collapse: collapse; |         border-collapse: collapse; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| h1 { |     h1 { | ||||||
|         color: #0e6942; |         color: #0e6942; | ||||||
|         text-transform: uppercase; |         text-transform: uppercase; | ||||||
|         font-weight: bolder; |         font-weight: bolder; | ||||||
|         padding-top: 2%; |         padding-top: 2%; | ||||||
|         font-size: 50px; |         font-size: 50px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| h2 { |     h2 { | ||||||
|         color: #0e6942; |         color: #0e6942; | ||||||
|         font-size: 30px; |         font-size: 30px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .join { |     .join { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: 20px; |         gap: 20px; | ||||||
|         margin-top: 50px; |         margin-top: 50px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .link { |     .link { | ||||||
|         color: #0b75bb; |         color: #0b75bb; | ||||||
|         text-decoration: underline; |         text-decoration: underline; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| main { |     main { | ||||||
|         margin-left: 30px; |         margin-left: 30px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| @media screen and (max-width: 850px) { |     @media screen and (max-width: 850px) { | ||||||
|         h1 { |         h1 { | ||||||
|             text-align: center; |             text-align: center; | ||||||
|             padding-left: 0; |             padding-left: 0; | ||||||
|  | @ -531,5 +536,5 @@ main { | ||||||
|             max-width: 100% !important; |             max-width: 100% !important; | ||||||
|             flex-basis: 100% !important; |             flex-basis: 100% !important; | ||||||
|         } |         } | ||||||
| } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,26 +1,26 @@ | ||||||
| <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(); | ||||||
|  | @ -31,14 +31,18 @@ onMounted(async () => { | ||||||
|         } finally { |         } finally { | ||||||
|             isLoading.value = false; |             isLoading.value = false; | ||||||
|         } |         } | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const isTeacher = computed(() => role.value === "teacher"); |     const isTeacher = computed(() => role.value === "teacher"); | ||||||
| const classesQueryResult = isTeacher.value ? useTeacherClassesQuery(username, true) : useStudentClassesQuery(username, true); |     const classesQueryResult = isTeacher.value | ||||||
|  |         ? useTeacherClassesQuery(username, true) | ||||||
|  |         : useStudentClassesQuery(username, true); | ||||||
| 
 | 
 | ||||||
| const assignmentsQueryResult = isTeacher.value ? useTeacherAssignmentsQuery(username, true) : useStudentAssignmentsQuery(username, true); |     const assignmentsQueryResult = isTeacher.value | ||||||
|  |         ? useTeacherAssignmentsQuery(username, true) | ||||||
|  |         : useStudentAssignmentsQuery(username, true); | ||||||
| 
 | 
 | ||||||
| const allAssignments = asyncComputed( |     const allAssignments = asyncComputed( | ||||||
|         async () => { |         async () => { | ||||||
|             const assignments = assignmentsQueryResult.data.value?.assignments; |             const assignments = assignmentsQueryResult.data.value?.assignments; | ||||||
|             if (!assignments) return []; |             if (!assignments) return []; | ||||||
|  | @ -73,30 +77,30 @@ const allAssignments = asyncComputed( | ||||||
|             }); |             }); | ||||||
|         }, |         }, | ||||||
|         [], |         [], | ||||||
|     {evaluating: true}, |         { evaluating: true }, | ||||||
| ); |     ); | ||||||
| 
 | 
 | ||||||
| async function goToCreateAssignment(): Promise<void> { |     async function goToCreateAssignment(): Promise<void> { | ||||||
|         await router.push("/assignment/create"); |         await router.push("/assignment/create"); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { |     async function goToAssignmentDetails(id: number, clsId: string): Promise<void> { | ||||||
|         await router.push(`/assignment/${clsId}/${id}`); |         await router.push(`/assignment/${clsId}/${id}`); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| const {mutate, data, isSuccess} = useDeleteAssignmentMutation(); |     const { mutate, data, isSuccess } = useDeleteAssignmentMutation(); | ||||||
| 
 | 
 | ||||||
| watch([isSuccess, data], async ([success, oldData]) => { |     watch([isSuccess, data], async ([success, oldData]) => { | ||||||
|         if (success && oldData?.assignment) { |         if (success && oldData?.assignment) { | ||||||
|             window.location.reload(); |             window.location.reload(); | ||||||
|         } |         } | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { |     async function goToDeleteAssignment(num: number, clsId: string): Promise<void> { | ||||||
|     mutate({cid: clsId, an: num}); |         mutate({ cid: clsId, an: num }); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function formatDate(date?: string | Date): string { |     function formatDate(date?: string | Date): string { | ||||||
|         if (!date) return "–"; |         if (!date) return "–"; | ||||||
|         const d = new Date(date); |         const d = new Date(date); | ||||||
| 
 | 
 | ||||||
|  | @ -111,9 +115,9 @@ function formatDate(date?: string | Date): string { | ||||||
|             hour: "numeric", |             hour: "numeric", | ||||||
|             minute: "2-digit", |             minute: "2-digit", | ||||||
|         }); |         }); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function getDeadlineClass(deadline?: string | Date): string { |     function getDeadlineClass(deadline?: string | Date): string { | ||||||
|         if (!deadline) return ""; |         if (!deadline) return ""; | ||||||
| 
 | 
 | ||||||
|         const date = new Date(deadline); |         const date = new Date(deadline); | ||||||
|  | @ -123,19 +127,17 @@ function getDeadlineClass(deadline?: string | Date): string { | ||||||
|         if (date.getTime() < now.getTime()) return "deadline-passed"; |         if (date.getTime() < now.getTime()) return "deadline-passed"; | ||||||
|         if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours"; |         if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours"; | ||||||
|         return "deadline-upcoming"; |         return "deadline-upcoming"; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| onMounted(async () => { |     onMounted(async () => { | ||||||
|         const user = await auth.loadUser(); |         const user = await auth.loadUser(); | ||||||
|         username.value = user?.profile?.preferred_username ?? ""; |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| onMounted(async () => { |     onMounted(async () => { | ||||||
|         const user = await auth.loadUser(); |         const user = await auth.loadUser(); | ||||||
|         username.value = user?.profile?.preferred_username ?? ""; |         username.value = user?.profile?.preferred_username ?? ""; | ||||||
| }); |     }); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </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 | ||||||
|  | @ -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: | ||||||
|  |             transform 0.2s, | ||||||
|             box-shadow 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