Merge remote-tracking branch 'origin/feat/class-functionality' into feat/class-functionality-fix-bugs

This commit is contained in:
Gabriellvl 2025-04-18 16:22:46 +02:00
commit 6c408d516c
9 changed files with 293 additions and 213 deletions

View file

@ -82,5 +82,7 @@
"reject": "zurückweisen", "reject": "zurückweisen",
"areusure": "Sind Sie sicher?", "areusure": "Sind Sie sicher?",
"yes": "ja", "yes": "ja",
"teachers": "Lehrer" "teachers": "Lehrer",
"rejected": "abgelehnt",
"accepted": "akzeptiert"
} }

View file

@ -82,5 +82,7 @@
"reject": "reject", "reject": "reject",
"areusure": "Are you sure?", "areusure": "Are you sure?",
"yes": "yes", "yes": "yes",
"teachers": "teachers" "teachers": "teachers",
"accepted": "accepted",
"rejected": "rejected"
} }

View file

@ -82,5 +82,7 @@
"reject": "rejeter", "reject": "rejeter",
"areusure": "Êtes-vous sûr?", "areusure": "Êtes-vous sûr?",
"yes": "oui", "yes": "oui",
"teachers": "enseignants" "teachers": "enseignants",
"accepted": "acceptée",
"rejected": "rejetée"
} }

View file

@ -82,5 +82,7 @@
"reject": "weiger", "reject": "weiger",
"areusure": "Bent u zeker?", "areusure": "Bent u zeker?",
"yes": "ja", "yes": "ja",
"teachers": "leerkrachten" "teachers": "leerkrachten",
"accepted": "geaccepteerd",
"rejected": "geweigerd"
} }

View file

@ -3,7 +3,6 @@ import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
import SingleClass from "@/views/classes/SingleClass.vue"; import SingleClass from "@/views/classes/SingleClass.vue";
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
import NotFound from "@/components/errors/NotFound.vue"; import NotFound from "@/components/errors/NotFound.vue";
import CreateClass from "@/views/classes/CreateClass.vue";
import CreateAssignment from "@/views/assignments/CreateAssignment.vue"; import CreateAssignment from "@/views/assignments/CreateAssignment.vue";
import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue";
import CallbackPage from "@/views/CallbackPage.vue"; import CallbackPage from "@/views/CallbackPage.vue";
@ -84,12 +83,6 @@ const router = createRouter({
component: SingleAssignment, component: SingleAssignment,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: "/class/create",
name: "CreateClass",
component: CreateClass,
meta: { requiresAuth: true },
},
{ {
path: "/class/:id", path: "/class/:id",
name: "SingleClass", name: "SingleClass",

View file

@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<main></main>
</template>
<style scoped></style>

View file

@ -2,72 +2,82 @@
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts"; import authState from "@/services/auth/auth-service.ts";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ClassController, type ClassResponse } from "@/controllers/classes"; import type { ClassResponse } from "@/controllers/classes";
import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students"; import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useTeacherJoinRequestsQuery, useUpdateJoinRequestMutation } from "@/queries/teachers"; import { useTeacherJoinRequestsQuery, useUpdateJoinRequestMutation } from "@/queries/teachers";
import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request"; import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request";
import { useClassDeleteStudentMutation, useClassQuery, useClassStudentsQuery } from "@/queries/classes";
const { t } = useI18n(); const { t } = useI18n();
// Username of logged in teacher
const username = ref<string | undefined>(undefined);
const classController: ClassController = new ClassController();
// Find class id from route
const route = useRoute(); const route = useRoute();
const classId: string = route.params.id as string; const classId: string = route.params.id as string;
const username = ref<string | undefined>(undefined);
const isLoading = ref(false);
const isError = ref(false);
const errorMessage = ref<string>("");
const isLoading = ref(true); // Queries used to access the backend and catch loading or errors
const currentClass = ref<ClassDTO | undefined>(undefined);
const students = ref<StudentDTO[]>([]);
// Gets the class a teacher wants to manage
const getClass = useClassQuery(classId);
// Get all students part of the class
const getStudents = useClassStudentsQuery(classId);
// Get all join requests for this class
const joinRequestsQuery = useTeacherJoinRequestsQuery(username, classId); const joinRequestsQuery = useTeacherJoinRequestsQuery(username, classId);
// Handle accepting or rejecting join requests
const { mutate } = useUpdateJoinRequestMutation(); const { mutate } = useUpdateJoinRequestMutation();
// Handle deletion of a student from the class
const { mutate: deleteStudentMutation } = useClassDeleteStudentMutation();
// Find the username of the logged in user so it can be used to fetch other information // Load current user before rendering the page
// When loading the page
onMounted(async () => { onMounted(async () => {
const userObject = await authState.loadUser(); isLoading.value = true;
username.value = userObject?.profile?.preferred_username ?? undefined; try {
const userObject = await authState.loadUser();
// Get class of which information should be shown username.value = userObject!.profile!.preferred_username;
const classResponse: ClassResponse = await classController.getById(classId); } catch (error) {
if (classResponse && classResponse.class) { isError.value = true;
currentClass.value = classResponse.class; errorMessage.value = error instanceof Error ? error.message : String(error);
} finally {
isLoading.value = false; isLoading.value = false;
} }
// Fetch all students of the class
const studentsResponse: StudentsResponse = await classController.getStudents(classId);
if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[];
}); });
// TODO: Boolean that handles visibility for dialogs // Used to set the visibility of the dialog
// Popup to verify removing student
const dialog = ref(false); const dialog = ref(false);
// Student selected for deletion
const selectedStudent = ref<StudentDTO | null>(null); const selectedStudent = ref<StudentDTO | null>(null);
// Let the teacher verify deletion of a student
function showPopup(s: StudentDTO): void { function showPopup(s: StudentDTO): void {
selectedStudent.value = s; selectedStudent.value = s;
dialog.value = true; dialog.value = true;
} }
// Remove student from class
async function removeStudentFromclass(): Promise<void> { async function removeStudentFromclass(): Promise<void> {
// TODO: replace by query // Delete student from class
if (selectedStudent.value) await classController.deleteStudent(classId, selectedStudent.value.username); deleteStudentMutation(
dialog.value = false; { id: classId, username: selectedStudent.value!.username },
{
selectedStudent.value = null; onSuccess: async () => {
//TODO when query; reload table so student not longer in table dialog.value = false;
await getStudents.refetch();
showSnackbar(t("success"), "success");
},
onError: (e) => {
dialog.value = false;
showSnackbar(t("failed") + ": " + e.message, "error");
},
},
);
} }
// TODO: query + relaoding function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean): void {
function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean) : void { // Handle acception or rejection of a join request
mutate( mutate(
{ {
teacherUsername: username.value!, teacherUsername: username.value!,
@ -76,22 +86,32 @@
accepted: accepted, accepted: accepted,
}, },
{ {
onSuccess: () => { onSuccess: async () => {
showSnackbar(t("sent"), "success"); if (accepted) {
await joinRequestsQuery.refetch();
await getStudents.refetch();
showSnackbar(t("accepted"), "success");
} else {
await joinRequestsQuery.refetch();
showSnackbar(t("rejected"), "success");
}
}, },
onError: (e) => { onError: (e) => {
// ShowSnackbar(t("failed") + ": " + e.message, "error"); showSnackbar(t("failed") + ": " + e.message, "error");
throw e;
}, },
}, },
); );
} }
// Default of snackbar values
const snackbar = ref({ const snackbar = ref({
visible: false, visible: false,
message: "", message: "",
color: "success", color: "success",
}); });
// Function to show snackbar on success or failure
function showSnackbar(message: string, color: string): void { function showSnackbar(message: string, color: string): void {
snackbar.value.message = message; snackbar.value.message = message;
snackbar.value.color = color; snackbar.value.color = color;
@ -101,131 +121,144 @@
<template> <template>
<main> <main>
<div <div
class="loading-div"
v-if="isLoading" v-if="isLoading"
class="text-center py-10"
> >
<v-progress-circular <v-progress-circular indeterminate></v-progress-circular>
indeterminate
color="primary"
/>
<p>Loading...</p>
</div> </div>
<div v-else> <div v-if="isError">
<h1 class="title">{{ currentClass!.displayName }}</h1> <v-empty-state
<v-container icon="mdi-alert-circle-outline"
fluid :text="errorMessage"
class="ma-4" :title="t('error_title')"
> ></v-empty-state>
<v-row </div>
no-gutters <using-query-result
fluid :query-result="getClass"
v-slot="classResponse: { data: ClassResponse }"
>
<div>
<h1 class="title">{{ classResponse.data.class.displayName }}</h1>
<using-query-result
:query-result="getStudents"
v-slot="studentsResponse: { data: StudentsResponse }"
> >
<v-col <v-container
cols="12" fluid
sm="6" class="ma-4"
md="6"
> >
<v-table class="table"> <v-row
<thead> no-gutters
<tr> fluid
<th class="header">{{ t("students") }}</th>
<th class="header"></th>
</tr>
</thead>
<tbody>
<tr
v-for="s in students"
:key="s.id"
>
<td>
{{ s.firstName + " " + s.lastName }}
</td>
<td>
<v-btn @click="showPopup(s)"> {{ t("remove") }} </v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col>
<using-query-result
:query-result="joinRequestsQuery"
v-slot="joinRequests: { data: JoinRequestsResponse }"
>
<v-col
cols="12"
sm="6"
md="6"
> >
<v-table class="table"> <v-col
<thead> cols="12"
<tr> sm="6"
<th class="header">{{ t("classJoinRequests") }}</th> md="6"
<th class="header">{{ t("accept") + "/" + t("reject") }}</th> >
</tr> <v-table class="table">
</thead> <thead>
<tbody> <tr>
<tr <th class="header">{{ t("students") }}</th>
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]" <th class="header"></th>
:key="(jr.class, jr.requester, jr.status)" </tr>
> </thead>
<td> <tbody>
{{ jr.requester.firstName + " " + jr.requester.lastName }} <tr
</td> v-for="s in studentsResponse.data.students as StudentDTO[]"
<td> :key="s.id"
<v-btn >
@click="handleJoinRequest(jr, true)" <td>
class="mr-2" {{ s.firstName + " " + s.lastName }}
color="green" </td>
> <td>
{{ t("accept") }}</v-btn <v-btn @click="showPopup(s)"> {{ t("remove") }} </v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col>
<using-query-result
:query-result="joinRequestsQuery"
v-slot="joinRequests: { data: JoinRequestsResponse }"
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classJoinRequests") }}</th>
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
:key="(jr.class, jr.requester, jr.status)"
> >
<td>
{{ jr.requester.firstName + " " + jr.requester.lastName }}
</td>
<td>
<v-btn
@click="handleJoinRequest(jr, true)"
class="mr-2"
color="green"
>
{{ t("accept") }}</v-btn
>
<v-btn <v-btn
@click="handleJoinRequest(jr, false)" @click="handleJoinRequest(jr, false)"
class="mr-2" class="mr-2"
color="red" color="red"
> >
{{ t("reject") }} {{ t("reject") }}
</v-btn> </v-btn>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>
</v-col> </v-col>
</using-query-result> </using-query-result>
</v-row> </v-row>
</v-container> </v-container>
</div> </using-query-result>
<v-dialog </div>
v-model="dialog" <v-dialog
max-width="400px" v-model="dialog"
> max-width="400px"
<v-card> >
<v-card-title class="headline">{{ t("areusure") }}</v-card-title> <v-card>
<v-card-title class="headline">{{ t("areusure") }}</v-card-title>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
text text
@click="dialog = false" @click="dialog = false"
> >
{{ t("cancel") }} {{ t("cancel") }}
</v-btn> </v-btn>
<v-btn <v-btn
text text
@click="removeStudentFromclass" @click="removeStudentFromclass"
>{{ t("yes") }}</v-btn >{{ t("yes") }}</v-btn
> >
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-snackbar <v-snackbar
v-model="snackbar.visible" v-model="snackbar.visible"
:color="snackbar.color" :color="snackbar.color"
timeout="3000" timeout="3000"
> >
{{ snackbar.message }} {{ snackbar.message }}
</v-snackbar> </v-snackbar>
</using-query-result>
</main> </main>
</template> </template>
<style scoped> <style scoped>

View file

@ -7,7 +7,7 @@
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { type ClassesResponse } from "@/controllers/classes"; import type { ClassesResponse } from "@/controllers/classes";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes"; import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
import type { StudentsResponse } from "@/controllers/students"; import type { StudentsResponse } from "@/controllers/students";
@ -17,16 +17,26 @@
// Username of logged in student // Username of logged in student
const username = ref<string | undefined>(undefined); const username = ref<string | undefined>(undefined);
const isLoading = ref(false);
const isError = ref(false);
const errorMessage = ref<string>("");
// Students of selected class are shown when logged in student presses on the member count // Students of selected class are shown when logged in student presses on the member count
const selectedClass = ref<ClassDTO | null>(null); const selectedClass = ref<ClassDTO | null>(null);
const getStudents = ref(false); const getStudents = ref(false);
// Find the username of the logged in user so it can be used to fetch other information // Load current user before rendering the page
// When loading the page
onMounted(async () => { onMounted(async () => {
const userObject = await authState.loadUser(); isLoading.value = true;
username.value = userObject?.profile?.preferred_username ?? undefined; try {
const userObject = await authState.loadUser();
username.value = userObject!.profile!.preferred_username;
} catch (error) {
isError.value = true;
errorMessage.value = error instanceof Error ? error.message : String(error);
} finally {
isLoading.value = false;
}
}); });
// Fetch all classes of the logged in student // Fetch all classes of the logged in student
@ -44,7 +54,7 @@
async function openStudentDialog(c: ClassDTO): Promise<void> { async function openStudentDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c; selectedClass.value = c;
// let the component know it should show the students in a class // Let the component know it should show the students in a class
getStudents.value = true; getStudents.value = true;
await getStudentsQuery.refetch(); await getStudentsQuery.refetch();
dialog.value = true; dialog.value = true;
@ -53,7 +63,7 @@
async function openTeacherDialog(c: ClassDTO): Promise<void> { async function openTeacherDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c; selectedClass.value = c;
// let the component know it should show teachers of a class // Let the component know it should show teachers of a class
getStudents.value = false; getStudents.value = false;
await getTeachersQuery.refetch(); await getTeachersQuery.refetch();
dialog.value = true; dialog.value = true;
@ -111,7 +121,20 @@
</script> </script>
<template> <template>
<main> <main>
<div> <div
class="loading-div"
v-if="isLoading"
>
<v-progress-circular indeterminate></v-progress-circular>
</div>
<div v-if="isError">
<v-empty-state
icon="mdi-alert-circle-outline"
:text="errorMessage"
:title="t('error_title')"
></v-empty-state>
</div>
<div v-else>
<h1 class="title">{{ t("classes") }}</h1> <h1 class="title">{{ t("classes") }}</h1>
<using-query-result <using-query-result
:query-result="classesQuery" :query-result="classesQuery"

View file

@ -6,7 +6,7 @@
import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import { useTeacherClassesQuery } from "@/queries/teachers"; import { useTeacherClassesQuery } from "@/queries/teachers";
import { type ClassesResponse, type ClassResponse } from "@/controllers/classes"; import type { ClassesResponse } from "@/controllers/classes";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useClassesQuery, useClassTeacherInvitationsQuery, useCreateClassMutation } from "@/queries/classes"; import { useClassesQuery, useClassTeacherInvitationsQuery, useCreateClassMutation } from "@/queries/classes";
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
@ -15,19 +15,29 @@
// Username of logged in teacher // Username of logged in teacher
const username = ref<string | undefined>(undefined); const username = ref<string | undefined>(undefined);
const isLoading = ref(false);
const isError = ref(false);
const errorMessage = ref<string>("");
// Find the username of the logged in user so it can be used to fetch other information // Load current user before rendering the page
// When loading the page
onMounted(async () => { onMounted(async () => {
const userObject = await authState.loadUser(); isLoading.value = true;
username.value = userObject?.profile?.preferred_username ?? undefined; try {
const userObject = await authState.loadUser();
username.value = userObject!.profile!.preferred_username;
} catch (error) {
isError.value = true;
errorMessage.value = error instanceof Error ? error.message : String(error);
} finally {
isLoading.value = false;
}
}); });
// Fetch all classes of the logged in teacher // Fetch all classes of the logged in teacher
const classesQuery = useTeacherClassesQuery(username, true); const classesQuery = useTeacherClassesQuery(username, true);
const allClassesQuery = useClassesQuery(); const allClassesQuery = useClassesQuery();
const { mutate } = useCreateClassMutation(); const { mutate } = useCreateClassMutation();
const getInvitationsQuery = useClassTeacherInvitationsQuery(username); const getInvitationsQuery = useClassTeacherInvitationsQuery(username); // TODO: use useTeacherInvitationsReceivedQuery
// Boolean that handles visibility for dialogs // Boolean that handles visibility for dialogs
// Creating a class will generate a popup with the generated code // Creating a class will generate a popup with the generated code
@ -111,6 +121,19 @@
</script> </script>
<template> <template>
<main> <main>
<div
class="loading-div"
v-if="isLoading"
>
<v-progress-circular indeterminate></v-progress-circular>
</div>
<div v-if="isError">
<v-empty-state
icon="mdi-alert-circle-outline"
:text="errorMessage"
:title="t('error_title')"
></v-empty-state>
</div v-else>
<div> <div>
<h1 class="title">{{ t("classes") }}</h1> <h1 class="title">{{ t("classes") }}</h1>
<using-query-result <using-query-result
@ -252,34 +275,41 @@
:query-result="getInvitationsQuery" :query-result="getInvitationsQuery"
v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
> >
<using-query-result :query-result="allClassesQuery" v-slot="classesResponse: {data: ClassesResponse}"> <using-query-result
<tr :query-result="allClassesQuery"
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" v-slot="classesResponse: { data: ClassesResponse }"
:key="i.classId"
> >
<td> <tr
{{ (classesResponse.data.classes as ClassDTO[]).filter((c) => c.id == i.classId)[0] }} v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
</td> :key="i.classId"
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td> >
<td class="text-right"> <td>
<div> {{
<v-btn (classesResponse.data.classes as ClassDTO[]).filter((c) => c.id == i.classId)[0]
color="green" }}
@click="acceptRequest" </td>
class="mr-2" <td>
> {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}
{{ t("accept") }} </td>
</v-btn> <td class="text-right">
<v-btn <div>
color="red" <v-btn
@click="denyRequest" color="green"
> @click="acceptRequest"
{{ t("deny") }} class="mr-2"
</v-btn> >
</div> {{ t("accept") }}
</td> </v-btn>
</tr> <v-btn
</using-query-result> color="red"
@click="denyRequest"
>
{{ t("deny") }}
</v-btn>
</div>
</td>
</tr>
</using-query-result>
</using-query-result> </using-query-result>
</tbody> </tbody>
</v-table> </v-table>