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:
Connor Johnstone
2025-08-30 13:44:54 -04:00
parent 78f1db7203
commit 071fc3099f

View File

@@ -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()