diff --git a/backend/src/handlers/events.rs b/backend/src/handlers/events.rs index cb19856..b80c91f 100644 --- a/backend/src/handlers/events.rs +++ b/backend/src/handlers/events.rs @@ -852,10 +852,11 @@ fn parse_event_datetime( .map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?; if all_day { - // For all-day events, use midnight UTC + // For all-day events, use noon UTC to avoid timezone boundary issues + // This ensures the date remains correct when converted to any local timezone let datetime = date - .and_hms_opt(0, 0, 0) - .ok_or_else(|| "Failed to create midnight datetime".to_string())?; + .and_hms_opt(12, 0, 0) + .ok_or_else(|| "Failed to create noon datetime".to_string())?; Ok(Utc.from_utc_datetime(&datetime)) } else { // Parse the time diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index c123a26..ed1d0a8 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -397,8 +397,9 @@ pub async fn update_event_series( }; let (start_datetime, end_datetime) = if request.all_day { + // For all-day events, use noon UTC to avoid timezone boundary issues let start_dt = start_date - .and_hms_opt(0, 0, 0) + .and_hms_opt(12, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?; // For all-day events, also preserve the original date pattern @@ -414,20 +415,13 @@ pub async fn update_event_series( }; let end_dt = end_date - .and_hms_opt(23, 59, 59) + .and_hms_opt(12, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; - // Convert from local time to UTC - let start_local = chrono::Local.from_local_datetime(&start_dt) - .single() - .ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?; - let end_local = chrono::Local.from_local_datetime(&end_dt) - .single() - .ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?; - + // For all-day events, use UTC directly (no local conversion needed) ( - start_local.with_timezone(&chrono::Utc), - end_local.with_timezone(&chrono::Utc), + chrono::Utc.from_utc_datetime(&start_dt), + chrono::Utc.from_utc_datetime(&end_dt), ) } else { let start_time = if !request.start_time.is_empty() { diff --git a/frontend/src/components/week_view.rs b/frontend/src/components/week_view.rs index 89c3d4a..d649cf2 100644 --- a/frontend/src/components/week_view.rs +++ b/frontend/src/components/week_view.rs @@ -1320,11 +1320,23 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3 // Check if an all-day event spans the given date fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool { - let start_date = event.dtstart.with_timezone(&Local).date_naive(); + let start_date = if event.all_day { + // For all-day events, extract date directly from UTC without timezone conversion + // since all-day events are stored at noon UTC to avoid timezone boundary issues + event.dtstart.date_naive() + } else { + event.dtstart.with_timezone(&Local).date_naive() + }; + let end_date = if let Some(dtend) = event.dtend { - // For all-day events, dtend is often set to the day after the last day - // So we need to subtract a day to get the actual last day of the event - dtend.with_timezone(&Local).date_naive() - chrono::Duration::days(1) + if event.all_day { + // For all-day events, dtend is set to the day after the last day (RFC 5545) + // Extract date directly from UTC and subtract a day to get actual last day + dtend.date_naive() - chrono::Duration::days(1) + } else { + // For timed events, use timezone conversion + dtend.with_timezone(&Local).date_naive() + } } else { // Single day event start_date