Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin pagina #131

Merged
merged 9 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ export default {
subjects: "My subjects",
announcements: "Announcements",
},
admin: {
users: "Users",
userTable: {
name: "Name",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Teacher",
isAdmin: "Is Admin",
},
},
subject: {
register: "Register to subject:",
academy_year: "Academic year",
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@ export default {
subjects: "Mijn vakken",
announcements: "Meldingen",
},

admin: {
users: "Gebruikers",
userTable: {
name: "Naam",
uid: "UGent ID",
email: "Email",
isTeacher: "Is Lesgever",
isAdmin: "Is Beheerder",
},
},
subject: {
register: "Registreer bij vak:",
academy_year: "Academiejaar",
Expand Down
71 changes: 59 additions & 12 deletions frontend/src/queries/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
type UseMutationReturnType,
} from "@tanstack/vue-query";
import type User from "@/models/User";
import { getMySubjects, getUser, toggleAdmin } from "@/services/user";
import { getMySubjects, getUser, getUsers, toggleAdmin, toggleTeacher } from "@/services/user";
import { type Ref, computed } from "vue";
import type { UserSubjectList } from "@/models/Subject";

function USER_QUERY_KEY(uid: string | null): string[] {
return uid ? ["user", uid] : ["user"];
}

function USERS_QUERY_KEY(): string[] {
return ["users"];
}

export function useUserQuery(uid: Ref<string | undefined> | null): UseQueryReturnType<User, Error> {
return useQuery<User, Error>({
queryKey: computed(() => USER_QUERY_KEY(uid?.value!)),
Expand All @@ -22,27 +26,70 @@
});
}

// TODO: Now only toggles current user
export function useToggleAdminMutation(): UseMutationReturnType<void, Error, User, void> {
export function useUsersQuery(): UseQueryReturnType<User[], Error> {
return useQuery<User[], Error>({
queryKey: USERS_QUERY_KEY(),
queryFn: () => getUsers(),
});
}

// TODO: Use USER_QUERY_KEY(uid) instead of USERS_QUERY_KEY() for invalidation
function useToggleMutation(
toggleFn: (uid: string) => Promise<User>,
getField: (_: User) => boolean,
setField: (_: User, value: boolean) => void
): UseMutationReturnType<User, Error, string, { previousUsers: User[] }> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleAdmin,
onMutate: async (user: User) => {
await queryClient.cancelQueries({ queryKey: USER_QUERY_KEY(null) });
queryClient.setQueryData<User>(USER_QUERY_KEY(null), () => {
return { ...user, is_admin: !user.is_admin };
mutationFn: async (uid) => await toggleFn(uid),
onMutate: async (uid: string) => {
const users = queryClient.getQueryData<User[]>(USERS_QUERY_KEY());
await queryClient.cancelQueries({ queryKey: USERS_QUERY_KEY() });
queryClient.setQueryData<User[]>(USERS_QUERY_KEY(), () => {
return users?.map((user: User) => {
const mappedUser = { ...user };
setField(mappedUser, user.uid === uid ? !getField(user) : getField(user));
return mappedUser;
});
});
return { previousUsers: users! };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY(null) });
onSettled: (_, __, uid, ctx) => {

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'_' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'__' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'uid' is defined but never used

Check warning on line 57 in frontend/src/queries/User.ts

View workflow job for this annotation

GitHub Actions / Run linters

'ctx' is defined but never used
queryClient.invalidateQueries({ queryKey: USERS_QUERY_KEY() });
},
onError: (_, user) => {
onError: (_, uid, ctx) => {
queryClient.setQueryData<User[]>(USERS_QUERY_KEY(), () => ctx!.previousUsers!);
alert("Could not update user");
queryClient.setQueryData<User>(USER_QUERY_KEY(null), () => user!);
},
});
}

export function useToggleAdminMutation(): UseMutationReturnType<
User,
Error,
string,
{ previousUsers: User[] }
> {
return useToggleMutation(
toggleAdmin,
(user) => user.is_admin,
(user, value) => (user.is_admin = value)
);
}

export function useToggleTeacherMutation(): UseMutationReturnType<
User,
Error,
string,
{ previousUsers: User[] }
> {
return useToggleMutation(
toggleTeacher,
(user) => user.is_teacher,
(user, value) => (user.is_teacher = value)
);
}

// Hook for fetching subjects for a user
export function useMySubjectsQuery(): UseQueryReturnType<UserSubjectList, Error> {
return useQuery<UserSubjectList, Error>({
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ export async function getUser(uid?: string): Promise<User> {
return authorized_fetch(`/api/users/${uid || "me"}`, { method: "GET" });
}

export async function toggleAdmin() {
return authorized_fetch<void>("/api/users/me", { method: "POST" });
export async function getUsers(): Promise<User[]> {
return authorized_fetch("/api/users", { method: "GET" });
}

export async function toggleAdmin(uid: string): Promise<User> {
return authorized_fetch<User>(`/api/users/${uid}/admin`, { method: "POST" });
}

export async function toggleTeacher(uid: string): Promise<User> {
return authorized_fetch<User>(`/api/users/${uid}/teacher`, { method: "POST" });
}

// Fetches all subjects for logged in user
Expand Down
126 changes: 125 additions & 1 deletion frontend/src/views/AdminView.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,127 @@
<template>
<h1>Admin Panel</h1>
<div class="adminpanel">
<v-card :title="$t('admin.users')" flat class="bg-white">
<v-card-title>
<v-text-field
prepend-inner-icon="mdi-magnify"
v-model="search"
label="Search"
single-line
hide-details
></v-text-field>
</v-card-title>
<v-data-table-virtual
:headers="headers"
:items="users"
:search="search"
v-model:sortBy="sortBy"
item-value="uid"
:loading="isUserLoading || isUsersLoading"
density="compact"
class="table"
>
<template v-slot:loading>
<v-skeleton-loader type="table-row@15" class="table"></v-skeleton-loader>
</template>
<template v-slot:[`item.is_teacher`]="{ item }">
<v-checkbox-btn
:model-value="item.is_teacher"
:disabled="item.uid === currentUser?.uid"
@update:model-value="() => onToggleTeacher(item)"
></v-checkbox-btn>
</template>
<template v-slot:[`item.is_admin`]="{ item }">
<v-checkbox-btn
:model-value="item.is_admin"
:disabled="item.uid === currentUser?.uid"
@update:model-value="() => onToggleAdmin(item)"
></v-checkbox-btn>
</template>
</v-data-table-virtual>
</v-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import {
useUserQuery,
useUsersQuery,
useToggleAdminMutation,
useToggleTeacherMutation,
} from "@/queries/User";
import type User from "@/models/User";

const { t } = useI18n();

const { data: currentUser, isLoading: isUserLoading } = useUserQuery(null);
const { data: users, isLoading: isUsersLoading } = useUsersQuery();
const { mutateAsync: toggleAdmin } = useToggleAdminMutation();
const { mutateAsync: toggleTeacher } = useToggleTeacherMutation();

/**
* Sorts boolean values in descending order.
*/
function sortBool(a: boolean, b: boolean): number {
return a === b ? 0 : a ? -1 : 1;
}

const search = ref("");
const sortBy = ref([{ key: "given_name", order: "asc" }]);

async function onToggleAdmin(user: User) {
await toggleAdmin(user.uid);
}

async function onToggleTeacher(user: User) {
await toggleTeacher(user.uid);
}

const headers = ref([
{
title: computed(() => t("admin.userTable.name")),
key: "given_name",
align: "start",
sortable: true,
},
{
title: computed(() => t("admin.userTable.uid")),
key: "uid",
align: "start",
sortable: false,
},
{
title: computed(() => t("admin.userTable.email")),
key: "mail",
align: "start",
sortable: false,
},
{
title: computed(() => t("admin.userTable.isTeacher")),
key: "is_teacher",
sortable: true,
filterable: false,
filter: () => true, // disable filter
sort: sortBool,
},
{
title: computed(() => t("admin.userTable.isAdmin")),
key: "is_admin",
sortable: true,
filterable: false,
filter: () => true, // disable filter
sort: sortBool,
},
]);
</script>
<style scoped>
.adminpanel {
margin: 15px;
}

.table {
color: black !important;
background-color: white;
width: 95%;
}
</style>
Loading