diff --git a/admin/src/pages/MeetingPlannerPage.tsx b/admin/src/pages/MeetingPlannerPage.tsx index 83aa99ce..f3a85acb 100644 --- a/admin/src/pages/MeetingPlannerPage.tsx +++ b/admin/src/pages/MeetingPlannerPage.tsx @@ -281,6 +281,19 @@ export default function MeetingPlannerPage() { } }; + const handleUpdateOption = async (optionId: string, field: string, value: string) => { + if (!editPoll) return; + try { + await api.put(`/meeting-planner/${editPoll.id}/options/${optionId}`, { [field]: value }); + // Refresh edit poll data + const { data } = await api.get(`/meeting-planner/${editPoll.id}`); + setEditPoll(data); + message.success('Option updated'); + } catch { + message.error('Failed to update option'); + } + }; + const handleAddOption = async (option: { date: string; startTime: string; endTime: string }) => { if (!editPoll) return; try { @@ -836,9 +849,31 @@ export default function MeetingPlannerPage() { {editPoll.options?.map((opt) => ( - - {dayjs(opt.date).format('MMM D, YYYY')} — {opt.startTime}–{opt.endTime} - + + { + if (d) handleUpdateOption(opt.id, 'date', d.format('YYYY-MM-DD')); + }} + /> + { + if (t) handleUpdateOption(opt.id, 'startTime', t.format('HH:mm')); + }} + /> + { + if (t) handleUpdateOption(opt.id, 'endTime', t.format('HH:mm')); + }} + /> + {(editPoll.options?.length ?? 0) > 1 && ( diff --git a/api/src/modules/meeting-planner/meeting-planner.routes.ts b/api/src/modules/meeting-planner/meeting-planner.routes.ts index 629b2ac1..87dad7f7 100644 --- a/api/src/modules/meeting-planner/meeting-planner.routes.ts +++ b/api/src/modules/meeting-planner/meeting-planner.routes.ts @@ -5,6 +5,7 @@ import { createPollSchema, updatePollSchema, addOptionsSchema, + updateOptionSchema, submitVotesSchema, submitCommentSchema, finalizePollSchema, @@ -76,6 +77,16 @@ adminRouter.post('/:id/options', validate(addOptionsSchema), async (req: Request } catch (err) { next(err); } }); +// Update option +adminRouter.put('/:id/options/:optionId', validate(updateOptionSchema), async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id as string; + const optionId = req.params.optionId as string; + const poll = await meetingPlannerService.updateOption(id, optionId, req.body); + res.json(poll); + } catch (err) { next(err); } +}); + // Remove option adminRouter.delete('/:id/options/:optionId', async (req: Request, res: Response, next: NextFunction) => { try { diff --git a/api/src/modules/meeting-planner/meeting-planner.schemas.ts b/api/src/modules/meeting-planner/meeting-planner.schemas.ts index e97b1c4b..45146593 100644 --- a/api/src/modules/meeting-planner/meeting-planner.schemas.ts +++ b/api/src/modules/meeting-planner/meeting-planner.schemas.ts @@ -60,6 +60,12 @@ export const convertToShiftSchema = z.object({ cutId: z.string().optional(), }); +export const updateOptionSchema = z.object({ + date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD').optional(), + startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM').optional(), + endTime: z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM').optional(), +}); + export const listPollsSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), @@ -74,4 +80,5 @@ export type SubmitVotesInput = z.infer; export type SubmitCommentInput = z.infer; export type FinalizePollInput = z.infer; export type ConvertToShiftInput = z.infer; +export type UpdateOptionInput = z.infer; export type ListPollsInput = z.infer; diff --git a/api/src/modules/meeting-planner/meeting-planner.service.ts b/api/src/modules/meeting-planner/meeting-planner.service.ts index 657b4e61..d06b6504 100644 --- a/api/src/modules/meeting-planner/meeting-planner.service.ts +++ b/api/src/modules/meeting-planner/meeting-planner.service.ts @@ -8,6 +8,7 @@ import type { CreatePollInput, UpdatePollInput, AddOptionsInput, + UpdateOptionInput, SubmitVotesInput, SubmitCommentInput, FinalizePollInput, @@ -219,6 +220,29 @@ export const meetingPlannerService = { return this.findById(pollId); }, + async updateOption(pollId: string, optionId: string, data: UpdateOptionInput) { + const poll = await prisma.schedulingPoll.findUnique({ where: { id: pollId } }); + if (!poll) throw new AppError(404, 'Poll not found'); + if (poll.status !== 'OPEN') throw new AppError(400, 'Cannot update options on a non-open poll'); + + const option = await prisma.schedulingPollOption.findFirst({ + where: { id: optionId, pollId }, + }); + if (!option) throw new AppError(404, 'Option not found'); + + const updateData: Prisma.SchedulingPollOptionUncheckedUpdateInput = {}; + if (data.date !== undefined) updateData.date = new Date(data.date); + if (data.startTime !== undefined) updateData.startTime = data.startTime; + if (data.endTime !== undefined) updateData.endTime = data.endTime; + + await prisma.schedulingPollOption.update({ + where: { id: optionId }, + data: updateData, + }); + + return this.findById(pollId); + }, + async removeOption(pollId: string, optionId: string) { const option = await prisma.schedulingPollOption.findFirst({ where: { id: optionId, pollId },