From c938f25951fc2e4328c5472334e701e8695211c5 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Wed, 24 Sep 2025 10:22:02 -0400 Subject: [PATCH] Fix recurring event series modification and UI issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend fixes: - Fix "this event only" EXDATE handling - ensure proper timezone conversion for exception dates - Remove debug logging for cleaner production output Frontend fixes: - Add EXDATE timezone conversion in convert_utc_to_local function - Fix event duplication when viewing weeks across month boundaries with deduplication logic - Update CSS theme colors for context menus, recurrence options, and recurring edit modals These changes ensure RFC 5545 compliance for recurring event exceptions and improve the user experience across different themes and calendar views. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/handlers/series.rs | 5 +-- frontend/src/components/calendar.rs | 14 +++++++ frontend/src/services/calendar_service.rs | 47 +++++++---------------- frontend/styles.css | 14 +++---- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index b065f24..10bb8fd 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -465,13 +465,10 @@ pub async fn update_event_series( }; // Update the event on the CalDAV server using the original event's href - println!("📤 Updating event on CalDAV server..."); let event_href = existing_event .href .as_ref() .ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?; - println!("📤 Using event href: {}", event_href); - println!("📤 Calendar path: {}", calendar_path); match client .update_event(&calendar_path, &updated_event, event_href) @@ -1026,7 +1023,7 @@ async fn update_single_occurrence( println!("✅ Created exception event successfully"); - // Return the original series (now with EXDATE) - main handler will update it on CalDAV + // Return the modified existing event with EXDATE for the main handler to update on CalDAV Ok((existing_event.clone(), 1)) // 1 occurrence modified (via exception) } diff --git a/frontend/src/components/calendar.rs b/frontend/src/components/calendar.rs index 8fdd529..2f20f4f 100644 --- a/frontend/src/components/calendar.rs +++ b/frontend/src/components/calendar.rs @@ -181,6 +181,20 @@ pub fn Calendar(props: &CalendarProps) -> Html { } } } + + // Deduplicate events that may appear in multiple month fetches + // This happens when a recurring event spans across month boundaries + all_events.sort_by(|a, b| { + // Sort by UID first, then by start time + match a.uid.cmp(&b.uid) { + std::cmp::Ordering::Equal => a.dtstart.cmp(&b.dtstart), + other => other, + } + }); + all_events.dedup_by(|a, b| { + // Remove duplicates with same UID and start time + a.uid == b.uid && a.dtstart == b.dtstart + }); // Process the combined events match Ok(all_events) as Result, String> diff --git a/frontend/src/services/calendar_service.rs b/frontend/src/services/calendar_service.rs index 3309044..1060643 100644 --- a/frontend/src/services/calendar_service.rs +++ b/frontend/src/services/calendar_service.rs @@ -317,6 +317,11 @@ impl CalendarService { event.last_modified = Some(modified_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64)); event.last_modified_tzid = None; } + + // Convert EXDATE entries from UTC to local time + event.exdate = event.exdate.into_iter() + .map(|exdate| exdate + chrono::Duration::minutes(-timezone_offset_minutes as i64)) + .collect(); } event @@ -333,8 +338,6 @@ impl CalendarService { // Convert UTC events to local time for proper display let event = Self::convert_utc_to_local(event); if let Some(ref rrule) = event.rrule { - - // Generate occurrences for recurring events using VEvent let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range); expanded_events.extend(occurrences); @@ -437,25 +440,14 @@ impl CalendarService { // Check if this occurrence is in the exception dates (EXDATE) let is_exception = base_event.exdate.iter().any(|exception_date| { - // Compare dates ignoring sub-second precision - let exception_naive = exception_date.and_utc(); - let occurrence_naive = occurrence_datetime.and_utc(); + // EXDATE from server is in local time, but stored as NaiveDateTime + // We need to compare both as local time (naive datetimes) instead of UTC + let exception_naive = *exception_date; + let occurrence_naive = occurrence_datetime; // Check if dates match (within a minute to handle minor time differences) let diff = occurrence_naive - exception_naive; - let matches = diff.num_seconds().abs() < 60; - - if matches { - web_sys::console::log_1( - &format!( - "🚫 Excluding occurrence {} due to EXDATE {}", - occurrence_naive, exception_naive - ) - .into(), - ); - } - - matches + diff.num_seconds().abs() < 60 }); if !is_exception { @@ -632,22 +624,11 @@ impl CalendarService { // Check if this occurrence is in the exception dates (EXDATE) let is_exception = base_event.exdate.iter().any(|exception_date| { - let exception_naive = exception_date.and_utc(); - let occurrence_naive = occurrence_datetime.and_utc(); + // Compare as local time (naive datetimes) instead of UTC + let exception_naive = *exception_date; + let occurrence_naive = occurrence_datetime; let diff = occurrence_naive - exception_naive; - let matches = diff.num_seconds().abs() < 60; - - if matches { - web_sys::console::log_1( - &format!( - "🚫 Excluding occurrence {} due to EXDATE {}", - occurrence_naive, exception_naive - ) - .into(), - ); - } - - matches + diff.num_seconds().abs() < 60 }); if !is_exception { diff --git a/frontend/styles.css b/frontend/styles.css index 7dac0db..df72328 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -2586,7 +2586,7 @@ body { display: flex; align-items: center; padding: 0.75rem 1.25rem; - color: #374151; + color: var(--text-primary); cursor: pointer; transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.875rem; @@ -2607,8 +2607,8 @@ body { } .context-menu-item:hover { - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - color: #1f2937; + background: var(--background-tertiary); + color: var(--text-primary); transform: translateX(2px); } @@ -2834,12 +2834,12 @@ body { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; - color: #333; + color: var(--text-primary); } .recurring-option .option-description { font-size: 0.9rem; - color: #666; + color: var(--text-secondary); line-height: 1.4; } @@ -4487,8 +4487,8 @@ body { .recurrence-options { margin-top: 1.5rem; padding: 1rem; - background: #f8f9fa; - border: 1px solid #e9ecef; + background: var(--background-secondary); + border: 1px solid var(--border-primary); border-radius: var(--border-radius-medium); }