From cb1bb23132718beb9b4904ca64ff77f218ad143b Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Sun, 21 Sep 2025 12:34:09 -0400 Subject: [PATCH] Fix all-day event end date handling by removing double conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Both frontend and backend were adding a day for all-day events: - Frontend: Converts inclusive UI dates (9/22-9/25) to exclusive (9/22-9/26) - Backend: Was incorrectly adding another day (9/22-9/27) causing display issues Fixed by: - Remove duplicate day addition in backend handlers (events.rs, series.rs) - Keep frontend conversion for proper RFC 5545 compliance - Add reverse conversion when loading events for editing - Maintain user-friendly inclusive dates in UI while storing exclusive dates Now properly handles: UI 9/22-9/25 ↔ Storage 9/22-9/26 (exclusive per spec) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/handlers/events.rs | 18 ++++++------------ backend/src/handlers/series.rs | 8 +++----- frontend/src/components/create_event_modal.rs | 8 +++++++- frontend/src/components/event_form/types.rs | 5 +++-- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/backend/src/handlers/events.rs b/backend/src/handlers/events.rs index a12ed2d..af413eb 100644 --- a/backend/src/handlers/events.rs +++ b/backend/src/handlers/events.rs @@ -456,14 +456,11 @@ pub async fn create_event( parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; - let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) + let end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; - // For all-day events, add one day to end date for RFC-5545 compliance - // RFC-5545 uses exclusive end dates for all-day events - if request.all_day { - end_datetime = end_datetime + chrono::Duration::days(1); - } + // Note: Frontend already converts inclusive UI dates to exclusive dates for all-day events + // No additional conversion needed here // Validate that end is after start (allow equal times for all-day events) if request.all_day { @@ -766,14 +763,11 @@ pub async fn update_event( parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; - let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) + let end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; - // For all-day events, add one day to end date for RFC-5545 compliance - // RFC-5545 uses exclusive end dates for all-day events - if request.all_day { - end_datetime = end_datetime + chrono::Duration::days(1); - } + // Note: Frontend already converts inclusive UI dates to exclusive dates for all-day events + // No additional conversion needed here // Validate that end is after start (allow equal times for all-day events) if request.all_day { diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index a05251b..b065f24 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -137,13 +137,11 @@ pub async fn create_event_series( let start_datetime = parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; - let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) + let end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; - // For all-day events, add one day to end date for RFC-5545 compliance - if request.all_day { - end_datetime = end_datetime + chrono::Duration::days(1); - } + // Note: Frontend already converts inclusive UI dates to exclusive dates for all-day events + // No additional conversion needed here // Generate a unique UID for the series let uid = format!("series-{}", uuid::Uuid::new_v4().to_string()); diff --git a/frontend/src/components/create_event_modal.rs b/frontend/src/components/create_event_modal.rs index 0230615..8b4b44a 100644 --- a/frontend/src/components/create_event_modal.rs +++ b/frontend/src/components/create_event_modal.rs @@ -257,7 +257,13 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend // Timing start_date: start_local.date(), - end_date: end_local.date(), + end_date: if event.all_day { + // For all-day events, subtract one day to convert from exclusive to inclusive end date + // (UI expects inclusive dates, but iCalendar stores exclusive end dates) + end_local.date() - chrono::Duration::days(1) + } else { + end_local.date() + }, start_time: start_local.time(), end_time: end_local.time(), diff --git a/frontend/src/components/event_form/types.rs b/frontend/src/components/event_form/types.rs index 79beced..68efc3b 100644 --- a/frontend/src/components/event_form/types.rs +++ b/frontend/src/components/event_form/types.rs @@ -156,8 +156,9 @@ impl EventCreationData { ) { // Use local date/times and timezone - no UTC conversion - let effective_end_date = if self.end_time == NaiveTime::from_hms_opt(0, 0, 0).unwrap() { - // If end time is midnight (00:00), treat it as beginning of next day + let effective_end_date = if self.all_day { + // For all-day events, add one day to convert from inclusive to exclusive end date + // (iCalendar spec requires exclusive end dates for all-day events) self.end_date + chrono::Duration::days(1) } else { self.end_date