fixed the reccurring event delete bug

This commit is contained in:
2026-03-28 22:28:46 -04:00
parent 43ff0f450d
commit 4b4ddf0825
3 changed files with 51 additions and 23 deletions

View File

@@ -419,19 +419,40 @@ router.delete('/:id', authMiddleware, async (req, res) => {
if (!event) return res.status(404).json({ error: 'Not found' }); if (!event) return res.status(404).json({ error: 'Not found' });
const itm = await isToolManagerFn(req.schema, req.user); const itm = await isToolManagerFn(req.schema, req.user);
if (!itm && event.created_by !== req.user.id) return res.status(403).json({ error: 'Access denied' }); if (!itm && event.created_by !== req.user.id) return res.status(403).json({ error: 'Access denied' });
const { recurringScope } = req.body || {}; const { recurringScope, occurrenceStart } = req.body || {};
if (recurringScope === 'future' && event.recurrence_rule) { const pad = n => String(n).padStart(2, '0');
// Delete this event and all future occurrences with same creator/title
await exec(req.schema, ` if (event.recurrence_rule && recurringScope === 'all') {
DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL // Delete the single base row — all virtual occurrences disappear with it
AND title=$2 AND start_at >= $3 await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]);
`, [event.created_by, event.title, event.start_at]);
} else if (recurringScope === 'all' && event.recurrence_rule) { } else if (event.recurrence_rule && recurringScope === 'future') {
// Delete present and future occurrences only — preserve past records // Truncate the series so it ends before this occurrence
await exec(req.schema, ` const occDate = new Date(occurrenceStart || event.start_at);
DELETE FROM events WHERE created_by=$1 AND recurrence_rule IS NOT NULL AND title=$2 AND end_at >= NOW() if (occDate <= new Date(event.start_at)) {
`, [event.created_by, event.title]); // 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 { } else {
// Non-recurring single delete
await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]); await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]);
} }
res.json({ success: true }); res.json({ success: true });

View File

@@ -1060,6 +1060,9 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) {
// Determine end condition // Determine end condition
const endDate = rule.ends === 'on' && rule.endDate ? new Date(rule.endDate + 'T23:59:59') : null; 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 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, // totalOcc counts ALL occurrences from origStart regardless of range,
// so endCount is respected even when rangeStart is after the event's start. // 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.setDate(weekStart.getDate() + dayNum);
occ.setHours(origStart.getHours(), origStart.getMinutes(), origStart.getSeconds()); occ.setHours(origStart.getHours(), origStart.getMinutes(), origStart.getSeconds());
if (!endDate || occ <= endDate) { if (!endDate || occ <= endDate) {
totalOcc++; if (!exceptions.has(_toDateStr(occ))) {
if (occ >= rangeStart && occ <= rangeEnd) { totalOcc++;
const occEnd = new Date(occ.getTime() + durMs); if (occ >= rangeStart && occ <= rangeEnd) {
occurrences.push({...ev, start_at: occ.toISOString(), end_at: occEnd.toISOString(), _virtual: true}); const occEnd = new Date(occ.getTime() + durMs);
occurrences.push({...ev, start_at: occ.toISOString(), end_at: occEnd.toISOString(), _virtual: true});
}
} }
} }
} }
cur = step(cur); cur = step(cur);
} else { } else {
totalOcc++; if (!exceptions.has(_toDateStr(cur))) {
if (cur >= rangeStart && cur <= rangeEnd) { totalOcc++;
const occEnd = new Date(cur.getTime() + durMs); if (cur >= rangeStart && cur <= rangeEnd) {
occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at}); 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); 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 // Virtual recurring occurrences carry their own start/end dates — overlay them so
// the modal shows the correct occurrence time and isPast evaluates against the // 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. // 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); setDetailEvent(event);
} catch { toast('Failed to load event','error'); } } catch { toast('Failed to load event','error'); }
}; };
@@ -1631,7 +1638,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
const e = deleteTarget; const e = deleteTarget;
setDeleteTarget(null); setDeleteTarget(null);
try { try {
await api.deleteEvent(e.id, scope); await api.deleteEvent(e.id, scope, e._virtual ? e.start_at : null);
toast('Deleted','success'); toast('Deleted','success');
setPanel('calendar'); setPanel('calendar');
setEditingEvent(null); setEditingEvent(null);

View File

@@ -118,7 +118,7 @@ export const api = {
getEvent: (id) => req('GET', `/schedule/${id}`), getEvent: (id) => req('GET', `/schedule/${id}`),
createEvent: (body) => req('POST', '/schedule', body), // body may include recurrenceRule: {freq,interval,byDay,ends,endDate,endCount} createEvent: (body) => req('POST', '/schedule', body), // body may include recurrenceRule: {freq,interval,byDay,ends,endDate,endCount}
updateEvent: (id, body) => req('PATCH', `/schedule/${id}`, body), 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 }), setAvailability: (id, response, note) => req('PUT', `/schedule/${id}/availability`, { response, note }),
setAvailabilityNote: (id, note) => req('PATCH', `/schedule/${id}/availability/note`, { note }), setAvailabilityNote: (id, note) => req('PATCH', `/schedule/${id}/availability/note`, { note }),
deleteAvailability: (id) => req('DELETE', `/schedule/${id}/availability`), deleteAvailability: (id) => req('DELETE', `/schedule/${id}/availability`),