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

Groups #182

Merged
merged 17 commits into from
May 10, 2024
69 changes: 69 additions & 0 deletions frontend/src/components/buttons/GroupButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<v-btn v-if="canLeaveGroup" @click="() => leaveGroup({ groupId: group.id })">
{{ $t("group.leave_group") }}
</v-btn>
<v-btn v-else-if="canJoinGroup" @click="() => joinGroup({ groupId: group.id })">
{{ $t("group.join_group") }}
</v-btn>
<v-btn v-if="isTeacher" @click="() => removeGroup({ groupId: group.id })">
{{ $t("group.remove_group") }}
</v-btn>
reyniersbram marked this conversation as resolved.
Show resolved Hide resolved
</template>

<script setup lang="ts">
import type Group from "@/models/Group.js";
import type Project from "@/models/Project.js";
import type User from "@/models/User.js";
import { computed, toRefs } from "vue";
import {
useJoinGroupUserMutation,
useLeaveGroupUserMutation,
useRemoveGroupMutation,
useUserGroupsQuery,
} from "@/queries/Group";

const props = defineProps<{
group: Group;
project: Project;
user: User;
amountOfMembers: number;
}>();

const { group, project, user, amountOfMembers } = toRefs(props);

const canJoinGroup = computed(() => {
return (
!isUserInGroup.value &&
amountOfMembers.value < project.value.capacity &&
!isTeacher.value &&
!isUserInAnotherGroup.value
);
});

const canLeaveGroup = computed(() => {
return project.value.capacity !== 1 && isUserInGroup.value && !isTeacher.value;
});

const isTeacher = computed(() => user.value.is_teacher || false);

const isUserInGroup = computed(() => {
return group.value.members.some((member) => member.uid === user.value.uid);
});

const { data: groups } = useUserGroupsQuery();

const isUserInAnotherGroup = computed(() => {
if (!groups.value) return true;
return groups.value.some((groupelem) => {
return groupelem.project_id === project.value.id && groupelem.id !== group.value.id;
});
});

const { mutateAsync: leaveGroup } = useLeaveGroupUserMutation();

const { mutateAsync: joinGroup } = useJoinGroupUserMutation();

const { mutateAsync: removeGroup } = useRemoveGroupMutation();
</script>

<style scoped></style>
48 changes: 48 additions & 0 deletions frontend/src/components/home/cards/GroupCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<v-card class="v-card-padding">
<v-row>
<v-col cols="8">
<router-link :to="`/groups/${group.id}`">
{{ group.team_name }}
</router-link>
</v-col>
<v-col cols="2">
{{ amountOfMembers + "/" + project.capacity }}
</v-col>
<v-col cols="2">
<GroupButtons
:amountOfMembers="amountOfMembers"
:group="group"
:project="project"
:user="user"
/>
</v-col>
</v-row>
</v-card>
</template>

<script setup lang="ts">
import { computed, toRefs } from "vue";
import type Project from "@/models/Project";
import type Group from "@/models/Group";
import type User from "@/models/User";
import GroupButtons from "@/components/buttons/GroupButtons.vue";

const props = defineProps<{
group: Group;
project: Project;
user: User;
}>();

const { group, project, user } = toRefs(props);

const amountOfMembers = computed(() => {
return group.value.members.length;
});
</script>

<style scoped>
.v-card-padding {
padding: 5px;
}
</style>
8 changes: 6 additions & 2 deletions frontend/src/components/project/ProjectSideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
{{ $t("project.group", { number: group!.id }) }}
</v-btn>
</router-link>
<router-link v-else-if="!isSoloProject && !isTeacher" :to="`/projects/${project!.id}/groups`">
<router-link v-else-if="!isSoloProject && !isTeacher" :to="`/project/${project!.id}/groups`">
<v-btn class="group-button" prepend-icon="mdi-account-group">
{{ $t("project.group_button") }}
</v-btn>
</router-link>
<NeedHelpButton v-if="!isTeacher" class="group-button" :email="subject!.email"></NeedHelpButton>
<NeedHelpButton
v-if="!isTeacher && subject!.email"
class="group-button"
:email="subject!.email"
></NeedHelpButton>
<router-link v-if="isTeacher" :to="`/projects/${project!.id}/edit`">
<v-btn class="group-button" prepend-icon="mdi-pencil">
{{ $t("project.edit") }}
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,17 @@ export default {
subjects: {
title: "My Subjects",
},
group: {
not_found: "Group not found",
not_found2: "No groups found",
groups: "Groups:",
members: "Members:",
actions: "Actions:",
no_members_found: "No members found.",
remove: "Remove",
join_group: "Join group",
leave_group: "Leave group",
remove_group: "Delete group",
create_group: "New group",
},
};
13 changes: 13 additions & 0 deletions frontend/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,17 @@ export default {
subjects: {
title: "Mijn vakken",
},
group: {
not_found: "Groep niet gevonden",
not_found2: "Geen groepen teruggevonden",
groups: "Groepen:",
members: "Leden:",
actions: "Acties:",
no_members_found: "Geen leden teruggevonden.",
remove: "Verwijderen",
join_group: "Aansluiten",
leave_group: "Verlaten",
remove_group: "Groep verwijderen",
create_group: "Nieuwe groep",
},
};
3 changes: 3 additions & 0 deletions frontend/src/models/Group.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import User from "@/models/User";

export default interface Group {
id: number;
project_id: number;
score: number;
team_name: string;
members: User[];
}

export interface GroupForm {
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/queries/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { computed } from "vue";
import {
createGroups,
deleteGroup,
getGroup,
getGroupsByProjectId,
getGroupWithProjectId,
getSubmissions,
getUserGroups,
joinGroup,
joinGroupUser,
leaveGroupUser,
removeUserFromGroup,
} from "@/services/group";
import type Submission from "@/models/Submission";

Expand All @@ -30,6 +35,10 @@ function submissionsQueryKey(groupId: number): (string | number)[] {
return ["submissions", groupId];
}

function PROJECT_GROUPS_QUERY_KEY(projectId: number): (string | number)[] {
return ["projectGroups", projectId];
}

export function useGroupQuery(groupId: Ref<number | undefined>): UseQueryReturnType<Group, Error> {
return useQuery<Group, Error>({
queryKey: GROUP_QUERY_KEY(groupId.value!),
Expand Down Expand Up @@ -115,3 +124,93 @@ export function useSubmissionsQuery(
enabled: computed(() => groupId.value !== undefined),
});
}

export function useProjectGroupsQuery(
projectId: Ref<number | undefined>
): UseQueryReturnType<Group[], Error> {
return useQuery<Group[], Error>({
queryKey: computed(() => PROJECT_GROUPS_QUERY_KEY(projectId.value!)),
queryFn: () => getGroupsByProjectId(projectId.value!),
enabled: computed(() => projectId.value !== undefined),
});
}

export function useJoinGroupUserMutation(): UseMutationReturnType<
Group,
Error,
{ groupId: number },
void
> {
const queryClient = useQueryClient();
return useMutation<Group, Error, { groupId: number }, void>({
mutationFn: ({ groupId }) => joinGroupUser(groupId), // Call the joinGroup service function
onSuccess: () => {
queryClient.invalidateQueries(/* specify the relevant query key */);
console.log("Successfully joined group");
},
onError: (error) => {
console.error("Error joining group:", error);
alert("Could not join group. Please try again.");
},
});
}

export function useLeaveGroupUserMutation(): UseMutationReturnType<
Group,
Error,
{ groupId: number },
void
> {
const queryClient = useQueryClient();
return useMutation<Group, Error, { groupId: number }, void>({
mutationFn: ({ groupId }) => leaveGroupUser(groupId), // Call the joinGroup service function
onSuccess: () => {
queryClient.invalidateQueries(/* specify the relevant query key */);
console.log("Successfully left group");
},
onError: (error) => {
console.error("Error leaving group:", error);
alert("Could not leave group. Please try again.");
},
});
}

export function useRemoveUserFromGroupMutation(): UseMutationReturnType<
Group,
Error,
{ groupId: number; uid: string },
void
> {
const queryClient = useQueryClient();
return useMutation<Group, Error, { groupId: number; uid: string }, void>({
mutationFn: ({ groupId, uid }) => removeUserFromGroup(groupId, uid), // Call the joinGroup service function
onSuccess: () => {
queryClient.invalidateQueries(/* specify the relevant query key */);
console.log("Successfully removed from group");
},
onError: (error) => {
console.error("Error removing from group:", error);
alert("Could not remove from group. Please try again.");
},
});
}

export function useRemoveGroupMutation(): UseMutationReturnType<
Group,
Error,
{ groupId: number },
void
> {
const queryClient = useQueryClient();
return useMutation<Group, Error, { groupId: number }, void>({
mutationFn: ({ groupId }) => deleteGroup(groupId), // Call the joinGroup service function
onSuccess: () => {
queryClient.invalidateQueries(/* specify the relevant query key */);
console.log("Successfully removed from group");
},
onError: (error) => {
console.error("Error removing from group:", error);
alert("Could not remove from group. Please try again.");
},
});
}
12 changes: 12 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ const router = createRouter({
component: () => import("../views/SubmitView.vue"),
props: (route) => ({ projectId: Number(route.params.projectId) }),
},
{
path: "/project/:projectId(\\d+)/groups",
name: "groups",
component: () => import("../views/GroupsView.vue"),
props: (route) => ({ projectId: Number(route.params.projectId) }),
},
{
path: "/groups/:groupId(\\d+)",
name: "group",
component: () => import("../views/GroupView.vue"),
props: (route) => ({ groupId: Number(route.params.groupId) }),
},
{
path: "/subjects",
name: "subjects",
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/services/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export async function getUserGroups(): Promise<Group[]> {
);
}

export async function getGroupsByProjectId(projectId: number): Promise<Group[]> {
return authorized_fetch<{ groups: Group[] }>(`/api/projects/${projectId}/groups`, {
method: "GET",
}).then((data) => data.groups);
}

export function getGroupWithProjectId(groups: Group[], projectId: number): Group | null {
for (const group of groups) {
if (group.project_id === projectId) {
Expand Down Expand Up @@ -54,3 +60,19 @@ export async function joinGroup(groupId: number, uid: string): Promise<void> {
export async function getSubmissions(groupId: number): Promise<Submission[]> {
return authorized_fetch(`/api/groups/${groupId}/submissions`, { method: "GET" });
}

export async function joinGroupUser(groupId: number): Promise<Group> {
return authorized_fetch(`/api/groups/${groupId}`, { method: "POST" });
}

export async function leaveGroupUser(groupId: number): Promise<Group> {
return authorized_fetch(`/api/groups/${groupId}/leave`, { method: "POST" });
}

export async function removeUserFromGroup(groupId: number, uid: string): Promise<Group> {
return authorized_fetch(`/api/groups/${groupId}/${uid}`, { method: "DELETE" });
}

export async function deleteGroup(groupId: number): Promise<Group> {
return authorized_fetch(`/api/groups/${groupId}`, { method: "DELETE" });
}
Loading
Loading