Add ability to remove voters from Meeting Planner polls
Add DELETE /meeting-planner/:id/voters/:voterKey endpoint and delete button on each voter row in the voting matrix. Includes voterKey in API response for voter identification. Bunker Admin
This commit is contained in:
parent
e3045966a0
commit
3c4465525c
@ -233,6 +233,17 @@ export default function MeetingPlannerPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveVoter = async (voterKey: string) => {
|
||||
if (!selectedPoll) return;
|
||||
try {
|
||||
await api.delete(`/meeting-planner/${selectedPoll.id}/voters/${encodeURIComponent(voterKey)}`);
|
||||
message.success('Voter removed');
|
||||
fetchPollDetail(selectedPoll.id);
|
||||
} catch {
|
||||
message.error('Failed to remove voter');
|
||||
}
|
||||
};
|
||||
|
||||
const openEditDrawer = async (id: string) => {
|
||||
try {
|
||||
const { data } = await api.get<PollDetailResponse>(`/meeting-planner/${id}`);
|
||||
@ -283,13 +294,23 @@ export default function MeetingPlannerPage() {
|
||||
|
||||
const handleUpdateOption = async (optionId: string, field: string, value: string) => {
|
||||
if (!editPoll) return;
|
||||
// Optimistic update: immediately reflect the change in local state
|
||||
const optimisticDate = field === 'date' ? value + 'T00:00:00.000Z' : undefined;
|
||||
setEditPoll((prev) => prev ? {
|
||||
...prev,
|
||||
options: prev.options.map((opt: any) =>
|
||||
opt.id === optionId
|
||||
? { ...opt, [field]: optimisticDate ?? value }
|
||||
: opt
|
||||
),
|
||||
} : prev);
|
||||
try {
|
||||
await api.put(`/meeting-planner/${editPoll.id}/options/${optionId}`, { [field]: value });
|
||||
// Refresh edit poll data
|
||||
const { data } = await api.get<PollDetailResponse>(`/meeting-planner/${editPoll.id}`);
|
||||
setEditPoll(data);
|
||||
message.success('Option updated');
|
||||
} catch {
|
||||
// Revert on failure by refetching
|
||||
const { data } = await api.get<PollDetailResponse>(`/meeting-planner/${editPoll.id}`);
|
||||
setEditPoll(data);
|
||||
message.error('Failed to update option');
|
||||
}
|
||||
};
|
||||
@ -399,6 +420,7 @@ export default function MeetingPlannerPage() {
|
||||
<th style={{ padding: '8px 12px', borderBottom: '2px solid #303030', textAlign: 'left', minWidth: 120 }}>
|
||||
Voter
|
||||
</th>
|
||||
<th style={{ padding: '8px 4px', borderBottom: '2px solid #303030', width: 36 }} />
|
||||
{poll.options.map((opt) => (
|
||||
<th
|
||||
key={opt.id}
|
||||
@ -425,6 +447,14 @@ export default function MeetingPlannerPage() {
|
||||
<td style={{ padding: '6px 12px', borderBottom: '1px solid #303030' }}>
|
||||
{voter.name}
|
||||
</td>
|
||||
<td style={{ padding: '6px 4px', borderBottom: '1px solid #303030', textAlign: 'center' }}>
|
||||
<Popconfirm
|
||||
title={`Remove ${voter.name}'s votes?`}
|
||||
onConfirm={() => handleRemoveVoter(voter.voterKey)}
|
||||
>
|
||||
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</td>
|
||||
{poll.options.map((opt) => {
|
||||
const value = voter.votes[opt.id] as PollVoteValue | undefined;
|
||||
return (
|
||||
@ -455,6 +485,7 @@ export default function MeetingPlannerPage() {
|
||||
{/* Tally row */}
|
||||
<tr style={{ fontWeight: 600 }}>
|
||||
<td style={{ padding: '8px 12px', borderTop: '2px solid #303030' }}>Score</td>
|
||||
<td style={{ borderTop: '2px solid #303030' }} />
|
||||
{poll.options.map((opt) => (
|
||||
<td
|
||||
key={opt.id}
|
||||
|
||||
@ -2849,6 +2849,7 @@ export interface PollDetailResponse extends SchedulingPoll {
|
||||
comments: SchedulingPollComment[];
|
||||
voters: Array<{
|
||||
name: string;
|
||||
voterKey: string;
|
||||
votes: Record<string, PollVoteValue>;
|
||||
}>;
|
||||
}
|
||||
|
||||
@ -124,6 +124,16 @@ adminRouter.post('/:id/convert-to-event', async (req: Request, res: Response, ne
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// Remove voter
|
||||
adminRouter.delete('/:id/voters/:voterKey', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id as string;
|
||||
const voterKey = decodeURIComponent(req.params.voterKey as string);
|
||||
const poll = await meetingPlannerService.removeVoter(id, voterKey);
|
||||
res.json(poll);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// Delete comment
|
||||
adminRouter.delete('/:id/comments/:commentId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
@ -61,11 +61,11 @@ function groupVotesByVoter(votes: Array<{
|
||||
optionId: string;
|
||||
value: PollVoteValue;
|
||||
}>) {
|
||||
const voterMap = new Map<string, { name: string; votes: Record<string, PollVoteValue> }>();
|
||||
const voterMap = new Map<string, { name: string; voterKey: string; votes: Record<string, PollVoteValue> }>();
|
||||
for (const vote of votes) {
|
||||
const key = vote.userId || vote.voterToken || vote.voterName;
|
||||
if (!voterMap.has(key)) {
|
||||
voterMap.set(key, { name: vote.voterName, votes: {} });
|
||||
voterMap.set(key, { name: vote.voterName, voterKey: key, votes: {} });
|
||||
}
|
||||
voterMap.get(key)!.votes[vote.optionId] = vote.value;
|
||||
}
|
||||
@ -343,6 +343,25 @@ export const meetingPlannerService = {
|
||||
});
|
||||
},
|
||||
|
||||
async removeVoter(pollId: string, voterKey: string) {
|
||||
const poll = await prisma.schedulingPoll.findUnique({ where: { id: pollId } });
|
||||
if (!poll) throw new AppError(404, 'Poll not found');
|
||||
|
||||
const result = await prisma.schedulingPollVote.deleteMany({
|
||||
where: {
|
||||
pollId,
|
||||
OR: [
|
||||
{ userId: voterKey },
|
||||
{ voterToken: voterKey },
|
||||
{ voterName: voterKey, userId: null, voterToken: null },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (result.count === 0) throw new AppError(404, 'Voter not found');
|
||||
return this.findById(pollId);
|
||||
},
|
||||
|
||||
async deleteComment(pollId: string, commentId: string) {
|
||||
const comment = await prisma.schedulingPollComment.findFirst({
|
||||
where: { id: commentId, pollId },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user