feat: bezig met edit groups

This commit is contained in:
Joyelle Ndagijimana 2025-05-16 00:35:30 +02:00
parent cc31effd61
commit 936a34b709
4 changed files with 351 additions and 266 deletions

View file

@ -27,6 +27,7 @@
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.2", "vue-i18n": "^11.1.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuedraggable": "^2.24.3",
"vuetify": "^3.7.12", "vuetify": "^3.7.12",
"wait-on": "^8.0.3" "wait-on": "^8.0.3"
}, },

View file

@ -1,75 +1,114 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref, } from "vue";
import { useI18n } from "vue-i18n"; import draggable from "vuedraggable";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import { useI18n } from "vue-i18n";
import type { StudentsResponse } from "@/controllers/students.ts";
import { useClassStudentsQuery } from "@/queries/classes.ts";
const props = defineProps<{ const props = defineProps<{
classId: string | undefined; classId: string | undefined;
groups: string[][]; groups: string[][];
}>(); }>();
const emit = defineEmits(["groupCreated"]); const emit = defineEmits(["done", "groupsUpdated"]);
const { t } = useI18n(); const { t } = useI18n();
const selectedStudents = ref([]); const groupList = ref(props.groups.map(g => [...g])); // deep copy
const unassigned = ref<string[]>([]); // voor vrije studenten
const studentQueryResult = useClassStudentsQuery(() => props.classId, true); function addNewGroup() {
groupList.value.push([]);
}
function filterStudents(data: StudentsResponse): { title: string; value: string }[] { function removeGroup(index: number) {
const students = data.students; unassigned.value.push(...groupList.value[index]);
const studentsInGroups = props.groups.flat(); groupList.value.splice(index, 1);
}
return students function saveChanges() {
?.map((st) => ({ emit("groupsUpdated", groupList.value);
title: `${st.firstName} ${st.lastName}`, emit("done");
value: st.username, }
}))
.filter((student) => !studentsInGroups.includes(student.value));
}
function createGroup(): void {
if (selectedStudents.value.length) {
// Extract only usernames (student.value)
const usernames = selectedStudents.value.map((student) => student.value);
emit("groupCreated", usernames);
selectedStudents.value = []; // Reset selection after creating group
}
}
</script> </script>
<template> <template>
<using-query-result <v-card>
:query-result="studentQueryResult" <v-card-title>{{ t("edit-groups") }}</v-card-title>
v-slot="{ data }: { data: StudentsResponse }"
>
<h3>{{ t("create-groups") }}</h3>
<v-card-text> <v-card-text>
<v-combobox <v-row>
v-model="selectedStudents" <!-- Ongegroepeerde studenten -->
:items="filterStudents(data)" <v-col cols="12" sm="4">
item-title="title" <h4>{{ t("unassigned") }}</h4>
item-value="value" <draggable
:label="t('choose-students')" v-model="unassigned"
variant="outlined" group="students"
clearable item-key="username"
multiple class="group-box"
hide-details >
density="compact" <template #item="{ element }">
chips <v-chip>{{ element }}</v-chip>
append-inner-icon="mdi-magnify" </template>
></v-combobox> </draggable>
</v-col>
<!-- Bestaande groepen -->
<v-col
v-for="(group, i) in groupList"
:key="i"
cols="12"
sm="4"
>
<h4>{{ t("group") }} {{ i + 1 }}</h4>
<draggable
v-model="groupList[i]"
group="students"
item-key="username"
class="group-box"
>
<template #item="{ element }">
<v-chip>{{ element }}</v-chip>
</template>
</draggable>
<v-btn
color="error"
size="x-small"
@click="removeGroup(i)"
class="mt-2"
>
{{ t("remove-group") }}
</v-btn>
</v-col>
</v-row>
<v-btn <v-btn
@click="createGroup"
color="primary" color="primary"
class="mt-2" class="mt-4"
size="small" @click="addNewGroup"
> >
{{ t("create-group") }} {{ t("add-group") }}
</v-btn> </v-btn>
</v-card-text> </v-card-text>
</using-query-result> <v-card-actions>
<v-btn
color="success"
@click="saveChanges"
>
{{ t("save") }}
</v-btn>
<v-btn
@click="$emit('done')"
variant="text"
>
{{ t("cancel") }}
</v-btn>
</v-card-actions>
</v-card>
</template> </template>
<style scoped></style> <style scoped>
.group-box {
min-height: 100px;
border: 1px dashed #ccc;
padding: 8px;
margin-bottom: 16px;
background-color: #fafafa;
}
</style>

View file

@ -17,7 +17,7 @@ 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 router from "@/router"; import GroupSelector from "@/components/assignments/GroupSelector.vue";
const props = defineProps<{ const props = defineProps<{
classId: string; classId: string;
@ -35,9 +35,12 @@ 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 editingLearningPath = ref(learningPath); const editingLearningPath = ref(learningPath);
const description = ref(""); const description = ref("");
const editGroups = ref(false);
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId); const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
@ -111,7 +114,7 @@ function goToGroupSubmissionLink(groupNo: number): string | undefined {
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) {
@ -120,9 +123,10 @@ watch([isSuccess, data], ([success, newData]) => {
}); });
async function saveChanges(): Promise<void> { async function saveChanges(): Promise<void> {
const {valid} = await form.value.validate();
if (!valid) return;
isEditing.value = false; isEditing.value = false;
//const { valid } = await form.value.validate();
//if (!valid) return;
const lp = learningPath.value; const lp = learningPath.value;
@ -133,8 +137,14 @@ async function saveChanges(): Promise<void> {
deadline: new Date(), deadline: new Date(),
}; };
mutate({ cid: assignmentQueryResult.data.value?.assignment.within, an: assignmentQueryResult.data.value?.assignment.id, data: assignmentDTO }); mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO
});
} }
</script> </script>
<template> <template>
@ -157,237 +167,256 @@ async function saveChanges(): Promise<void> {
md="6" md="6"
class="responsive-col" class="responsive-col"
> >
<v-card <v-form ref="form" validate-on="submit lazy" @submit.prevent="saveChanges">
v-if="assignmentResponse" <v-card
class="assignment-card" v-if="assignmentResponse"
> class="assignment-card"
<div class="top-buttons"> >
<div class="top-buttons-wrapper"> <div class="top-buttons">
<v-btn <div class="top-buttons-wrapper">
icon
variant="text"
class="back-btn"
to="/user/assignment"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<div class="right-buttons">
<v-btn <v-btn
v-if="!isEditing"
icon icon
variant="text" variant="text"
class="top_next_to_right_button" class="back-btn"
@click=" to="/user/assignment"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<div class="right-buttons">
<v-btn
v-if="!isEditing"
icon
variant="text"
class="top_next_to_right_button"
@click="
() => { () => {
isEditing = true; isEditing = true;
description = assignmentResponse.data.assignment.description; description = assignmentResponse.data.assignment.description;
} }
" "
> >
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
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"
icon icon
variant="text" variant="text"
class="top-right-btn" class="top-right-btn"
@click=" @click="
deleteAssignment( deleteAssignment(
assignmentResponse.data.assignment.id, assignmentResponse.data.assignment.id,
assignmentResponse.data.assignment.within, assignmentResponse.data.assignment.within,
) )
" "
> >
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-else v-else
icon icon
variant="text" variant="text"
class="top_next_to_right_button" class="top_next_to_right_button"
@click="saveChanges" @click="saveChanges"
> >
<v-icon>mdi-content-save-edit-outline</v-icon> <v-icon>mdi-content-save-edit-outline</v-icon>
</v-btn> </v-btn>
</div>
</div> </div>
</div> </div>
</div>
<v-card-title class="text-h4 assignmentTopTitle">{{ <v-card-title class="text-h4 assignmentTopTitle">{{
assignmentResponse.data.assignment.title assignmentResponse.data.assignment.title
}} }}
</v-card-title> </v-card-title>
<v-card-subtitle <v-card-subtitle
v-if="!isEditing" v-if="!isEditing"
class="subtitle-section" class="subtitle-section"
>
<using-query-result
:query-result="lpQueryResult"
v-slot="{ data: lpData }"
> >
<v-btn <using-query-result
v-if="lpData" :query-result="lpQueryResult"
:to="goToLearningPathLink()" v-slot="{ data: lpData }"
variant="tonal"
color="primary"
> >
{{ t("learning-path") }} <v-btn
</v-btn> v-if="lpData"
</using-query-result> :to="goToLearningPathLink()"
</v-card-subtitle> variant="tonal"
<using-query-result color="primary"
v-else >
:query-result="learningPathsQueryResults" {{ t("learning-path") }}
v-slot="{ data }: { data: LearningPath[] }" </v-btn>
> </using-query-result>
<v-card-text> </v-card-subtitle>
<v-combobox <using-query-result
v-model="editingLearningPath" v-else
:items="data" :query-result="learningPathsQueryResults"
:label="t('choose-lp')" v-slot="{ data }: { data: LearningPath[] }"
:rules="learningPathRules" >
variant="outlined" <v-card-text>
clearable <v-combobox
hide-details v-model="editingLearningPath"
density="compact" :items="data"
append-inner-icon="mdi-magnify" :label="t('choose-lp')"
item-title="title" :rules="learningPathRules"
item-value="hruid" variant="outlined"
required clearable
:filter=" hide-details
density="compact"
append-inner-icon="mdi-magnify"
item-title="title"
item-value="hruid"
required
:filter="
(item, query: string) => (item, query: string) =>
item.title.toLowerCase().includes(query.toLowerCase()) item.title.toLowerCase().includes(query.toLowerCase())
" "
></v-combobox> ></v-combobox>
</v-card-text>
</using-query-result>
<v-card-text
v-if="!isEditing"
class="description"
>
{{ assignmentResponse.data.assignment.description }}
</v-card-text>
<v-card-text v-else>
<v-textarea
v-model="description"
:label="t('description')"
variant="outlined"
density="compact"
auto-grow
rows="3"
:rules="descriptionRules"
></v-textarea>
</v-card-text>
<v-dialog
v-model="dialog"
max-width="50%"
>
<v-card>
<v-card-title class="headline">{{ t("members") }}</v-card-title>
<v-card-text>
<v-list>
<v-list-item
v-for="(member, index) in selectedGroup.members"
:key="index"
>
<v-list-item-content>
<v-list-item-title
>{{ member.firstName + " " + member.lastName }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card-text> </v-card-text>
<v-card-actions> </using-query-result>
<v-btn
color="primary" <v-card-text
@click="dialog = false" v-if="!isEditing"
>Close class="description"
</v-btn >
{{ assignmentResponse.data.assignment.description }}
</v-card-text>
<v-card-text v-else>
<v-textarea
v-model="description"
:label="t('description')"
variant="outlined"
density="compact"
auto-grow
rows="3"
:rules="descriptionRules"
></v-textarea>
</v-card-text>
</v-card>
</v-form>
<!-- A pop up to show group members -->
<v-dialog
v-model="dialog"
max-width="50%"
>
<v-card>
<v-card-title class="headline">{{ t("members") }}</v-card-title>
<v-card-text>
<v-list>
<v-list-item
v-for="(member, index) in selectedGroup.members"
:key="index"
> >
</v-card-actions> <v-list-item-content>
</v-card> <v-list-item-title
</v-dialog> >{{ member.firstName + " " + member.lastName }}
</v-card> </v-list-item-title>
</v-col> </v-list-item-content>
<v-col </v-list-item>
cols="12" </v-list>
sm="6" </v-card-text>
md="6" <v-card-actions>
class="responsive-col"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("group") }}</th>
<th class="header">{{ t("progress") }}</th>
<th class="header">{{ t("submission") }}</th>
<th class="header">
<v-icon>mdi-pencil</v-icon>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="g in allGroups"
:key="g.originalGroupNo"
>
<td>
<v-btn <v-btn
@click="openGroupDetails(g)" color="primary"
variant="text" @click="dialog = false"
>Close
</v-btn
> >
{{ g.name }} </v-card-actions>
<v-icon end>mdi-menu-right</v-icon> </v-card>
</v-btn> </v-dialog>
</td>
<td>
<GroupProgressRow
:group-number="g.originalGroupNo"
:learning-path="learningPath"
:language="lang"
:assignment-id="assignmentId"
:class-id="classId"
/>
</td>
<td>
<GroupSubmissionStatus
:group="g"
:assignment-id="assignmentId"
:class-id="classId"
:language="lang"
:go-to-group-submission-link="goToGroupSubmissionLink"
/>
</td>
<!-- Edit icon -->
<td>
<v-btn
to="/user"
variant="text"
>
<v-icon color="red"> mdi-delete</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col> </v-col>
<!-- The second column of the screen -->
<template v-if="!editGroups">
<v-col
cols="12"
sm="6"
md="6"
class="responsive-col"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("group") }}</th>
<th class="header">{{ t("progress") }}</th>
<th class="header">{{ t("submission") }}</th>
<th class="header">
<v-btn
@click="editGroups = true"
variant="text"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="g in allGroups"
:key="g.originalGroupNo"
>
<td>
<v-btn
@click="openGroupDetails(g)"
variant="text"
>
{{ g.name }}
<v-icon end>mdi-menu-right</v-icon>
</v-btn>
</td>
<td>
<GroupProgressRow
:group-number="g.originalGroupNo"
:learning-path="learningPath"
:language="lang"
:assignment-id="assignmentId"
:class-id="classId"
/>
</td>
<td>
<GroupSubmissionStatus
:group="g"
:assignment-id="assignmentId"
:class-id="classId"
:language="lang"
:go-to-group-submission-link="goToGroupSubmissionLink"
/>
</td>
<!-- Edit icon -->
<td>
<v-btn
@click=""
variant="text"
>
<v-icon color="red">mdi-delete</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</template>
<template v-else>
<GroupSelector
:groups="allGroups"
:class-id="classId"
@groupsUpdated="handleUpdatedGroups"
/>
</template>
</v-row> </v-row>
</v-container> </v-container>
</using-query-result> </using-query-result>

16
package-lock.json generated
View file

@ -107,6 +107,7 @@
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.2", "vue-i18n": "^11.1.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuedraggable": "^2.24.3",
"vuetify": "^3.7.12", "vuetify": "^3.7.12",
"wait-on": "^8.0.3" "wait-on": "^8.0.3"
}, },
@ -7599,6 +7600,12 @@
"node": ">= 6.0.0" "node": ">= 6.0.0"
} }
}, },
"node_modules/sortablejs": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
@ -8970,6 +8977,15 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vuedraggable": {
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.10.2"
}
},
"node_modules/vuetify": { "node_modules/vuetify": {
"version": "3.8.2", "version": "3.8.2",
"license": "MIT", "license": "MIT",