use crate::components::{ CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, ViewMode, WeekView, }; use crate::models::ical::VEvent; use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService}; use chrono::{Datelike, Duration, Local, NaiveDate}; use gloo_storage::{LocalStorage, Storage}; use std::collections::HashMap; use web_sys::MouseEvent; use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct CalendarProps { #[prop_or_default] pub user_info: Option, #[prop_or_default] pub external_calendar_events: Vec, #[prop_or_default] pub external_calendars: Vec, #[prop_or_default] pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, #[prop_or_default] pub view: ViewMode, #[prop_or_default] pub on_create_event_request: Option>, #[prop_or_default] pub on_event_update_request: Option< Callback<( VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>, Option, Option, )>, >, #[prop_or_default] pub context_menus_open: bool, } #[function_component] pub fn Calendar(props: &CalendarProps) -> Html { let today = Local::now().date_naive(); // Event management state let events = use_state(|| HashMap::>::new()); let loading = use_state(|| true); let error = use_state(|| None::); let refreshing_event_uid = use_state(|| None::); // Track the currently selected date (the actual day the user has selected) let selected_date = use_state(|| { // Try to load saved selected date from localStorage if let Ok(saved_date_str) = LocalStorage::get::("calendar_selected_date") { if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { saved_date } else { today } } else { // Check for old key for backward compatibility if let Ok(saved_date_str) = LocalStorage::get::("calendar_current_month") { if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { saved_date } else { today } } else { today } } }); // Track the display date (what to show in the view) let current_date = use_state(|| match props.view { ViewMode::Month => selected_date.with_day(1).unwrap_or(*selected_date), ViewMode::Week => *selected_date, }); let selected_event = use_state(|| None::); // State for create event modal let show_create_modal = use_state(|| false); let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); // State for time increment snapping (15 or 30 minutes) let time_increment = use_state(|| { // Try to load saved time increment from localStorage if let Ok(saved_increment) = LocalStorage::get::("calendar_time_increment") { if saved_increment == 15 || saved_increment == 30 { saved_increment } else { 15 } } else { 15 } }); // Fetch events when current_date changes { let events = events.clone(); let loading = loading.clone(); let error = error.clone(); let current_date = current_date.clone(); let external_events = props.external_calendar_events.clone(); // Clone before the effect let view = props.view.clone(); // Clone before the effect use_effect_with((*current_date, view.clone(), external_events.len(), props.user_info.clone()), move |(date, _view, _external_len, user_info)| { let auth_token: Option = LocalStorage::get("auth_token").ok(); let date = *date; // Clone the date to avoid lifetime issues let external_events = external_events.clone(); // Clone external events to avoid lifetime issues let user_info = user_info.clone(); // Clone user_info to avoid lifetime issues if let Some(token) = auth_token { let events = events.clone(); let loading = loading.clone(); let error = error.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() }; let current_year = date.year(); let current_month = date.month(); match calendar_service .fetch_events_for_month_vevent( &token, &password, current_year, current_month, ) .await { Ok(vevents) => { // Filter CalDAV events based on calendar visibility let mut filtered_events = if let Some(user_info) = user_info.as_ref() { vevents.into_iter() .filter(|event| { if let Some(calendar_path) = event.calendar_path.as_ref() { // Find the calendar info for this event user_info.calendars.iter() .find(|cal| &cal.path == calendar_path) .map(|cal| cal.is_visible) .unwrap_or(true) // Default to visible if not found } else { true // Show events without calendar path } }) .collect() } else { vevents // Show all events if no user info }; // Mark external events as external by adding a special category let marked_external_events: Vec = external_events .into_iter() .map(|mut event| { // Add a special category to identify external events event.categories.push("__EXTERNAL_CALENDAR__".to_string()); event }) .collect(); filtered_events.extend(marked_external_events); let grouped_events = CalendarService::group_events_by_date(filtered_events); events.set(grouped_events); loading.set(false); } Err(err) => { error.set(Some(format!("Failed to load events: {}", err))); loading.set(false); } } }); } else { loading.set(false); error.set(Some("No authentication token found".to_string())); } || () }); } // Handle event click to refresh individual events let on_event_click = { let events = events.clone(); let refreshing_event_uid = refreshing_event_uid.clone(); Callback::from(move |event: VEvent| { let auth_token: Option = LocalStorage::get("auth_token").ok(); if let Some(token) = auth_token { let events = events.clone(); let refreshing_event_uid = refreshing_event_uid.clone(); let uid = event.uid.clone(); refreshing_event_uid.set(Some(uid.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 .refresh_event(&token, &password, &uid) .await { Ok(Some(refreshed_event)) => { let refreshed_vevent = refreshed_event; let mut updated_events = (*events).clone(); for (_, day_events) in updated_events.iter_mut() { day_events.retain(|e| e.uid != uid); } if refreshed_vevent.rrule.is_some() { let new_occurrences = CalendarService::expand_recurring_events(vec![ refreshed_vevent.clone(), ]); for occurrence in new_occurrences { let date = occurrence.get_date(); updated_events .entry(date) .or_insert_with(Vec::new) .push(occurrence); } } else { let date = refreshed_vevent.get_date(); updated_events .entry(date) .or_insert_with(Vec::new) .push(refreshed_vevent); } events.set(updated_events); } Ok(None) => { let mut updated_events = (*events).clone(); for (_, day_events) in updated_events.iter_mut() { day_events.retain(|e| e.uid != uid); } events.set(updated_events); } Err(_err) => {} } refreshing_event_uid.set(None); }); } }) }; // Handle view mode changes - adjust current_date format when switching between month/week { let current_date = current_date.clone(); let selected_date = selected_date.clone(); let view = props.view.clone(); use_effect_with(view, move |view_mode| { let selected = *selected_date; let new_display_date = match view_mode { ViewMode::Month => selected.with_day(1).unwrap_or(selected), ViewMode::Week => selected, // Show the week containing the selected date }; current_date.set(new_display_date); || {} }); } let on_prev = { let current_date = current_date.clone(); let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_: MouseEvent| { let (new_selected, new_display) = match view { ViewMode::Month => { // Go to previous month, select the 1st day let prev_month = *current_date - Duration::days(1); let first_of_prev = prev_month.with_day(1).unwrap(); (first_of_prev, first_of_prev) } ViewMode::Week => { // Go to previous week let prev_week = *selected_date - Duration::weeks(1); (prev_week, prev_week) } }; selected_date.set(new_selected); current_date.set(new_display); let _ = LocalStorage::set( "calendar_selected_date", new_selected.format("%Y-%m-%d").to_string(), ); }) }; let on_next = { let current_date = current_date.clone(); let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_: MouseEvent| { let (new_selected, new_display) = match view { ViewMode::Month => { // Go to next month, select the 1st day let next_month = if current_date.month() == 12 { NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap() } else { NaiveDate::from_ymd_opt(current_date.year(), current_date.month() + 1, 1) .unwrap() }; (next_month, next_month) } ViewMode::Week => { // Go to next week let next_week = *selected_date + Duration::weeks(1); (next_week, next_week) } }; selected_date.set(new_selected); current_date.set(new_display); let _ = LocalStorage::set( "calendar_selected_date", new_selected.format("%Y-%m-%d").to_string(), ); }) }; let on_today = { let current_date = current_date.clone(); let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_| { let today = Local::now().date_naive(); let (new_selected, new_display) = match view { ViewMode::Month => { let first_of_today = today.with_day(1).unwrap(); (today, first_of_today) // Select today, but display the month } ViewMode::Week => (today, today), // Select and display today }; selected_date.set(new_selected); current_date.set(new_display); let _ = LocalStorage::set( "calendar_selected_date", new_selected.format("%Y-%m-%d").to_string(), ); }) }; // Handle time increment toggle let on_time_increment_toggle = { let time_increment = time_increment.clone(); Callback::from(move |_: MouseEvent| { let current = *time_increment; let next = if current == 15 { 30 } else { 15 }; time_increment.set(next); let _ = LocalStorage::set("calendar_time_increment", next); }) }; // Handle drag-to-create event let on_create_event = { let show_create_modal = show_create_modal.clone(); let create_event_data = create_event_data.clone(); Callback::from( move |(_date, start_datetime, end_datetime): ( NaiveDate, chrono::NaiveDateTime, chrono::NaiveDateTime, )| { // For drag-to-create, we don't need the temporary event approach // Instead, just pass the local times directly via initial_time props create_event_data.set(Some(( start_datetime.date(), start_datetime.time(), end_datetime.time(), ))); show_create_modal.set(true); }, ) }; // Handle drag-to-move event let on_event_update = { let on_event_update_request = props.on_event_update_request.clone(); Callback::from( move |( event, new_start, new_end, preserve_rrule, until_date, update_scope, occurrence_date, ): ( VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>, Option, Option, )| { if let Some(callback) = &on_event_update_request { callback.emit(( event, new_start, new_end, preserve_rrule, until_date, update_scope, occurrence_date, )); } }, ) }; html! {
Some("week-view"), _ => None })}> { if *loading { html! {

{"Loading calendar events..."}

} } else if let Some(err) = (*error).clone() { html! {

{format!("Error: {}", err)}

} } else { match props.view { ViewMode::Month => { let on_day_select = { let selected_date = selected_date.clone(); Callback::from(move |date: NaiveDate| { selected_date.set(date); let _ = LocalStorage::set("calendar_selected_date", date.format("%Y-%m-%d").to_string()); }) }; html! { } }, ViewMode::Week => html! { }, } } } // Event details modal // Create event modal
} }