diff --git a/src/pages/groups/[groupId].tsx b/src/pages/groups/[groupId].tsx index 7e4d485..a7e1534 100644 --- a/src/pages/groups/[groupId].tsx +++ b/src/pages/groups/[groupId].tsx @@ -53,6 +53,14 @@ const BalancePage: NextPageWithUser<{ const expensesQuery = api.group.getExpenses.useQuery({ groupId }); const deleteGroupMutation = api.group.delete.useMutation(); const leaveGroupMutation = api.group.leaveGroup.useMutation(); + const removeMemberMutation = api.group.removeMember.useMutation({ + onSuccess: () => { + void groupDetailQuery.refetch(); + }, + onError: (err) => { + toast.error(err.message ?? 'Failed to remove member'); + }, + }); const [isInviteCopied, setIsInviteCopied] = useState(false); const [showDeleteTrigger, setShowDeleteTrigger] = useState(false); @@ -188,15 +196,54 @@ const BalancePage: NextPageWithUser<{

Members

- {groupDetailQuery.data?.groupUsers.map((groupUser) => ( -
- -

{groupUser.user.name ?? groupUser.user.email}

-
- ))} + {groupDetailQuery.data?.groupUsers.map((groupUser) => { + const hasBalance = groupDetailQuery.data?.groupBalances.some( + (b) => b.userId === groupUser.userId && b.amount !== 0 + ); + const canDeleteUser = + isAdmin && groupUser.userId !== user.id && !hasBalance; + + return ( +
+
+ +

{groupUser.user.name ?? groupUser.user.email}

+
+ {canDeleteUser && ( + + + + + + + Remove member? + + This action cannot be undone. The user will be removed from this group. + + + + Cancel + + + + + )} +
+ ); + })} +
{groupDetailQuery?.data?.createdAt && ( diff --git a/src/server/api/routers/group.ts b/src/server/api/routers/group.ts index 8e0066a..72950c6 100644 --- a/src/server/api/routers/group.ts +++ b/src/server/api/routers/group.ts @@ -350,4 +350,47 @@ export const groupRouter = createTRPCRouter({ return group; }), -}); + + removeMember: groupProcedure + .input(z.object({ groupId: z.number(), userId: z.number() })) + .mutation(async ({ ctx, input }) => { + const { groupId, userId } = input; + + // 確保呼叫者是 group 擁有者 + const group = await ctx.db.group.findUnique({ + where: { id: groupId }, + }); + + if (group?.userId !== ctx.session.user.id) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Only admin can remove members' }); + } + + if (userId === ctx.session.user.id) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'You cannot remove yourself' }); + } + + const balance = await ctx.db.groupBalance.findFirst({ + where: { + groupId, + userId, + amount: { not: 0 }, + }, + }); + + if (balance) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'This member has unsettled balance and cannot be removed', + }); + } + + await ctx.db.groupUser.delete({ + where: { + groupId_userId: { groupId, userId }, + }, + }); + + return { success: true }; + }), + +}); \ No newline at end of file