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,16 +215,39 @@ 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 {
|
||||
vec![path.clone()]
|
||||
@@ -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