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); }