use crate::components::{ CalendarContextMenu, CalendarManagementModal, ContextMenu, CreateEventModal, DeleteAction, EditAction, EventContextMenu, EventModal, EventCreationData, MobileWarningModal, RouteHandler, Sidebar, Theme, ViewMode, }; use crate::components::mobile_warning_modal::is_mobile_device; use crate::components::sidebar::{Style}; use crate::models::ical::VEvent; use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService}; use chrono::NaiveDate; use gloo_storage::{LocalStorage, Storage}; use gloo_timers::callback::Interval; use wasm_bindgen::JsCast; use web_sys::MouseEvent; use yew::prelude::*; use yew_router::prelude::*; fn get_theme_event_colors() -> Vec { if let Some(window) = web_sys::window() { if let Some(document) = window.document() { if let Some(root) = document.document_element() { if let Ok(Some(computed_style)) = window.get_computed_style(&root) { if let Ok(colors_string) = computed_style.get_property_value("--event-colors") { if !colors_string.is_empty() { return colors_string .split(',') .map(|color| color.trim().to_string()) .filter(|color| !color.is_empty()) .collect(); } } } } } } vec![ "#3B82F6".to_string(), "#10B981".to_string(), "#F59E0B".to_string(), "#EF4444".to_string(), "#8B5CF6".to_string(), "#06B6D4".to_string(), "#84CC16".to_string(), "#F97316".to_string(), "#EC4899".to_string(), "#6366F1".to_string(), "#14B8A6".to_string(), "#F3B806".to_string(), "#8B5A2B".to_string(), "#6B7280".to_string(), "#DC2626".to_string(), "#7C3AED".to_string(), ] } #[function_component] pub fn App() -> Html { let auth_token = use_state(|| -> Option { None }); // Validate token on app startup { let auth_token = auth_token.clone(); use_effect_with((), move |_| { let auth_token = auth_token.clone(); wasm_bindgen_futures::spawn_local(async move { // Check if there's a stored token if let Ok(stored_token) = LocalStorage::get::("auth_token") { // Verify the stored token let auth_service = crate::auth::AuthService::new(); match auth_service.verify_token(&stored_token).await { Ok(true) => { // Token is valid, set it web_sys::console::log_1(&"✅ Stored auth token is valid".into()); auth_token.set(Some(stored_token)); } _ => { // Token is invalid or verification failed, clear it web_sys::console::log_1(&"❌ Stored auth token is invalid, clearing".into()); let _ = LocalStorage::delete("auth_token"); let _ = LocalStorage::delete("session_token"); let _ = LocalStorage::delete("caldav_credentials"); auth_token.set(None); } } } else { // No stored token web_sys::console::log_1(&"ℹ️ No stored auth token found".into()); auth_token.set(None); } }); || () }); } let user_info = use_state(|| -> Option { None }); let color_picker_open = use_state(|| -> Option { None }); let calendar_management_modal_open = use_state(|| false); let context_menu_open = use_state(|| false); let context_menu_pos = use_state(|| (0i32, 0i32)); 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 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 }); let create_event_modal_open = use_state(|| false); let selected_date_for_event = use_state(|| -> Option { None }); let event_edit_scope = use_state(|| -> Option { None }); let view_event_modal_open = use_state(|| false); let view_event_modal_event = use_state(|| -> Option { None }); let refreshing_calendar_id = use_state(|| -> Option { None }); let _recurring_edit_modal_open = use_state(|| false); let _recurring_edit_event = use_state(|| -> Option { None }); let _recurring_edit_data = use_state(|| -> Option { None }); // External calendar state let external_calendars = use_state(|| -> Vec { Vec::new() }); let external_calendar_events = use_state(|| -> Vec { Vec::new() }); // Mobile warning state let mobile_warning_open = use_state(|| is_mobile_device()); let refresh_interval = use_state(|| -> Option { None }); // Calendar view state - load from localStorage if available let current_view = use_state(|| { // Try to load saved view mode from localStorage if let Ok(saved_view) = LocalStorage::get::("calendar_view_mode") { match saved_view.as_str() { "week" => ViewMode::Week, _ => ViewMode::Month, } } else { ViewMode::Month // Default to month view } }); // Theme state - load from localStorage if available let current_theme = use_state(|| { // Try to load saved theme from localStorage if let Ok(saved_theme) = LocalStorage::get::("calendar_theme") { Theme::from_value(&saved_theme) } else { Theme::Default // Default theme } }); // Style state - load from localStorage if available let current_style = use_state(|| { // Try to load saved style from localStorage if let Ok(saved_style) = LocalStorage::get::("calendar_style") { Style::from_value(&saved_style) } else { Style::Default // Default style } }); let available_colors = use_state(|| get_theme_event_colors()); // Function to refresh calendar data without full page reload let refresh_calendar_data = { let user_info = user_info.clone(); let auth_token = auth_token.clone(); let external_calendars = external_calendars.clone(); let external_calendar_events = external_calendar_events.clone(); Callback::from(move |_| { let user_info = user_info.clone(); let auth_token = auth_token.clone(); let external_calendars = external_calendars.clone(); let external_calendar_events = external_calendar_events.clone(); wasm_bindgen_futures::spawn_local(async move { // Refresh main calendar data if authenticated if let Some(token) = (*auth_token).clone() { let calendar_service = CalendarService::new(); 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 { String::new() } } else { String::new() }; if !password.is_empty() { match calendar_service.fetch_user_info(&token, &password).await { Ok(mut info) => { // Apply saved colors if let Ok(saved_colors_json) = LocalStorage::get::("calendar_colors") { if let Ok(saved_info) = serde_json::from_str::(&saved_colors_json) { for saved_cal in &saved_info.calendars { for cal in &mut info.calendars { if cal.path == saved_cal.path { cal.color = saved_cal.color.clone(); } } } } } // Add timestamp to force re-render info.last_updated = (js_sys::Date::now() / 1000.0) as u64; user_info.set(Some(info)); } Err(err) => { web_sys::console::log_1( &format!("Failed to refresh main calendar data: {}", err).into(), ); } } } } // Refresh external calendars data match CalendarService::get_external_calendars().await { Ok(calendars) => { external_calendars.set(calendars.clone()); // Load events for visible external calendars let mut all_external_events = Vec::new(); for calendar in calendars { if calendar.is_visible { match CalendarService::fetch_external_calendar_events(calendar.id).await { Ok(mut events) => { // Set calendar_path for color matching for event in &mut events { event.calendar_path = Some(format!("external_{}", calendar.id)); } all_external_events.extend(events); } Err(e) => { web_sys::console::log_1( &format!("Failed to fetch events for external calendar {}: {}", calendar.id, e).into(), ); } } } } external_calendar_events.set(all_external_events); } Err(e) => { web_sys::console::log_1( &format!("Failed to refresh external calendars: {}", e).into(), ); } } }); }) }; let on_login = { let auth_token = auth_token.clone(); Callback::from(move |token: String| { auth_token.set(Some(token)); }) }; let on_logout = { let auth_token = auth_token.clone(); let user_info = user_info.clone(); Callback::from(move |_| { let _ = LocalStorage::delete("auth_token"); auth_token.set(None); user_info.set(None); }) }; let on_mobile_warning_close = { let mobile_warning_open = mobile_warning_open.clone(); Callback::from(move |_| { mobile_warning_open.set(false); }) }; let on_view_change = { let current_view = current_view.clone(); Callback::from(move |new_view: ViewMode| { // Save view mode to localStorage let view_string = match new_view { ViewMode::Month => "month", ViewMode::Week => "week", }; let _ = LocalStorage::set("calendar_view_mode", view_string); // Update state current_view.set(new_view); }) }; let on_theme_change = { let current_theme = current_theme.clone(); let available_colors = available_colors.clone(); Callback::from(move |new_theme: Theme| { // Save theme to localStorage let _ = LocalStorage::set("calendar_theme", new_theme.value()); // Apply theme to document root if let Some(document) = web_sys::window().and_then(|w| w.document()) { if let Some(root) = document.document_element() { let _ = root.set_attribute("data-theme", new_theme.value()); } } // Update state current_theme.set(new_theme); // Update available colors after theme change available_colors.set(get_theme_event_colors()); }) }; let on_style_change = { let current_style = current_style.clone(); Callback::from(move |new_style: Style| { // Save style to localStorage let _ = LocalStorage::set("calendar_style", new_style.value()); // Hot-swap stylesheet if let Some(window) = web_sys::window() { if let Some(document) = window.document() { // Remove existing style link if it exists if let Some(existing_link) = document.get_element_by_id("dynamic-style") { existing_link.remove(); } // Create and append new stylesheet link only if style has a path if let Some(stylesheet_path) = new_style.stylesheet_path() { if let Ok(link) = document.create_element("link") { let link = link.dyn_into::().unwrap(); link.set_id("dynamic-style"); link.set_rel("stylesheet"); link.set_href(stylesheet_path); if let Some(head) = document.head() { let _ = head.append_child(&link); } } } // If stylesheet_path is None (Default style), just removing the dynamic link is enough } } // Update state current_style.set(new_style); }) }; // Apply initial theme on mount { let current_theme = current_theme.clone(); use_effect_with((), move |_| { let theme = (*current_theme).clone(); if let Some(document) = web_sys::window().and_then(|w| w.document()) { if let Some(root) = document.document_element() { let _ = root.set_attribute("data-theme", theme.value()); } } }); } // Apply initial style on mount { let current_style = current_style.clone(); use_effect_with((), move |_| { let style = (*current_style).clone(); if let Some(window) = web_sys::window() { if let Some(document) = window.document() { // Create and append stylesheet link for initial style only if it has a path if let Some(stylesheet_path) = style.stylesheet_path() { if let Ok(link) = document.create_element("link") { let link = link.dyn_into::().unwrap(); link.set_id("dynamic-style"); link.set_rel("stylesheet"); link.set_href(stylesheet_path); if let Some(head) = document.head() { let _ = head.append_child(&link); } } } // If initial style is Default (None), no additional stylesheet needed } } }); } // Fetch user info when token is available { let user_info = user_info.clone(); let auth_token = auth_token.clone(); use_effect_with((*auth_token).clone(), move |token| { if let Some(token) = token { let user_info = user_info.clone(); let token = token.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); 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 { String::new() } } else { String::new() }; if !password.is_empty() { match calendar_service.fetch_user_info(&token, &password).await { Ok(mut info) => { if let Ok(saved_colors_json) = LocalStorage::get::("calendar_colors") { if let Ok(saved_info) = serde_json::from_str::(&saved_colors_json) { for saved_cal in &saved_info.calendars { for cal in &mut info.calendars { if cal.path == saved_cal.path { cal.color = saved_cal.color.clone(); } } } } } user_info.set(Some(info)); } Err(err) => { web_sys::console::log_1( &format!("Failed to fetch user info: {}", err).into(), ); } } } }); } else { user_info.set(None); } || () }); } // Function to refresh external calendars let refresh_external_calendars = { let external_calendars = external_calendars.clone(); let external_calendar_events = external_calendar_events.clone(); Callback::from(move |_| { let external_calendars = external_calendars.clone(); let external_calendar_events = external_calendar_events.clone(); wasm_bindgen_futures::spawn_local(async move { // Load external calendars match CalendarService::get_external_calendars().await { Ok(calendars) => { external_calendars.set(calendars.clone()); // Load events for visible external calendars let mut all_events = Vec::new(); for calendar in calendars { if calendar.is_visible { if let Ok(mut events) = CalendarService::fetch_external_calendar_events(calendar.id).await { // Set calendar_path for color matching for event in &mut events { event.calendar_path = Some(format!("external_{}", calendar.id)); } all_events.extend(events); } } } external_calendar_events.set(all_events); } Err(err) => { web_sys::console::log_1( &format!("Failed to load external calendars: {}", err).into(), ); } } }); }) }; // Load external calendars when auth token is available and set up auto-refresh { let auth_token = auth_token.clone(); let refresh_external_calendars = refresh_external_calendars.clone(); let refresh_interval = refresh_interval.clone(); let external_calendars = external_calendars.clone(); let external_calendar_events = external_calendar_events.clone(); use_effect_with((*auth_token).clone(), move |token| { if let Some(_) = token { // Initial load refresh_external_calendars.emit(()); // Set up 5-minute refresh interval let refresh_external_calendars = refresh_external_calendars.clone(); let interval = Interval::new(5 * 60 * 1000, move || { refresh_external_calendars.emit(()); }); refresh_interval.set(Some(interval)); } else { // Clear data and interval when logged out external_calendars.set(Vec::new()); external_calendar_events.set(Vec::new()); refresh_interval.set(None); } // Cleanup function let refresh_interval = refresh_interval.clone(); move || { // Clear interval on cleanup refresh_interval.set(None); } }); } let on_outside_click = { let color_picker_open = color_picker_open.clone(); let context_menu_open = context_menu_open.clone(); let event_context_menu_open = event_context_menu_open.clone(); let calendar_context_menu_open = calendar_context_menu_open.clone(); Callback::from(move |e: MouseEvent| { // Check if any context menu or color picker is open let any_menu_open = color_picker_open.is_some() || *context_menu_open || *event_context_menu_open || *calendar_context_menu_open; if any_menu_open { // Prevent the default action and stop event propagation e.prevent_default(); e.stop_propagation(); } // Close all open menus/pickers color_picker_open.set(None); context_menu_open.set(false); event_context_menu_open.set(false); calendar_context_menu_open.set(false); }) }; // Compute if any context menu is open let any_context_menu_open = color_picker_open.is_some() || *context_menu_open || *event_context_menu_open || *calendar_context_menu_open; let on_color_change = { let user_info = user_info.clone(); let external_calendars = external_calendars.clone(); let color_picker_open = color_picker_open.clone(); Callback::from(move |(calendar_path, color): (String, String)| { if calendar_path.starts_with("external_") { // Handle external calendar color change if let Ok(id_str) = calendar_path.strip_prefix("external_").unwrap_or("").parse::() { let external_calendars = external_calendars.clone(); let color = color.clone(); wasm_bindgen_futures::spawn_local(async move { // Find the external calendar to get its current details if let Some(cal) = (*external_calendars).iter().find(|c| c.id == id_str) { match CalendarService::update_external_calendar( id_str, &cal.name, &cal.url, &color, cal.is_visible, ).await { Ok(_) => { // Update the local state let mut updated_calendars = (*external_calendars).clone(); for calendar in &mut updated_calendars { if calendar.id == id_str { calendar.color = color.clone(); break; } } external_calendars.set(updated_calendars); // No need to refresh events - they will automatically pick up the new color // from the calendar when rendered since they use the same calendar_path matching } Err(e) => { web_sys::console::error_1(&format!("Failed to update external calendar color: {}", e).into()); } } } }); } } else { // Handle CalDAV calendar color change (existing logic) if let Some(mut info) = (*user_info).clone() { for calendar in &mut info.calendars { if calendar.path == calendar_path { calendar.color = color.clone(); break; } } user_info.set(Some(info.clone())); if let Ok(json) = serde_json::to_string(&info) { let _ = LocalStorage::set("calendar_colors", json); } } } color_picker_open.set(None); }) }; let on_color_picker_toggle = { let color_picker_open = color_picker_open.clone(); Callback::from(move |calendar_path: String| { if color_picker_open.as_ref() == Some(&calendar_path) { color_picker_open.set(None); } else { color_picker_open.set(Some(calendar_path)); } }) }; let on_calendar_context_menu = { let context_menu_open = context_menu_open.clone(); let context_menu_pos = context_menu_pos.clone(); let context_menu_calendar_path = context_menu_calendar_path.clone(); Callback::from(move |(event, calendar_path): (MouseEvent, String)| { context_menu_open.set(true); context_menu_pos.set((event.client_x(), event.client_y())); context_menu_calendar_path.set(Some(calendar_path)); }) }; let on_event_context_menu = { 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, 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)); }) }; let on_calendar_date_context_menu = { let calendar_context_menu_open = calendar_context_menu_open.clone(); let calendar_context_menu_pos = calendar_context_menu_pos.clone(); let calendar_context_menu_date = calendar_context_menu_date.clone(); Callback::from(move |(event, date): (MouseEvent, NaiveDate)| { calendar_context_menu_open.set(true); calendar_context_menu_pos.set((event.client_x(), event.client_y())); calendar_context_menu_date.set(Some(date)); }) }; let on_create_event_click = { let create_event_modal_open = create_event_modal_open.clone(); let selected_date_for_event = selected_date_for_event.clone(); let calendar_context_menu_date = calendar_context_menu_date.clone(); Callback::from(move |_: MouseEvent| { create_event_modal_open.set(true); selected_date_for_event.set((*calendar_context_menu_date).clone()); }) }; let on_event_create = { let create_event_modal_open = create_event_modal_open.clone(); let auth_token = auth_token.clone(); let refresh_calendar_data = refresh_calendar_data.clone(); Callback::from(move |event_data: EventCreationData| { // Check if this is an update operation (has original_uid) or a create operation if let Some(original_uid) = event_data.original_uid.clone() { web_sys::console::log_1(&format!("Updating event via modal: {:?}", event_data).into()); create_event_modal_open.set(false); // Handle the update operation using the existing backend update logic if let Some(token) = (*auth_token).clone() { let event_data_for_update = event_data.clone(); let refresh_callback = refresh_calendar_data.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); // Get CalDAV password from storage 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 { String::new() } } else { String::new() }; // Convert EventCreationData to update parameters let params = event_data_for_update.to_create_event_params(); // Determine if this is a recurring event update let is_recurring = matches!(event_data_for_update.recurrence, crate::components::event_form::RecurrenceType::Daily | crate::components::event_form::RecurrenceType::Weekly | crate::components::event_form::RecurrenceType::Monthly | crate::components::event_form::RecurrenceType::Yearly); let update_result = if is_recurring && event_data_for_update.edit_scope.is_some() { // Use series update endpoint for recurring events let edit_action = event_data_for_update.edit_scope.unwrap(); let scope = match edit_action { crate::components::EditAction::EditAll => "all_in_series".to_string(), crate::components::EditAction::EditFuture => "this_and_future".to_string(), crate::components::EditAction::EditThis => "this_only".to_string(), }; calendar_service .update_series( &token, &password, original_uid.clone(), 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.18, // recurrence_count params.19, // recurrence_until params.17, // calendar_path scope, event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date ) .await } else { // Use regular update endpoint for single events calendar_service .update_event( &token, &password, original_uid.clone(), 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 vec![], // exception_dates - empty for simple updates None, // update_action - None for regular updates None, // until_date - None for regular updates ) .await }; match update_result { Ok(_) => { web_sys::console::log_1(&"Event updated successfully via modal".into()); // Refresh calendar data without page reload refresh_callback.emit(()); } Err(err) => { web_sys::console::error_1( &format!("Failed to update event: {}", err).into(), ); web_sys::window() .unwrap() .alert_with_message(&format!("Failed to update event: {}", err)) .unwrap(); } } }); } return; } web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into()); // Save the selected calendar as the last used calendar if let Some(ref calendar_path) = event_data.selected_calendar { let _ = LocalStorage::set("last_used_calendar", calendar_path); // Also sync to backend asynchronously let calendar_path_for_sync = calendar_path.clone(); wasm_bindgen_futures::spawn_local(async move { let preferences_service = crate::services::preferences::PreferencesService::new(); if let Err(e) = preferences_service.update_last_used_calendar(&calendar_path_for_sync).await { web_sys::console::warn_1(&format!("Failed to sync last used calendar to backend: {}", e).into()); } }); } create_event_modal_open.set(false); if let Some(_token) = (*auth_token).clone() { let refresh_callback = refresh_calendar_data.clone(); wasm_bindgen_futures::spawn_local(async move { let _calendar_service = CalendarService::new(); // Get CalDAV password from storage 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 { String::new() } } else { String::new() }; 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.18, // recurrence_count params.19, // recurrence_until params.17, // calendar_path ) .await; match create_result { Ok(_) => { web_sys::console::log_1(&"Event created successfully".into()); // Refresh calendar data without page reload refresh_callback.emit(()); } Err(err) => { web_sys::console::error_1( &format!("Failed to create event: {}", err).into(), ); web_sys::window() .unwrap() .alert_with_message(&format!("Failed to create event: {}", err)) .unwrap(); } } }); } }) }; let on_event_update = { let auth_token = auth_token.clone(); let refresh_calendar_data = refresh_calendar_data.clone(); Callback::from( move |( original_event, new_start, new_end, preserve_rrule, until_date, update_scope, occurrence_date, ): ( VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>, Option, Option, )| { web_sys::console::log_1( &format!( "Updating event: {} to new times: {} - {}", original_event.uid, new_start.format("%Y-%m-%d %H:%M"), new_end.format("%Y-%m-%d %H:%M") ) .into(), ); // Use the original UID for all updates let backend_uid = original_event.uid.clone(); if let Some(token) = (*auth_token).clone() { let original_event = original_event.clone(); let backend_uid = backend_uid.clone(); let refresh_callback = refresh_calendar_data.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); // Get CalDAV password from storage 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 { String::new() } } else { String::new() }; // Convert local naive datetime to UTC before sending to backend use chrono::TimeZone; let local_tz = chrono::Local; let start_utc = local_tz.from_local_datetime(&new_start) .single() .unwrap_or_else(|| { // Fallback for ambiguous times (DST transitions) local_tz.from_local_datetime(&new_start).earliest().unwrap() }) .with_timezone(&chrono::Utc); let end_utc = local_tz.from_local_datetime(&new_end) .single() .unwrap_or_else(|| { // Fallback for ambiguous times (DST transitions) local_tz.from_local_datetime(&new_end).earliest().unwrap() }) .with_timezone(&chrono::Utc); let start_date = start_utc.format("%Y-%m-%d").to_string(); let start_time = start_utc.format("%H:%M").to_string(); let end_date = end_utc.format("%Y-%m-%d").to_string(); let end_time = end_utc.format("%H:%M").to_string(); // Convert existing event data to string formats for the API let status_str = match original_event.status { 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 { 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.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.rrule.unwrap_or_default(); let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence // Determine if this is a recurring event that needs series endpoint let has_recurrence = !recurrence_str.is_empty() && recurrence_str.to_uppercase() != "NONE"; let result = if let Some(scope) = update_scope.as_ref() { // Use series endpoint for recurring event operations if !has_recurrence { web_sys::console::log_1(&"⚠️ Warning: update_scope provided for non-recurring event, using regular endpoint instead".into()); // Fall through to regular endpoint None } else { Some( calendar_service .update_series( &token, &password, backend_uid.clone(), original_event.summary.clone().unwrap_or_default(), original_event.description.clone().unwrap_or_default(), start_date.clone(), start_time.clone(), end_date.clone(), end_time.clone(), original_event.location.clone().unwrap_or_default(), original_event.all_day, status_str.clone(), class_str.clone(), original_event.priority, 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.clone(), recurrence_str.clone(), vec![false; 7], None, None, original_event.calendar_path.clone(), scope.clone(), occurrence_date, ) .await, ) } } else { None }; let result = if let Some(series_result) = result { series_result } else { // Use regular endpoint calendar_service .update_event( &token, &password, backend_uid, original_event.summary.unwrap_or_default(), original_event.description.unwrap_or_default(), start_date, start_time, end_date, end_time, original_event.location.unwrap_or_default(), original_event.all_day, status_str, class_str, original_event.priority, 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.exdate.clone(), if preserve_rrule { Some("update_series".to_string()) } else { Some("this_and_future".to_string()) }, until_date, ) .await }; match result { Ok(_) => { web_sys::console::log_1(&"Event updated successfully".into()); // Refresh calendar data without page reload refresh_callback.emit(()); } Err(err) => { web_sys::console::error_1( &format!("Failed to update event: {}", err).into(), ); web_sys::window() .unwrap() .alert_with_message(&format!("Failed to update event: {}", err)) .unwrap(); } } }); } }, ) }; let refresh_calendars = { let auth_token = auth_token.clone(); let user_info = user_info.clone(); Callback::from(move |_| { if let Some(token) = (*auth_token).clone() { let user_info = user_info.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); 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 { String::new() } } else { String::new() }; match calendar_service.fetch_user_info(&token, &password).await { Ok(mut info) => { if let Ok(saved_colors_json) = LocalStorage::get::("calendar_colors") { if let Ok(saved_info) = serde_json::from_str::(&saved_colors_json) { for saved_cal in &saved_info.calendars { for cal in &mut info.calendars { if cal.path == saved_cal.path { cal.color = saved_cal.color.clone(); } } } } } user_info.set(Some(info)); } Err(err) => { web_sys::console::log_1( &format!("Failed to refresh calendars: {}", err).into(), ); } } }); } }) }; // Debug logging web_sys::console::log_1( &format!("App rendering: auth_token = {:?}", auth_token.is_some()).into(), ); html! {
{ if auth_token.is_some() { html! { <> { web_sys::console::log_1(&format!("✅ Successfully refreshed calendar {} with {} events", id, events.len()).into()); // Set calendar_path for color matching for event in &mut events { event.calendar_path = Some(format!("external_{}", id)); } // Update events for this calendar let mut all_events = (*external_calendar_events).clone(); // Remove old events from this calendar all_events.retain(|e| { if let Some(ref calendar_path) = e.calendar_path { calendar_path != &format!("external_{}", id) } else { true } }); // Add new events all_events.extend(events); external_calendar_events.set(all_events); // Update the last_fetched timestamp in calendars list match CalendarService::get_external_calendars().await { Ok(calendars) => { external_calendars.set(calendars); web_sys::console::log_1(&"✅ Calendar list updated with new timestamps".into()); } Err(err) => { web_sys::console::error_1(&format!("⚠️ Failed to update calendar list: {}", err).into()); } } // Clear loading state on success refreshing_calendar_id.set(None); } Err(err) => { web_sys::console::error_1(&format!("❌ Failed to refresh calendar {}: {}", id, err).into()); // Show error to user if let Some(window) = web_sys::window() { let _ = window.alert_with_message(&format!("Failed to refresh calendar: {}", err)); } // Clear loading state on error refreshing_calendar_id.set(None); } } }); } })} color_picker_open={(*color_picker_open).clone()} on_color_change={on_color_change} on_color_picker_toggle={on_color_picker_toggle} available_colors={(*available_colors).clone()} refreshing_calendar_id={(*refreshing_calendar_id).clone()} on_calendar_context_menu={on_calendar_context_menu} on_calendar_visibility_toggle={Callback::from({ let user_info = user_info.clone(); move |calendar_path: String| { let user_info = user_info.clone(); wasm_bindgen_futures::spawn_local(async move { if let Some(mut info) = (*user_info).clone() { // Toggle the visibility if let Some(calendar) = info.calendars.iter_mut().find(|c| c.path == calendar_path) { calendar.is_visible = !calendar.is_visible; user_info.set(Some(info)); } } }); } })} current_view={(*current_view).clone()} on_view_change={on_view_change} current_theme={(*current_theme).clone()} on_theme_change={on_theme_change} current_style={(*current_style).clone()} on_style_change={on_style_change} />
} } else { html! { } } } , Option)| { if let Some(token) = (*auth_token).clone() { let refresh_calendars = refresh_calendars.clone(); let calendar_management_modal_open = calendar_management_modal_open.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); 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 { String::new() } } else { String::new() }; match calendar_service.create_calendar(&token, &password, name, description, color).await { Ok(_) => { web_sys::console::log_1(&"Calendar created successfully!".into()); refresh_calendars.emit(()); calendar_management_modal_open.set(false); } Err(err) => { web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into()); calendar_management_modal_open.set(false); } } }); } } })} on_external_success={Callback::from({ let external_calendars = external_calendars.clone(); let calendar_management_modal_open = calendar_management_modal_open.clone(); move |new_id: i32| { // Refresh external calendars list let external_calendars = external_calendars.clone(); let calendar_management_modal_open = calendar_management_modal_open.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); match CalendarService::get_external_calendars().await { Ok(calendars) => { external_calendars.set(calendars); calendar_management_modal_open.set(false); web_sys::console::log_1(&format!("External calendar {} added successfully!", new_id).into()); } Err(err) => { web_sys::console::error_1(&format!("Failed to refresh external calendars: {}", err).into()); calendar_management_modal_open.set(false); } } }); } })} available_colors={available_colors.iter().map(|c| c.to_string()).collect::>()} /> ("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; match calendar_service.delete_calendar(&token, &password, calendar_path).await { Ok(_) => { web_sys::console::log_1(&"Calendar deleted successfully!".into()); refresh_calendars.emit(()); } Err(err) => { web_sys::console::log_1(&format!("Failed to delete calendar: {}", err).into()); } } }); } } })} /> web_sys::console::log_1(&"Delete this event".into()), DeleteAction::DeleteFollowing => web_sys::console::log_1(&"Delete following events".into()), DeleteAction::DeleteSeries => web_sys::console::log_1(&"Delete entire series".into()), } let refresh_callback = refresh_calendar_data.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); 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 { String::new() } } else { String::new() }; if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) { // Convert delete action to string and get occurrence date let action_str = match delete_action { DeleteAction::DeleteThis => "delete_this".to_string(), DeleteAction::DeleteFollowing => "delete_following".to_string(), DeleteAction::DeleteSeries => "delete_series".to_string(), }; // Get the occurrence date from the clicked event 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.dtstart).into()); web_sys::console::log_1(&format!("🔄 Occurrence date: {:?}", occurrence_date).into()); match calendar_service.delete_event( &token, &password, calendar_path.clone(), event_href.clone(), action_str, occurrence_date ).await { Ok(message) => { web_sys::console::log_1(&format!("Delete response: {}", message).into()); // Show the message to the user to explain what actually happened if message.contains("Warning") { web_sys::window().unwrap().alert_with_message(&message).unwrap(); } // Close the context menu event_context_menu_open.set(false); // Refresh calendar data without page reload refresh_callback.emit(()); } Err(err) => { web_sys::console::log_1(&format!("Failed to delete event: {}", err).into()); web_sys::window().unwrap().alert_with_message(&format!("Failed to delete event: {}", err)).unwrap(); } } } else { web_sys::console::log_1(&"Missing calendar_path or href - cannot delete event".into()); } }); } } })} on_view_details={Callback::from({ let event_context_menu_open = event_context_menu_open.clone(); let view_event_modal_open = view_event_modal_open.clone(); let view_event_modal_event = view_event_modal_event.clone(); move |event: VEvent| { // Set the event for viewing (read-only mode) view_event_modal_event.set(Some(event)); event_context_menu_open.set(false); view_event_modal_open.set(true); } })} /> // Mobile warning modal
} }