diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index 0278f75..d5cdd6f 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -50,6 +50,9 @@ pub struct CalendarEvent { /// Recurrence rule (RRULE) pub recurrence_rule: Option, + /// Exception dates - dates to exclude from recurrence (EXDATE) + pub exception_dates: Vec>, + /// All-day event flag pub all_day: bool, @@ -361,6 +364,9 @@ impl CalDAVClient { let last_modified = properties.get("LAST-MODIFIED") .and_then(|s| self.parse_datetime(s, None).ok()); + // Parse exception dates (EXDATE) + let exception_dates = self.parse_exception_dates(&event); + Ok(CalendarEvent { uid, summary: properties.get("SUMMARY").cloned(), @@ -377,6 +383,7 @@ impl CalDAVClient { created, last_modified, recurrence_rule: properties.get("RRULE").cloned(), + exception_dates, all_day, reminders: self.parse_alarms(&event)?, etag: None, // Set by caller @@ -591,6 +598,28 @@ impl CalDAVClient { Err(CalDAVError::ParseError(format!("Unable to parse datetime: {}", datetime_str))) } + /// Parse EXDATE properties from an iCal event + fn parse_exception_dates(&self, event: &ical::parser::ical::component::IcalEvent) -> Vec> { + let mut exception_dates = Vec::new(); + + // Look for EXDATE properties + for property in &event.properties { + if property.name.to_uppercase() == "EXDATE" { + if let Some(value) = &property.value { + // EXDATE can contain multiple comma-separated dates + for date_str in value.split(',') { + // Try to parse the date (the parse_datetime method will handle different formats) + if let Ok(date) = self.parse_datetime(date_str.trim(), None) { + exception_dates.push(date); + } + } + } + } + } + + exception_dates + } + /// Create a new calendar on the CalDAV server using MKCALENDAR pub async fn create_calendar(&self, name: &str, description: Option<&str>, color: Option<&str>) -> Result<(), CalDAVError> { // Sanitize calendar name for URL path @@ -758,6 +787,56 @@ impl CalDAVClient { } } + /// Update an existing event on the CalDAV server + pub async fn update_event(&self, calendar_path: &str, event: &CalendarEvent, event_href: &str) -> Result<(), CalDAVError> { + // Construct the full URL for the event + let full_url = if event_href.starts_with("http") { + event_href.to_string() + } else if event_href.starts_with("/dav.php") { + // Event href is already a full path, combine with base server URL (without /dav.php) + let base_url = self.config.server_url.trim_end_matches('/').trim_end_matches("/dav.php"); + format!("{}{}", base_url, event_href) + } else { + // Event href is just a filename, combine with calendar path + let clean_path = if calendar_path.starts_with("/dav.php") { + calendar_path.trim_start_matches("/dav.php") + } else { + calendar_path + }; + format!("{}/dav.php{}/{}", self.config.server_url.trim_end_matches('/'), clean_path, event_href) + }; + + println!("📝 Updating event at: {}", full_url); + + // Generate iCalendar data for the event + let ical_data = self.generate_ical_event(event)?; + + println!("📝 Updated iCal data: {}", ical_data); + println!("📝 Event has {} exception dates", event.exception_dates.len()); + + let response = self.http_client + .put(&full_url) + .header("Authorization", format!("Basic {}", self.config.get_basic_auth())) + .header("Content-Type", "text/calendar; charset=utf-8") + .header("User-Agent", "calendar-app/0.1.0") + .body(ical_data) + .send() + .await + .map_err(|e| CalDAVError::ParseError(e.to_string()))?; + + println!("Event update response status: {}", response.status()); + + if response.status().is_success() || response.status().as_u16() == 201 || response.status().as_u16() == 204 { + println!("✅ Event updated successfully"); + Ok(()) + } else { + let status = response.status(); + let error_body = response.text().await.unwrap_or_default(); + println!("❌ Event update failed: {} - {}", status, error_body); + Err(CalDAVError::ServerError(status.as_u16())) + } + } + /// Generate iCalendar data for a CalendarEvent fn generate_ical_event(&self, event: &CalendarEvent) -> Result { let now = chrono::Utc::now(); @@ -871,6 +950,15 @@ impl CalDAVClient { ical.push_str(&format!("RRULE:{}\r\n", rrule)); } + // Exception dates (EXDATE) + for exception_date in &event.exception_dates { + if event.all_day { + ical.push_str(&format!("EXDATE;VALUE=DATE:{}\r\n", format_date(exception_date))); + } else { + ical.push_str(&format!("EXDATE:{}\r\n", format_datetime(exception_date))); + } + } + ical.push_str("END:VEVENT\r\n"); ical.push_str("END:VCALENDAR\r\n"); diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index db42caa..1a8c682 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -5,7 +5,7 @@ use axum::{ }; use serde::Deserialize; use std::sync::Arc; -use chrono::Datelike; +use chrono::{Datelike, TimeZone}; use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse}}; use crate::calendar::{CalDAVClient, CalendarEvent}; @@ -350,12 +350,37 @@ pub async fn delete_calendar( })) } +/// Helper function to fetch an event by its href +async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_href: &str) -> Result, crate::calendar::CalDAVError> { + // Get all events from the calendar + let events = client.fetch_events(calendar_path).await?; + + // Find the event with matching href + for event in events { + if let Some(href) = &event.href { + // Compare the href (handle both full URLs and relative paths) + let href_matches = if event_href.starts_with("http") { + href == event_href + } else { + href.ends_with(event_href) || href == event_href + }; + + if href_matches { + return Ok(Some(event)); + } + } + } + + Ok(None) +} + pub async fn delete_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { - println!("🗑️ Delete event request received: calendar_path='{}', event_href='{}'", request.calendar_path, request.event_href); + println!("🗑️ Delete event request received: calendar_path='{}', event_href='{}', action='{}'", + request.calendar_path, request.event_href, request.delete_action); // Extract and verify token let token = extract_bearer_token(&headers)?; @@ -373,15 +398,146 @@ pub async fn delete_event( let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); - // Delete the event - client.delete_event(&request.calendar_path, &request.event_href) - .await - .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; + // Handle different delete actions + match request.delete_action.as_str() { + "delete_this" => { + // Add EXDATE to exclude this specific occurrence + if let Some(occurrence_date) = &request.occurrence_date { + println!("🔄 Adding EXDATE for occurrence: {}", occurrence_date); + + // First, fetch the current event to get its data + match fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await { + Ok(Some(mut event)) => { + // Check if it has recurrence rule + if event.recurrence_rule.is_some() { + // Parse the occurrence date and calculate the correct EXDATE datetime + if let Ok(occurrence_date_parsed) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") { + // Calculate the exact datetime for this occurrence by using the original event's time + let original_time = event.start.time(); + let occurrence_datetime = occurrence_date_parsed.and_time(original_time); + let exception_utc = chrono::Utc.from_utc_datetime(&occurrence_datetime); + + println!("🔄 Original event start: {}", event.start); + println!("🔄 Occurrence date: {}", occurrence_date); + println!("🔄 Calculated EXDATE: {}", exception_utc); + + // Add the exception date + event.exception_dates.push(exception_utc); + + // Update the event with the new EXDATE + client.update_event(&request.calendar_path, &event, &request.event_href) + .await + .map_err(|e| ApiError::Internal(format!("Failed to update event with EXDATE: {}", e)))?; + + Ok(Json(DeleteEventResponse { + success: true, + message: "Individual occurrence excluded from series successfully".to_string(), + })) + } else { + Err(ApiError::BadRequest("Invalid occurrence date format".to_string())) + } + } else { + // Not a recurring event, just delete it completely + client.delete_event(&request.calendar_path, &request.event_href) + .await + .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; + + Ok(Json(DeleteEventResponse { + success: true, + message: "Event deleted successfully".to_string(), + })) + } + }, + Ok(None) => Err(ApiError::NotFound("Event not found".to_string())), + Err(e) => Err(ApiError::Internal(format!("Failed to fetch event: {}", e))), + } + } else { + Err(ApiError::BadRequest("Occurrence date is required for 'delete_this' action".to_string())) + } + }, + "delete_following" => { + // Modify RRULE to end before the selected occurrence + if let Some(occurrence_date) = &request.occurrence_date { + println!("🔄 Modifying RRULE to end before: {}", occurrence_date); + + // First, fetch the current event to get its data + match fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await { + Ok(Some(mut event)) => { + // Check if it has recurrence rule + if let Some(ref rrule) = event.recurrence_rule { + // Parse the occurrence date and calculate the UNTIL date + if let Ok(occurrence_date_parsed) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") { + // Calculate the datetime for the occurrence we want to stop before + let original_time = event.start.time(); + let occurrence_datetime = occurrence_date_parsed.and_time(original_time); + let occurrence_utc = chrono::Utc.from_utc_datetime(&occurrence_datetime); + + // UNTIL should be the last occurrence we want to keep (day before the selected occurrence) + let until_date = occurrence_utc - chrono::Duration::days(1); + let until_str = until_date.format("%Y%m%dT%H%M%SZ").to_string(); + + + println!("🔄 Original event start: {}", event.start); + println!("🔄 Occurrence to stop before: {}", occurrence_utc); + println!("🔄 UNTIL date (last to keep): {}", until_date); + println!("🔄 UNTIL string: {}", until_str); + println!("🔄 Original RRULE: {}", rrule); + + // Modify the RRULE to add UNTIL clause + let new_rrule = if rrule.contains("UNTIL=") { + // Replace existing UNTIL + regex::Regex::new(r"UNTIL=[^;]+").unwrap().replace(rrule, &format!("UNTIL={}", until_str)).to_string() + } else { + // Add UNTIL clause + format!("{};UNTIL={}", rrule, until_str) + }; + + println!("🔄 New RRULE: {}", new_rrule); + event.recurrence_rule = Some(new_rrule); + + // Update the event with the modified RRULE + client.update_event(&request.calendar_path, &event, &request.event_href) + .await + .map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?; + + Ok(Json(DeleteEventResponse { + success: true, + message: "Following occurrences removed from series successfully".to_string(), + })) + } else { + Err(ApiError::BadRequest("Invalid occurrence date format".to_string())) + } + } else { + // Not a recurring event, just delete it completely + client.delete_event(&request.calendar_path, &request.event_href) + .await + .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; + + Ok(Json(DeleteEventResponse { + success: true, + message: "Event deleted successfully".to_string(), + })) + } + }, + Ok(None) => Err(ApiError::NotFound("Event not found".to_string())), + Err(e) => Err(ApiError::Internal(format!("Failed to fetch event: {}", e))), + } + } else { + Err(ApiError::BadRequest("Occurrence date is required for 'delete_following' action".to_string())) + } + }, + "delete_series" | _ => { + // Delete the entire event/series (current default behavior) + client.delete_event(&request.calendar_path, &request.event_href) + .await + .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; - Ok(Json(DeleteEventResponse { - success: true, - message: "Event deleted successfully".to_string(), - })) + Ok(Json(DeleteEventResponse { + success: true, + message: "Event series deleted successfully".to_string(), + })) + } + } } pub async fn create_event( @@ -587,6 +743,7 @@ pub async fn create_event( created: Some(chrono::Utc::now()), last_modified: Some(chrono::Utc::now()), recurrence_rule, + exception_dates: Vec::new(), // No exception dates for new events all_day: request.all_day, reminders, etag: None, diff --git a/backend/src/models.rs b/backend/src/models.rs index 1ca248c..793934e 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -62,6 +62,8 @@ pub struct DeleteCalendarResponse { pub struct DeleteEventRequest { pub calendar_path: String, pub event_href: String, + pub delete_action: String, // "delete_this", "delete_following", or "delete_series" + pub occurrence_date: Option, // ISO date string for the specific occurrence } #[derive(Debug, Serialize)] diff --git a/src/app.rs b/src/app.rs index 3345f2c..5930766 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use yew::prelude::*; use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; use web_sys::MouseEvent; -use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType}; +use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; use chrono::NaiveDate; @@ -475,6 +475,7 @@ pub fn App() -> Html { is_open={*event_context_menu_open} x={event_context_menu_pos.0} y={event_context_menu_pos.1} + event={(*event_context_menu_event).clone()} on_close={Callback::from({ let event_context_menu_open = event_context_menu_open.clone(); move |_| event_context_menu_open.set(false) @@ -484,11 +485,18 @@ pub fn App() -> Html { let event_context_menu_event = event_context_menu_event.clone(); let event_context_menu_open = event_context_menu_open.clone(); let refresh_calendars = refresh_calendars.clone(); - move |_: MouseEvent| { + move |delete_action: DeleteAction| { if let (Some(token), Some(event)) = ((*auth_token).clone(), (*event_context_menu_event).clone()) { let _refresh_calendars = refresh_calendars.clone(); let event_context_menu_open = event_context_menu_open.clone(); + // Log the delete action for now - we'll implement different behaviors later + match delete_action { + DeleteAction::DeleteThis => 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()), + } + wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); @@ -503,9 +511,37 @@ pub fn App() -> Html { }; if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) { - match calendar_service.delete_event(&token, &password, calendar_path.clone(), event_href.clone()).await { - Ok(_) => { - web_sys::console::log_1(&"Event deleted successfully!".into()); + // 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.start.date_naive().format("%Y-%m-%d").to_string()); + + web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into()); + web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into()); + web_sys::console::log_1(&format!("🔄 Event start: {}", event.start).into()); + web_sys::console::log_1(&format!("🔄 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); // Force a page reload to refresh the calendar events @@ -513,6 +549,7 @@ pub fn App() -> Html { } 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 { diff --git a/src/components/event_context_menu.rs b/src/components/event_context_menu.rs index d843cc8..c11966d 100644 --- a/src/components/event_context_menu.rs +++ b/src/components/event_context_menu.rs @@ -1,12 +1,21 @@ use yew::prelude::*; use web_sys::MouseEvent; +use crate::services::calendar_service::CalendarEvent; + +#[derive(Clone, PartialEq, Debug)] +pub enum DeleteAction { + DeleteThis, + DeleteFollowing, + DeleteSeries, +} #[derive(Properties, PartialEq)] pub struct EventContextMenuProps { pub is_open: bool, pub x: i32, pub y: i32, - pub on_delete: Callback, + pub event: Option, + pub on_delete: Callback, pub on_close: Callback<()>, } @@ -23,11 +32,16 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { props.x, props.y ); - let on_delete_click = { + // Check if the event is recurring + let is_recurring = props.event.as_ref() + .map(|event| event.recurrence_rule.is_some()) + .unwrap_or(false); + + let create_delete_callback = |action: DeleteAction| { let on_delete = props.on_delete.clone(); let on_close = props.on_close.clone(); - Callback::from(move |e: MouseEvent| { - on_delete.emit(e); + Callback::from(move |_: MouseEvent| { + on_delete.emit(action.clone()); on_close.emit(()); }) }; @@ -38,10 +52,33 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { class="context-menu" style={style} > -
- {"🗑️"} - {"Delete Event"} -
+ { + if is_recurring { + html! { + <> +
+ {"🗑️"} + {"Delete This Event"} +
+
+ {"🗑️"} + {"Delete Following Events"} +
+
+ {"🗑️"} + {"Delete Entire Series"} +
+ + } + } else { + html! { +
+ {"🗑️"} + {"Delete Event"} +
+ } + } + } } } \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index cd819f1..6777b50 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -15,7 +15,7 @@ pub use calendar::Calendar; pub use event_modal::EventModal; pub use create_calendar_modal::CreateCalendarModal; pub use context_menu::ContextMenu; -pub use event_context_menu::EventContextMenu; +pub use event_context_menu::{EventContextMenu, DeleteAction}; pub use calendar_context_menu::CalendarContextMenu; pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; pub use sidebar::Sidebar; diff --git a/src/services/calendar_service.rs b/src/services/calendar_service.rs index 7e4f9aa..879b586 100644 --- a/src/services/calendar_service.rs +++ b/src/services/calendar_service.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc, NaiveDate, Datelike, Weekday, Duration}; +use chrono::{DateTime, Utc, NaiveDate, Datelike, Weekday, Duration, TimeZone}; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; @@ -56,6 +56,7 @@ pub struct CalendarEvent { pub created: Option>, pub last_modified: Option>, pub recurrence_rule: Option, + pub exception_dates: Vec>, pub all_day: bool, pub reminders: Vec, pub etag: Option, @@ -267,8 +268,26 @@ impl CalendarService { for event in events { if let Some(ref rrule) = event.recurrence_rule { + web_sys::console::log_1(&format!("📅 Processing recurring event '{}' with RRULE: {}", + event.summary.as_deref().unwrap_or("Untitled"), + rrule + ).into()); + + // Log if event has exception dates + if !event.exception_dates.is_empty() { + web_sys::console::log_1(&format!("📅 Event '{}' has {} exception dates: {:?}", + event.summary.as_deref().unwrap_or("Untitled"), + event.exception_dates.len(), + event.exception_dates + ).into()); + } + // Generate occurrences for recurring events let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range); + web_sys::console::log_1(&format!("📅 Generated {} occurrences for event '{}'", + occurrences.len(), + event.summary.as_deref().unwrap_or("Untitled") + ).into()); expanded_events.extend(occurrences); } else { // Non-recurring event - add as-is @@ -290,6 +309,8 @@ impl CalendarService { // 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| { @@ -316,25 +337,75 @@ impl CalendarService { .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 + // Try different parsing approaches for UTC dates + 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.start.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 { - if current_date >= start_range { - // Create occurrence event - let mut occurrence_event = base_event.clone(); - - // Adjust dates - let days_diff = current_date.signed_duration_since(start_date).num_days(); - occurrence_event.start = base_event.start + Duration::days(days_diff); - - if let Some(end) = base_event.end { - occurrence_event.end = Some(end + Duration::days(days_diff)); + // Check UNTIL constraint - stop if current occurrence is after UNTIL date + if let Some(until) = until_date { + let current_datetime = base_event.start + 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.start + Duration::days(days_diff); - occurrences.push(occurrence_event); + // Check if this occurrence is in the exception dates (EXDATE) + let is_exception = base_event.exception_dates.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 + let mut occurrence_event = base_event.clone(); + occurrence_event.start = occurrence_datetime; + + if let Some(end) = base_event.end { + occurrence_event.end = Some(end + Duration::days(days_diff)); + } + + occurrences.push(occurrence_event); + } } // Calculate next occurrence date @@ -534,8 +605,10 @@ impl CalendarService { token: &str, password: &str, calendar_path: String, - event_href: String - ) -> Result<(), String> { + event_href: String, + delete_action: String, + occurrence_date: Option + ) -> Result { let window = web_sys::window().ok_or("No global window exists")?; let opts = RequestInit::new(); @@ -544,7 +617,9 @@ impl CalendarService { let body = serde_json::json!({ "calendar_path": calendar_path, - "event_href": event_href + "event_href": event_href, + "delete_action": delete_action, + "occurrence_date": occurrence_date }); let body_string = serde_json::to_string(&body) @@ -580,7 +655,11 @@ impl CalendarService { .ok_or("Response text is not a string")?; if resp.ok() { - 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)) }