[feat] Group Admin can remove settle-up member.

This commit is contained in:
kk034kk034 2025-05-01 17:15:19 +08:00
parent 92ae97a57b
commit e9854e3cb7
2 changed files with 100 additions and 10 deletions

View File

@ -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<{
<div className="">
<p className="font-semibold">Members</p>
<div className="mt-2 flex flex-col gap-2">
{groupDetailQuery.data?.groupUsers.map((groupUser) => (
<div
key={groupUser.userId}
className={clsx('flex items-center gap-2 rounded-md py-1.5')}
>
<UserAvatar user={groupUser.user} />
<p>{groupUser.user.name ?? groupUser.user.email}</p>
</div>
))}
{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 (
<div
key={groupUser.userId}
className="flex items-center justify-between gap-2 rounded-md py-1.5"
>
<div className="flex items-center gap-2">
<UserAvatar user={groupUser.user} />
<p>{groupUser.user.name ?? groupUser.user.email}</p>
</div>
{canDeleteUser && (
<AlertDialog>
<AlertDialogTrigger asChild>
<button>
<Trash2 className="h-4 w-4 text-red-500 hover:opacity-70" />
</button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-xs rounded-lg">
<AlertDialogHeader>
<AlertDialogTitle>Remove member?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. The user will be removed from this group.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button
variant="destructive"
onClick={() =>
removeMemberMutation.mutate({ groupId, userId: groupUser.userId })
}
>
Remove
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
);
})}
</div>
</div>
{groupDetailQuery?.data?.createdAt && (

View File

@ -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 };
}),
});