fixed the reccurring event delete bug
This commit is contained in:
@@ -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 {
|
} 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]);
|
await exec(req.schema, 'DELETE FROM events WHERE id=$1', [req.params.id]);
|
||||||
}
|
}
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|||||||
@@ -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,6 +1086,7 @@ 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) {
|
||||||
|
if (!exceptions.has(_toDateStr(occ))) {
|
||||||
totalOcc++;
|
totalOcc++;
|
||||||
if (occ >= rangeStart && occ <= rangeEnd) {
|
if (occ >= rangeStart && occ <= rangeEnd) {
|
||||||
const occEnd = new Date(occ.getTime() + durMs);
|
const occEnd = new Date(occ.getTime() + durMs);
|
||||||
@@ -1090,13 +1094,16 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cur = step(cur);
|
cur = step(cur);
|
||||||
} else {
|
} else {
|
||||||
|
if (!exceptions.has(_toDateStr(cur))) {
|
||||||
totalOcc++;
|
totalOcc++;
|
||||||
if (cur >= rangeStart && cur <= rangeEnd) {
|
if (cur >= rangeStart && cur <= rangeEnd) {
|
||||||
const occEnd = new Date(cur.getTime() + durMs);
|
const occEnd = new Date(cur.getTime() + durMs);
|
||||||
occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at});
|
occurrences.push({...ev, start_at: cur.toISOString(), end_at: occEnd.toISOString(), _virtual: cur.toISOString() !== ev.start_at});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cur = step(cur);
|
cur = step(cur);
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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`),
|
||||||
|
|||||||
Reference in New Issue
Block a user