Fix series update to preserve original date when dragging events
- When dragging recurring events, preserve original series start date - Only update time components, not date, to prevent series from shifting days - For timed events: use original date with new start/end times from drag - For all-day events: preserve original date and duration pattern - Added debug logging to track date preservation behavior - Maintains event duration for both timed and all-day recurring events Fixes issue where dragging a recurring event would change the date for all occurrences instead of just updating the time. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -38,11 +38,31 @@ pub async fn create_event_series( | ||||
|         return Err(ApiError::BadRequest("Use regular create endpoint for non-recurring events".to_string())); | ||||
|     } | ||||
|  | ||||
|     // Validate recurrence type | ||||
|     match request.recurrence.to_lowercase().as_str() { | ||||
|         "daily" | "weekly" | "monthly" | "yearly" => {}, | ||||
|         _ => return Err(ApiError::BadRequest("Invalid recurrence type. Must be daily, weekly, monthly, or yearly".to_string())), | ||||
|     } | ||||
|     // Validate recurrence type - handle both simple strings and RRULE strings | ||||
|     let recurrence_freq = if request.recurrence.contains("FREQ=") { | ||||
|         // Parse RRULE to extract frequency | ||||
|         if request.recurrence.contains("FREQ=DAILY") { | ||||
|             "daily" | ||||
|         } else if request.recurrence.contains("FREQ=WEEKLY") { | ||||
|             "weekly" | ||||
|         } else if request.recurrence.contains("FREQ=MONTHLY") { | ||||
|             "monthly" | ||||
|         } else if request.recurrence.contains("FREQ=YEARLY") { | ||||
|             "yearly" | ||||
|         } else { | ||||
|             return Err(ApiError::BadRequest("Invalid RRULE frequency. Must be DAILY, WEEKLY, MONTHLY, or YEARLY".to_string())); | ||||
|         } | ||||
|     } else { | ||||
|         // Handle simple strings | ||||
|         let lower = request.recurrence.to_lowercase(); | ||||
|         match lower.as_str() { | ||||
|             "daily" => "daily", | ||||
|             "weekly" => "weekly", | ||||
|             "monthly" => "monthly",  | ||||
|             "yearly" => "yearly", | ||||
|             _ => return Err(ApiError::BadRequest("Invalid recurrence type. Must be daily, weekly, monthly, or yearly".to_string())), | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Create CalDAV config from token and password | ||||
|     let config = state.auth_service.caldav_config_from_token(&token, &password)?; | ||||
| @@ -142,7 +162,7 @@ pub async fn create_event_series( | ||||
|     event.priority = request.priority; | ||||
|  | ||||
|     // Generate the RRULE for recurrence | ||||
|     let rrule = build_series_rrule(&request)?; | ||||
|     let rrule = build_series_rrule_with_freq(&request, recurrence_freq)?; | ||||
|     event.rrule = Some(rrule); | ||||
|  | ||||
|     println!("🔁 Generated RRULE: {:?}", event.rrule); | ||||
| @@ -195,15 +215,38 @@ pub async fn update_event_series( | ||||
|         _ => return Err(ApiError::BadRequest("Invalid update_scope. Must be: this_only, this_and_future, or all_in_series".to_string())), | ||||
|     } | ||||
|  | ||||
|     // Validate recurrence type | ||||
|     match request.recurrence.to_lowercase().as_str() { | ||||
|         "daily" | "weekly" | "monthly" | "yearly" => {}, | ||||
|         _ => return Err(ApiError::BadRequest("Invalid recurrence type. Must be daily, weekly, monthly, or yearly".to_string())), | ||||
|     } | ||||
|     // Validate recurrence type - handle both simple strings and RRULE strings | ||||
|     let recurrence_freq = if request.recurrence.contains("FREQ=") { | ||||
|         // Parse RRULE to extract frequency | ||||
|         if request.recurrence.contains("FREQ=DAILY") { | ||||
|             "daily" | ||||
|         } else if request.recurrence.contains("FREQ=WEEKLY") { | ||||
|             "weekly" | ||||
|         } else if request.recurrence.contains("FREQ=MONTHLY") { | ||||
|             "monthly" | ||||
|         } else if request.recurrence.contains("FREQ=YEARLY") { | ||||
|             "yearly" | ||||
|         } else { | ||||
|             return Err(ApiError::BadRequest("Invalid RRULE frequency. Must be DAILY, WEEKLY, MONTHLY, or YEARLY".to_string())); | ||||
|         } | ||||
|     } else { | ||||
|         // Handle simple strings | ||||
|         let lower = request.recurrence.to_lowercase(); | ||||
|         match lower.as_str() { | ||||
|             "daily" => "daily", | ||||
|             "weekly" => "weekly",  | ||||
|             "monthly" => "monthly", | ||||
|             "yearly" => "yearly", | ||||
|             _ => return Err(ApiError::BadRequest("Invalid recurrence type. Must be daily, weekly, monthly, or yearly".to_string())), | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Create CalDAV config from token and password | ||||
|     let config = state.auth_service.caldav_config_from_token(&token, &password)?; | ||||
|     let client = CalDAVClient::new(config); | ||||
|      | ||||
|     // Use the parsed frequency for further processing (avoiding unused variable warning) | ||||
|     let _freq_for_processing = recurrence_freq; | ||||
|  | ||||
|     // Determine which calendar to search (or search all calendars) | ||||
|     let calendar_paths = if let Some(ref path) = request.calendar_path { | ||||
| @@ -236,16 +279,27 @@ pub async fn update_event_series( | ||||
|     println!("📅 Found series event in calendar: {}", calendar_path); | ||||
|  | ||||
|     // Parse datetime components for the update | ||||
|     let start_date = chrono::NaiveDate::parse_from_str(&request.start_date, "%Y-%m-%d") | ||||
|     // For recurring events, preserve the original series start date and only update the time | ||||
|     // to prevent the entire series from shifting to a different date | ||||
|     let original_start_date = existing_event.dtstart.date_naive(); | ||||
|     let start_date = original_start_date; // Always use original series date | ||||
|      | ||||
|     // Log what we're doing for debugging | ||||
|     let requested_date = chrono::NaiveDate::parse_from_str(&request.start_date, "%Y-%m-%d") | ||||
|         .map_err(|_| ApiError::BadRequest("Invalid start_date format. Expected YYYY-MM-DD".to_string()))?; | ||||
|     println!("📅 Preserving original series date {} (requested: {})", original_start_date, requested_date); | ||||
|  | ||||
|     let (start_datetime, end_datetime) = if request.all_day { | ||||
|         let start_dt = start_date.and_hms_opt(0, 0, 0) | ||||
|             .ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?; | ||||
|          | ||||
|         // For all-day events, also preserve the original date pattern   | ||||
|         let end_date = if !request.end_date.is_empty() { | ||||
|             chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d") | ||||
|                 .map_err(|_| ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string()))? | ||||
|             // Calculate the duration from the original event | ||||
|             let original_duration_days = existing_event.dtend | ||||
|                 .map(|end| (end.date_naive() - existing_event.dtstart.date_naive()).num_days()) | ||||
|                 .unwrap_or(0); | ||||
|             start_date + chrono::Duration::days(original_duration_days) | ||||
|         } else { | ||||
|             start_date | ||||
|         }; | ||||
| @@ -272,12 +326,16 @@ pub async fn update_event_series( | ||||
|         }; | ||||
|  | ||||
|         let start_dt = start_date.and_time(start_time); | ||||
|         let end_dt = if !request.end_date.is_empty() { | ||||
|             let end_date = chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d") | ||||
|                 .map_err(|_| ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string()))?; | ||||
|             end_date.and_time(end_time) | ||||
|         } else { | ||||
|         // For timed events, preserve the original date and only update times | ||||
|         let end_dt = if !request.end_time.is_empty() { | ||||
|             // Use the new end time with the preserved original date | ||||
|             start_date.and_time(end_time) | ||||
|         } else { | ||||
|             // Calculate end time based on original duration | ||||
|             let original_duration = existing_event.dtend | ||||
|                 .map(|end| end - existing_event.dtstart) | ||||
|                 .unwrap_or_else(|| chrono::Duration::hours(1)); | ||||
|             (chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc() | ||||
|         }; | ||||
|  | ||||
|         (chrono::Utc.from_utc_datetime(&start_dt), chrono::Utc.from_utc_datetime(&end_dt)) | ||||
| @@ -300,10 +358,10 @@ pub async fn update_event_series( | ||||
|         _ => unreachable!(), // Already validated above | ||||
|     }; | ||||
|  | ||||
|     // Update the event on the CalDAV server | ||||
|     // Generate event href from UID | ||||
|     let event_href = format!("{}.ics", request.series_uid); | ||||
|     client.update_event(&calendar_path, &updated_event, &event_href) | ||||
|     // Update the event on the CalDAV server using the original event's href | ||||
|     let event_href = existing_event.href.as_ref() | ||||
|         .ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?; | ||||
|     client.update_event(&calendar_path, &updated_event, event_href) | ||||
|         .await | ||||
|         .map_err(|e| ApiError::Internal(format!("Failed to update event series: {}", e)))?; | ||||
|  | ||||
| @@ -382,15 +440,44 @@ pub async fn delete_event_series( | ||||
| // Helper functions | ||||
|  | ||||
| fn build_series_rrule(request: &CreateEventSeriesRequest) -> Result<String, ApiError> { | ||||
|     // Extract frequency from request | ||||
|     let recurrence_freq = if request.recurrence.contains("FREQ=") { | ||||
|         // Parse RRULE to extract frequency | ||||
|         if request.recurrence.contains("FREQ=DAILY") { | ||||
|             "daily" | ||||
|         } else if request.recurrence.contains("FREQ=WEEKLY") { | ||||
|             "weekly" | ||||
|         } else if request.recurrence.contains("FREQ=MONTHLY") { | ||||
|             "monthly" | ||||
|         } else if request.recurrence.contains("FREQ=YEARLY") { | ||||
|             "yearly" | ||||
|         } else { | ||||
|             return Err(ApiError::BadRequest("Invalid RRULE frequency".to_string())); | ||||
|         } | ||||
|     } else { | ||||
|         let lower = request.recurrence.to_lowercase(); | ||||
|         match lower.as_str() { | ||||
|             "daily" => "daily", | ||||
|             "weekly" => "weekly", | ||||
|             "monthly" => "monthly",  | ||||
|             "yearly" => "yearly", | ||||
|             _ => return Err(ApiError::BadRequest("Invalid recurrence type".to_string())), | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|     build_series_rrule_with_freq(request, recurrence_freq) | ||||
| } | ||||
|  | ||||
| fn build_series_rrule_with_freq(request: &CreateEventSeriesRequest, freq: &str) -> Result<String, ApiError> { | ||||
|     let mut rrule_parts = Vec::new(); | ||||
|      | ||||
|     // Add frequency | ||||
|     match request.recurrence.to_lowercase().as_str() { | ||||
|     match freq { | ||||
|         "daily" => rrule_parts.push("FREQ=DAILY".to_string()), | ||||
|         "weekly" => rrule_parts.push("FREQ=WEEKLY".to_string()), | ||||
|         "monthly" => rrule_parts.push("FREQ=MONTHLY".to_string()), | ||||
|         "yearly" => rrule_parts.push("FREQ=YEARLY".to_string()), | ||||
|         _ => return Err(ApiError::BadRequest("Invalid recurrence type".to_string())), | ||||
|         _ => return Err(ApiError::BadRequest("Invalid recurrence frequency".to_string())), | ||||
|     } | ||||
|      | ||||
|     // Add interval if specified and greater than 1 | ||||
| @@ -401,7 +488,7 @@ fn build_series_rrule(request: &CreateEventSeriesRequest) -> Result<String, ApiE | ||||
|     } | ||||
|      | ||||
|     // Handle weekly recurrence with specific days (BYDAY) | ||||
|     if request.recurrence.to_lowercase() == "weekly" && request.recurrence_days.len() == 7 { | ||||
|     if freq == "weekly" && request.recurrence_days.len() == 7 { | ||||
|         let selected_days: Vec<&str> = request.recurrence_days | ||||
|             .iter() | ||||
|             .enumerate() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone