use chrono::{DateTime, Datelike, Duration, NaiveDate, TimeZone, Utc, Weekday}; use gloo_storage::{LocalStorage, Storage}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; // Import RFC 5545 compliant VEvent from shared library use calendar_models::VEvent; // Create type alias for backward compatibility pub type CalendarEvent = VEvent; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct EventReminder { pub minutes_before: i32, pub action: ReminderAction, pub description: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ReminderAction { Display, Email, Audio, } impl Default for ReminderAction { fn default() -> Self { ReminderAction::Display } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct UserInfo { pub username: String, pub server_url: String, pub calendars: Vec, #[serde(default = "default_timestamp")] pub last_updated: u64, } fn default_timestamp() -> u64 { 0 } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct CalendarInfo { pub path: String, pub display_name: String, pub color: String, pub is_visible: bool, } // CalendarEvent, EventStatus, and EventClass are now imported from shared library // ==================== V2 API MODELS ==================== #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AttendeeV2 { pub email: String, pub name: Option, pub role: Option, pub status: Option, pub rsvp: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AttendeeRoleV2 { Chair, Required, Optional, NonParticipant, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ParticipationStatusV2 { NeedsAction, Accepted, Declined, Tentative, Delegated, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AlarmV2 { pub action: AlarmActionV2, pub trigger_minutes: i32, // Minutes before event (negative = before) pub description: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AlarmActionV2 { Audio, Display, Email, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum DeleteActionV2 { DeleteThis, DeleteFollowing, DeleteSeries, } // V2 Request/Response Models #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct EventSummaryV2 { pub uid: String, pub summary: Option, pub dtstart: DateTime, pub dtend: Option>, pub location: Option, pub all_day: bool, pub href: Option, pub etag: Option, } pub struct CalendarService { base_url: String, } impl CalendarService { pub fn new() -> Self { let base_url = option_env!("BACKEND_API_URL") .unwrap_or("http://localhost:3000/api") .to_string(); Self { base_url } } /// Fetch user info including available calendars pub async fn fetch_user_info(&self, token: &str, password: &str) -> Result { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let url = format!("{}/user/info", self.base_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { let user_info: UserInfo = serde_json::from_str(&text_string) .map_err(|e| format!("JSON parsing failed: {}", e))?; Ok(user_info) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Fetch calendar events for a specific month /// Fetch events for month as VEvent with RFC 5545 compliance pub async fn fetch_events_for_month_vevent( &self, token: &str, password: &str, year: i32, month: u32, ) -> Result, String> { // Since CalendarEvent is now a type alias for VEvent, just call the main method self.fetch_events_for_month(token, password, year, month) .await } pub async fn fetch_events_for_month( &self, token: &str, password: &str, year: i32, month: u32, ) -> Result, String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let url = format!( "{}/calendar/events?year={}&month={}", self.base_url, year, month ); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { let events: Vec = serde_json::from_str(&text_string) .map_err(|e| format!("JSON parsing failed: {}", e))?; Ok(events) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Convert events to a HashMap grouped by date for calendar display /// Internally uses VEvent for RFC 5545 compliance while maintaining CalendarEvent API /// Group VEvents by date with RFC 5545 compliance pub fn group_events_by_date(events: Vec) -> HashMap> { let mut grouped = HashMap::new(); // Expand recurring events using VEvent (better RFC 5545 compliance) let expanded_vevents = Self::expand_recurring_events(events); // Group by date for vevent in expanded_vevents { let date = vevent.get_date(); grouped.entry(date).or_insert_with(Vec::new).push(vevent); } grouped } /// Expand recurring events using VEvent (RFC 5545 compliant) pub fn expand_recurring_events(events: Vec) -> Vec { let mut expanded_events = Vec::new(); let today = chrono::Utc::now().date_naive(); let start_range = today - Duration::days(36500); // Show past 100 years (to catch any historical yearly events) let end_range = today + Duration::days(36500); // Show next 100 years for event in events { if let Some(ref rrule) = event.rrule { web_sys::console::log_1( &format!( "📅 Processing recurring VEvent '{}' with RRULE: {}", event.summary.as_deref().unwrap_or("Untitled"), rrule ) .into(), ); // Log if event has exception dates if !event.exdate.is_empty() { web_sys::console::log_1( &format!( "📅 VEvent '{}' has {} exception dates: {:?}", event.summary.as_deref().unwrap_or("Untitled"), event.exdate.len(), event.exdate ) .into(), ); } // Generate occurrences for recurring events using VEvent let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range); web_sys::console::log_1( &format!( "📅 Generated {} occurrences for VEvent '{}'", occurrences.len(), event.summary.as_deref().unwrap_or("Untitled") ) .into(), ); expanded_events.extend(occurrences); } else { // Non-recurring event - add as-is expanded_events.push(event); } } expanded_events } /// Generate occurrence dates for a recurring VEvent based on RRULE (RFC 5545 compliant) fn generate_occurrences( base_event: &VEvent, rrule: &str, start_range: NaiveDate, end_range: NaiveDate, ) -> Vec { let mut occurrences = Vec::new(); // Parse RRULE components let rrule_upper = rrule.to_uppercase(); web_sys::console::log_1(&format!("🔄 Parsing RRULE: {}", rrule_upper).into()); let components: HashMap = rrule_upper .split(';') .filter_map(|part| { let mut split = part.split('='); if let (Some(key), Some(value)) = (split.next(), split.next()) { Some((key.to_string(), value.to_string())) } else { None } }) .collect(); // Get frequency let freq = components .get("FREQ") .map(|s| s.as_str()) .unwrap_or("DAILY"); // Get interval (default 1) let interval: i32 = components .get("INTERVAL") .and_then(|s| s.parse().ok()) .unwrap_or(1); // Get count limit (default 100 to prevent infinite loops) let count: usize = components .get("COUNT") .and_then(|s| s.parse().ok()) .unwrap_or(100) .min(365); // Cap at 365 occurrences for performance // Get UNTIL date if specified let until_date = components.get("UNTIL").and_then(|until_str| { // Parse UNTIL date in YYYYMMDDTHHMMSSZ format if let Ok(dt) = chrono::NaiveDateTime::parse_from_str( until_str.trim_end_matches('Z'), "%Y%m%dT%H%M%S", ) { Some(chrono::Utc.from_utc_datetime(&dt)) } else if let Ok(dt) = chrono::DateTime::parse_from_str(until_str, "%Y%m%dT%H%M%SZ") { Some(dt.with_timezone(&chrono::Utc)) } else if let Ok(date) = chrono::NaiveDate::parse_from_str(until_str, "%Y%m%d") { // Handle date-only UNTIL Some(chrono::Utc.from_utc_datetime(&date.and_hms_opt(23, 59, 59).unwrap())) } else { web_sys::console::log_1( &format!("⚠️ Failed to parse UNTIL date: {}", until_str).into(), ); None } }); if let Some(until) = until_date { web_sys::console::log_1(&format!("📅 RRULE has UNTIL: {}", until).into()); } let start_date = base_event.dtstart.date_naive(); let mut current_date = start_date; let mut occurrence_count = 0; // Generate occurrences based on frequency while current_date <= end_range && occurrence_count < count { // Check UNTIL constraint - stop if current occurrence is after UNTIL date if let Some(until) = until_date { let current_datetime = base_event.dtstart + Duration::days(current_date.signed_duration_since(start_date).num_days()); if current_datetime > until { web_sys::console::log_1( &format!("🛑 Stopping at {} due to UNTIL {}", current_datetime, until) .into(), ); break; } } if current_date >= start_range { // Calculate the occurrence datetime let days_diff = current_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); // Check if this occurrence is in the exception dates (EXDATE) let is_exception = base_event.exdate.iter().any(|exception_date| { // Compare dates ignoring sub-second precision let exception_naive = exception_date.naive_utc(); let occurrence_naive = occurrence_datetime.naive_utc(); // Check if dates match (within a minute to handle minor time differences) let diff = occurrence_naive - exception_naive; let matches = diff.num_seconds().abs() < 60; if matches { web_sys::console::log_1( &format!( "🚫 Excluding occurrence {} due to EXDATE {}", occurrence_naive, exception_naive ) .into(), ); } matches }); if !is_exception { // Create occurrence event with updated DTSTAMP let mut occurrence_event = base_event.clone(); occurrence_event.dtstart = occurrence_datetime; occurrence_event.dtstamp = chrono::Utc::now(); // Update DTSTAMP for each occurrence if let Some(base_end) = base_event.dtend { if base_event.all_day { // For all-day events, maintain the RFC-5545 end date pattern // End date should always be exactly one day after start date occurrence_event.dtend = Some(occurrence_datetime + Duration::days(1)); } else { // For timed events, preserve the original duration occurrence_event.dtend = Some(base_end + Duration::days(days_diff)); } } occurrences.push(occurrence_event); occurrence_count += 1; // Only count when we actually add an occurrence } } // Calculate next occurrence date match freq { "DAILY" => { current_date = current_date + Duration::days(interval as i64); } "WEEKLY" => { if let Some(byday) = components.get("BYDAY") { // For BYDAY weekly events, we need to handle multiple days per week // Break out of the single-occurrence loop and use specialized logic return Self::generate_weekly_byday_occurrences( base_event, byday, interval, start_range, end_range, until_date, count, ); } else { current_date = current_date + Duration::weeks(interval as i64); } } "MONTHLY" => { // Handle MONTHLY with BYMONTHDAY or BYDAY if let Some(bymonthday) = components.get("BYMONTHDAY") { // Monthly by day of month (e.g., 15th of every month) return Self::generate_monthly_bymonthday_occurrences( base_event, bymonthday, interval, start_range, end_range, until_date, count, ); } else if let Some(byday) = components.get("BYDAY") { // Monthly by weekday position (e.g., first Monday) return Self::generate_monthly_byday_occurrences( base_event, byday, interval, start_range, end_range, until_date, count, ); } else { // Simple monthly increment (same day of month) if let Some(next_month) = Self::add_months(current_date, interval) { current_date = next_month; } else { break; // Invalid date } } } "YEARLY" => { if let Some(bymonth) = components.get("BYMONTH") { // Yearly with specific months (e.g., January, March, June) return Self::generate_yearly_bymonth_occurrences( base_event, bymonth, interval, start_range, end_range, until_date, count, ); } else { // Simple yearly increment (same date each year) if let Some(next_year) = Self::add_years(current_date, interval) { current_date = next_year; } else { break; // Invalid date } } } _ => break, // Unsupported frequency } } occurrences } /// Generate occurrences for WEEKLY frequency with BYDAY (handles multiple days per week) fn generate_weekly_byday_occurrences( base_event: &VEvent, byday: &str, interval: i32, start_range: NaiveDate, end_range: NaiveDate, until_date: Option>, count: usize, ) -> Vec { let mut occurrences = Vec::new(); let weekdays = Self::parse_byday(byday); if weekdays.is_empty() { return occurrences; } let start_date = base_event.dtstart.date_naive(); // Find the Monday of the week containing the start_date (reference week) let reference_week_start = start_date - Duration::days(start_date.weekday().num_days_from_monday() as i64); let mut total_events_generated = 0; // Count of actual events generated (matches RFC 5545 COUNT) let mut week_interval_number = 0; // Which interval week we're processing (0, 1, 2, ...) let max_weeks = (count * 10).min(520); // Prevent infinite loops - max 10 years // Continue generating until we reach count or date limits while total_events_generated < count && week_interval_number < max_weeks { // Calculate the actual week we're processing: original + (interval_number * interval) weeks let current_week_start = reference_week_start + Duration::weeks((week_interval_number as i32 * interval) as i64); // Stop if we've gone past the end range if current_week_start > end_range { break; } // Generate occurrences for all matching weekdays in this week for &weekday in &weekdays { let occurrence_date = current_week_start + Duration::days(weekday.num_days_from_monday() as i64); // Skip if occurrence is before start_range or after end_range if occurrence_date < start_range || occurrence_date > end_range { continue; } // Skip if this occurrence is before the original event date if occurrence_date < start_date { continue; } // Check UNTIL constraint if let Some(until) = until_date { let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); if occurrence_datetime > until { web_sys::console::log_1( &format!( "🛑 Stopping at {} due to UNTIL {}", occurrence_datetime, until ) .into(), ); return occurrences; } } // Calculate the occurrence datetime let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); // Check if this occurrence is in the exception dates (EXDATE) let is_exception = base_event.exdate.iter().any(|exception_date| { let exception_naive = exception_date.naive_utc(); let occurrence_naive = occurrence_datetime.naive_utc(); let diff = occurrence_naive - exception_naive; let matches = diff.num_seconds().abs() < 60; if matches { web_sys::console::log_1( &format!( "🚫 Excluding occurrence {} due to EXDATE {}", occurrence_naive, exception_naive ) .into(), ); } matches }); if !is_exception { // Create occurrence event let mut occurrence_event = base_event.clone(); occurrence_event.dtstart = occurrence_datetime; occurrence_event.dtstamp = chrono::Utc::now(); if let Some(end) = base_event.dtend { occurrence_event.dtend = Some(end + Duration::days(days_diff)); } total_events_generated += 1; occurrences.push(occurrence_event); // Stop if we've reached the count limit if total_events_generated >= count { return occurrences; } } } // Move to the next interval week week_interval_number += 1; } occurrences } /// Generate occurrences for MONTHLY frequency with BYMONTHDAY fn generate_monthly_bymonthday_occurrences( base_event: &VEvent, bymonthday: &str, interval: i32, start_range: NaiveDate, end_range: NaiveDate, until_date: Option>, count: usize, ) -> Vec { let mut occurrences = Vec::new(); // Parse BYMONTHDAY (e.g., "15" or "1,15,31") let monthdays: Vec = bymonthday .split(',') .filter_map(|day| day.trim().parse().ok()) .filter(|&day| day >= 1 && day <= 31) .collect(); if monthdays.is_empty() { return occurrences; } let start_date = base_event.dtstart.date_naive(); let mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap(); let mut total_occurrences = 0; let mut months_processed = 0; let max_months = (count * 12).min(120); // Prevent infinite loops - max 10 years // Generate occurrences month by month while current_month_start <= end_range && total_occurrences < count && months_processed < max_months { // Generate occurrences for all matching days in this month for &day in &monthdays { // Try to create the date, skip if invalid (e.g., Feb 31) if let Some(occurrence_date) = NaiveDate::from_ymd_opt( current_month_start.year(), current_month_start.month(), day, ) { // Skip if occurrence is before start_range or after end_range if occurrence_date < start_range || occurrence_date > end_range { continue; } // Skip if we've reached the count limit if total_occurrences >= count { break; } // Skip if this occurrence is before the original event date if occurrence_date < start_date { continue; } // Check UNTIL constraint if let Some(until) = until_date { let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); if occurrence_datetime > until { web_sys::console::log_1( &format!( "🛑 Stopping at {} due to UNTIL {}", occurrence_datetime, until ) .into(), ); return occurrences; } } // Calculate the occurrence datetime let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); // Check if this occurrence is in the exception dates (EXDATE) let is_exception = base_event.exdate.iter().any(|exception_date| { let exception_naive = exception_date.naive_utc(); let occurrence_naive = occurrence_datetime.naive_utc(); let diff = occurrence_naive - exception_naive; diff.num_seconds().abs() < 60 }); if !is_exception { // Create occurrence event let mut occurrence_event = base_event.clone(); occurrence_event.dtstart = occurrence_datetime; occurrence_event.dtstamp = chrono::Utc::now(); if let Some(end) = base_event.dtend { occurrence_event.dtend = Some(end + Duration::days(days_diff)); } occurrences.push(occurrence_event); total_occurrences += 1; } } } // Move to next month interval months_processed += 1; if let Some(next_month_start) = Self::add_months(current_month_start, interval) { current_month_start = NaiveDate::from_ymd_opt(next_month_start.year(), next_month_start.month(), 1) .unwrap(); } else { break; } } occurrences } /// Generate occurrences for MONTHLY frequency with BYDAY (e.g., "1MO" = first Monday) fn generate_monthly_byday_occurrences( base_event: &VEvent, byday: &str, interval: i32, start_range: NaiveDate, end_range: NaiveDate, until_date: Option>, count: usize, ) -> Vec { let mut occurrences = Vec::new(); // Parse BYDAY for monthly (e.g., "1MO" = first Monday, "-1FR" = last Friday) if let Some((position, weekday)) = Self::parse_monthly_byday(byday) { let start_date = base_event.dtstart.date_naive(); let mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap(); let mut total_occurrences = 0; // Generate occurrences month by month while current_month_start <= end_range && total_occurrences < count { if let Some(occurrence_date) = Self::find_nth_weekday_in_month(current_month_start, position, weekday) { // Skip if occurrence is before start_range or after end_range if occurrence_date < start_range || occurrence_date > end_range { } else if occurrence_date >= start_date && total_occurrences < count { // Check UNTIL constraint if let Some(until) = until_date { let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); if occurrence_datetime > until { return occurrences; } } // Calculate the occurrence datetime let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); // Check EXDATE let is_exception = base_event.exdate.iter().any(|exception_date| { let exception_naive = exception_date.naive_utc(); let occurrence_naive = occurrence_datetime.naive_utc(); let diff = occurrence_naive - exception_naive; diff.num_seconds().abs() < 60 }); if !is_exception { let mut occurrence_event = base_event.clone(); occurrence_event.dtstart = occurrence_datetime; occurrence_event.dtstamp = chrono::Utc::now(); if let Some(end) = base_event.dtend { occurrence_event.dtend = Some(end + Duration::days(days_diff)); } occurrences.push(occurrence_event); total_occurrences += 1; } } } // Move to next month interval if let Some(next_month) = Self::add_months(current_month_start, interval) { current_month_start = NaiveDate::from_ymd_opt(next_month.year(), next_month.month(), 1).unwrap(); } else { break; } } } occurrences } /// Generate occurrences for YEARLY frequency with BYMONTH fn generate_yearly_bymonth_occurrences( base_event: &VEvent, bymonth: &str, interval: i32, start_range: NaiveDate, end_range: NaiveDate, until_date: Option>, count: usize, ) -> Vec { let mut occurrences = Vec::new(); // Parse BYMONTH (e.g., "1,3,6" -> [January, March, June]) let months: Vec = bymonth .split(',') .filter_map(|month| month.trim().parse().ok()) .filter(|&month| month >= 1 && month <= 12) .collect(); if months.is_empty() { return occurrences; } let start_date = base_event.dtstart.date_naive(); let mut current_year = start_date.year(); let mut total_occurrences = 0; // Generate occurrences year by year while total_occurrences < count { // Generate occurrences for all matching months in this year for &month in &months { // Create the date for this year/month/day if let Some(occurrence_date) = NaiveDate::from_ymd_opt(current_year, month, start_date.day()) { // Skip if occurrence is before start_range or after end_range if occurrence_date < start_range || occurrence_date > end_range { continue; } // Skip if we've reached the count limit if total_occurrences >= count { break; } // Skip if this occurrence is before the original event date if occurrence_date < start_date { continue; } // Check UNTIL constraint if let Some(until) = until_date { let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); if occurrence_datetime > until { return occurrences; } } // Calculate the occurrence datetime let days_diff = occurrence_date.signed_duration_since(start_date).num_days(); let occurrence_datetime = base_event.dtstart + Duration::days(days_diff); // Check EXDATE let is_exception = base_event.exdate.iter().any(|exception_date| { let exception_naive = exception_date.naive_utc(); let occurrence_naive = occurrence_datetime.naive_utc(); let diff = occurrence_naive - exception_naive; diff.num_seconds().abs() < 60 }); if !is_exception { let mut occurrence_event = base_event.clone(); occurrence_event.dtstart = occurrence_datetime; occurrence_event.dtstamp = chrono::Utc::now(); if let Some(end) = base_event.dtend { occurrence_event.dtend = Some(end + Duration::days(days_diff)); } occurrences.push(occurrence_event); total_occurrences += 1; } } } // Move to next year interval current_year += interval; // Stop if we've gone beyond reasonable range if current_year > end_range.year() + 10 { break; } } occurrences } /// Parse BYDAY parameter (e.g., "MO,WE,FR" -> [Monday, Wednesday, Friday]) fn parse_byday(byday: &str) -> Vec { byday .split(',') .filter_map(|day| match day { "MO" => Some(Weekday::Mon), "TU" => Some(Weekday::Tue), "WE" => Some(Weekday::Wed), "TH" => Some(Weekday::Thu), "FR" => Some(Weekday::Fri), "SA" => Some(Weekday::Sat), "SU" => Some(Weekday::Sun), _ => None, }) .collect() } /// Add months to a date (handling month boundary issues) fn add_months(date: NaiveDate, months: i32) -> Option { let mut year = date.year(); let mut month = date.month() as i32 + months; while month > 12 { year += 1; month -= 12; } while month < 1 { year -= 1; month += 12; } // Handle day overflow (e.g., Jan 31 -> Feb 28) let day = date.day().min(Self::days_in_month(year, month as u32)); NaiveDate::from_ymd_opt(year, month as u32, day) } /// Add years to a date fn add_years(date: NaiveDate, years: i32) -> Option { NaiveDate::from_ymd_opt(date.year() + years, date.month(), date.day()) } /// Get number of days in a month fn days_in_month(year: i32, month: u32) -> u32 { match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => { if Self::is_leap_year(year) { 29 } else { 28 } } _ => 30, } } /// Check if year is leap year fn is_leap_year(year: i32) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } /// Create a new calendar on the CalDAV server pub async fn create_calendar( &self, token: &str, password: &str, name: String, description: Option, color: Option, ) -> Result<(), String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); let body = serde_json::json!({ "name": name, "description": description, "color": color }); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let url = format!("{}/calendar/create", self.base_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { Ok(()) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Delete an event from the CalDAV server pub async fn delete_event( &self, token: &str, password: &str, calendar_path: String, event_href: String, delete_action: String, occurrence_date: Option, ) -> Result { // Forward to delete_event_with_uid with extracted UID let event_uid = event_href.trim_end_matches(".ics").to_string(); self.delete_event_with_uid( token, password, calendar_path, event_href, delete_action, occurrence_date, event_uid, None, // No recurrence info available ) .await } /// Delete an event from the CalDAV server with UID and recurrence support pub async fn delete_event_with_uid( &self, token: &str, password: &str, calendar_path: String, event_href: String, delete_action: String, occurrence_date: Option, event_uid: String, recurrence: Option, ) -> Result { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); // Determine if this is a series event based on recurrence let is_series = recurrence .as_ref() .map(|r| !r.is_empty() && r.to_uppercase() != "NONE") .unwrap_or(false); let (body, url) = if is_series { // Use series-specific endpoint and payload for recurring events // Map delete_action to delete_scope for series endpoint let delete_scope = match delete_action.as_str() { "delete_this" => "this_only", "delete_following" => "this_and_future", "delete_series" => "all_in_series", _ => "this_only", // Default to single occurrence }; let body = serde_json::json!({ "series_uid": event_uid, "calendar_path": calendar_path, "event_href": event_href, "delete_scope": delete_scope, "occurrence_date": occurrence_date }); let url = format!("{}/calendar/events/series/delete", self.base_url); (body, url) } else { // Use regular endpoint for non-recurring events let body = serde_json::json!({ "calendar_path": calendar_path, "event_href": event_href, "delete_action": delete_action, "occurrence_date": occurrence_date }); let url = format!("{}/calendar/events/delete", self.base_url); (body, url) }; let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { // Parse the response to get the message let response: serde_json::Value = serde_json::from_str(&text_string) .map_err(|e| format!("Failed to parse response JSON: {}", e))?; let message = response["message"] .as_str() .unwrap_or("Event deleted successfully") .to_string(); Ok(message) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Create a new event on the CalDAV server pub async fn create_event( &self, token: &str, password: &str, title: String, description: String, start_date: String, start_time: String, end_date: String, end_time: String, location: String, all_day: bool, status: String, class: String, priority: Option, organizer: String, attendees: String, categories: String, reminder: String, recurrence: String, recurrence_days: Vec, recurrence_count: Option, recurrence_until: Option, calendar_path: Option, ) -> Result<(), String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); // Determine if this is a series event based on recurrence let is_series = !recurrence.is_empty() && recurrence.to_uppercase() != "NONE"; let (body, url) = if is_series { // Use series-specific endpoint and payload for recurring events let body = serde_json::json!({ "title": title, "description": description, "start_date": start_date, "start_time": start_time, "end_date": end_date, "end_time": end_time, "location": location, "all_day": all_day, "status": status, "class": class, "priority": priority, "organizer": organizer, "attendees": attendees, "categories": categories, "reminder": reminder, "recurrence": recurrence, "recurrence_days": recurrence_days, "recurrence_interval": 1_u32, // Default interval "recurrence_end_date": recurrence_until, "recurrence_count": recurrence_count, "calendar_path": calendar_path }); let url = format!("{}/calendar/events/series/create", self.base_url); (body, url) } else { // Use regular endpoint for non-recurring events let body = serde_json::json!({ "title": title, "description": description, "start_date": start_date, "start_time": start_time, "end_date": end_date, "end_time": end_time, "location": location, "all_day": all_day, "status": status, "class": class, "priority": priority, "organizer": organizer, "attendees": attendees, "categories": categories, "reminder": reminder, "recurrence": recurrence, "recurrence_days": recurrence_days, "calendar_path": calendar_path }); let url = format!("{}/calendar/events/create", self.base_url); (body, url) }; let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { Ok(()) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } pub async fn update_event( &self, token: &str, password: &str, event_uid: String, title: String, description: String, start_date: String, start_time: String, end_date: String, end_time: String, location: String, all_day: bool, status: String, class: String, priority: Option, organizer: String, attendees: String, categories: String, reminder: String, recurrence: String, recurrence_days: Vec, calendar_path: Option, exception_dates: Vec>, update_action: Option, until_date: Option>, ) -> Result<(), String> { // Forward to update_event_with_scope with default scope self.update_event_with_scope( token, password, event_uid, title, description, start_date, start_time, end_date, end_time, location, all_day, status, class, priority, organizer, attendees, categories, reminder, recurrence, recurrence_days, calendar_path, exception_dates, update_action, until_date, ) .await } pub async fn update_event_with_scope( &self, token: &str, password: &str, event_uid: String, title: String, description: String, start_date: String, start_time: String, end_date: String, end_time: String, location: String, all_day: bool, status: String, class: String, priority: Option, organizer: String, attendees: String, categories: String, reminder: String, recurrence: String, recurrence_days: Vec, calendar_path: Option, exception_dates: Vec>, update_action: Option, until_date: Option>, ) -> Result<(), String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); // Always use regular endpoint - recurring events should use update_series() instead let body = serde_json::json!({ "uid": event_uid, "title": title, "description": description, "start_date": start_date, "start_time": start_time, "end_date": end_date, "end_time": end_time, "location": location, "all_day": all_day, "status": status, "class": class, "priority": priority, "organizer": organizer, "attendees": attendees, "categories": categories, "reminder": reminder, "recurrence": recurrence, "recurrence_days": recurrence_days, "calendar_path": calendar_path, "update_action": update_action, "occurrence_date": null, "exception_dates": exception_dates.iter().map(|dt| dt.to_rfc3339()).collect::>(), "until_date": until_date.as_ref().map(|dt| dt.to_rfc3339()) }); let url = format!("{}/calendar/events/update", self.base_url); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { Ok(()) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Delete a calendar from the CalDAV server pub async fn delete_calendar( &self, token: &str, password: &str, path: String, ) -> Result<(), String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); let body = serde_json::json!({ "path": path }); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let url = format!("{}/calendar/delete", self.base_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { Ok(()) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Refresh a single event by UID from the CalDAV server pub async fn refresh_event( &self, token: &str, password: &str, uid: &str, ) -> Result, String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let url = format!("{}/calendar/events/{}", self.base_url, uid); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { let event: Option = serde_json::from_str(&text_string) .map_err(|e| format!("JSON parsing failed: {}", e))?; Ok(event) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } pub async fn update_series( &self, token: &str, password: &str, series_uid: String, title: String, description: String, start_date: String, start_time: String, end_date: String, end_time: String, location: String, all_day: bool, status: String, class: String, priority: Option, organizer: String, attendees: String, categories: String, reminder: String, recurrence: String, recurrence_days: Vec, recurrence_count: Option, recurrence_until: Option, calendar_path: Option, update_scope: String, occurrence_date: Option, ) -> Result<(), String> { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); let body = serde_json::json!({ "series_uid": series_uid, "title": title, "description": description, "start_date": start_date, "start_time": start_time, "end_date": end_date, "end_time": end_time, "location": location, "all_day": all_day, "status": status, "class": class, "priority": priority, "organizer": organizer, "attendees": attendees, "categories": categories, "reminder": reminder, "recurrence": recurrence, "recurrence_days": recurrence_days, "recurrence_interval": 1_u32, // Default interval - could be enhanced to be a parameter "recurrence_end_date": recurrence_until, "recurrence_count": recurrence_count, "calendar_path": calendar_path, "update_scope": update_scope, "occurrence_date": occurrence_date }); let url = format!("{}/calendar/events/series/update", self.base_url); web_sys::console::log_1( &format!("🔄 update_series: Making request to URL: {}", url).into(), ); web_sys::console::log_1( &format!( "🔄 update_series: Request body: {}", serde_json::to_string_pretty(&body).unwrap_or_default() ) .into(), ); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("X-CalDAV-Password", password) .map_err(|e| format!("Password header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Network request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response cast failed: {:?}", e))?; let text = JsFuture::from( resp.text() .map_err(|e| format!("Text extraction failed: {:?}", e))?, ) .await .map_err(|e| format!("Text promise failed: {:?}", e))?; let text_string = text.as_string().ok_or("Response text is not a string")?; if resp.ok() { Ok(()) } else { Err(format!( "Request failed with status {}: {}", resp.status(), text_string )) } } /// Parse monthly BYDAY (e.g., "1MO" -> (1, Monday), "-1FR" -> (-1, Friday)) fn parse_monthly_byday(byday: &str) -> Option<(i8, chrono::Weekday)> { if byday.len() < 3 { return None; } // Extract position and weekday let (position_str, weekday_str) = if byday.starts_with('-') { (&byday[..2], &byday[2..]) } else { (&byday[..1], &byday[1..]) }; let position = position_str.parse::().ok()?; let weekday = match weekday_str { "MO" => chrono::Weekday::Mon, "TU" => chrono::Weekday::Tue, "WE" => chrono::Weekday::Wed, "TH" => chrono::Weekday::Thu, "FR" => chrono::Weekday::Fri, "SA" => chrono::Weekday::Sat, "SU" => chrono::Weekday::Sun, _ => return None, }; Some((position, weekday)) } /// Find nth weekday in a month (e.g., 1st Monday, 2nd Tuesday, -1 = last) fn find_nth_weekday_in_month( month_start: NaiveDate, position: i8, weekday: chrono::Weekday, ) -> Option { let year = month_start.year(); let month = month_start.month(); if position > 0 { // Find nth occurrence from beginning of month let mut current = NaiveDate::from_ymd_opt(year, month, 1)?; let mut count = 0; while current.month() == month { if current.weekday() == weekday { count += 1; if count == position as u8 { return Some(current); } } current = current.succ_opt()?; } } else if position < 0 { // Find nth occurrence from end of month let next_month = if month == 12 { NaiveDate::from_ymd_opt(year + 1, 1, 1)? } else { NaiveDate::from_ymd_opt(year, month + 1, 1)? }; let mut current = next_month.pred_opt()?; // Last day of current month let mut count = 0; while current.month() == month { if current.weekday() == weekday { count += 1; if count == (-position) as u8 { return Some(current); } } current = current.pred_opt()?; } } None } // ==================== EXTERNAL CALENDAR METHODS ==================== pub async fn get_external_calendars() -> Result, String> { let token = LocalStorage::get::("auth_token") .map_err(|_| "No authentication token found".to_string())?; let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let service = Self::new(); let url = format!("{}/external-calendars", service.base_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response casting failed: {:?}", e))?; if !resp.ok() { return Err(format!("HTTP error: {}", resp.status())); } let json = JsFuture::from(resp.json().unwrap()) .await .map_err(|e| format!("JSON parsing failed: {:?}", e))?; let external_calendars: Vec = serde_wasm_bindgen::from_value(json) .map_err(|e| format!("Deserialization failed: {:?}", e))?; Ok(external_calendars) } pub async fn create_external_calendar(name: &str, url: &str, color: &str) -> Result { let token = LocalStorage::get::("auth_token") .map_err(|_| "No authentication token found".to_string())?; let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); let body = serde_json::json!({ "name": name, "url": url, "color": color }); let service = Self::new(); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let url = format!("{}/external-calendars", service.base_url); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response casting failed: {:?}", e))?; if !resp.ok() { return Err(format!("HTTP error: {}", resp.status())); } let json = JsFuture::from(resp.json().unwrap()) .await .map_err(|e| format!("JSON parsing failed: {:?}", e))?; let external_calendar: ExternalCalendar = serde_wasm_bindgen::from_value(json) .map_err(|e| format!("Deserialization failed: {:?}", e))?; Ok(external_calendar) } pub async fn update_external_calendar( id: i32, name: &str, url: &str, color: &str, is_visible: bool, ) -> Result<(), String> { let token = LocalStorage::get::("auth_token") .map_err(|_| "No authentication token found".to_string())?; let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("POST"); opts.set_mode(RequestMode::Cors); let body = serde_json::json!({ "name": name, "url": url, "color": color, "is_visible": is_visible }); let service = Self::new(); let body_string = serde_json::to_string(&body) .map_err(|e| format!("JSON serialization failed: {}", e))?; opts.set_body(&body_string.into()); let url = format!("{}/external-calendars/{}", service.base_url, id); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; request .headers() .set("Content-Type", "application/json") .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response casting failed: {:?}", e))?; if !resp.ok() { return Err(format!("HTTP error: {}", resp.status())); } Ok(()) } pub async fn delete_external_calendar(id: i32) -> Result<(), String> { let token = LocalStorage::get::("auth_token") .map_err(|_| "No authentication token found".to_string())?; let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("DELETE"); opts.set_mode(RequestMode::Cors); let service = Self::new(); let url = format!("{}/external-calendars/{}", service.base_url, id); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response casting failed: {:?}", e))?; if !resp.ok() { return Err(format!("HTTP error: {}", resp.status())); } Ok(()) } pub async fn fetch_external_calendar_events(id: i32) -> Result, String> { let token = LocalStorage::get::("auth_token") .map_err(|_| "No authentication token found".to_string())?; let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); let service = Self::new(); let url = format!("{}/external-calendars/{}/events", service.base_url, id); let request = Request::new_with_str_and_init(&url, &opts) .map_err(|e| format!("Request creation failed: {:?}", e))?; request .headers() .set("Authorization", &format!("Bearer {}", token)) .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)) .await .map_err(|e| format!("Request failed: {:?}", e))?; let resp: Response = resp_value .dyn_into() .map_err(|e| format!("Response casting failed: {:?}", e))?; if !resp.ok() { return Err(format!("HTTP error: {}", resp.status())); } let json = JsFuture::from(resp.json().unwrap()) .await .map_err(|e| format!("JSON parsing failed: {:?}", e))?; #[derive(Deserialize)] struct ExternalCalendarEventsResponse { events: Vec, last_fetched: chrono::DateTime, } let response: ExternalCalendarEventsResponse = serde_wasm_bindgen::from_value(json) .map_err(|e| format!("Deserialization failed: {:?}", e))?; Ok(response.events) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ExternalCalendar { pub id: i32, pub name: String, pub url: String, pub color: String, pub is_visible: bool, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, pub last_fetched: Option>, }