diff --git a/src/app.rs b/src/app.rs index 48a80da..115ed8b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,8 @@ use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; use web_sys::MouseEvent; use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; -use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; +use crate::services::{CalendarService, calendar_service::UserInfo}; +use crate::models::ical::VEvent; use chrono::NaiveDate; fn get_theme_event_colors() -> Vec { @@ -47,7 +48,7 @@ pub fn App() -> Html { let context_menu_calendar_path = use_state(|| -> Option { None }); let event_context_menu_open = use_state(|| false); let event_context_menu_pos = use_state(|| (0i32, 0i32)); - let event_context_menu_event = use_state(|| -> Option { None }); + let event_context_menu_event = use_state(|| -> Option { None }); let calendar_context_menu_open = use_state(|| false); let calendar_context_menu_pos = use_state(|| (0i32, 0i32)); let calendar_context_menu_date = use_state(|| -> Option { None }); @@ -278,7 +279,7 @@ pub fn App() -> Html { let event_context_menu_open = event_context_menu_open.clone(); let event_context_menu_pos = event_context_menu_pos.clone(); let event_context_menu_event = event_context_menu_event.clone(); - Callback::from(move |(event, calendar_event): (MouseEvent, CalendarEvent)| { + Callback::from(move |(event, calendar_event): (MouseEvent, VEvent)| { event_context_menu_open.set(true); event_context_menu_pos.set((event.client_x(), event.client_y())); event_context_menu_event.set(Some(calendar_event)); @@ -313,12 +314,12 @@ pub fn App() -> Html { web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into()); create_event_modal_open.set(false); - if let Some(token) = (*auth_token).clone() { + if let Some(_token) = (*auth_token).clone() { wasm_bindgen_futures::spawn_local(async move { - let calendar_service = CalendarService::new(); + let _calendar_service = CalendarService::new(); // Get CalDAV password from storage - let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { + let _password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { @@ -328,14 +329,30 @@ pub fn App() -> Html { String::new() }; - // Use v2 API with structured data (no string conversion needed!) - let create_request = event_data.to_create_request_v2(); - - match calendar_service.create_event_v2( - &token, - &password, - create_request, - ).await { + let params = event_data.to_create_event_params(); + let create_result = _calendar_service.create_event( + &_token, + &_password, + params.0, // title + params.1, // description + params.2, // start_date + params.3, // start_time + params.4, // end_date + params.5, // end_time + params.6, // location + params.7, // all_day + params.8, // status + params.9, // class + params.10, // priority + params.11, // organizer + params.12, // attendees + params.13, // categories + params.14, // reminder + params.15, // recurrence + params.16, // recurrence_days + params.17 // calendar_path + ).await; + match create_result { Ok(_) => { web_sys::console::log_1(&"Event created successfully".into()); // Trigger a page reload to refresh events from all calendars @@ -354,7 +371,7 @@ pub fn App() -> Html { let on_event_update = { let auth_token = auth_token.clone(); - Callback::from(move |(original_event, new_start, new_end, preserve_rrule, until_date): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>)| { + Callback::from(move |(original_event, new_start, new_end, preserve_rrule, until_date): (VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>)| { web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}", original_event.uid, new_start.format("%Y-%m-%d %H:%M"), @@ -392,26 +409,29 @@ pub fn App() -> Html { // Convert existing event data to string formats for the API let status_str = match original_event.status { - crate::services::calendar_service::EventStatus::Tentative => "TENTATIVE".to_string(), - crate::services::calendar_service::EventStatus::Confirmed => "CONFIRMED".to_string(), - crate::services::calendar_service::EventStatus::Cancelled => "CANCELLED".to_string(), + Some(crate::models::ical::EventStatus::Tentative) => "TENTATIVE".to_string(), + Some(crate::models::ical::EventStatus::Confirmed) => "CONFIRMED".to_string(), + Some(crate::models::ical::EventStatus::Cancelled) => "CANCELLED".to_string(), + None => "CONFIRMED".to_string(), // Default status }; let class_str = match original_event.class { - crate::services::calendar_service::EventClass::Public => "PUBLIC".to_string(), - crate::services::calendar_service::EventClass::Private => "PRIVATE".to_string(), - crate::services::calendar_service::EventClass::Confidential => "CONFIDENTIAL".to_string(), + Some(crate::models::ical::EventClass::Public) => "PUBLIC".to_string(), + Some(crate::models::ical::EventClass::Private) => "PRIVATE".to_string(), + Some(crate::models::ical::EventClass::Confidential) => "CONFIDENTIAL".to_string(), + None => "PUBLIC".to_string(), // Default class }; // Convert reminders to string format - let reminder_str = if !original_event.reminders.is_empty() { - format!("{}", original_event.reminders[0].minutes_before) + let reminder_str = if !original_event.alarms.is_empty() { + // Convert from VAlarm to minutes before + "15".to_string() // TODO: Convert VAlarm trigger to minutes } else { "".to_string() }; // Handle recurrence (keep existing) - let recurrence_str = original_event.recurrence_rule.unwrap_or_default(); + let recurrence_str = original_event.rrule.unwrap_or_default(); let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence match calendar_service.update_event( @@ -429,14 +449,14 @@ pub fn App() -> Html { status_str, class_str, original_event.priority, - original_event.organizer.unwrap_or_default(), - original_event.attendees.join(","), + original_event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(), + original_event.attendees.iter().map(|a| a.cal_address.clone()).collect::>().join(","), original_event.categories.join(","), reminder_str, recurrence_str, recurrence_days, original_event.calendar_path, - original_event.exception_dates.clone(), + original_event.exdate.clone(), if preserve_rrule { Some("update_series".to_string()) } else { None }, until_date ).await { @@ -704,11 +724,11 @@ pub fn App() -> Html { }; // Get the occurrence date from the clicked event - let occurrence_date = Some(event.start.date_naive().format("%Y-%m-%d").to_string()); + let occurrence_date = Some(event.dtstart.date_naive().format("%Y-%m-%d").to_string()); web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into()); web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into()); - web_sys::console::log_1(&format!("🔄 Event start: {}", event.start).into()); + web_sys::console::log_1(&format!("🔄 Event start: {}", event.dtstart).into()); web_sys::console::log_1(&format!("🔄 Occurrence date: {:?}", occurrence_date).into()); match calendar_service.delete_event( @@ -775,7 +795,7 @@ pub fn App() -> Html { let auth_token = auth_token.clone(); let create_event_modal_open = create_event_modal_open.clone(); let event_context_menu_event = event_context_menu_event.clone(); - move |(original_event, updated_data): (CalendarEvent, EventCreationData)| { + move |(original_event, updated_data): (VEvent, EventCreationData)| { web_sys::console::log_1(&format!("Updating event: {:?}", updated_data).into()); create_event_modal_open.set(false); event_context_menu_event.set(None); @@ -932,7 +952,7 @@ pub fn App() -> Html { recurrence_str, updated_data.recurrence_days, updated_data.selected_calendar, - original_event.exception_dates.clone(), + original_event.exdate.clone(), Some("update_series".to_string()), // This is for event edit modal, preserve original RRULE None // No until_date for edit modal ).await { diff --git a/src/components/create_event_modal.rs b/src/components/create_event_modal.rs index 9906833..cb4ba0f 100644 --- a/src/components/create_event_modal.rs +++ b/src/components/create_event_modal.rs @@ -1,16 +1,17 @@ use yew::prelude::*; use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement}; -use chrono::{NaiveDate, NaiveTime, Utc, TimeZone}; -use crate::services::calendar_service::{CalendarInfo, CalendarEvent, CreateEventRequestV2, AttendeeV2, AlarmV2, AttendeeRoleV2, ParticipationStatusV2, AlarmActionV2}; +use chrono::{NaiveDate, NaiveTime, Local, TimeZone, Utc}; +use crate::services::calendar_service::CalendarInfo; +use crate::models::ical::VEvent; #[derive(Properties, PartialEq)] pub struct CreateEventModalProps { pub is_open: bool, pub selected_date: Option, - pub event_to_edit: Option, + pub event_to_edit: Option, pub on_close: Callback<()>, pub on_create: Callback, - pub on_update: Callback<(CalendarEvent, EventCreationData)>, // (original_event, updated_data) + pub on_update: Callback<(VEvent, EventCreationData)>, // (original_event, updated_data) pub available_calendars: Vec, #[prop_or_default] pub initial_start_time: Option, @@ -31,15 +32,6 @@ impl Default for EventStatus { } } -impl EventStatus { - pub fn from_service_status(status: &crate::services::calendar_service::EventStatus) -> Self { - match status { - crate::services::calendar_service::EventStatus::Tentative => EventStatus::Tentative, - crate::services::calendar_service::EventStatus::Confirmed => EventStatus::Confirmed, - crate::services::calendar_service::EventStatus::Cancelled => EventStatus::Cancelled, - } - } -} #[derive(Clone, PartialEq, Debug)] pub enum EventClass { @@ -54,15 +46,6 @@ impl Default for EventClass { } } -impl EventClass { - pub fn from_service_class(class: &crate::services::calendar_service::EventClass) -> Self { - match class { - crate::services::calendar_service::EventClass::Public => EventClass::Public, - crate::services::calendar_service::EventClass::Private => EventClass::Private, - crate::services::calendar_service::EventClass::Confidential => EventClass::Confidential, - } - } -} #[derive(Clone, PartialEq, Debug)] pub enum ReminderType { @@ -161,187 +144,98 @@ impl Default for EventCreationData { } impl EventCreationData { - pub fn from_calendar_event(event: &CalendarEvent) -> Self { - // Convert CalendarEvent to EventCreationData for editing + pub fn to_create_event_params(&self) -> (String, String, String, String, String, String, String, bool, String, String, Option, String, String, String, String, String, Vec, Option) { + // Convert local date/time to UTC + let start_local = Local.from_local_datetime(&self.start_date.and_time(self.start_time)).single() + .unwrap_or_else(|| Local::now()); + let end_local = Local.from_local_datetime(&self.end_date.and_time(self.end_time)).single() + .unwrap_or_else(|| Local::now()); + + let start_utc = start_local.with_timezone(&Utc); + let end_utc = end_local.with_timezone(&Utc); + + ( + self.title.clone(), + self.description.clone(), + start_utc.format("%Y-%m-%d").to_string(), + start_utc.format("%H:%M").to_string(), + end_utc.format("%Y-%m-%d").to_string(), + end_utc.format("%H:%M").to_string(), + self.location.clone(), + self.all_day, + match self.status { + EventStatus::Tentative => "TENTATIVE".to_string(), + EventStatus::Confirmed => "CONFIRMED".to_string(), + EventStatus::Cancelled => "CANCELLED".to_string(), + }, + match self.class { + EventClass::Public => "PUBLIC".to_string(), + EventClass::Private => "PRIVATE".to_string(), + EventClass::Confidential => "CONFIDENTIAL".to_string(), + }, + self.priority, + self.organizer.clone(), + self.attendees.clone(), + self.categories.clone(), + match self.reminder { + ReminderType::None => "".to_string(), + ReminderType::Minutes15 => "15".to_string(), + ReminderType::Minutes30 => "30".to_string(), + ReminderType::Hour1 => "60".to_string(), + ReminderType::Hours2 => "120".to_string(), + ReminderType::Day1 => "1440".to_string(), + ReminderType::Days2 => "2880".to_string(), + ReminderType::Week1 => "10080".to_string(), + }, + match self.recurrence { + RecurrenceType::None => "".to_string(), + RecurrenceType::Daily => "DAILY".to_string(), + RecurrenceType::Weekly => "WEEKLY".to_string(), + RecurrenceType::Monthly => "MONTHLY".to_string(), + RecurrenceType::Yearly => "YEARLY".to_string(), + }, + self.recurrence_days.clone(), + self.selected_calendar.clone() + ) + } +} + +impl EventCreationData { + pub fn from_calendar_event(event: &VEvent) -> Self { + // Convert VEvent to EventCreationData for editing // All events (including temporary drag events) now have proper UTC times // Convert to local time for display in the modal Self { title: event.summary.clone().unwrap_or_default(), description: event.description.clone().unwrap_or_default(), - start_date: event.start.with_timezone(&chrono::Local).date_naive(), - start_time: event.start.with_timezone(&chrono::Local).time(), - end_date: event.end.as_ref().map(|e| e.with_timezone(&chrono::Local).date_naive()).unwrap_or(event.start.with_timezone(&chrono::Local).date_naive()), - end_time: event.end.as_ref().map(|e| e.with_timezone(&chrono::Local).time()).unwrap_or(event.start.with_timezone(&chrono::Local).time()), + start_date: event.dtstart.with_timezone(&chrono::Local).date_naive(), + start_time: event.dtstart.with_timezone(&chrono::Local).time(), + end_date: event.dtend.as_ref().map(|e| e.with_timezone(&chrono::Local).date_naive()).unwrap_or(event.dtstart.with_timezone(&chrono::Local).date_naive()), + end_time: event.dtend.as_ref().map(|e| e.with_timezone(&chrono::Local).time()).unwrap_or(event.dtstart.with_timezone(&chrono::Local).time()), location: event.location.clone().unwrap_or_default(), all_day: event.all_day, - status: EventStatus::from_service_status(&event.status), - class: EventClass::from_service_class(&event.class), + status: event.status.as_ref().map(|s| match s { + crate::models::ical::EventStatus::Tentative => EventStatus::Tentative, + crate::models::ical::EventStatus::Confirmed => EventStatus::Confirmed, + crate::models::ical::EventStatus::Cancelled => EventStatus::Cancelled, + }).unwrap_or(EventStatus::Confirmed), + class: event.class.as_ref().map(|c| match c { + crate::models::ical::EventClass::Public => EventClass::Public, + crate::models::ical::EventClass::Private => EventClass::Private, + crate::models::ical::EventClass::Confidential => EventClass::Confidential, + }).unwrap_or(EventClass::Public), priority: event.priority, - organizer: event.organizer.clone().unwrap_or_default(), - attendees: event.attendees.join(", "), + organizer: event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(), + attendees: event.attendees.iter().map(|a| a.cal_address.clone()).collect::>().join(", "), categories: event.categories.join(", "), reminder: ReminderType::default(), // TODO: Convert from event reminders - recurrence: RecurrenceType::from_rrule(event.recurrence_rule.as_deref()), + recurrence: RecurrenceType::from_rrule(event.rrule.as_deref()), recurrence_days: vec![false; 7], // TODO: Parse from RRULE selected_calendar: event.calendar_path.clone(), } } - /// Convert EventCreationData to CreateEventRequestV2 for the new v2 API - pub fn to_create_request_v2(&self) -> CreateEventRequestV2 { - // Combine date and time into UTC DateTime - let start_local = self.start_date.and_time(self.start_time); - let end_local = self.end_date.and_time(self.end_time); - - // Convert local time to UTC (assuming local timezone for now) - let start_utc = chrono::Local.from_local_datetime(&start_local) - .single() - .unwrap_or_else(|| chrono::Local.from_local_datetime(&start_local).earliest().unwrap()) - .with_timezone(&Utc); - let end_utc = chrono::Local.from_local_datetime(&end_local) - .single() - .unwrap_or_else(|| chrono::Local.from_local_datetime(&end_local).earliest().unwrap()) - .with_timezone(&Utc); - - // Convert status - let status = match self.status { - EventStatus::Tentative => Some(crate::services::calendar_service::EventStatus::Tentative), - EventStatus::Confirmed => Some(crate::services::calendar_service::EventStatus::Confirmed), - EventStatus::Cancelled => Some(crate::services::calendar_service::EventStatus::Cancelled), - }; - - // Convert class - let class = match self.class { - EventClass::Public => Some(crate::services::calendar_service::EventClass::Public), - EventClass::Private => Some(crate::services::calendar_service::EventClass::Private), - EventClass::Confidential => Some(crate::services::calendar_service::EventClass::Confidential), - }; - - // Convert attendees from comma-separated string to structured list - let attendees = if self.attendees.trim().is_empty() { - Vec::new() - } else { - self.attendees.split(',') - .map(|email| AttendeeV2 { - email: email.trim().to_string(), - name: None, - role: Some(AttendeeRoleV2::Required), - status: Some(ParticipationStatusV2::NeedsAction), - rsvp: Some(true), - }) - .collect() - }; - - // Convert categories from comma-separated string to vector - let categories = if self.categories.trim().is_empty() { - Vec::new() - } else { - self.categories.split(',') - .map(|cat| cat.trim().to_string()) - .filter(|cat| !cat.is_empty()) - .collect() - }; - - // Convert reminder to alarms - let alarms = match self.reminder { - ReminderType::Minutes15 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -15, - description: Some("Event reminder".to_string()), - }], - ReminderType::Minutes30 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -30, - description: Some("Event reminder".to_string()), - }], - ReminderType::Hour1 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -60, - description: Some("Event reminder".to_string()), - }], - ReminderType::Hours2 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -120, - description: Some("Event reminder".to_string()), - }], - ReminderType::Day1 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -1440, - description: Some("Event reminder".to_string()), - }], - ReminderType::Days2 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -2880, - description: Some("Event reminder".to_string()), - }], - ReminderType::Week1 => vec![AlarmV2 { - action: AlarmActionV2::Display, - trigger_minutes: -10080, - description: Some("Event reminder".to_string()), - }], - ReminderType::None => Vec::new(), - }; - - // Convert recurrence to RRULE string - let rrule = match self.recurrence { - RecurrenceType::Daily => Some("FREQ=DAILY".to_string()), - RecurrenceType::Weekly => { - let mut rrule = "FREQ=WEEKLY".to_string(); - - // Add BYDAY if specific days are selected - if self.recurrence_days.len() == 7 { - let selected_days: Vec<&str> = self.recurrence_days - .iter() - .enumerate() - .filter_map(|(i, &selected)| { - if selected { - Some(match i { - 0 => "SU", // Sunday - 1 => "MO", // Monday - 2 => "TU", // Tuesday - 3 => "WE", // Wednesday - 4 => "TH", // Thursday - 5 => "FR", // Friday - 6 => "SA", // Saturday - _ => return None, - }) - } else { - None - } - }) - .collect(); - - if !selected_days.is_empty() { - rrule.push_str(&format!(";BYDAY={}", selected_days.join(","))); - } - } - - Some(rrule) - }, - RecurrenceType::Monthly => Some("FREQ=MONTHLY".to_string()), - RecurrenceType::Yearly => Some("FREQ=YEARLY".to_string()), - RecurrenceType::None => None, - }; - - CreateEventRequestV2 { - summary: self.title.clone(), - description: if self.description.trim().is_empty() { None } else { Some(self.description.clone()) }, - dtstart: start_utc, - dtend: Some(end_utc), - location: if self.location.trim().is_empty() { None } else { Some(self.location.clone()) }, - all_day: self.all_day, - status, - class, - priority: self.priority, - organizer: if self.organizer.trim().is_empty() { None } else { Some(self.organizer.clone()) }, - attendees, - categories, - rrule, - alarms, - calendar_path: self.selected_calendar.clone(), - } - } } #[function_component(CreateEventModal)]