From 4b4ddf0825dcff8a33a56138a4ae4469ab271f4d Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sat, 28 Mar 2026 22:28:46 -0400 Subject: [PATCH] fixed the reccurring event delete bug --- backend/src/routes/schedule.js | 45 +++++++++++++++++------- frontend/src/components/SchedulePage.jsx | 27 ++++++++------ frontend/src/utils/api.js | 2 +- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/backend/src/routes/schedule.js b/backend/src/routes/schedule.js index 88cc666..82f732e 100644 --- a/backend/src/routes/schedule.js +++ b/backend/src/routes/schedule.js @@ -419,19 +419,40 @@ router.delete('/:id', authMiddleware, async (req, res) => { if (!event) return res.status(404).json({ error: 'Not found' }); const itm = await isToolManagerFn(req.schema, req.user); if (!itm && event.created_by !== req.user.id) return res.status(403).json({ error: 'Access denied' }); - const { recurringScope } = req.body || {}; - if (recurringScope === 'future' && event.recurrence_rule) { - // Delete this event and all future occurrences with same creator/title - await exec(req.schema, ` - DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL - AND title=$2 AND start_at >= $3 - `, [event.created_by, event.title, event.start_at]); - } else if (recurringScope === 'all' && event.recurrence_rule) { - // Delete present and future occurrences only — preserve past records - await exec(req.schema, ` - DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL AND title=$2 AND end_at >= NOW() - `, [event.created_by, event.title]); + const { recurringScope, occurrenceStart } = req.body || {}; + const pad = n => String(n).padStart(2, '0'); + + if (event.recurrence_rule && recurringScope === 'all') { + // Delete the single base row — all virtual occurrences disappear with it + await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]); + + } else if (event.recurrence_rule && recurringScope === 'future') { + // Truncate the series so it ends before this occurrence + const occDate = new Date(occurrenceStart || event.start_at); + if (occDate <= new Date(event.start_at)) { + // Occurrence is at or before the base start — delete the whole series + await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]); + } else { + const endBefore = new Date(occDate); + endBefore.setDate(endBefore.getDate() - 1); + const rule = { ...event.recurrence_rule }; + rule.ends = 'on'; + rule.endDate = `${endBefore.getFullYear()}-${pad(endBefore.getMonth()+1)}-${pad(endBefore.getDate())}`; + delete rule.endCount; + await exec(req.schema, 'UPDATE events SET recurrence_rule=$1 WHERE id=$2', [JSON.stringify(rule), req.params.id]); + } + + } else if (event.recurrence_rule && recurringScope === 'this') { + // Add occurrence date to exceptions — base row and other occurrences are untouched + const occDate = new Date(occurrenceStart || event.start_at); + const occDateStr = `${occDate.getFullYear()}-${pad(occDate.getMonth()+1)}-${pad(occDate.getDate())}`; + const rule = { ...event.recurrence_rule }; + const existing = Array.isArray(rule.exceptions) ? rule.exceptions : []; + rule.exceptions = [...existing.filter(d => d !== occDateStr), occDateStr]; + await exec(req.schema, 'UPDATE events SET recurrence_rule=$1 WHERE id=$2', [JSON.stringify(rule), req.params.id]); + } else { + // Non-recurring single delete await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]); } res.json({ success: true }); diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index eaea711..09eeb3b 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -1060,6 +1060,9 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) { // Determine end condition const endDate = rule.ends === 'on' && rule.endDate ? new Date(rule.endDate + 'T23:59:59') : null; const endCount = rule.ends === 'after' ? (rule.endCount || 13) : null; + const exceptions = new Set(rule.exceptions || []); + const _pad = n => String(n).padStart(2, '0'); + const _toDateStr = d => `${d.getFullYear()}-${_pad(d.getMonth()+1)}-${_pad(d.getDate())}`; // totalOcc counts ALL occurrences from origStart regardless of range, // so endCount is respected even when rangeStart is after the event's start. @@ -1083,19 +1086,23 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) { occ.setDate(weekStart.getDate() + dayNum); occ.setHours(origStart.getHours(), origStart.getMinutes(), origStart.getSeconds()); if (!endDate || occ <= endDate) { - totalOcc++; - if (occ >= rangeStart && occ <= rangeEnd) { - const occEnd = new Date(occ.getTime() + durMs); - occurrences.push({...ev, start_at: occ.toISOString(), end_at: occEnd.toISOString(), _virtual: true}); + if (!exceptions.has(_toDateStr(occ))) { + totalOcc++; + if (occ >= rangeStart && occ <= rangeEnd) { + const occEnd = new Date(occ.getTime() + durMs); + occurrences.push({...ev, start_at: occ.toISOString(), end_at: occEnd.toISOString(), _virtual: true}); + } } } } cur = step(cur); } else { - totalOcc++; - if (cur >= rangeStart && cur <= rangeEnd) { - const occEnd = new Date(cur.getTime() + durMs); - occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at}); + if (!exceptions.has(_toDateStr(cur))) { + totalOcc++; + if (cur >= rangeStart && cur <= rangeEnd) { + const occEnd = new Date(cur.getTime() + durMs); + occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at}); + } } cur = step(cur); } @@ -1619,7 +1626,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel // Virtual recurring occurrences carry their own start/end dates — overlay them so // the modal shows the correct occurrence time and isPast evaluates against the // occurrence's end_at, not the base event's first-occurrence end_at. - if (e._virtual) { event.start_at = e.start_at; event.end_at = e.end_at; } + if (e._virtual) { event.start_at = e.start_at; event.end_at = e.end_at; event._virtual = true; } setDetailEvent(event); } catch { toast('Failed to load event','error'); } }; @@ -1631,7 +1638,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel const e = deleteTarget; setDeleteTarget(null); try { - await api.deleteEvent(e.id, scope); + await api.deleteEvent(e.id, scope, e._virtual ? e.start_at : null); toast('Deleted','success'); setPanel('calendar'); setEditingEvent(null); diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index e9bccb4..f49748d 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -118,7 +118,7 @@ export const api = { getEvent: (id) => req('GET', `/schedule/${id}`), createEvent: (body) => req('POST', '/schedule', body), // body may include recurrenceRule: {freq,interval,byDay,ends,endDate,endCount} updateEvent: (id, body) => req('PATCH', `/schedule/${id}`, body), - deleteEvent: (id, scope = 'this') => req('DELETE', `/schedule/${id}`, { recurringScope: scope }), + deleteEvent: (id, scope = 'this', occurrenceStart = null) => req('DELETE', `/schedule/${id}`, { recurringScope: scope, occurrenceStart }), setAvailability: (id, response, note) => req('PUT', `/schedule/${id}/availability`, { response, note }), setAvailabilityNote: (id, note) => req('PATCH', `/schedule/${id}/availability/note`, { note }), deleteAvailability: (id) => req('DELETE', `/schedule/${id}/availability`),