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
+
+ removeMemberMutation.mutate({ groupId, userId: groupUser.userId })
+ }
+ >
+ Remove
+
+
+
+
+ )}
+
+ );
+ })}
+
{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