diff --git a/backend/src/handlers/ics_fetcher.rs b/backend/src/handlers/ics_fetcher.rs index 1fe5429..8130bd4 100644 --- a/backend/src/handlers/ics_fetcher.rs +++ b/backend/src/handlers/ics_fetcher.rs @@ -285,17 +285,25 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result Result= 0 && days_diff % interval as i64 == 0 { // Check if times match (allowing for timezone differences within same day) @@ -845,7 +852,7 @@ fn would_event_be_generated_by_rrule(recurring_event: &VEvent, single_event: &VE } else if rrule.contains("FREQ=WEEKLY") { // Weekly recurrence let interval = extract_interval_from_rrule(rrule).unwrap_or(1); - let days_diff = (single_event.dtstart.date_naive() - recurring_event.dtstart.date_naive()).num_days(); + let days_diff = (single_event.dtstart.date() - recurring_event.dtstart.date()).num_days(); // First check if it's the same day of week and time let recurring_weekday = recurring_event.dtstart.weekday(); diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index f1b8584..5dad060 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -14,6 +14,33 @@ use calendar_models::{EventClass, EventStatus, VEvent}; use super::auth::{extract_bearer_token, extract_password_header}; +fn parse_event_datetime_local( + date_str: &str, + time_str: &str, + all_day: bool, +) -> Result { + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + // Parse the date + let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?; + + if all_day { + // For all-day events, use start of day + let datetime = date + .and_hms_opt(0, 0, 0) + .ok_or_else(|| "Failed to create start-of-day datetime".to_string())?; + Ok(datetime) + } else { + // Parse the time + let time = NaiveTime::parse_from_str(time_str, "%H:%M") + .map_err(|_| format!("Invalid time format: {}. Expected HH:MM", time_str))?; + + // Combine date and time - now keeping as local time + Ok(NaiveDateTime::new(date, time)) + } +} + /// Create a new recurring event series pub async fn create_event_series( State(state): State>, @@ -106,84 +133,29 @@ pub async fn create_event_series( println!("📅 Using calendar path: {}", calendar_path); - // Parse datetime components - let start_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()) - })?; + // Parse dates and times as local times (no UTC conversion) + 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 (start_datetime, end_datetime) = if request.all_day { - // For all-day events, use the dates as-is - let start_dt = start_date - .and_hms_opt(0, 0, 0) - .ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?; + let mut 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)))?; - 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()) - })? - } else { - start_date - }; - - let end_dt = end_date - .and_hms_opt(23, 59, 59) - .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; - - // Frontend now sends UTC times, so treat as UTC directly - let start_local = chrono::Utc.from_utc_datetime(&start_dt); - let end_local = chrono::Utc.from_utc_datetime(&end_dt); - - ( - start_local.with_timezone(&chrono::Utc), - end_local.with_timezone(&chrono::Utc), - ) - } else { - // Parse times for timed events - let start_time = if !request.start_time.is_empty() { - chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| { - ApiError::BadRequest("Invalid start_time format. Expected HH:MM".to_string()) - })? - } else { - chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap() // Default to 9 AM - }; - - let end_time = if !request.end_time.is_empty() { - chrono::NaiveTime::parse_from_str(&request.end_time, "%H:%M").map_err(|_| { - ApiError::BadRequest("Invalid end_time format. Expected HH:MM".to_string()) - })? - } else { - chrono::NaiveTime::from_hms_opt(10, 0, 0).unwrap() // Default to 1 hour duration - }; - - 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 { - start_date.and_time(end_time) - }; - - // Frontend now sends UTC times, so treat as UTC directly - let start_local = chrono::Utc.from_utc_datetime(&start_dt); - let end_local = chrono::Utc.from_utc_datetime(&end_dt); - - ( - start_local.with_timezone(&chrono::Utc), - end_local.with_timezone(&chrono::Utc), - ) - }; + // 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); + } // Generate a unique UID for the series let uid = format!("series-{}", uuid::Uuid::new_v4().to_string()); - // Create the VEvent for the series + // Create the VEvent for the series with local times let mut event = VEvent::new(uid.clone(), start_datetime); event.dtend = Some(end_datetime); event.all_day = request.all_day; // Set the all_day flag properly + + // Set timezone information from client + event.dtstart_tzid = Some(request.timezone.clone()); + event.dtend_tzid = Some(request.timezone.clone()); event.summary = if request.title.trim().is_empty() { None } else { @@ -372,7 +344,7 @@ pub async fn update_event_series( ); // Parse datetime components for the update - let original_start_date = existing_event.dtstart.date_naive(); + let original_start_date = existing_event.dtstart.date(); // For "this_and_future" and "this_only" updates, use the occurrence date for the modified event // For "all_in_series" updates, preserve the original series start date @@ -399,7 +371,7 @@ pub async fn update_event_series( // 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()) + .map(|end| (end.date() - existing_event.dtstart.date()).num_days()) .unwrap_or(0); start_date + chrono::Duration::days(original_duration_days) } else { @@ -410,11 +382,8 @@ pub async fn update_event_series( .and_hms_opt(12, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; - // For all-day events, use UTC directly (no local conversion needed) - ( - chrono::Utc.from_utc_datetime(&start_dt), - chrono::Utc.from_utc_datetime(&end_dt), - ) + // For all-day events, use local times directly + (start_dt, end_dt) } else { let start_time = if !request.start_time.is_empty() { chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| { @@ -445,17 +414,11 @@ pub async fn update_event_series( .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() + start_dt + original_duration }; - // Frontend now sends UTC times, so treat as UTC directly - let start_local = chrono::Utc.from_utc_datetime(&start_dt); - let end_local = chrono::Utc.from_utc_datetime(&end_dt); - - ( - start_local.with_timezone(&chrono::Utc), - end_local.with_timezone(&chrono::Utc), - ) + // Frontend now sends local times, so use them directly + (start_dt, end_dt) }; // Handle different update scopes @@ -702,8 +665,8 @@ fn build_series_rrule_with_freq( fn update_entire_series( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, - start_datetime: chrono::DateTime, - end_datetime: chrono::DateTime, + start_datetime: chrono::NaiveDateTime, + end_datetime: chrono::NaiveDateTime, ) -> Result<(VEvent, u32), ApiError> { // Clone the existing event to preserve all metadata let mut updated_event = existing_event.clone(); @@ -711,6 +674,8 @@ fn update_entire_series( // Update only the modified properties from the request updated_event.dtstart = start_datetime; updated_event.dtend = Some(end_datetime); + updated_event.dtstart_tzid = Some(request.timezone.clone()); + updated_event.dtend_tzid = Some(request.timezone.clone()); updated_event.summary = if request.title.trim().is_empty() { existing_event.summary.clone() // Keep original if empty } else { @@ -743,8 +708,9 @@ fn update_entire_series( // Update timestamps let now = chrono::Utc::now(); + let now_naive = now.naive_utc(); updated_event.dtstamp = now; - updated_event.last_modified = Some(now); + updated_event.last_modified = Some(now_naive); // Keep original created timestamp to preserve event history // Update RRULE if recurrence parameters are provided @@ -832,8 +798,8 @@ fn update_entire_series( async fn update_this_and_future( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, - start_datetime: chrono::DateTime, - end_datetime: chrono::DateTime, + start_datetime: chrono::NaiveDateTime, + end_datetime: chrono::NaiveDateTime, client: &CalDAVClient, calendar_path: &str, ) -> Result<(VEvent, u32), ApiError> { @@ -881,6 +847,8 @@ async fn update_this_and_future( new_series.uid = new_series_uid.clone(); new_series.dtstart = start_datetime; new_series.dtend = Some(end_datetime); + new_series.dtstart_tzid = Some(request.timezone.clone()); + new_series.dtend_tzid = Some(request.timezone.clone()); new_series.summary = if request.title.trim().is_empty() { None } else { @@ -913,9 +881,10 @@ async fn update_this_and_future( // Update timestamps let now = chrono::Utc::now(); + let now_naive = now.naive_utc(); new_series.dtstamp = now; - new_series.created = Some(now); - new_series.last_modified = Some(now); + new_series.created = Some(now_naive); + new_series.last_modified = Some(now_naive); new_series.href = None; // Will be set when created println!( @@ -943,8 +912,8 @@ async fn update_this_and_future( async fn update_single_occurrence( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, - start_datetime: chrono::DateTime, - end_datetime: chrono::DateTime, + start_datetime: chrono::NaiveDateTime, + end_datetime: chrono::NaiveDateTime, client: &CalDAVClient, calendar_path: &str, _original_event_href: &str, @@ -969,21 +938,20 @@ async fn update_single_occurrence( // Create the EXDATE datetime using the original event's time let original_time = existing_event.dtstart.time(); let exception_datetime = exception_date.and_time(original_time); - let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime); // Add the exception date to the original series println!( "📝 BEFORE adding EXDATE: existing_event.exdate = {:?}", existing_event.exdate ); - existing_event.exdate.push(exception_utc); + existing_event.exdate.push(exception_datetime); println!( "📝 AFTER adding EXDATE: existing_event.exdate = {:?}", existing_event.exdate ); println!( "🚫 Added EXDATE for single occurrence modification: {}", - exception_utc.format("%Y-%m-%d %H:%M:%S") + exception_datetime.format("%Y-%m-%d %H:%M:%S") ); // Create exception event by cloning the existing event to preserve all metadata @@ -1027,8 +995,9 @@ async fn update_single_occurrence( // Update timestamps for the exception event let now = chrono::Utc::now(); + let now_naive = now.naive_utc(); exception_event.dtstamp = now; - exception_event.last_modified = Some(now); + exception_event.last_modified = Some(now_naive); // Keep original created timestamp to preserve event history // Set RECURRENCE-ID to point to the original occurrence @@ -1044,7 +1013,7 @@ async fn update_single_occurrence( println!( "✨ Created exception event with RECURRENCE-ID: {}", - exception_utc.format("%Y-%m-%d %H:%M:%S") + exception_datetime.format("%Y-%m-%d %H:%M:%S") ); // Create the exception event as a new event (original series will be updated by main handler) @@ -1172,15 +1141,14 @@ async fn delete_single_occurrence( // Create the EXDATE datetime (use the same time as the original event) let original_time = existing_event.dtstart.time(); let exception_datetime = exception_date.and_time(original_time); - let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime); // Add the exception date to the event's EXDATE list let mut updated_event = existing_event; - updated_event.exdate.push(exception_utc); + updated_event.exdate.push(exception_datetime); println!( "🗑️ Added EXDATE for single occurrence deletion: {}", - exception_utc.format("%Y%m%dT%H%M%SZ") + exception_datetime.format("%Y%m%dT%H%M%S") ); // Update the event on the CalDAV server diff --git a/calendar-models/src/vevent.rs b/calendar-models/src/vevent.rs index 48930c4..54328e2 100644 --- a/calendar-models/src/vevent.rs +++ b/calendar-models/src/vevent.rs @@ -1,7 +1,7 @@ //! VEvent - RFC 5545 compliant calendar event structure use crate::common::*; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; // ==================== VEVENT COMPONENT ==================== @@ -9,12 +9,14 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct VEvent { // Required properties - pub dtstamp: DateTime, // Date-time stamp (DTSTAMP) - REQUIRED - pub uid: String, // Unique identifier (UID) - REQUIRED - pub dtstart: DateTime, // Start date-time (DTSTART) - REQUIRED + pub dtstamp: DateTime, // Date-time stamp (DTSTAMP) - REQUIRED (always UTC) + pub uid: String, // Unique identifier (UID) - REQUIRED + pub dtstart: NaiveDateTime, // Start date-time (DTSTART) - REQUIRED (local time) + pub dtstart_tzid: Option, // Timezone ID for DTSTART (TZID parameter) // Optional properties (commonly used) - pub dtend: Option>, // End date-time (DTEND) + pub dtend: Option, // End date-time (DTEND) (local time) + pub dtend_tzid: Option, // Timezone ID for DTEND (TZID parameter) pub duration: Option, // Duration (DURATION) - alternative to DTEND pub summary: Option, // Summary/title (SUMMARY) pub description: Option, // Description (DESCRIPTION) @@ -43,14 +45,19 @@ pub struct VEvent { // Versioning and modification pub sequence: Option, // Sequence number (SEQUENCE) - pub created: Option>, // Creation time (CREATED) - pub last_modified: Option>, // Last modified (LAST-MODIFIED) + pub created: Option, // Creation time (CREATED) (local time) + pub created_tzid: Option, // Timezone ID for CREATED + pub last_modified: Option, // Last modified (LAST-MODIFIED) (local time) + pub last_modified_tzid: Option, // Timezone ID for LAST-MODIFIED // Recurrence - pub rrule: Option, // Recurrence rule (RRULE) - pub rdate: Vec>, // Recurrence dates (RDATE) - pub exdate: Vec>, // Exception dates (EXDATE) - pub recurrence_id: Option>, // Recurrence ID (RECURRENCE-ID) + pub rrule: Option, // Recurrence rule (RRULE) + pub rdate: Vec, // Recurrence dates (RDATE) (local time) + pub rdate_tzid: Option, // Timezone ID for RDATE + pub exdate: Vec, // Exception dates (EXDATE) (local time) + pub exdate_tzid: Option, // Timezone ID for EXDATE + pub recurrence_id: Option, // Recurrence ID (RECURRENCE-ID) (local time) + pub recurrence_id_tzid: Option, // Timezone ID for RECURRENCE-ID // Alarms and attachments pub alarms: Vec, // VALARM components @@ -64,13 +71,15 @@ pub struct VEvent { } impl VEvent { - /// Create a new VEvent with required fields - pub fn new(uid: String, dtstart: DateTime) -> Self { + /// Create a new VEvent with required fields (local time) + pub fn new(uid: String, dtstart: NaiveDateTime) -> Self { Self { dtstamp: Utc::now(), uid, dtstart, + dtstart_tzid: None, dtend: None, + dtend_tzid: None, duration: None, summary: None, description: None, @@ -89,12 +98,17 @@ impl VEvent { url: None, geo: None, sequence: None, - created: Some(Utc::now()), - last_modified: Some(Utc::now()), + created: Some(chrono::Local::now().naive_local()), + created_tzid: None, + last_modified: Some(chrono::Local::now().naive_local()), + last_modified_tzid: None, rrule: None, rdate: Vec::new(), + rdate_tzid: None, exdate: Vec::new(), + exdate_tzid: None, recurrence_id: None, + recurrence_id_tzid: None, alarms: Vec::new(), attachments: Vec::new(), etag: None, @@ -105,7 +119,7 @@ impl VEvent { } /// Helper method to get effective end time (dtend or dtstart + duration) - pub fn get_end_time(&self) -> DateTime { + pub fn get_end_time(&self) -> NaiveDateTime { if let Some(dtend) = self.dtend { dtend } else if let Some(duration) = self.duration { @@ -136,7 +150,7 @@ impl VEvent { /// Helper method to get start date for UI compatibility pub fn get_date(&self) -> chrono::NaiveDate { - self.dtstart.date_naive() + self.dtstart.date() } /// Check if event is recurring diff --git a/frontend/src/components/calendar.rs b/frontend/src/components/calendar.rs index 02629c4..0c5cc23 100644 --- a/frontend/src/components/calendar.rs +++ b/frontend/src/components/calendar.rs @@ -32,7 +32,7 @@ pub struct CalendarProps { chrono::NaiveDateTime, chrono::NaiveDateTime, bool, - Option>, + Option, Option, Option, )>, @@ -437,7 +437,7 @@ pub fn Calendar(props: &CalendarProps) -> Html { chrono::NaiveDateTime, chrono::NaiveDateTime, bool, - Option>, + Option, Option, Option, )| { diff --git a/frontend/src/components/calendar_management_modal.rs b/frontend/src/components/calendar_management_modal.rs index 1b731c4..a91f7d4 100644 --- a/frontend/src/components/calendar_management_modal.rs +++ b/frontend/src/components/calendar_management_modal.rs @@ -195,7 +195,7 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { let on_external_success = on_external_success.clone(); wasm_bindgen_futures::spawn_local(async move { - let calendar_service = CalendarService::new(); + let _calendar_service = CalendarService::new(); match CalendarService::create_external_calendar(&name, &url, &color).await { Ok(calendar) => { diff --git a/frontend/src/components/create_event_modal.rs b/frontend/src/components/create_event_modal.rs index 1335ff9..478fb03 100644 --- a/frontend/src/components/create_event_modal.rs +++ b/frontend/src/components/create_event_modal.rs @@ -238,12 +238,11 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { // Convert VEvent to EventCreationData for editing fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calendars: &[CalendarInfo]) -> EventCreationData { - use chrono::Local; - // Convert start datetime from UTC to local - let start_local = event.dtstart.with_timezone(&Local).naive_local(); + // VEvent fields are already local time (NaiveDateTime) + let start_local = event.dtstart; let end_local = if let Some(dtend) = event.dtend { - dtend.with_timezone(&Local).naive_local() + dtend } else { // Default to 1 hour after start if no end time start_local + chrono::Duration::hours(1) diff --git a/frontend/src/components/event_modal.rs b/frontend/src/components/event_modal.rs index bc1723e..dd5207a 100644 --- a/frontend/src/components/event_modal.rs +++ b/frontend/src/components/event_modal.rs @@ -1,5 +1,4 @@ use crate::models::ical::VEvent; -use chrono::{DateTime, Utc}; use yew::prelude::*; #[derive(Properties, PartialEq)] @@ -213,7 +212,7 @@ pub fn EventModal(props: &EventModalProps) -> Html { } } -fn format_datetime(dt: &DateTime, all_day: bool) -> String { +fn format_datetime(dt: &chrono::NaiveDateTime, all_day: bool) -> String { if all_day { dt.format("%B %d, %Y").to_string() } else { @@ -221,7 +220,7 @@ fn format_datetime(dt: &DateTime, all_day: bool) -> String { } } -fn format_datetime_end(dt: &DateTime, all_day: bool) -> String { +fn format_datetime_end(dt: &chrono::NaiveDateTime, all_day: bool) -> String { if all_day { // For all-day events, subtract one day from end date for display // RFC-5545 uses exclusive end dates, but users expect inclusive display diff --git a/frontend/src/components/route_handler.rs b/frontend/src/components/route_handler.rs index bf4bf7f..0a655c0 100644 --- a/frontend/src/components/route_handler.rs +++ b/frontend/src/components/route_handler.rs @@ -38,7 +38,7 @@ pub struct RouteHandlerProps { chrono::NaiveDateTime, chrono::NaiveDateTime, bool, - Option>, + Option, Option, Option, )>, @@ -136,7 +136,7 @@ pub struct CalendarViewProps { chrono::NaiveDateTime, chrono::NaiveDateTime, bool, - Option>, + Option, Option, Option, )>, diff --git a/frontend/src/components/week_view.rs b/frontend/src/components/week_view.rs index 14965a6..c2b3356 100644 --- a/frontend/src/components/week_view.rs +++ b/frontend/src/components/week_view.rs @@ -33,7 +33,7 @@ pub struct WeekViewProps { NaiveDateTime, NaiveDateTime, bool, - Option>, + Option, Option, Option, )>, @@ -285,18 +285,14 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Calculate the day before this occurrence for UNTIL clause let until_date = - edit.event.dtstart.date_naive() - chrono::Duration::days(1); + edit.event.dtstart.date() - chrono::Duration::days(1); let until_datetime = until_date .and_time(chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap()); - let until_utc = - chrono::DateTime::::from_naive_utc_and_offset( - until_datetime, - chrono::Utc, - ); + let until_naive = until_datetime; // Use local time directly web_sys::console::log_1(&format!("🔄 Will set UNTIL {} for original series to end before occurrence {}", - until_utc.format("%Y-%m-%d %H:%M:%S UTC"), - edit.event.dtstart.format("%Y-%m-%d %H:%M:%S UTC")).into()); + until_naive.format("%Y-%m-%d %H:%M:%S"), + edit.event.dtstart.format("%Y-%m-%d %H:%M:%S")).into()); // Critical: Use the dragged times (new_start/new_end) not the original series times // This ensures the new series reflects the user's drag operation @@ -317,7 +313,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { new_start, // Dragged start time for new series new_end, // Dragged end time for new series true, // preserve_rrule = true - Some(until_utc), // UNTIL date for original series + Some(until_naive), // UNTIL date for original series Some("this_and_future".to_string()), // Update scope Some(occurrence_date), // Date of occurrence being modified )); @@ -617,10 +613,9 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Keep the original end time let original_end = if let Some(end) = event.dtend { - end.with_timezone(&chrono::Local).naive_local() - } else { + end } else { // If no end time, use start time + 1 hour as default - event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) + event.dtstart + chrono::Duration::hours(1) }; let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); @@ -651,8 +646,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Calculate new end time based on drag position let new_end_time = pixels_to_time(current_drag.current_y, time_increment); - // Keep the original start time - let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); + // Keep the original start time (already local) + let original_start = event.dtstart; let new_end_datetime = NaiveDateTime::new(current_drag.start_date, new_end_time); @@ -827,9 +822,9 @@ pub fn week_view(props: &WeekViewProps) -> Html { let time_display = if event.all_day { "All Day".to_string() } else { - let local_start = event.dtstart.with_timezone(&Local); + let local_start = event.dtstart; if let Some(end) = event.dtend { - let local_end = end.with_timezone(&Local); + let local_end = end; // Check if both times are in same AM/PM period to avoid redundancy let start_is_am = local_start.hour() < 12; @@ -1056,14 +1051,13 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Show the event being resized from the start let new_start_time = pixels_to_time(drag.current_y, props.time_increment); let original_end = if let Some(end) = event.dtend { - end.with_timezone(&chrono::Local).naive_local() - } else { - event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) + end } else { + event.dtstart + chrono::Duration::hours(1) }; // Calculate positions for the preview let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour); - let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local()); + let original_duration = original_end.signed_duration_since(event.dtstart); let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32); let new_start_pixels = drag.current_y; @@ -1089,7 +1083,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { DragType::ResizeEventEnd(event) => { // Show the event being resized from the end let new_end_time = pixels_to_time(drag.current_y, props.time_increment); - let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); + let original_start = event.dtstart; // Calculate positions for the preview let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour); @@ -1227,12 +1221,12 @@ fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime { } fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32, print_pixels_per_hour: Option, print_start_hour: Option) -> (f32, f32, bool) { - // Convert UTC times to local time for display - let local_start = event.dtstart.with_timezone(&Local); + // Events are already in local time + let local_start = event.dtstart; - // Events should display based on their stored date (which now preserves the original local date) - // not the calculated local date from UTC conversion, since we fixed the creation logic - let event_date = event.dtstart.date_naive(); // Use the stored date, not the converted local date + // Events should display based on their local date, since we now store proper UTC times + // Convert the UTC stored time back to local time to determine display date + let event_date = local_start.date(); if event_date != date { return (0.0, 0.0, false); // Event not on this date @@ -1263,8 +1257,8 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32 // Calculate duration and height let duration_pixels = if let Some(end) = event.dtend { - let local_end = end.with_timezone(&Local); - let end_date = local_end.date_naive(); + let local_end = end; + let end_date = local_end.date(); // Handle events that span multiple days by capping at midnight if end_date > date { @@ -1291,16 +1285,16 @@ fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool { return false; } - let start1 = event1.dtstart.with_timezone(&Local).naive_local(); + let start1 = event1.dtstart; let end1 = if let Some(end) = event1.dtend { - end.with_timezone(&Local).naive_local() + end } else { start1 + chrono::Duration::hours(1) // Default 1 hour duration }; - let start2 = event2.dtstart.with_timezone(&Local).naive_local(); + let start2 = event2.dtstart; let end2 = if let Some(end) = event2.dtend { - end.with_timezone(&Local).naive_local() + end } else { start2 + chrono::Duration::hours(1) // Default 1 hour duration }; @@ -1322,8 +1316,8 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3 } let (_, _, _) = calculate_event_position(event, date, time_increment, None, None); - let local_start = event.dtstart.with_timezone(&Local); - let event_date = local_start.date_naive(); + let local_start = event.dtstart; + let event_date = local_start.date(); if event_date == date || (event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20) { Some((idx, event)) @@ -1334,7 +1328,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3 .collect(); // Sort by start time - day_events.sort_by_key(|(_, event)| event.dtstart.with_timezone(&Local).naive_local()); + day_events.sort_by_key(|(_, event)| event.dtstart); // For each event, find all events it overlaps with let mut event_columns = vec![(0, 1); events.len()]; // (column_idx, total_columns) @@ -1359,7 +1353,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3 } else { // This event overlaps - we need to calculate column layout // Sort the overlapping group by start time - overlapping_events.sort_by_key(|&idx| day_events[idx].1.dtstart.with_timezone(&Local).naive_local()); + overlapping_events.sort_by_key(|&idx| day_events[idx].1.dtstart); // Assign columns using a greedy algorithm let mut columns: Vec> = Vec::new(); @@ -1407,19 +1401,19 @@ fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool { 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() + event.dtstart.date() } else { - event.dtstart.with_timezone(&Local).date_naive() + event.dtstart.date() }; let end_date = if let Some(dtend) = event.dtend { 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) + dtend.date() - chrono::Duration::days(1) } else { // For timed events, use timezone conversion - dtend.with_timezone(&Local).date_naive() + dtend.date() } } else { // Single day event