Merge branch 'dev' into feat/assignment-page
This commit is contained in:
		
						commit
						c29b4f8c29
					
				
					 21 changed files with 1004 additions and 490 deletions
				
			
		| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
<script setup lang="ts"></script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <main></main>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,135 +1,358 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
    import { useI18n } from "vue-i18n";
 | 
			
		||||
    import authState from "@/services/auth/auth-service.ts";
 | 
			
		||||
    import { onMounted, ref } from "vue";
 | 
			
		||||
    import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
 | 
			
		||||
    import { onMounted, ref, watchEffect } from "vue";
 | 
			
		||||
    import { useRoute } from "vue-router";
 | 
			
		||||
    import { ClassController, type ClassResponse } from "@/controllers/classes";
 | 
			
		||||
    import type { StudentsResponse } from "@/controllers/students";
 | 
			
		||||
    import type { ClassResponse } from "@/controllers/classes";
 | 
			
		||||
    import type { JoinRequestsResponse, StudentsResponse } from "@/controllers/students";
 | 
			
		||||
    import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
 | 
			
		||||
    import UsingQueryResult from "@/components/UsingQueryResult.vue";
 | 
			
		||||
    import { useTeacherJoinRequestsQuery, useUpdateJoinRequestMutation } from "@/queries/teachers";
 | 
			
		||||
    import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request";
 | 
			
		||||
    import { useClassDeleteStudentMutation, useClassQuery, useClassStudentsQuery } from "@/queries/classes";
 | 
			
		||||
    import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
 | 
			
		||||
    import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
 | 
			
		||||
    import { useDisplay } from "vuetify";
 | 
			
		||||
 | 
			
		||||
    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 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 usernameTeacher = ref<string | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
    const isLoading = ref(true);
 | 
			
		||||
    const currentClass = ref<ClassDTO | undefined>(undefined);
 | 
			
		||||
    const students = ref<StudentDTO[]>([]);
 | 
			
		||||
    // Queries used to access the backend and catch loading or errors
 | 
			
		||||
 | 
			
		||||
    // Find the username of the logged in user so it can be used to fetch other information
 | 
			
		||||
    // When loading the page
 | 
			
		||||
    // 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);
 | 
			
		||||
    // Handle accepting or rejecting join requests
 | 
			
		||||
    const { mutate } = useUpdateJoinRequestMutation();
 | 
			
		||||
    // Handle deletion of a student from the class
 | 
			
		||||
    const { mutate: deleteStudentMutation } = useClassDeleteStudentMutation();
 | 
			
		||||
    // Handle creation of teacher invites
 | 
			
		||||
    const { mutate: sentInviteMutation } = useCreateTeacherInvitationMutation();
 | 
			
		||||
 | 
			
		||||
    // Load current user before rendering the page
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
        const userObject = await authState.loadUser();
 | 
			
		||||
        username.value = userObject?.profile?.preferred_username ?? undefined;
 | 
			
		||||
 | 
			
		||||
        // Get class of which information should be shown
 | 
			
		||||
        const classResponse: ClassResponse = await classController.getById(classId);
 | 
			
		||||
        if (classResponse && classResponse.class) {
 | 
			
		||||
            currentClass.value = classResponse.class;
 | 
			
		||||
        isLoading.value = true;
 | 
			
		||||
        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 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
 | 
			
		||||
    // Popup to verify removing student
 | 
			
		||||
    // Used to set the visibility of the dialog
 | 
			
		||||
    const dialog = ref(false);
 | 
			
		||||
    // Student selected for deletion
 | 
			
		||||
    const selectedStudent = ref<StudentDTO | null>(null);
 | 
			
		||||
 | 
			
		||||
    // Let the teacher verify deletion of a student
 | 
			
		||||
    function showPopup(s: StudentDTO): void {
 | 
			
		||||
        selectedStudent.value = s;
 | 
			
		||||
        dialog.value = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove student from class
 | 
			
		||||
    function removeStudentFromclass(): void {
 | 
			
		||||
        dialog.value = false;
 | 
			
		||||
    async function removeStudentFromclass(): Promise<void> {
 | 
			
		||||
        // Delete student from class
 | 
			
		||||
        deleteStudentMutation(
 | 
			
		||||
            { id: classId, username: selectedStudent.value!.username },
 | 
			
		||||
            {
 | 
			
		||||
                onSuccess: async () => {
 | 
			
		||||
                    dialog.value = false;
 | 
			
		||||
                    await getStudents.refetch();
 | 
			
		||||
                    showSnackbar(t("success"), "success");
 | 
			
		||||
                },
 | 
			
		||||
                onError: (e) => {
 | 
			
		||||
                    dialog.value = false;
 | 
			
		||||
                    showSnackbar(t("failed") + ": " + e.message, "error");
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function handleJoinRequest(c: ClassJoinRequestDTO, accepted: boolean): void {
 | 
			
		||||
        // Handle acception or rejection of a join request
 | 
			
		||||
        mutate(
 | 
			
		||||
            {
 | 
			
		||||
                teacherUsername: username.value!,
 | 
			
		||||
                studentUsername: c.requester.username,
 | 
			
		||||
                classId: c.class,
 | 
			
		||||
                accepted: accepted,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                onSuccess: async () => {
 | 
			
		||||
                    if (accepted) {
 | 
			
		||||
                        await joinRequestsQuery.refetch();
 | 
			
		||||
                        await getStudents.refetch();
 | 
			
		||||
 | 
			
		||||
                        showSnackbar(t("accepted"), "success");
 | 
			
		||||
                    } else {
 | 
			
		||||
                        await joinRequestsQuery.refetch();
 | 
			
		||||
                        showSnackbar(t("rejected"), "success");
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                onError: (e) => {
 | 
			
		||||
                    showSnackbar(t("failed") + ": " + e.message, "error");
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function sentInvite(): void {
 | 
			
		||||
        if (!usernameTeacher.value) {
 | 
			
		||||
            showSnackbar(t("please enter a valid username"), "error");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const data: TeacherInvitationData = {
 | 
			
		||||
            sender: username.value!,
 | 
			
		||||
            receiver: usernameTeacher.value,
 | 
			
		||||
            class: classId,
 | 
			
		||||
        };
 | 
			
		||||
        sentInviteMutation(data, {
 | 
			
		||||
            onSuccess: () => {
 | 
			
		||||
                usernameTeacher.value = "";
 | 
			
		||||
            },
 | 
			
		||||
            onError: (e) => {
 | 
			
		||||
                showSnackbar(t("failed") + ": " + e.message, "error");
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Default of snackbar values
 | 
			
		||||
    const snackbar = ref({
 | 
			
		||||
        visible: false,
 | 
			
		||||
        message: "",
 | 
			
		||||
        color: "success",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Function to show snackbar on success or failure
 | 
			
		||||
    function showSnackbar(message: string, color: string): void {
 | 
			
		||||
        snackbar.value.message = message;
 | 
			
		||||
        snackbar.value.color = color;
 | 
			
		||||
        snackbar.value.visible = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Custom breakpoints
 | 
			
		||||
    const customBreakpoints = {
 | 
			
		||||
        xs: 0,
 | 
			
		||||
        sm: 500,
 | 
			
		||||
        md: 1370,
 | 
			
		||||
        lg: 1400,
 | 
			
		||||
        xl: 1600,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Logic for small screens
 | 
			
		||||
    const display = useDisplay();
 | 
			
		||||
 | 
			
		||||
    // Reactive variables to hold custom logic based on breakpoints
 | 
			
		||||
    const isSmAndDown = ref(false);
 | 
			
		||||
    const isMdAndDown = ref(false);
 | 
			
		||||
 | 
			
		||||
    watchEffect(() => {
 | 
			
		||||
        // Custom breakpoint logic
 | 
			
		||||
        isSmAndDown.value = display.width.value < customBreakpoints.sm;
 | 
			
		||||
        isMdAndDown.value = display.width.value < customBreakpoints.md;
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
    <main>
 | 
			
		||||
        <div
 | 
			
		||||
            class="loading-div"
 | 
			
		||||
            v-if="isLoading"
 | 
			
		||||
            class="text-center py-10"
 | 
			
		||||
        >
 | 
			
		||||
            <v-progress-circular
 | 
			
		||||
                indeterminate
 | 
			
		||||
                color="primary"
 | 
			
		||||
            />
 | 
			
		||||
            <p>Loading...</p>
 | 
			
		||||
            <v-progress-circular indeterminate></v-progress-circular>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
            <h1 class="title">{{ currentClass!.displayName }}</h1>
 | 
			
		||||
            <v-container
 | 
			
		||||
                fluid
 | 
			
		||||
                class="ma-4"
 | 
			
		||||
            >
 | 
			
		||||
                <v-row
 | 
			
		||||
                    no-gutters
 | 
			
		||||
                    fluid
 | 
			
		||||
        <div v-if="isError">
 | 
			
		||||
            <v-empty-state
 | 
			
		||||
                icon="mdi-alert-circle-outline"
 | 
			
		||||
                :text="errorMessage"
 | 
			
		||||
                :title="t('error_title')"
 | 
			
		||||
            ></v-empty-state>
 | 
			
		||||
        </div>
 | 
			
		||||
        <using-query-result
 | 
			
		||||
            :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
 | 
			
		||||
                        cols="12"
 | 
			
		||||
                        sm="6"
 | 
			
		||||
                        md="6"
 | 
			
		||||
                    <v-container
 | 
			
		||||
                        fluid
 | 
			
		||||
                        class="ma-4"
 | 
			
		||||
                    >
 | 
			
		||||
                        <v-table class="table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th class="header">{{ t("students") }}</th>
 | 
			
		||||
                                    <th class="header"></th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                <tr
 | 
			
		||||
                                    v-for="s in students"
 | 
			
		||||
                                    :key="s.id"
 | 
			
		||||
                        <v-row
 | 
			
		||||
                            no-gutters
 | 
			
		||||
                            fluid
 | 
			
		||||
                        >
 | 
			
		||||
                            <v-col
 | 
			
		||||
                                cols="12"
 | 
			
		||||
                                sm="6"
 | 
			
		||||
                                md="6"
 | 
			
		||||
                            >
 | 
			
		||||
                                <v-table class="table">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th class="header">{{ t("students") }}</th>
 | 
			
		||||
                                            <th class="header"></th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        <tr
 | 
			
		||||
                                            v-for="s in studentsResponse.data.students as StudentDTO[]"
 | 
			
		||||
                                            :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"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {{ s.firstName + " " + s.lastName }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <v-btn @click="showPopup"> {{ t("remove") }} </v-btn>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </v-table>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                </v-row>
 | 
			
		||||
            </v-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <v-dialog
 | 
			
		||||
            v-model="dialog"
 | 
			
		||||
            max-width="400px"
 | 
			
		||||
        >
 | 
			
		||||
            <v-card>
 | 
			
		||||
                <v-card-title class="headline">{{ t("areusure") }}</v-card-title>
 | 
			
		||||
                                    <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>
 | 
			
		||||
                                                    <span v-if="!isSmAndDown && !isMdAndDown">
 | 
			
		||||
                                                        <v-btn
 | 
			
		||||
                                                            @click="handleJoinRequest(jr, true)"
 | 
			
		||||
                                                            class="mr-2"
 | 
			
		||||
                                                            color="green"
 | 
			
		||||
                                                        >
 | 
			
		||||
                                                            {{ t("accept") }}</v-btn
 | 
			
		||||
                                                        >
 | 
			
		||||
 | 
			
		||||
                <v-card-actions>
 | 
			
		||||
                    <v-spacer></v-spacer>
 | 
			
		||||
                    <v-btn
 | 
			
		||||
                        text
 | 
			
		||||
                        @click="dialog = false"
 | 
			
		||||
                                                        <v-btn
 | 
			
		||||
                                                            @click="handleJoinRequest(jr, false)"
 | 
			
		||||
                                                            class="mr-2"
 | 
			
		||||
                                                            color="red"
 | 
			
		||||
                                                        >
 | 
			
		||||
                                                            {{ t("reject") }}
 | 
			
		||||
                                                        </v-btn>
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                    <span v-else>
 | 
			
		||||
                                                        <v-btn
 | 
			
		||||
                                                            @click="handleJoinRequest(jr, true)"
 | 
			
		||||
                                                            icon="mdi-check-circle"
 | 
			
		||||
                                                            class="mr-2"
 | 
			
		||||
                                                            color="green"
 | 
			
		||||
                                                            variant="text"
 | 
			
		||||
                                                        ></v-btn>
 | 
			
		||||
                                                        <v-btn
 | 
			
		||||
                                                            @click="handleJoinRequest(jr, false)"
 | 
			
		||||
                                                            icon="mdi-close-circle"
 | 
			
		||||
                                                            class="mr-2"
 | 
			
		||||
                                                            color="red"
 | 
			
		||||
                                                            variant="text"
 | 
			
		||||
                                                        ></v-btn>
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                </td>
 | 
			
		||||
                                            </tr>
 | 
			
		||||
                                        </tbody>
 | 
			
		||||
                                    </v-table>
 | 
			
		||||
                                </v-col>
 | 
			
		||||
                            </using-query-result>
 | 
			
		||||
                        </v-row>
 | 
			
		||||
                    </v-container>
 | 
			
		||||
                </using-query-result>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <div class="join">
 | 
			
		||||
                    <h2>{{ t("invitations") }}</h2>
 | 
			
		||||
                    <p>{{ t("enterUsername") }}</p>
 | 
			
		||||
 | 
			
		||||
                    <v-sheet
 | 
			
		||||
                        class="pa-4 sheet"
 | 
			
		||||
                        max-width="400"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ t("cancel") }}
 | 
			
		||||
                    </v-btn>
 | 
			
		||||
                    <v-btn
 | 
			
		||||
                        text
 | 
			
		||||
                        @click="removeStudentFromclass"
 | 
			
		||||
                        >{{ t("yes") }}</v-btn
 | 
			
		||||
                    >
 | 
			
		||||
                </v-card-actions>
 | 
			
		||||
            </v-card>
 | 
			
		||||
        </v-dialog>
 | 
			
		||||
                        <v-form @submit.prevent>
 | 
			
		||||
                            <v-text-field
 | 
			
		||||
                                :label="`${t('username')}`"
 | 
			
		||||
                                v-model="usernameTeacher"
 | 
			
		||||
                                :placeholder="`${t('username')}`"
 | 
			
		||||
                                variant="outlined"
 | 
			
		||||
                            ></v-text-field>
 | 
			
		||||
                            <v-btn
 | 
			
		||||
                                class="mt-4"
 | 
			
		||||
                                color="#f6faf2"
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                @click="sentInvite"
 | 
			
		||||
                                block
 | 
			
		||||
                                >{{ t("invite") }}</v-btn
 | 
			
		||||
                            >
 | 
			
		||||
                        </v-form>
 | 
			
		||||
                    </v-sheet>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <v-dialog
 | 
			
		||||
                v-model="dialog"
 | 
			
		||||
                max-width="400px"
 | 
			
		||||
            >
 | 
			
		||||
                <v-card>
 | 
			
		||||
                    <v-card-title class="headline">{{ t("areusure") }}</v-card-title>
 | 
			
		||||
 | 
			
		||||
                    <v-card-actions>
 | 
			
		||||
                        <v-spacer></v-spacer>
 | 
			
		||||
                        <v-btn
 | 
			
		||||
                            text
 | 
			
		||||
                            @click="dialog = false"
 | 
			
		||||
                        >
 | 
			
		||||
                            {{ t("cancel") }}
 | 
			
		||||
                        </v-btn>
 | 
			
		||||
                        <v-btn
 | 
			
		||||
                            text
 | 
			
		||||
                            @click="removeStudentFromclass"
 | 
			
		||||
                            >{{ t("yes") }}</v-btn
 | 
			
		||||
                        >
 | 
			
		||||
                    </v-card-actions>
 | 
			
		||||
                </v-card>
 | 
			
		||||
            </v-dialog>
 | 
			
		||||
            <v-snackbar
 | 
			
		||||
                v-model="snackbar.visible"
 | 
			
		||||
                :color="snackbar.color"
 | 
			
		||||
                timeout="3000"
 | 
			
		||||
            >
 | 
			
		||||
                {{ snackbar.message }}
 | 
			
		||||
            </v-snackbar>
 | 
			
		||||
        </using-query-result>
 | 
			
		||||
    </main>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,51 +1,51 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
    import { useI18n } from "vue-i18n";
 | 
			
		||||
    import authState from "@/services/auth/auth-service.ts";
 | 
			
		||||
    import { computed, onMounted, ref, type ComputedRef } from "vue";
 | 
			
		||||
    import { computed, onMounted, ref } from "vue";
 | 
			
		||||
    import { validate, version } from "uuid";
 | 
			
		||||
    import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
 | 
			
		||||
    import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
 | 
			
		||||
    import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
 | 
			
		||||
    import { StudentController } from "@/controllers/students";
 | 
			
		||||
    import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
 | 
			
		||||
    import { TeacherController } from "@/controllers/teachers";
 | 
			
		||||
    import type { ClassesResponse } from "@/controllers/classes";
 | 
			
		||||
    import UsingQueryResult from "@/components/UsingQueryResult.vue";
 | 
			
		||||
    import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
 | 
			
		||||
    import type { StudentsResponse } from "@/controllers/students";
 | 
			
		||||
    import type { TeachersResponse } from "@/controllers/teachers";
 | 
			
		||||
 | 
			
		||||
    const { t } = useI18n();
 | 
			
		||||
    const studentController: StudentController = new StudentController();
 | 
			
		||||
    const teacherController: TeacherController = new TeacherController();
 | 
			
		||||
 | 
			
		||||
    // Username of logged in student
 | 
			
		||||
    const username = ref<string | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
    // Find the username of the logged in user so it can be used to fetch other information
 | 
			
		||||
    // When loading the page
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
        const userObject = await authState.loadUser();
 | 
			
		||||
        username.value = userObject?.profile?.preferred_username ?? undefined;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Fetch all classes of the logged in student
 | 
			
		||||
    const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username);
 | 
			
		||||
 | 
			
		||||
    // Empty list when classes are not yet loaded, else the list of classes of the user
 | 
			
		||||
    const classes: ComputedRef<ClassDTO[]> = computed(() => {
 | 
			
		||||
        // The classes are not yet fetched
 | 
			
		||||
        if (!classesResponse.value) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        // The user has no classes
 | 
			
		||||
        if (classesResponse.value.classes.length === 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        return classesResponse.value.classes as ClassDTO[];
 | 
			
		||||
    });
 | 
			
		||||
    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
 | 
			
		||||
    const selectedClass = ref<ClassDTO | null>(null);
 | 
			
		||||
    const students = ref<StudentDTO[]>([]);
 | 
			
		||||
    const teachers = ref<TeacherDTO[]>([]);
 | 
			
		||||
    const getStudents = ref(false);
 | 
			
		||||
 | 
			
		||||
    // Load current user before rendering the page
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
        isLoading.value = true;
 | 
			
		||||
        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
 | 
			
		||||
    const classesQuery = useStudentClassesQuery(username);
 | 
			
		||||
    // Fetch all students of the class
 | 
			
		||||
    const getStudentsQuery = useClassStudentsQuery(computed(() => selectedClass.value?.id));
 | 
			
		||||
    // Fetch all teachers of the class
 | 
			
		||||
    const getTeachersQuery = useClassTeachersQuery(computed(() => selectedClass.value?.id));
 | 
			
		||||
 | 
			
		||||
    // Boolean that handles visibility for dialogs
 | 
			
		||||
    // Clicking on membercount will show a dialog with all members
 | 
			
		||||
    const dialog = ref(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -54,48 +54,19 @@
 | 
			
		|||
    async function openStudentDialog(c: ClassDTO): Promise<void> {
 | 
			
		||||
        selectedClass.value = c;
 | 
			
		||||
 | 
			
		||||
        // Clear previous value
 | 
			
		||||
        // Let the component know it should show the students in a class
 | 
			
		||||
        getStudents.value = true;
 | 
			
		||||
        students.value = [];
 | 
			
		||||
        await getStudentsQuery.refetch();
 | 
			
		||||
        dialog.value = true;
 | 
			
		||||
 | 
			
		||||
        // Fetch students from their usernames to display their full names
 | 
			
		||||
        const studentDTOs: (StudentDTO | null)[] = await Promise.all(
 | 
			
		||||
            c.students.map(async (uid) => {
 | 
			
		||||
                try {
 | 
			
		||||
                    const res = await studentController.getByUsername(uid);
 | 
			
		||||
                    return res.student;
 | 
			
		||||
                } catch (_) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Only show students that are not fetched ass *null*
 | 
			
		||||
        students.value = studentDTOs.filter(Boolean) as StudentDTO[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function openTeacherDialog(c: ClassDTO): Promise<void> {
 | 
			
		||||
        selectedClass.value = c;
 | 
			
		||||
 | 
			
		||||
        // Clear previous value
 | 
			
		||||
        // Let the component know it should show teachers of a class
 | 
			
		||||
        getStudents.value = false;
 | 
			
		||||
        teachers.value = [];
 | 
			
		||||
        await getTeachersQuery.refetch();
 | 
			
		||||
        dialog.value = true;
 | 
			
		||||
 | 
			
		||||
        // Fetch names of teachers
 | 
			
		||||
        const teacherDTOs: (TeacherDTO | null)[] = await Promise.all(
 | 
			
		||||
            c.teachers.map(async (uid) => {
 | 
			
		||||
                try {
 | 
			
		||||
                    const res = await teacherController.getByUsername(uid);
 | 
			
		||||
                    return res.teacher;
 | 
			
		||||
                } catch (_) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Hold the code a student gives in to join a class
 | 
			
		||||
| 
						 | 
				
			
			@ -151,100 +122,111 @@
 | 
			
		|||
<template>
 | 
			
		||||
    <main>
 | 
			
		||||
        <div
 | 
			
		||||
            class="loading-div"
 | 
			
		||||
            v-if="isLoading"
 | 
			
		||||
            class="text-center py-10"
 | 
			
		||||
        >
 | 
			
		||||
            <v-progress-circular
 | 
			
		||||
                indeterminate
 | 
			
		||||
                color="primary"
 | 
			
		||||
            />
 | 
			
		||||
            <p>Loading...</p>
 | 
			
		||||
            <v-progress-circular indeterminate></v-progress-circular>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-else-if="error"
 | 
			
		||||
            class="text-center py-10 text-error"
 | 
			
		||||
        >
 | 
			
		||||
            <v-icon large>mdi-alert-circle</v-icon>
 | 
			
		||||
            <p>Error loading: {{ error.message }}</p>
 | 
			
		||||
        <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>
 | 
			
		||||
            <v-container
 | 
			
		||||
                fluid
 | 
			
		||||
                class="ma-4"
 | 
			
		||||
            <using-query-result
 | 
			
		||||
                :query-result="classesQuery"
 | 
			
		||||
                v-slot="classResponse: { data: ClassesResponse }"
 | 
			
		||||
            >
 | 
			
		||||
                <v-row
 | 
			
		||||
                    no-gutters
 | 
			
		||||
                <v-container
 | 
			
		||||
                    fluid
 | 
			
		||||
                    class="ma-4"
 | 
			
		||||
                >
 | 
			
		||||
                    <v-col
 | 
			
		||||
                        cols="12"
 | 
			
		||||
                        sm="6"
 | 
			
		||||
                        md="6"
 | 
			
		||||
                    <v-row
 | 
			
		||||
                        no-gutters
 | 
			
		||||
                        fluid
 | 
			
		||||
                    >
 | 
			
		||||
                        <v-table class="table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th class="header">{{ t("classes") }}</th>
 | 
			
		||||
                                    <th class="header">{{ t("teachers") }}</th>
 | 
			
		||||
                                    <th class="header">{{ t("members") }}</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                <tr
 | 
			
		||||
                                    v-for="c in classes"
 | 
			
		||||
                                    :key="c.id"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <td>{{ c.displayName }}</td>
 | 
			
		||||
                                    <td
 | 
			
		||||
                                        class="link"
 | 
			
		||||
                                        @click="openTeacherDialog(c)"
 | 
			
		||||
                        <v-col
 | 
			
		||||
                            cols="12"
 | 
			
		||||
                            sm="6"
 | 
			
		||||
                            md="6"
 | 
			
		||||
                        >
 | 
			
		||||
                            <v-table class="table">
 | 
			
		||||
                                <thead>
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <th class="header">{{ t("classes") }}</th>
 | 
			
		||||
                                        <th class="header">{{ t("teachers") }}</th>
 | 
			
		||||
                                        <th class="header">{{ t("members") }}</th>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </thead>
 | 
			
		||||
                                <tbody>
 | 
			
		||||
                                    <tr
 | 
			
		||||
                                        v-for="c in classResponse.data.classes as ClassDTO[]"
 | 
			
		||||
                                        :key="c.id"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {{ c.teachers.length }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td
 | 
			
		||||
                                        class="link"
 | 
			
		||||
                                        @click="openStudentDialog(c)"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {{ c.students.length }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </v-table>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                </v-row>
 | 
			
		||||
            </v-container>
 | 
			
		||||
                                        <td>{{ c.displayName }}</td>
 | 
			
		||||
                                        <td
 | 
			
		||||
                                            class="link"
 | 
			
		||||
                                            @click="openTeacherDialog(c)"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {{ c.teachers.length }}
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td
 | 
			
		||||
                                            class="link"
 | 
			
		||||
                                            @click="openStudentDialog(c)"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {{ c.students.length }}
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </tbody>
 | 
			
		||||
                            </v-table>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                    </v-row>
 | 
			
		||||
                </v-container>
 | 
			
		||||
            </using-query-result>
 | 
			
		||||
 | 
			
		||||
            <v-dialog
 | 
			
		||||
                v-if="selectedClass"
 | 
			
		||||
                v-model="dialog"
 | 
			
		||||
                width="400"
 | 
			
		||||
            >
 | 
			
		||||
                <v-card>
 | 
			
		||||
                    <v-card-title> {{ selectedClass?.displayName }} </v-card-title>
 | 
			
		||||
                    <v-card-title> {{ selectedClass!.displayName }} </v-card-title>
 | 
			
		||||
                    <v-card-text>
 | 
			
		||||
                        <ul v-if="getStudents">
 | 
			
		||||
                            <li
 | 
			
		||||
                                v-for="student in students"
 | 
			
		||||
                                :key="student.username"
 | 
			
		||||
                            <using-query-result
 | 
			
		||||
                                :query-result="getStudentsQuery"
 | 
			
		||||
                                v-slot="studentsResponse: { data: StudentsResponse }"
 | 
			
		||||
                            >
 | 
			
		||||
                                {{ student.firstName + " " + student.lastName }}
 | 
			
		||||
                            </li>
 | 
			
		||||
                                <li
 | 
			
		||||
                                    v-for="student in studentsResponse.data.students as StudentDTO[]"
 | 
			
		||||
                                    :key="student.username"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {{ student.firstName + " " + student.lastName }}
 | 
			
		||||
                                </li>
 | 
			
		||||
                            </using-query-result>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                        <ul v-else>
 | 
			
		||||
                            <li
 | 
			
		||||
                                v-for="teacher in teachers"
 | 
			
		||||
                                :key="teacher.username"
 | 
			
		||||
                            <using-query-result
 | 
			
		||||
                                :query-result="getTeachersQuery"
 | 
			
		||||
                                v-slot="teachersResponse: { data: TeachersResponse }"
 | 
			
		||||
                            >
 | 
			
		||||
                                {{ teacher.firstName + " " + teacher.lastName }}
 | 
			
		||||
                            </li>
 | 
			
		||||
                                <li
 | 
			
		||||
                                    v-for="teacher in teachersResponse.data.teachers as TeacherDTO[]"
 | 
			
		||||
                                    :key="teacher.username"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {{ teacher.firstName + " " + teacher.lastName }}
 | 
			
		||||
                                </li>
 | 
			
		||||
                            </using-query-result>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </v-card-text>
 | 
			
		||||
                    <v-card-actions>
 | 
			
		||||
                        <v-btn
 | 
			
		||||
                            color="primary"
 | 
			
		||||
                            @click="dialog = false"
 | 
			
		||||
                            >Close</v-btn
 | 
			
		||||
                            >{{ t("close") }}</v-btn
 | 
			
		||||
                        >
 | 
			
		||||
                    </v-card-actions>
 | 
			
		||||
                </v-card>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,41 +1,49 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
    import { useI18n } from "vue-i18n";
 | 
			
		||||
    import authState from "@/services/auth/auth-service.ts";
 | 
			
		||||
    import { computed, onMounted, ref, type ComputedRef } from "vue";
 | 
			
		||||
    import { onMounted, ref, watchEffect } from "vue";
 | 
			
		||||
    import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
 | 
			
		||||
    import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
 | 
			
		||||
    import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
 | 
			
		||||
    import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
 | 
			
		||||
    import { useTeacherClassesQuery } from "@/queries/teachers";
 | 
			
		||||
    import { ClassController, type ClassResponse } from "@/controllers/classes";
 | 
			
		||||
    import type { ClassesResponse } from "@/controllers/classes";
 | 
			
		||||
    import UsingQueryResult from "@/components/UsingQueryResult.vue";
 | 
			
		||||
    import { useClassesQuery, useCreateClassMutation } from "@/queries/classes";
 | 
			
		||||
    import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
 | 
			
		||||
    import {
 | 
			
		||||
        useRespondTeacherInvitationMutation,
 | 
			
		||||
        useTeacherInvitationsReceivedQuery,
 | 
			
		||||
    } from "@/queries/teacher-invitations";
 | 
			
		||||
    import { useDisplay } from "vuetify";
 | 
			
		||||
 | 
			
		||||
    const { t } = useI18n();
 | 
			
		||||
    const classController = new ClassController();
 | 
			
		||||
 | 
			
		||||
    // Username of logged in teacher
 | 
			
		||||
    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
 | 
			
		||||
    // When loading the page
 | 
			
		||||
    // Load current user before rendering the page
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
        const userObject = await authState.loadUser();
 | 
			
		||||
        username.value = userObject?.profile?.preferred_username ?? undefined;
 | 
			
		||||
        isLoading.value = true;
 | 
			
		||||
        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
 | 
			
		||||
    const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true);
 | 
			
		||||
 | 
			
		||||
    // Empty list when classes are not yet loaded, else the list of classes of the user
 | 
			
		||||
    const classes: ComputedRef<ClassDTO[]> = computed(() => {
 | 
			
		||||
        // The classes are not yet fetched
 | 
			
		||||
        if (!classesResponse.value) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        // The user has no classes
 | 
			
		||||
        if (classesResponse.value.classes.length === 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        return classesResponse.value.classes as ClassDTO[];
 | 
			
		||||
    });
 | 
			
		||||
    const classesQuery = useTeacherClassesQuery(username, true);
 | 
			
		||||
    const allClassesQuery = useClassesQuery();
 | 
			
		||||
    const { mutate } = useCreateClassMutation();
 | 
			
		||||
    const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username);
 | 
			
		||||
    const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation();
 | 
			
		||||
 | 
			
		||||
    // Boolean that handles visibility for dialogs
 | 
			
		||||
    // Creating a class will generate a popup with the generated code
 | 
			
		||||
| 
						 | 
				
			
			@ -44,19 +52,26 @@
 | 
			
		|||
    // Code generated when new class was created
 | 
			
		||||
    const code = ref<string>("");
 | 
			
		||||
 | 
			
		||||
    // TODO: waiting on frontend controllers
 | 
			
		||||
    const invitations = ref<TeacherInvitationDTO[]>([]);
 | 
			
		||||
    // Function to handle an invitation request
 | 
			
		||||
    function handleInvitation(ti: TeacherInvitationDTO, accepted: boolean): void {
 | 
			
		||||
        const data: TeacherInvitationData = {
 | 
			
		||||
            sender: (ti.sender as TeacherDTO).id,
 | 
			
		||||
            receiver: (ti.receiver as TeacherDTO).id,
 | 
			
		||||
            class: ti.classId,
 | 
			
		||||
            accepted: accepted,
 | 
			
		||||
        };
 | 
			
		||||
        respondToInvitation(data, {
 | 
			
		||||
            onSuccess: async () => {
 | 
			
		||||
                if (accepted) {
 | 
			
		||||
                    await classesQuery.refetch();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
    // Function to handle a accepted invitation request
 | 
			
		||||
    function acceptRequest(): void {
 | 
			
		||||
        //TODO: avoid linting issues when merging by filling the function
 | 
			
		||||
        invitations.value = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Function to handle a denied invitation request
 | 
			
		||||
    function denyRequest(): void {
 | 
			
		||||
        //TODO: avoid linting issues when merging by filling the function
 | 
			
		||||
        invitations.value = [];
 | 
			
		||||
                await getInvitationsQuery.refetch();
 | 
			
		||||
            },
 | 
			
		||||
            onError: (e) => {
 | 
			
		||||
                showSnackbar(t("failed") + ": " + e.message, "error");
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Teacher should be able to set a displayname when making a class
 | 
			
		||||
| 
						 | 
				
			
			@ -76,25 +91,25 @@
 | 
			
		|||
    async function createClass(): Promise<void> {
 | 
			
		||||
        // Check if the class name is valid
 | 
			
		||||
        if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
 | 
			
		||||
            try {
 | 
			
		||||
                const classDto: ClassDTO = {
 | 
			
		||||
                    id: "",
 | 
			
		||||
                    displayName: className.value,
 | 
			
		||||
                    teachers: [username.value!],
 | 
			
		||||
                    students: [],
 | 
			
		||||
                    joinRequests: [],
 | 
			
		||||
                };
 | 
			
		||||
                const classResponse: ClassResponse = await classController.createClass(classDto);
 | 
			
		||||
                const createdClass: ClassDTO = classResponse.class;
 | 
			
		||||
                code.value = createdClass.id;
 | 
			
		||||
                dialog.value = true;
 | 
			
		||||
                showSnackbar(t("created"), "success");
 | 
			
		||||
            const classDto: ClassDTO = {
 | 
			
		||||
                id: "",
 | 
			
		||||
                displayName: className.value,
 | 
			
		||||
                teachers: [username.value!],
 | 
			
		||||
                students: [],
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
                // Reload the table with classes so the new class appears
 | 
			
		||||
                await refetch();
 | 
			
		||||
            } catch (_) {
 | 
			
		||||
                showSnackbar(t("wrong"), "error");
 | 
			
		||||
            }
 | 
			
		||||
            mutate(classDto, {
 | 
			
		||||
                onSuccess: async (classResponse) => {
 | 
			
		||||
                    showSnackbar(t("classCreated"), "success");
 | 
			
		||||
                    const createdClass: ClassDTO = classResponse.class;
 | 
			
		||||
                    code.value = createdClass.id;
 | 
			
		||||
                    await classesQuery.refetch();
 | 
			
		||||
                },
 | 
			
		||||
                onError: (err) => {
 | 
			
		||||
                    showSnackbar(t("creationFailed") + ": " + err.message, "error");
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
            dialog.value = true;
 | 
			
		||||
        }
 | 
			
		||||
        if (!className.value || className.value === "") {
 | 
			
		||||
            showSnackbar(t("name is mandatory"), "error");
 | 
			
		||||
| 
						 | 
				
			
			@ -121,188 +136,272 @@
 | 
			
		|||
        await navigator.clipboard.writeText(code.value);
 | 
			
		||||
        copied.value = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Custom breakpoints
 | 
			
		||||
    const customBreakpoints = {
 | 
			
		||||
        xs: 0,
 | 
			
		||||
        sm: 500,
 | 
			
		||||
        md: 1370,
 | 
			
		||||
        lg: 1400,
 | 
			
		||||
        xl: 1600,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Logic for small screens
 | 
			
		||||
    const display = useDisplay();
 | 
			
		||||
 | 
			
		||||
    // Reactive variables to hold custom logic based on breakpoints
 | 
			
		||||
    const isMdAndDown = ref(false);
 | 
			
		||||
    const isSmAndDown = ref(false);
 | 
			
		||||
 | 
			
		||||
    watchEffect(() => {
 | 
			
		||||
        // Custom breakpoint logic
 | 
			
		||||
        isMdAndDown.value = display.width.value < customBreakpoints.md;
 | 
			
		||||
        isSmAndDown.value = display.width.value < customBreakpoints.sm;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Code display dialog logic
 | 
			
		||||
    const viewCodeDialog = ref(false);
 | 
			
		||||
    const selectedCode = ref("");
 | 
			
		||||
    function openCodeDialog(codeToView: string): void {
 | 
			
		||||
        selectedCode.value = codeToView;
 | 
			
		||||
        viewCodeDialog.value = true;
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
    <main>
 | 
			
		||||
        <div
 | 
			
		||||
            class="loading-div"
 | 
			
		||||
            v-if="isLoading"
 | 
			
		||||
            class="text-center py-10"
 | 
			
		||||
        >
 | 
			
		||||
            <v-progress-circular
 | 
			
		||||
                indeterminate
 | 
			
		||||
                color="primary"
 | 
			
		||||
            />
 | 
			
		||||
            <p>Loading...</p>
 | 
			
		||||
            <v-progress-circular indeterminate></v-progress-circular>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-else-if="error"
 | 
			
		||||
            class="text-center py-10 text-error"
 | 
			
		||||
        >
 | 
			
		||||
            <v-icon large>mdi-alert-circle</v-icon>
 | 
			
		||||
            <p>Error loading: {{ error.message }}</p>
 | 
			
		||||
        <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>
 | 
			
		||||
            <v-container
 | 
			
		||||
                fluid
 | 
			
		||||
                class="ma-4"
 | 
			
		||||
            <using-query-result
 | 
			
		||||
                :query-result="classesQuery"
 | 
			
		||||
                v-slot="classesResponse: { data: ClassesResponse }"
 | 
			
		||||
            >
 | 
			
		||||
                <v-row
 | 
			
		||||
                    no-gutters
 | 
			
		||||
                <v-container
 | 
			
		||||
                    fluid
 | 
			
		||||
                    class="ma-4"
 | 
			
		||||
                >
 | 
			
		||||
                    <v-col
 | 
			
		||||
                        cols="12"
 | 
			
		||||
                        sm="6"
 | 
			
		||||
                        md="6"
 | 
			
		||||
                    <v-row
 | 
			
		||||
                        no-gutters
 | 
			
		||||
                        class="custom-breakpoint"
 | 
			
		||||
                    >
 | 
			
		||||
                        <v-table class="table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th class="header">{{ t("classes") }}</th>
 | 
			
		||||
                                    <th class="header">
 | 
			
		||||
                                        {{ t("code") }}
 | 
			
		||||
                                    </th>
 | 
			
		||||
                                    <th class="header">{{ t("members") }}</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                <tr
 | 
			
		||||
                                    v-for="c in classes"
 | 
			
		||||
                                    :key="c.id"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <v-btn
 | 
			
		||||
                                            :to="`/class/${c.id}`"
 | 
			
		||||
                                            variant="text"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {{ c.displayName }}
 | 
			
		||||
                                            <v-icon end> mdi-menu-right </v-icon>
 | 
			
		||||
                                        </v-btn>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ c.id }}</td>
 | 
			
		||||
                                    <td>{{ c.students.length }}</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </v-table>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col
 | 
			
		||||
                        cols="12"
 | 
			
		||||
                        sm="6"
 | 
			
		||||
                        md="6"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h2>{{ t("createClass") }}</h2>
 | 
			
		||||
 | 
			
		||||
                            <v-sheet
 | 
			
		||||
                                class="pa-4 sheet"
 | 
			
		||||
                                max-width="600px"
 | 
			
		||||
                            >
 | 
			
		||||
                                <p>{{ t("createClassInstructions") }}</p>
 | 
			
		||||
                                <v-form @submit.prevent>
 | 
			
		||||
                                    <v-text-field
 | 
			
		||||
                                        class="mt-4"
 | 
			
		||||
                                        :label="`${t('classname')}`"
 | 
			
		||||
                                        v-model="className"
 | 
			
		||||
                                        :placeholder="`${t('EnterNameOfClass')}`"
 | 
			
		||||
                                        :rules="nameRules"
 | 
			
		||||
                                        variant="outlined"
 | 
			
		||||
                                    ></v-text-field>
 | 
			
		||||
                                    <v-btn
 | 
			
		||||
                                        class="mt-4"
 | 
			
		||||
                                        color="#f6faf2"
 | 
			
		||||
                                        type="submit"
 | 
			
		||||
                                        @click="createClass"
 | 
			
		||||
                                        block
 | 
			
		||||
                                        >{{ t("create") }}</v-btn
 | 
			
		||||
                        <v-col
 | 
			
		||||
                            cols="12"
 | 
			
		||||
                            sm="6"
 | 
			
		||||
                            md="6"
 | 
			
		||||
                            class="responsive-col"
 | 
			
		||||
                        >
 | 
			
		||||
                            <v-table class="table">
 | 
			
		||||
                                <thead>
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <th class="header">{{ t("classes") }}</th>
 | 
			
		||||
                                        <th class="header">
 | 
			
		||||
                                            {{ t("code") }}
 | 
			
		||||
                                        </th>
 | 
			
		||||
                                        <th class="header">{{ t("members") }}</th>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </thead>
 | 
			
		||||
                                <tbody>
 | 
			
		||||
                                    <tr
 | 
			
		||||
                                        v-for="c in classesResponse.data.classes as ClassDTO[]"
 | 
			
		||||
                                        :key="c.id"
 | 
			
		||||
                                    >
 | 
			
		||||
                                </v-form>
 | 
			
		||||
                            </v-sheet>
 | 
			
		||||
                            <v-container>
 | 
			
		||||
                                <v-dialog
 | 
			
		||||
                                    v-model="dialog"
 | 
			
		||||
                                    max-width="400px"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <v-card>
 | 
			
		||||
                                        <v-card-title class="headline">code</v-card-title>
 | 
			
		||||
                                        <v-card-text>
 | 
			
		||||
                                            <v-text-field
 | 
			
		||||
                                                v-model="code"
 | 
			
		||||
                                                readonly
 | 
			
		||||
                                                append-inner-icon="mdi-content-copy"
 | 
			
		||||
                                                @click:append-inner="copyToClipboard"
 | 
			
		||||
                                            ></v-text-field>
 | 
			
		||||
                                            <v-slide-y-transition>
 | 
			
		||||
                                                <div
 | 
			
		||||
                                                    v-if="copied"
 | 
			
		||||
                                                    class="text-center mt-2"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {{ t("copied") }}
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </v-slide-y-transition>
 | 
			
		||||
                                        </v-card-text>
 | 
			
		||||
                                        <v-card-actions>
 | 
			
		||||
                                            <v-spacer></v-spacer>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            <v-btn
 | 
			
		||||
                                                text
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    dialog = false;
 | 
			
		||||
                                                    copied = false;
 | 
			
		||||
                                                "
 | 
			
		||||
                                                :to="`/class/${c.id}`"
 | 
			
		||||
                                                variant="text"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {{ t("close") }}
 | 
			
		||||
                                                {{ c.displayName }}
 | 
			
		||||
                                                <v-icon end> mdi-menu-right </v-icon>
 | 
			
		||||
                                            </v-btn>
 | 
			
		||||
                                        </v-card-actions>
 | 
			
		||||
                                    </v-card>
 | 
			
		||||
                                </v-dialog>
 | 
			
		||||
                            </v-container>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                </v-row>
 | 
			
		||||
            </v-container>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            <span v-if="!isMdAndDown">{{ c.id }}</span>
 | 
			
		||||
                                            <span
 | 
			
		||||
                                                v-else
 | 
			
		||||
                                                style="cursor: pointer"
 | 
			
		||||
                                                @click="openCodeDialog(c.id)"
 | 
			
		||||
                                                ><v-icon icon="mdi-eye"></v-icon
 | 
			
		||||
                                            ></span>
 | 
			
		||||
                                        </td>
 | 
			
		||||
 | 
			
		||||
                                        <td>{{ c.students.length }}</td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </tbody>
 | 
			
		||||
                            </v-table>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                        <v-col
 | 
			
		||||
                            cols="12"
 | 
			
		||||
                            sm="6"
 | 
			
		||||
                            md="6"
 | 
			
		||||
                            class="responsive-col"
 | 
			
		||||
                        >
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <h2>{{ t("createClass") }}</h2>
 | 
			
		||||
 | 
			
		||||
                                <v-sheet
 | 
			
		||||
                                    class="pa-4 sheet"
 | 
			
		||||
                                    max-width="600px"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <p>{{ t("createClassInstructions") }}</p>
 | 
			
		||||
                                    <v-form @submit.prevent>
 | 
			
		||||
                                        <v-text-field
 | 
			
		||||
                                            class="mt-4"
 | 
			
		||||
                                            :label="`${t('classname')}`"
 | 
			
		||||
                                            v-model="className"
 | 
			
		||||
                                            :placeholder="`${t('EnterNameOfClass')}`"
 | 
			
		||||
                                            :rules="nameRules"
 | 
			
		||||
                                            variant="outlined"
 | 
			
		||||
                                        ></v-text-field>
 | 
			
		||||
                                        <v-btn
 | 
			
		||||
                                            class="mt-4"
 | 
			
		||||
                                            color="#f6faf2"
 | 
			
		||||
                                            type="submit"
 | 
			
		||||
                                            @click="createClass"
 | 
			
		||||
                                            block
 | 
			
		||||
                                            >{{ t("create") }}</v-btn
 | 
			
		||||
                                        >
 | 
			
		||||
                                    </v-form>
 | 
			
		||||
                                </v-sheet>
 | 
			
		||||
                                <v-container>
 | 
			
		||||
                                    <v-dialog
 | 
			
		||||
                                        v-model="dialog"
 | 
			
		||||
                                        max-width="400px"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <v-card>
 | 
			
		||||
                                            <v-card-title class="headline">code</v-card-title>
 | 
			
		||||
                                            <v-card-text>
 | 
			
		||||
                                                <v-text-field
 | 
			
		||||
                                                    v-model="code"
 | 
			
		||||
                                                    readonly
 | 
			
		||||
                                                    append-inner-icon="mdi-content-copy"
 | 
			
		||||
                                                    @click:append-inner="copyToClipboard"
 | 
			
		||||
                                                ></v-text-field>
 | 
			
		||||
                                                <v-slide-y-transition>
 | 
			
		||||
                                                    <div
 | 
			
		||||
                                                        v-if="copied"
 | 
			
		||||
                                                        class="text-center mt-2"
 | 
			
		||||
                                                    >
 | 
			
		||||
                                                        {{ t("copied") }}
 | 
			
		||||
                                                    </div>
 | 
			
		||||
                                                </v-slide-y-transition>
 | 
			
		||||
                                            </v-card-text>
 | 
			
		||||
                                            <v-card-actions>
 | 
			
		||||
                                                <v-spacer></v-spacer>
 | 
			
		||||
                                                <v-btn
 | 
			
		||||
                                                    text
 | 
			
		||||
                                                    @click="
 | 
			
		||||
                                                        dialog = false;
 | 
			
		||||
                                                        copied = false;
 | 
			
		||||
                                                    "
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {{ t("close") }}
 | 
			
		||||
                                                </v-btn>
 | 
			
		||||
                                            </v-card-actions>
 | 
			
		||||
                                        </v-card>
 | 
			
		||||
                                    </v-dialog>
 | 
			
		||||
                                </v-container>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                    </v-row>
 | 
			
		||||
                </v-container>
 | 
			
		||||
            </using-query-result>
 | 
			
		||||
 | 
			
		||||
            <h1 class="title">
 | 
			
		||||
                {{ t("invitations") }}
 | 
			
		||||
            </h1>
 | 
			
		||||
            <v-table class="table">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th class="header">{{ t("class") }}</th>
 | 
			
		||||
                        <th class="header">{{ t("sender") }}</th>
 | 
			
		||||
                        <th class="header"></th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <tr
 | 
			
		||||
                        v-for="i in invitations"
 | 
			
		||||
                        :key="i.classId"
 | 
			
		||||
                    >
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {{ i.classId }}
 | 
			
		||||
                            <!-- TODO fetch display name via classId because db only returns classId field -->
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
 | 
			
		||||
                        <td class="text-right">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <v-btn
 | 
			
		||||
                                    color="green"
 | 
			
		||||
                                    @click="acceptRequest"
 | 
			
		||||
                                    class="mr-2"
 | 
			
		||||
            <v-container
 | 
			
		||||
                fluid
 | 
			
		||||
                class="ma-4"
 | 
			
		||||
            >
 | 
			
		||||
                <v-table class="table">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th class="header">{{ t("class") }}</th>
 | 
			
		||||
                            <th class="header">{{ t("sender") }}</th>
 | 
			
		||||
                            <th class="header">{{ t("accept") + "/" + t("reject") }}</th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                        <using-query-result
 | 
			
		||||
                            :query-result="getInvitationsQuery"
 | 
			
		||||
                            v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
 | 
			
		||||
                        >
 | 
			
		||||
                            <using-query-result
 | 
			
		||||
                                :query-result="allClassesQuery"
 | 
			
		||||
                                v-slot="classesResponse: { data: ClassesResponse }"
 | 
			
		||||
                            >
 | 
			
		||||
                                <tr
 | 
			
		||||
                                    v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
 | 
			
		||||
                                    :key="i.classId"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {{ t("accept") }}
 | 
			
		||||
                                </v-btn>
 | 
			
		||||
                                <v-btn
 | 
			
		||||
                                    color="red"
 | 
			
		||||
                                    @click="denyRequest"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {{ t("deny") }}
 | 
			
		||||
                                </v-btn>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </v-table>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {{
 | 
			
		||||
                                            (classesResponse.data.classes as ClassDTO[]).filter(
 | 
			
		||||
                                                (c) => c.id == i.classId,
 | 
			
		||||
                                            )[0].displayName
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {{
 | 
			
		||||
                                            (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td class="text-right">
 | 
			
		||||
                                        <span v-if="!isSmAndDown">
 | 
			
		||||
                                            <div>
 | 
			
		||||
                                                <v-btn
 | 
			
		||||
                                                    color="green"
 | 
			
		||||
                                                    @click="handleInvitation(i, true)"
 | 
			
		||||
                                                    class="mr-2"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {{ t("accept") }}
 | 
			
		||||
                                                </v-btn>
 | 
			
		||||
                                                <v-btn
 | 
			
		||||
                                                    color="red"
 | 
			
		||||
                                                    @click="handleInvitation(i, false)"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {{ t("deny") }}
 | 
			
		||||
                                                </v-btn>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                        <span v-else>
 | 
			
		||||
                                            <div>
 | 
			
		||||
                                                <v-btn
 | 
			
		||||
                                                    @click="handleInvitation(i, true)"
 | 
			
		||||
                                                    class="mr-2"
 | 
			
		||||
                                                    icon="mdi-check-circle"
 | 
			
		||||
                                                    color="green"
 | 
			
		||||
                                                    variant="text"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                </v-btn>
 | 
			
		||||
                                                <v-btn
 | 
			
		||||
                                                    @click="handleInvitation(i, false)"
 | 
			
		||||
                                                    class="mr-2"
 | 
			
		||||
                                                    icon="mdi-close-circle"
 | 
			
		||||
                                                    color="red"
 | 
			
		||||
                                                    variant="text"
 | 
			
		||||
                                                >
 | 
			
		||||
                                                </v-btn></div
 | 
			
		||||
                                        ></span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </using-query-result>
 | 
			
		||||
                        </using-query-result>
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </v-table>
 | 
			
		||||
            </v-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <v-snackbar
 | 
			
		||||
            v-model="snackbar.visible"
 | 
			
		||||
| 
						 | 
				
			
			@ -311,6 +410,42 @@
 | 
			
		|||
        >
 | 
			
		||||
            {{ snackbar.message }}
 | 
			
		||||
        </v-snackbar>
 | 
			
		||||
        <v-dialog
 | 
			
		||||
            v-model="viewCodeDialog"
 | 
			
		||||
            max-width="400px"
 | 
			
		||||
        >
 | 
			
		||||
            <v-card>
 | 
			
		||||
                <v-card-title class="headline">{{ t("code") }}</v-card-title>
 | 
			
		||||
                <v-card-text>
 | 
			
		||||
                    <v-text-field
 | 
			
		||||
                        v-model="selectedCode"
 | 
			
		||||
                        readonly
 | 
			
		||||
                        append-inner-icon="mdi-content-copy"
 | 
			
		||||
                        @click:append-inner="copyToClipboard"
 | 
			
		||||
                    ></v-text-field>
 | 
			
		||||
                    <v-slide-y-transition>
 | 
			
		||||
                        <div
 | 
			
		||||
                            v-if="copied"
 | 
			
		||||
                            class="text-center mt-2"
 | 
			
		||||
                        >
 | 
			
		||||
                            {{ t("copied") }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </v-slide-y-transition>
 | 
			
		||||
                </v-card-text>
 | 
			
		||||
                <v-card-actions>
 | 
			
		||||
                    <v-spacer></v-spacer>
 | 
			
		||||
                    <v-btn
 | 
			
		||||
                        text
 | 
			
		||||
                        @click="
 | 
			
		||||
                            viewCodeDialog = false;
 | 
			
		||||
                            copied = false;
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ t("close") }}
 | 
			
		||||
                    </v-btn>
 | 
			
		||||
                </v-card-actions>
 | 
			
		||||
            </v-card>
 | 
			
		||||
        </v-dialog>
 | 
			
		||||
    </main>
 | 
			
		||||
</template>
 | 
			
		||||
<style scoped>
 | 
			
		||||
| 
						 | 
				
			
			@ -378,7 +513,7 @@
 | 
			
		|||
        margin-left: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @media screen and (max-width: 800px) {
 | 
			
		||||
    @media screen and (max-width: 850px) {
 | 
			
		||||
        h1 {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            padding-left: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -401,5 +536,18 @@
 | 
			
		|||
            justify-content: center;
 | 
			
		||||
            margin: 5px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .custom-breakpoint {
 | 
			
		||||
            flex-direction: column !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .table {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .responsive-col {
 | 
			
		||||
            max-width: 100% !important;
 | 
			
		||||
            flex-basis: 100% !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue