diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index 0fffaf0..c4c88e4 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -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 { + // 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 { 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 = request.recurrence_days .iter() .enumerate()