diff --git a/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/confirm-delete-group-membership-modal.tsx b/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/confirm-delete-group-membership-modal.tsx new file mode 100644 index 0000000000..d261a75b9a --- /dev/null +++ b/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/confirm-delete-group-membership-modal.tsx @@ -0,0 +1,19 @@ +import { useConfirmDeleteModal } from "@/components/molecules/ConfirmDeleteModal/confirm-delete-modal" +import type { GroupMembership } from "@dotkomonline/types" +import { useDeleteGroupMembershipMutation } from "../../mutations" + +export const useConfirmDeleteGroupMembershipModal = () => { + const deleteMembership = useDeleteGroupMembershipMutation() + + return ({ groupMembership }: { groupMembership: GroupMembership }) => { + return useConfirmDeleteModal({ + title: "Slett gruppemedlemskap", + text: "Er du sikker på at du vil slette dette gruppemedlemskapet?", + confirmText: "Slett gruppemedlemskap", + cancelText: "Avbryt", + onConfirm: () => { + deleteMembership.mutate(groupMembership.id) + }, + }) + } +} diff --git a/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/use-group-membership-table.tsx b/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/use-group-membership-table.tsx index 72e418b28a..84d5f25cc2 100644 --- a/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/use-group-membership-table.tsx +++ b/apps/dashboard/src/app/(internal)/group/[id]/[memberId]/use-group-membership-table.tsx @@ -5,6 +5,7 @@ import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/re import { formatDate } from "date-fns" import { useMemo } from "react" import { useEditGroupMembershipModal } from "../../modals/edit-group-membership-modal" +import { useConfirmDeleteGroupMembershipModal } from "./confirm-delete-group-membership-modal" interface Props { groupMember: GroupMember @@ -13,6 +14,7 @@ interface Props { export const useGroupMembershipTable = ({ groupMember }: Props) => { const columnHelper = createColumnHelper() const openGroupEditModal = useEditGroupMembershipModal() + const openConfirmDeleteModal = useConfirmDeleteGroupMembershipModal() const columns = useMemo( () => [ @@ -64,11 +66,36 @@ export const useGroupMembershipTable = ({ groupMember }: Props) => { ) } + return button + }, + }), + columnHelper.accessor((membership) => membership, { + id: "delete", + header: () => "Slett", + cell: (info) => { + const groupMembership = info.getValue() + const isActive = groupMembership.end === null + + const button = ( + + ) + + if (isActive) { + return {button} + } + return button }, }), ], - [columnHelper, openGroupEditModal] + [columnHelper, openGroupEditModal, openConfirmDeleteModal] ) return useReactTable({ diff --git a/apps/dashboard/src/app/(internal)/group/mutations.ts b/apps/dashboard/src/app/(internal)/group/mutations.ts index 68f5df7005..9b31c721df 100644 --- a/apps/dashboard/src/app/(internal)/group/mutations.ts +++ b/apps/dashboard/src/app/(internal)/group/mutations.ts @@ -249,6 +249,37 @@ export const useSyncWorkspaceGroupMutation = () => { ) } +export const useDeleteGroupMembershipMutation = () => { + const trpc = useTRPC() + const queryClient = useQueryClient() + const notification = useQueryNotification() + + return useMutation( + trpc.group.deleteMembership.mutationOptions({ + onError: () => { + notification.fail({ + title: "Feil", + message: "Det oppsto en feil under sletting av medlemskap.", + }) + }, + onMutate: () => { + notification.loading({ + title: "Sletter medlemskap", + message: "Medlemskapet blir slettet.", + }) + }, + onSuccess: async (_, input) => { + notification.complete({ + title: "Medlemskap slettet", + message: "Medlemskapet har blitt slettet.", + }) + + await queryClient.invalidateQueries({ queryKey: trpc.group.getMember.queryKey() }) + }, + }) + ) +} + export const useLinkGroupMutation = () => { const trpc = useTRPC() const queryClient = useQueryClient() diff --git a/apps/rpc/src/modules/group/group-repository.ts b/apps/rpc/src/modules/group/group-repository.ts index aef080b33b..0d29cf2733 100644 --- a/apps/rpc/src/modules/group/group-repository.ts +++ b/apps/rpc/src/modules/group/group-repository.ts @@ -37,6 +37,7 @@ export interface GroupRepository { data: GroupMembershipWrite, roleIds: Set ): Promise + deleteMembership(handle: DBHandle, id: GroupMembershipId): Promise createRoles(handle: DBHandle, roles: GroupRoleWrite[]): Promise updateRole(handle: DBHandle, id: GroupRoleId, role: Partial): Promise } @@ -207,6 +208,13 @@ export function getGroupRepository(): GroupRepository { roles: membership.roles.map((role) => role.role), }) }, + async deleteMembership(handle, id) { + await handle.groupMembership.delete({ + where: { + id, + }, + }) + }, async createRoles(handle, roles) { const rows = await handle.groupRole.createManyAndReturn({ data: roles, diff --git a/apps/rpc/src/modules/group/group-router.ts b/apps/rpc/src/modules/group/group-router.ts index 600b5a9610..04977ee5e3 100644 --- a/apps/rpc/src/modules/group/group-router.ts +++ b/apps/rpc/src/modules/group/group-router.ts @@ -83,6 +83,13 @@ export const groupRouter = t.router({ ctx.groupService.endMembership(handle, input.userId, input.groupId) ) ), + + deleteMembership: staffProcedure + .input(GroupMembershipSchema.shape.id) + .mutation(async ({ input, ctx }) => + ctx.executeAuditedTransaction(async (handle) => ctx.groupService.deleteMembership(handle, input)) + ), + updateMembership: staffProcedure .input( z.object({ diff --git a/apps/rpc/src/modules/group/group-service.ts b/apps/rpc/src/modules/group/group-service.ts index 7ded994a96..872e8a79a8 100644 --- a/apps/rpc/src/modules/group/group-service.ts +++ b/apps/rpc/src/modules/group/group-service.ts @@ -4,6 +4,7 @@ import { type GroupId, type GroupMember, type GroupMembership, + type GroupMembershipId, type GroupMembershipWrite, type GroupRole, type GroupRoleId, @@ -46,6 +47,7 @@ export interface GroupService { getAllByMember(handle: DBHandle, userId: UserId): Promise startMembership(handle: DBHandle, userId: UserId, groupId: GroupId, roleIds: Set): Promise endMembership(handle: DBHandle, userId: UserId, groupId: GroupId): Promise + deleteMembership(handle: DBHandle, id: GroupMembershipId): Promise /** * Attempts to update a membership if it doesn't overlap with existing memberships * @@ -204,6 +206,20 @@ export function getGroupService(groupRepository: GroupRepository, userService: U return await Promise.all(endMembershipPromises) }, + + async deleteMembership(handle, id) { + const membership = await groupRepository.getMembershipById(handle, id) + if (!membership) { + throw new NotFoundError(`GroupMembership(ID=${id}) not found`) + } + + if (!membership.end) { + throw new FailedPreconditionError(`Cannot delete active GroupMembership(ID=${id})`) + } + + return await groupRepository.deleteMembership(handle, id) + }, + async updateMembership(handle, id, data, roleIds) { const currentMembership = await groupRepository.getMembershipById(handle, id) if (!currentMembership) {