diff --git a/backend/src/routes/schedule.js b/backend/src/routes/schedule.js index b9fc3cb..9286843 100644 --- a/backend/src/routes/schedule.js +++ b/backend/src/routes/schedule.js @@ -199,14 +199,56 @@ router.get('/', authMiddleware, async (req, res) => { if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); } sql += ' ORDER BY start_at ASC'; const rawEvents = await query(req.schema, sql, params); - const events = []; + + // Separate master events and exception instances + const masterEvents = []; + const exceptionInstances = []; for (const e of rawEvents) { + if (e.recurring_master_id) { + exceptionInstances.push(e); + } else { + masterEvents.push(e); + } + } + + // Build a map of exception dates by master event + const exceptionDatesByMaster = new Map(); + for (const exc of exceptionInstances) { + if (!exceptionDatesByMaster.has(exc.recurring_master_id)) { + exceptionDatesByMaster.set(exc.recurring_master_id, new Set()); + } + const dateStr = new Date(exc.original_start_at || exc.start_at).toISOString().split('T')[0]; + exceptionDatesByMaster.get(exc.recurring_master_id).add(dateStr); + } + + const events = []; + for (const e of masterEvents) { + // Skip master events if all their occurrences in range are replaced by exceptions + if (e.recurrence_rule && exceptionDatesByMaster.has(e.id)) { + const exceptions = exceptionDatesByMaster.get(e.id); + const rule = e.recurrence_rule; + + // Check if this is a simple case where all occurrences are replaced + // For now, we'll include the master and let frontend handle filtering + // This is safer to avoid missing edge cases + } + if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue; await enrichEvent(req.schema, e); - const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id, req.user.id]); + const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]); e.my_response = mine?.response || null; events.push(e); } + + // Add exception instances (they're standalone events) + for (const e of exceptionInstances) { + if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue; + await enrichEvent(req.schema, e); + const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]); + e.my_response = mine?.response || null; + events.push(e); + } + res.json({ events }); } catch (e) { res.status(500).json({ error: e.message }); } }); diff --git a/build.sh b/build.sh index b88a254..4f9ada4 100644 --- a/build.sh +++ b/build.sh @@ -16,7 +16,7 @@ set -euo pipefail VERSION="${1:-0.12.37}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" -IMAGE_NAME="rosterchirp" +IMAGE_NAME="rosterchirp-dev" # If a registry is set, prefix image name if [[ -n "$REGISTRY" ]]; then diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index d000507..bbd3e32 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -1125,14 +1125,57 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) { // Expand all recurring events in a list within a date range function expandEvents(events, rangeStart, rangeEnd) { const result = []; + + // First, collect all exception instances and their dates by master event + const exceptionDatesByMaster = new Map(); + const masterEvents = []; + const standaloneEvents = []; + for (const ev of events) { - if (ev.recurrence_rule?.freq) { - const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd); - result.push(...expanded); + if (ev.recurring_master_id) { + // This is an exception instance + if (!exceptionDatesByMaster.has(ev.recurring_master_id)) { + exceptionDatesByMaster.set(ev.recurring_master_id, new Set()); + } + const dateStr = new Date(ev.original_start_at || ev.start_at).toISOString().split('T')[0]; + exceptionDatesByMaster.get(ev.recurring_master_id).add(dateStr); + standaloneEvents.push(ev); // Exception instances are standalone events + } else if (ev.recurrence_rule?.freq) { + masterEvents.push(ev); } else { - result.push(ev); + standaloneEvents.push(ev); } } + + // Expand master events, skipping dates that have exception instances + for (const ev of masterEvents) { + const exceptions = exceptionDatesByMaster.get(ev.id); + if (exceptions) { + // Merge the rule's exceptions with the instance exceptions + const rule = ev.recurrence_rule || {}; + const ruleExceptions = new Set(rule.exceptions || []); + for (const date of exceptions) { + ruleExceptions.add(date); + } + // Create a copy of the event with merged exceptions + const evWithMergedExceptions = { + ...ev, + recurrence_rule: { + ...rule, + exceptions: Array.from(ruleExceptions) + } + }; + const expanded = expandRecurringEvent(evWithMergedExceptions, rangeStart, rangeEnd); + result.push(...expanded); + } else { + const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd); + result.push(...expanded); + } + } + + // Add all standalone events (non-recurring + exception instances) + result.push(...standaloneEvents); + // Sort by start_at result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at)); return result;