Remove v2 API endpoints and fix warnings
- Remove all v2 API routes (/api/v2/calendar/events/*) - Delete models_v2.rs file and associated types - Remove create_event_v2, update_event_v2, delete_event_v2 handlers - Remove unused occurrence_date and exception_dates from UpdateEventRequest - Remove unused ConfigError variant from CalDAVError - Simplify backend to single unified v1 API using VEvent structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -1060,9 +1060,6 @@ pub enum CalDAVError { | |||||||
|      |      | ||||||
|     #[error("Failed to parse calendar data: {0}")] |     #[error("Failed to parse calendar data: {0}")] | ||||||
|     ParseError(String), |     ParseError(String), | ||||||
|      |  | ||||||
|     #[error("Configuration error: {0}")] |  | ||||||
|     ConfigError(String), |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ use std::sync::Arc; | |||||||
| use chrono::{Datelike, TimeZone}; | use chrono::{Datelike, TimeZone}; | ||||||
|  |  | ||||||
| use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, Attendee, VAlarm, AlarmAction, AlarmTrigger}; | use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, Attendee, VAlarm, AlarmAction, AlarmTrigger}; | ||||||
| use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}, models_v2::{CreateEventRequestV2, CreateEventResponseV2, UpdateEventRequestV2, UpdateEventResponseV2, DeleteEventRequestV2, DeleteEventResponseV2, DeleteActionV2}}; | use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}}; | ||||||
| use crate::calendar::{CalDAVClient, CalendarEvent}; | use crate::calendar::{CalDAVClient, CalendarEvent}; | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| @@ -375,161 +375,6 @@ async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_h | |||||||
|     Ok(None) |     Ok(None) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Delete event using v2 API with enum-based delete actions |  | ||||||
| pub async fn delete_event_v2( |  | ||||||
|     State(state): State<Arc<AppState>>, |  | ||||||
|     headers: HeaderMap, |  | ||||||
|     Json(request): Json<DeleteEventRequestV2>, |  | ||||||
| ) -> Result<Json<DeleteEventResponseV2>, ApiError> { |  | ||||||
|     println!("🗑️ Delete event v2 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)?; |  | ||||||
|     let password = extract_password_header(&headers)?; |  | ||||||
|  |  | ||||||
|     // Validate request |  | ||||||
|     if request.calendar_path.trim().is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("Calendar path is required".to_string())); |  | ||||||
|     } |  | ||||||
|     if request.event_href.trim().is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("Event href is required".to_string())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Create CalDAV config from token and password |  | ||||||
|     let config = state.auth_service.caldav_config_from_token(&token, &password)?; |  | ||||||
|     let client = CalDAVClient::new(config); |  | ||||||
|  |  | ||||||
|     // Handle different delete actions |  | ||||||
|     match request.delete_action { |  | ||||||
|         DeleteActionV2::DeleteThis => { |  | ||||||
|             // 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.rrule.is_some() { |  | ||||||
|                             // Calculate the exact datetime for this occurrence by using the original event's time |  | ||||||
|                             let original_time = event.dtstart.time(); |  | ||||||
|                             let occurrence_datetime = occurrence_date.date_naive().and_time(original_time); |  | ||||||
|                             let exception_utc = chrono::Utc.from_utc_datetime(&occurrence_datetime); |  | ||||||
|                              |  | ||||||
|                             println!("🔄 Original event start: {}", event.dtstart); |  | ||||||
|                             println!("🔄 Occurrence date: {}", occurrence_date); |  | ||||||
|                             println!("🔄 Calculated EXDATE: {}", exception_utc); |  | ||||||
|                              |  | ||||||
|                             // Add the exception date |  | ||||||
|                             event.exdate.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(DeleteEventResponseV2 { |  | ||||||
|                                 success: true, |  | ||||||
|                                 message: "Individual occurrence excluded from series successfully".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(DeleteEventResponseV2 { |  | ||||||
|                                 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())) |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         DeleteActionV2::DeleteFollowing => { |  | ||||||
|             // 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.rrule { |  | ||||||
|                             // Calculate the datetime for the occurrence we want to stop before |  | ||||||
|                             let original_time = event.dtstart.time(); |  | ||||||
|                             let occurrence_datetime = occurrence_date.date_naive().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.dtstart); |  | ||||||
|                             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.rrule = 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(DeleteEventResponseV2 { |  | ||||||
|                                 success: true, |  | ||||||
|                                 message: "Following occurrences removed from series successfully".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(DeleteEventResponseV2 { |  | ||||||
|                                 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())) |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         DeleteActionV2::DeleteSeries => { |  | ||||||
|             // 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(DeleteEventResponseV2 { |  | ||||||
|                 success: true, |  | ||||||
|                 message: "Event series deleted successfully".to_string(), |  | ||||||
|             })) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn delete_event( | pub async fn delete_event( | ||||||
|     State(state): State<Arc<AppState>>, |     State(state): State<Arc<AppState>>, | ||||||
| @@ -697,168 +542,6 @@ pub async fn delete_event( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Create event using v2 API with direct DateTime support (no string parsing) |  | ||||||
| pub async fn create_event_v2( |  | ||||||
|     State(state): State<Arc<AppState>>, |  | ||||||
|     headers: HeaderMap, |  | ||||||
|     Json(request): Json<CreateEventRequestV2>, |  | ||||||
| ) -> Result<Json<CreateEventResponseV2>, ApiError> { |  | ||||||
|     println!("📝 Create event v2 request received: summary='{}', all_day={}, calendar_path={:?}",  |  | ||||||
|              request.summary, request.all_day, request.calendar_path); |  | ||||||
|      |  | ||||||
|     // Extract and verify token |  | ||||||
|     let token = extract_bearer_token(&headers)?; |  | ||||||
|     let password = extract_password_header(&headers)?; |  | ||||||
|  |  | ||||||
|     // Validate request |  | ||||||
|     if request.summary.trim().is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("Event summary is required".to_string())); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if request.summary.len() > 200 { |  | ||||||
|         return Err(ApiError::BadRequest("Event summary too long (max 200 characters)".to_string())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Create CalDAV config from token and password |  | ||||||
|     let config = state.auth_service.caldav_config_from_token(&token, &password)?; |  | ||||||
|     let client = CalDAVClient::new(config); |  | ||||||
|  |  | ||||||
|     // Determine which calendar to use |  | ||||||
|     let calendar_path = if let Some(path) = request.calendar_path { |  | ||||||
|         path |  | ||||||
|     } else { |  | ||||||
|         // Use the first available calendar |  | ||||||
|         let calendar_paths = client.discover_calendars() |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; |  | ||||||
|          |  | ||||||
|         if calendar_paths.is_empty() { |  | ||||||
|             return Err(ApiError::BadRequest("No calendars available for event creation".to_string())); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         calendar_paths[0].clone() |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Validate that end is after start |  | ||||||
|     if let Some(end) = request.dtend { |  | ||||||
|         if end <= request.dtstart { |  | ||||||
|             return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Generate a unique UID for the event |  | ||||||
|     let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp()); |  | ||||||
|  |  | ||||||
|     // Convert V2 enums to calendar module enums |  | ||||||
|     let status = match request.status.unwrap_or_default() { |  | ||||||
|         crate::models_v2::EventStatusV2::Tentative => EventStatus::Tentative, |  | ||||||
|         crate::models_v2::EventStatusV2::Cancelled => EventStatus::Cancelled, |  | ||||||
|         crate::models_v2::EventStatusV2::Confirmed => EventStatus::Confirmed, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let class = match request.class.unwrap_or_default() { |  | ||||||
|         crate::models_v2::EventClassV2::Private => EventClass::Private, |  | ||||||
|         crate::models_v2::EventClassV2::Confidential => EventClass::Confidential, |  | ||||||
|         crate::models_v2::EventClassV2::Public => EventClass::Public, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Convert attendees from V2 to simple email list (for now) |  | ||||||
|     let attendees: Vec<String> = request.attendees.into_iter() |  | ||||||
|         .map(|att| att.email) |  | ||||||
|         .collect(); |  | ||||||
|  |  | ||||||
|     // Convert alarms to alarms |  | ||||||
|     let alarms: Vec<crate::calendar::EventReminder> = request.alarms.into_iter() |  | ||||||
|         .map(|alarm| crate::calendar::EventReminder { |  | ||||||
|             minutes_before: -alarm.trigger_minutes, // Convert to positive minutes before |  | ||||||
|             action: match alarm.action { |  | ||||||
|                 crate::models_v2::AlarmActionV2::Display => crate::calendar::ReminderAction::Display, |  | ||||||
|                 crate::models_v2::AlarmActionV2::Email => crate::calendar::ReminderAction::Email, |  | ||||||
|                 crate::models_v2::AlarmActionV2::Audio => crate::calendar::ReminderAction::Audio, |  | ||||||
|             }, |  | ||||||
|             description: alarm.description, |  | ||||||
|         }) |  | ||||||
|         .collect(); |  | ||||||
|  |  | ||||||
|     // Create the CalendarEvent struct - much simpler now! |  | ||||||
|     // Create VEvent with required fields |  | ||||||
|     let mut event = VEvent::new(uid, request.dtstart); |  | ||||||
|      |  | ||||||
|     // Set optional fields |  | ||||||
|     event.dtend = request.dtend; |  | ||||||
|     event.summary = Some(request.summary.clone()); |  | ||||||
|     event.description = request.description; |  | ||||||
|     event.location = request.location; |  | ||||||
|     event.status = Some(status); |  | ||||||
|     event.class = Some(class); |  | ||||||
|     event.priority = request.priority; |  | ||||||
|     event.organizer = request.organizer.map(|org| CalendarUser { |  | ||||||
|         cal_address: org, |  | ||||||
|         common_name: None, |  | ||||||
|         dir_entry_ref: None, |  | ||||||
|         sent_by: None, |  | ||||||
|         language: None, |  | ||||||
|     }); |  | ||||||
|     event.attendees = attendees.into_iter().map(|email| Attendee { |  | ||||||
|         cal_address: email, |  | ||||||
|         common_name: None, |  | ||||||
|         role: None, |  | ||||||
|         part_stat: None, |  | ||||||
|         rsvp: None, |  | ||||||
|         cu_type: None, |  | ||||||
|         member: Vec::new(), |  | ||||||
|         delegated_to: Vec::new(), |  | ||||||
|         delegated_from: Vec::new(), |  | ||||||
|         sent_by: None, |  | ||||||
|         dir_entry_ref: None, |  | ||||||
|         language: None, |  | ||||||
|     }).collect(); |  | ||||||
|     event.categories = request.categories; |  | ||||||
|     event.rrule = request.rrule; |  | ||||||
|     event.all_day = request.all_day; |  | ||||||
|     event.alarms = alarms.into_iter().map(|alarm| VAlarm { |  | ||||||
|         action: match alarm.action { |  | ||||||
|             crate::calendar::ReminderAction::Display => calendar_models::AlarmAction::Display, |  | ||||||
|             crate::calendar::ReminderAction::Email => calendar_models::AlarmAction::Email, |  | ||||||
|             crate::calendar::ReminderAction::Audio => calendar_models::AlarmAction::Audio, |  | ||||||
|         }, |  | ||||||
|         trigger: calendar_models::AlarmTrigger::Duration(chrono::Duration::minutes(-alarm.minutes_before as i64)), |  | ||||||
|         duration: None, |  | ||||||
|         repeat: None, |  | ||||||
|         description: alarm.description, |  | ||||||
|         summary: None, |  | ||||||
|         attendees: Vec::new(), |  | ||||||
|         attach: Vec::new(), |  | ||||||
|     }).collect(); |  | ||||||
|     event.calendar_path = Some(calendar_path.clone()); |  | ||||||
|  |  | ||||||
|     // Create the event on the CalDAV server |  | ||||||
|     let event_href = client.create_event(&calendar_path, &event) |  | ||||||
|         .await |  | ||||||
|         .map_err(|e| ApiError::Internal(format!("Failed to create event: {}", e)))?; |  | ||||||
|  |  | ||||||
|     // Fetch the created event to get its details |  | ||||||
|     let created_event = fetch_event_by_href(&client, &calendar_path, &event_href) |  | ||||||
|         .await |  | ||||||
|         .map_err(|e| ApiError::Internal(format!("Failed to fetch created event: {}", e)))?; |  | ||||||
|  |  | ||||||
|     let event_summary = created_event.map(|e| crate::models_v2::EventSummaryV2 { |  | ||||||
|         uid: e.uid, |  | ||||||
|         summary: e.summary, |  | ||||||
|         dtstart: e.dtstart, |  | ||||||
|         dtend: e.dtend, |  | ||||||
|         location: e.location, |  | ||||||
|         all_day: e.all_day, |  | ||||||
|         href: e.href, |  | ||||||
|         etag: e.etag, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     Ok(Json(CreateEventResponseV2 { |  | ||||||
|         success: true, |  | ||||||
|         message: "Event created successfully".to_string(), |  | ||||||
|         event: event_summary, |  | ||||||
|     })) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn create_event( | pub async fn create_event( | ||||||
|     State(state): State<Arc<AppState>>, |     State(state): State<Arc<AppState>>, | ||||||
| @@ -1072,286 +755,6 @@ pub async fn create_event( | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /// Update event using v2 API with direct DateTime support (no string parsing) |  | ||||||
| pub async fn update_event_v2( |  | ||||||
|     State(state): State<Arc<AppState>>, |  | ||||||
|     headers: HeaderMap, |  | ||||||
|     Json(request): Json<UpdateEventRequestV2>, |  | ||||||
| ) -> Result<Json<UpdateEventResponseV2>, ApiError> { |  | ||||||
|     println!("🔄 Update event v2 request received: uid='{}', summary='{}', update_action={:?}",  |  | ||||||
|              request.uid, request.summary, request.update_action); |  | ||||||
|      |  | ||||||
|     // Extract and verify token |  | ||||||
|     let token = extract_bearer_token(&headers)?; |  | ||||||
|     let password = extract_password_header(&headers)?; |  | ||||||
|  |  | ||||||
|     // Validate request |  | ||||||
|     if request.uid.trim().is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("Event UID is required".to_string())); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if request.summary.trim().is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("Event summary is required".to_string())); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if request.summary.len() > 200 { |  | ||||||
|         return Err(ApiError::BadRequest("Event summary too long (max 200 characters)".to_string())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Create CalDAV config from token and password |  | ||||||
|     let config = state.auth_service.caldav_config_from_token(&token, &password)?; |  | ||||||
|     let client = CalDAVClient::new(config); |  | ||||||
|  |  | ||||||
|     // Find the event across all calendars (or in the specified calendar) |  | ||||||
|     let calendar_paths = if let Some(path) = &request.calendar_path { |  | ||||||
|         vec![path.clone()] |  | ||||||
|     } else { |  | ||||||
|         client.discover_calendars() |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))? |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     if calendar_paths.is_empty() { |  | ||||||
|         return Err(ApiError::BadRequest("No calendars available for event update".to_string())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Validate that end is after start |  | ||||||
|     if let Some(end) = request.dtend { |  | ||||||
|         if end <= request.dtstart { |  | ||||||
|             return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Determine if this is a series update |  | ||||||
|     let search_uid = request.uid.clone(); |  | ||||||
|     let is_series_update = request.update_action.as_deref() == Some("update_series"); |  | ||||||
|  |  | ||||||
|     // Search for the event by UID across the specified calendars |  | ||||||
|     let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, href) |  | ||||||
|     for calendar_path in &calendar_paths { |  | ||||||
|         // First try exact match |  | ||||||
|         match client.fetch_event_by_uid(calendar_path, &search_uid).await { |  | ||||||
|             Ok(Some(event)) => { |  | ||||||
|                 if let Some(href) = event.href.clone() { |  | ||||||
|                     found_event = Some((event, calendar_path.clone(), href)); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             Ok(None) => { |  | ||||||
|                 // If exact match fails, try to find by base UID pattern for recurring events |  | ||||||
|                 println!("🔍 Exact match failed for '{}', searching by base UID pattern", search_uid); |  | ||||||
|                 match client.fetch_events(calendar_path).await { |  | ||||||
|                     Ok(events) => { |  | ||||||
|                         for event in events { |  | ||||||
|                             if let Some(href) = &event.href { |  | ||||||
|                                 if event.uid.starts_with(&search_uid) && event.uid != search_uid { |  | ||||||
|                                     println!("🎯 Found recurring event by pattern: '{}' matches '{}'", event.uid, search_uid); |  | ||||||
|                                     found_event = Some((event.clone(), calendar_path.clone(), href.clone())); |  | ||||||
|                                     break; |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         if found_event.is_some() { |  | ||||||
|                             break; |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     Err(e) => { |  | ||||||
|                         eprintln!("Error fetching events from {}: {:?}", calendar_path, e); |  | ||||||
|                         continue; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             Err(e) => { |  | ||||||
|                 eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e); |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let (mut event, calendar_path, event_href) = found_event |  | ||||||
|         .ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", search_uid)))?; |  | ||||||
|  |  | ||||||
|     // Convert V2 enums to calendar module enums |  | ||||||
|     let status = match request.status.unwrap_or_default() { |  | ||||||
|         crate::models_v2::EventStatusV2::Tentative => EventStatus::Tentative, |  | ||||||
|         crate::models_v2::EventStatusV2::Cancelled => EventStatus::Cancelled, |  | ||||||
|         crate::models_v2::EventStatusV2::Confirmed => EventStatus::Confirmed, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let class = match request.class.unwrap_or_default() { |  | ||||||
|         crate::models_v2::EventClassV2::Private => EventClass::Private, |  | ||||||
|         crate::models_v2::EventClassV2::Confidential => EventClass::Confidential, |  | ||||||
|         crate::models_v2::EventClassV2::Public => EventClass::Public, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Convert attendees from V2 to simple email list (for now) |  | ||||||
|     let attendees: Vec<String> = request.attendees.into_iter() |  | ||||||
|         .map(|att| att.email) |  | ||||||
|         .collect(); |  | ||||||
|  |  | ||||||
|     // Convert alarms to alarms |  | ||||||
|     let alarms: Vec<crate::calendar::EventReminder> = request.alarms.into_iter() |  | ||||||
|         .map(|alarm| crate::calendar::EventReminder { |  | ||||||
|             minutes_before: -alarm.trigger_minutes, |  | ||||||
|             action: match alarm.action { |  | ||||||
|                 crate::models_v2::AlarmActionV2::Display => crate::calendar::ReminderAction::Display, |  | ||||||
|                 crate::models_v2::AlarmActionV2::Email => crate::calendar::ReminderAction::Email, |  | ||||||
|                 crate::models_v2::AlarmActionV2::Audio => crate::calendar::ReminderAction::Audio, |  | ||||||
|             }, |  | ||||||
|             description: alarm.description, |  | ||||||
|         }) |  | ||||||
|         .collect(); |  | ||||||
|  |  | ||||||
|     // Update the event fields with new data |  | ||||||
|     event.summary = Some(request.summary.clone()); |  | ||||||
|     event.description = request.description; |  | ||||||
|      |  | ||||||
|     // Handle date/time updates based on update type |  | ||||||
|     if is_series_update { |  | ||||||
|         // For series updates, only update the TIME, keep the original DATE |  | ||||||
|         let original_start_date = event.dtstart.date_naive(); |  | ||||||
|         let original_end_date = event.dtend.map(|e| e.date_naive()).unwrap_or(original_start_date); |  | ||||||
|          |  | ||||||
|         let new_start_time = request.dtstart.time(); |  | ||||||
|         let new_end_time = request.dtend.map(|dt| dt.time()).unwrap_or(new_start_time); |  | ||||||
|          |  | ||||||
|         // Combine original date with new time |  | ||||||
|         let updated_start = original_start_date.and_time(new_start_time).and_utc(); |  | ||||||
|         let updated_end = original_end_date.and_time(new_end_time).and_utc(); |  | ||||||
|          |  | ||||||
|         event.dtstart = updated_start; |  | ||||||
|         event.dtend = Some(updated_end); |  | ||||||
|     } else { |  | ||||||
|         // For regular updates, update both date and time |  | ||||||
|         event.dtstart = request.dtstart; |  | ||||||
|         event.dtend = request.dtend; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     event.location = request.location; |  | ||||||
|     event.status = Some(status); |  | ||||||
|     event.class = Some(class); |  | ||||||
|     event.priority = request.priority; |  | ||||||
|     event.organizer = request.organizer.map(|org| CalendarUser { |  | ||||||
|         cal_address: org, |  | ||||||
|         common_name: None, |  | ||||||
|         dir_entry_ref: None, |  | ||||||
|         sent_by: None, |  | ||||||
|         language: None, |  | ||||||
|     }); |  | ||||||
|     event.attendees = attendees.into_iter().map(|email| Attendee { |  | ||||||
|         cal_address: email, |  | ||||||
|         common_name: None, |  | ||||||
|         role: None, |  | ||||||
|         part_stat: None, |  | ||||||
|         rsvp: None, |  | ||||||
|         cu_type: None, |  | ||||||
|         member: Vec::new(), |  | ||||||
|         delegated_to: Vec::new(), |  | ||||||
|         delegated_from: Vec::new(), |  | ||||||
|         sent_by: None, |  | ||||||
|         dir_entry_ref: None, |  | ||||||
|         language: None, |  | ||||||
|     }).collect(); |  | ||||||
|     event.categories = request.categories; |  | ||||||
|     event.last_modified = Some(chrono::Utc::now()); |  | ||||||
|     event.all_day = request.all_day; |  | ||||||
|     event.alarms = alarms.into_iter().map(|alarm| VAlarm { |  | ||||||
|         action: match alarm.action { |  | ||||||
|             crate::calendar::ReminderAction::Display => calendar_models::AlarmAction::Display, |  | ||||||
|             crate::calendar::ReminderAction::Email => calendar_models::AlarmAction::Email, |  | ||||||
|             crate::calendar::ReminderAction::Audio => calendar_models::AlarmAction::Audio, |  | ||||||
|         }, |  | ||||||
|         trigger: calendar_models::AlarmTrigger::Duration(chrono::Duration::minutes(-alarm.minutes_before as i64)), |  | ||||||
|         duration: None, |  | ||||||
|         repeat: None, |  | ||||||
|         description: alarm.description, |  | ||||||
|         summary: None, |  | ||||||
|         attendees: Vec::new(), |  | ||||||
|         attach: Vec::new(), |  | ||||||
|     }).collect(); |  | ||||||
|  |  | ||||||
|     // Handle recurrence rule and UID for series updates |  | ||||||
|     if is_series_update { |  | ||||||
|         // For series updates, preserve existing recurrence rule and convert UID to base UID |  | ||||||
|         let parts: Vec<&str> = request.uid.split('-').collect(); |  | ||||||
|         if parts.len() > 1 { |  | ||||||
|             let last_part = parts[parts.len() - 1]; |  | ||||||
|             if last_part.chars().all(|c| c.is_numeric()) { |  | ||||||
|                 let base_uid = parts[0..parts.len()-1].join("-"); |  | ||||||
|                 event.uid = base_uid; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Handle exception dates |  | ||||||
|         if let Some(exdate) = request.exception_dates { |  | ||||||
|             let mut new_exdate = Vec::new(); |  | ||||||
|             for date in exdate { |  | ||||||
|                 new_exdate.push(date); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // Merge with existing exception dates (avoid duplicates) |  | ||||||
|             for new_date in new_exdate { |  | ||||||
|                 if !event.exdate.contains(&new_date) { |  | ||||||
|                     event.exdate.push(new_date); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             println!("🔄 Updated exception dates: {} total", event.exdate.len()); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Handle UNTIL date modification for "This and Future Events" |  | ||||||
|         if let Some(until_date) = request.until_date { |  | ||||||
|             println!("🔄 Adding UNTIL clause to RRULE: {}", until_date); |  | ||||||
|              |  | ||||||
|             if let Some(ref rrule) = event.rrule { |  | ||||||
|                 // Remove existing UNTIL if present and add new one |  | ||||||
|                 let rrule_without_until = rrule.split(';') |  | ||||||
|                     .filter(|part| !part.starts_with("UNTIL=")) |  | ||||||
|                     .collect::<Vec<&str>>() |  | ||||||
|                     .join(";"); |  | ||||||
|                  |  | ||||||
|                 let until_formatted = until_date.format("%Y%m%dT%H%M%SZ").to_string(); |  | ||||||
|                  |  | ||||||
|                 event.rrule = Some(format!("{};UNTIL={}", rrule_without_until, until_formatted)); |  | ||||||
|                 println!("🔄 Modified RRULE: {}", event.rrule.as_ref().unwrap()); |  | ||||||
|                  |  | ||||||
|                 // Clear exception dates since we're using UNTIL instead |  | ||||||
|                 event.exdate.clear(); |  | ||||||
|                 println!("🔄 Cleared exception dates for UNTIL approach"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         // For regular updates, use the new recurrence rule |  | ||||||
|         event.rrule = request.rrule; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Update the event on the CalDAV server |  | ||||||
|     client.update_event(&calendar_path, &event, &event_href) |  | ||||||
|         .await |  | ||||||
|         .map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?; |  | ||||||
|  |  | ||||||
|     // Fetch the updated event to return its details |  | ||||||
|     let updated_event = fetch_event_by_href(&client, &calendar_path, &event_href) |  | ||||||
|         .await |  | ||||||
|         .map_err(|e| ApiError::Internal(format!("Failed to fetch updated event: {}", e)))?; |  | ||||||
|  |  | ||||||
|     let event_summary = updated_event.map(|e| crate::models_v2::EventSummaryV2 { |  | ||||||
|         uid: e.uid, |  | ||||||
|         summary: e.summary, |  | ||||||
|         dtstart: e.dtstart, |  | ||||||
|         dtend: e.dtend, |  | ||||||
|         location: e.location, |  | ||||||
|         all_day: e.all_day, |  | ||||||
|         href: e.href, |  | ||||||
|         etag: e.etag, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     Ok(Json(UpdateEventResponseV2 { |  | ||||||
|         success: true, |  | ||||||
|         message: "Event updated successfully".to_string(), |  | ||||||
|         event: event_summary, |  | ||||||
|     })) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn update_event( | pub async fn update_event( | ||||||
|     State(state): State<Arc<AppState>>, |     State(state): State<Arc<AppState>>, | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ use std::sync::Arc; | |||||||
|  |  | ||||||
| mod auth; | mod auth; | ||||||
| mod models; | mod models; | ||||||
| mod models_v2; |  | ||||||
| mod handlers; | mod handlers; | ||||||
| mod calendar; | mod calendar; | ||||||
| mod config; | mod config; | ||||||
| @@ -45,10 +44,6 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> { | |||||||
|         .route("/api/calendar/events/create", post(handlers::create_event)) |         .route("/api/calendar/events/create", post(handlers::create_event)) | ||||||
|         .route("/api/calendar/events/update", post(handlers::update_event)) |         .route("/api/calendar/events/update", post(handlers::update_event)) | ||||||
|         .route("/api/calendar/events/delete", post(handlers::delete_event)) |         .route("/api/calendar/events/delete", post(handlers::delete_event)) | ||||||
|         // V2 API routes with better type safety |  | ||||||
|         .route("/api/v2/calendar/events/create", post(handlers::create_event_v2)) |  | ||||||
|         .route("/api/v2/calendar/events/update", post(handlers::update_event_v2)) |  | ||||||
|         .route("/api/v2/calendar/events/delete", post(handlers::delete_event_v2)) |  | ||||||
|         .route("/api/calendar/events/:uid", get(handlers::refresh_event)) |         .route("/api/calendar/events/:uid", get(handlers::refresh_event)) | ||||||
|         .layer( |         .layer( | ||||||
|             CorsLayer::new() |             CorsLayer::new() | ||||||
|   | |||||||
| @@ -123,8 +123,6 @@ pub struct UpdateEventRequest { | |||||||
|     pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence |     pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence | ||||||
|     pub calendar_path: Option<String>, // Optional - search all calendars if not specified |     pub calendar_path: Option<String>, // Optional - search all calendars if not specified | ||||||
|     pub update_action: Option<String>, // "update_series" for recurring events |     pub update_action: Option<String>, // "update_series" for recurring events | ||||||
|     pub occurrence_date: Option<String>, // ISO date string for specific occurrence |  | ||||||
|     pub exception_dates: Option<Vec<String>>, // ISO datetime strings for EXDATE |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|     pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause |     pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,295 +0,0 @@ | |||||||
| // Simplified RFC 5545-based API models |  | ||||||
| // Axum imports removed - not needed for model definitions |  | ||||||
| use chrono::{DateTime, Utc}; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
|  |  | ||||||
| // ==================== CALENDAR REQUESTS ==================== |  | ||||||
|  |  | ||||||
| #[derive(Debug, Deserialize)] |  | ||||||
| pub struct CreateCalendarRequestV2 { |  | ||||||
|     pub name: String, |  | ||||||
|     pub description: Option<String>, |  | ||||||
|     pub color: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Deserialize)] |  | ||||||
| pub struct DeleteCalendarRequestV2 { |  | ||||||
|     pub path: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ==================== EVENT REQUESTS ==================== |  | ||||||
|  |  | ||||||
| // Simplified create event request using proper DateTime instead of string parsing |  | ||||||
| #[derive(Debug, Deserialize)] |  | ||||||
| pub struct CreateEventRequestV2 { |  | ||||||
|     pub summary: String,                   // title -> summary (RFC 5545 term) |  | ||||||
|     pub description: Option<String>,       // Optional in RFC 5545 |  | ||||||
|     pub dtstart: DateTime<Utc>,           // Direct DateTime, no string parsing! |  | ||||||
|     pub dtend: Option<DateTime<Utc>>,     // Optional, alternative to duration |  | ||||||
|     pub location: Option<String>, |  | ||||||
|     pub all_day: bool, |  | ||||||
|      |  | ||||||
|     // Status and classification |  | ||||||
|     pub status: Option<EventStatusV2>,     // Use enum instead of string |  | ||||||
|     pub class: Option<EventClassV2>,       // Use enum instead of string |  | ||||||
|     pub priority: Option<u8>,              // 0-9 priority level |  | ||||||
|      |  | ||||||
|     // People |  | ||||||
|     pub organizer: Option<String>,         // Organizer email |  | ||||||
|     pub attendees: Vec<AttendeeV2>,        // Rich attendee objects |  | ||||||
|      |  | ||||||
|     // Categorization |  | ||||||
|     pub categories: Vec<String>,           // Direct Vec instead of comma-separated |  | ||||||
|      |  | ||||||
|     // Recurrence (simplified for now) |  | ||||||
|     pub rrule: Option<String>,            // Standard RRULE format |  | ||||||
|      |  | ||||||
|     // Reminders (simplified for now)  |  | ||||||
|     pub alarms: Vec<AlarmV2>,             // Structured alarms |  | ||||||
|      |  | ||||||
|     // Calendar context |  | ||||||
|     pub calendar_path: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Deserialize)] |  | ||||||
| pub struct UpdateEventRequestV2 { |  | ||||||
|     pub uid: String,                      // Event UID to identify which event to update |  | ||||||
|     pub summary: String,                   |  | ||||||
|     pub description: Option<String>,       |  | ||||||
|     pub dtstart: DateTime<Utc>,          // Direct DateTime, no string parsing! |  | ||||||
|     pub dtend: Option<DateTime<Utc>>,     |  | ||||||
|     pub location: Option<String>, |  | ||||||
|     pub all_day: bool, |  | ||||||
|      |  | ||||||
|     // Status and classification |  | ||||||
|     pub status: Option<EventStatusV2>,     |  | ||||||
|     pub class: Option<EventClassV2>,       |  | ||||||
|     pub priority: Option<u8>,              |  | ||||||
|      |  | ||||||
|     // People |  | ||||||
|     pub organizer: Option<String>,         |  | ||||||
|     pub attendees: Vec<AttendeeV2>,        |  | ||||||
|      |  | ||||||
|     // Categorization |  | ||||||
|     pub categories: Vec<String>,           |  | ||||||
|      |  | ||||||
|     // Recurrence |  | ||||||
|     pub rrule: Option<String>,            |  | ||||||
|      |  | ||||||
|     // Reminders |  | ||||||
|     pub alarms: Vec<AlarmV2>,             |  | ||||||
|      |  | ||||||
|     // Context |  | ||||||
|     pub calendar_path: Option<String>, |  | ||||||
|     pub update_action: Option<String>,    // "update_series" for recurring events |  | ||||||
|     pub occurrence_date: Option<DateTime<Utc>>, // Specific occurrence |  | ||||||
|     pub exception_dates: Option<Vec<DateTime<Utc>>>, // EXDATE |  | ||||||
|     pub until_date: Option<DateTime<Utc>>, // RRULE UNTIL clause |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Deserialize)] |  | ||||||
| pub struct DeleteEventRequestV2 { |  | ||||||
|     pub calendar_path: String, |  | ||||||
|     pub event_href: String, |  | ||||||
|     pub delete_action: DeleteActionV2,    // Use enum instead of string |  | ||||||
|     pub occurrence_date: Option<DateTime<Utc>>, // Direct DateTime |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ==================== SUPPORTING TYPES ==================== |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub enum EventStatusV2 { |  | ||||||
|     Tentative, |  | ||||||
|     Confirmed, |  | ||||||
|     Cancelled, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Default for EventStatusV2 { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         EventStatusV2::Confirmed |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub enum EventClassV2 { |  | ||||||
|     Public, |  | ||||||
|     Private, |  | ||||||
|     Confidential, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Default for EventClassV2 { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         EventClassV2::Public |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub enum DeleteActionV2 { |  | ||||||
|     DeleteThis,      // "delete_this" |  | ||||||
|     DeleteFollowing, // "delete_following"  |  | ||||||
|     DeleteSeries,    // "delete_series" |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub struct AttendeeV2 { |  | ||||||
|     pub email: String,                    // Calendar address |  | ||||||
|     pub name: Option<String>,             // Common name (CN parameter) |  | ||||||
|     pub role: Option<AttendeeRoleV2>,     // Role (ROLE parameter) |  | ||||||
|     pub status: Option<ParticipationStatusV2>, // Participation status (PARTSTAT parameter) |  | ||||||
|     pub rsvp: Option<bool>,               // RSVP expectation (RSVP parameter) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub enum AttendeeRoleV2 { |  | ||||||
|     Chair, |  | ||||||
|     Required,         // REQ-PARTICIPANT |  | ||||||
|     Optional,         // OPT-PARTICIPANT   |  | ||||||
|     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,            // Action (AUDIO, DISPLAY, EMAIL) |  | ||||||
|     pub trigger_minutes: i32,             // Minutes before event (negative = before) |  | ||||||
|     pub description: Option<String>,      // Description for display/email |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub enum AlarmActionV2 { |  | ||||||
|     Audio, |  | ||||||
|     Display, |  | ||||||
|     Email, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ==================== RESPONSES ==================== |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] |  | ||||||
| pub struct CreateEventResponseV2 { |  | ||||||
|     pub success: bool, |  | ||||||
|     pub message: String, |  | ||||||
|     pub event: Option<EventSummaryV2>,    // Return created event summary |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] |  | ||||||
| pub struct UpdateEventResponseV2 { |  | ||||||
|     pub success: bool, |  | ||||||
|     pub message: String, |  | ||||||
|     pub event: Option<EventSummaryV2>,    // Return updated event summary |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize)] |  | ||||||
| pub struct DeleteEventResponseV2 { |  | ||||||
|     pub success: bool, |  | ||||||
|     pub message: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |  | ||||||
| pub struct EventSummaryV2 { |  | ||||||
|     pub uid: String, |  | ||||||
|     pub summary: Option<String>, |  | ||||||
|     pub dtstart: DateTime<Utc>, |  | ||||||
|     pub dtend: Option<DateTime<Utc>>, |  | ||||||
|     pub location: Option<String>, |  | ||||||
|     pub all_day: bool, |  | ||||||
|     pub href: Option<String>, |  | ||||||
|     pub etag: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ==================== CONVERSION HELPERS ==================== |  | ||||||
|  |  | ||||||
| // Convert from old request format to new for backward compatibility |  | ||||||
| impl From<crate::models::CreateEventRequest> for CreateEventRequestV2 { |  | ||||||
|     fn from(old: crate::models::CreateEventRequest) -> Self { |  | ||||||
|         use chrono::{NaiveDate, NaiveTime, TimeZone, Utc}; |  | ||||||
|          |  | ||||||
|         // Parse the old string-based date/time format |  | ||||||
|         let start_date = NaiveDate::parse_from_str(&old.start_date, "%Y-%m-%d") |  | ||||||
|             .unwrap_or_else(|_| chrono::Utc::now().date_naive()); |  | ||||||
|         let start_time = NaiveTime::parse_from_str(&old.start_time, "%H:%M") |  | ||||||
|             .unwrap_or_else(|_| NaiveTime::from_hms_opt(0, 0, 0).unwrap()); |  | ||||||
|         let dtstart = Utc.from_utc_datetime(&start_date.and_time(start_time)); |  | ||||||
|          |  | ||||||
|         let end_date = NaiveDate::parse_from_str(&old.end_date, "%Y-%m-%d") |  | ||||||
|             .unwrap_or_else(|_| chrono::Utc::now().date_naive()); |  | ||||||
|         let end_time = NaiveTime::parse_from_str(&old.end_time, "%H:%M") |  | ||||||
|             .unwrap_or_else(|_| NaiveTime::from_hms_opt(1, 0, 0).unwrap()); |  | ||||||
|         let dtend = Some(Utc.from_utc_datetime(&end_date.and_time(end_time))); |  | ||||||
|          |  | ||||||
|         // Parse comma-separated categories |  | ||||||
|         let categories: Vec<String> = if old.categories.trim().is_empty() { |  | ||||||
|             Vec::new() |  | ||||||
|         } else { |  | ||||||
|             old.categories.split(',').map(|s| s.trim().to_string()).collect() |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         // Parse comma-separated attendees |  | ||||||
|         let attendees: Vec<AttendeeV2> = if old.attendees.trim().is_empty() { |  | ||||||
|             Vec::new() |  | ||||||
|         } else { |  | ||||||
|             old.attendees.split(',').map(|email| AttendeeV2 { |  | ||||||
|                 email: email.trim().to_string(), |  | ||||||
|                 name: None, |  | ||||||
|                 role: Some(AttendeeRoleV2::Required), |  | ||||||
|                 status: Some(ParticipationStatusV2::NeedsAction), |  | ||||||
|                 rsvp: Some(true), |  | ||||||
|             }).collect() |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         // Convert status string to enum |  | ||||||
|         let status = match old.status.as_str() { |  | ||||||
|             "tentative" => Some(EventStatusV2::Tentative), |  | ||||||
|             "confirmed" => Some(EventStatusV2::Confirmed), |  | ||||||
|             "cancelled" => Some(EventStatusV2::Cancelled), |  | ||||||
|             _ => Some(EventStatusV2::Confirmed), |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         // Convert class string to enum |  | ||||||
|         let class = match old.class.as_str() { |  | ||||||
|             "public" => Some(EventClassV2::Public), |  | ||||||
|             "private" => Some(EventClassV2::Private), |  | ||||||
|             "confidential" => Some(EventClassV2::Confidential), |  | ||||||
|             _ => Some(EventClassV2::Public), |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         // Create basic alarm if reminder specified |  | ||||||
|         let alarms = if old.reminder == "none" { |  | ||||||
|             Vec::new() |  | ||||||
|         } else { |  | ||||||
|             // Default to 15 minutes before for now |  | ||||||
|             vec![AlarmV2 { |  | ||||||
|                 action: AlarmActionV2::Display, |  | ||||||
|                 trigger_minutes: 15, |  | ||||||
|                 description: Some("Event reminder".to_string()), |  | ||||||
|             }] |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         Self { |  | ||||||
|             summary: old.title, |  | ||||||
|             description: if old.description.trim().is_empty() { None } else { Some(old.description) }, |  | ||||||
|             dtstart, |  | ||||||
|             dtend, |  | ||||||
|             location: if old.location.trim().is_empty() { None } else { Some(old.location) }, |  | ||||||
|             all_day: old.all_day, |  | ||||||
|             status, |  | ||||||
|             class, |  | ||||||
|             priority: old.priority, |  | ||||||
|             organizer: if old.organizer.trim().is_empty() { None } else { Some(old.organizer) }, |  | ||||||
|             attendees, |  | ||||||
|             categories, |  | ||||||
|             rrule: None, // TODO: Convert recurrence string to RRULE |  | ||||||
|             alarms, |  | ||||||
|             calendar_path: old.calendar_path, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Error handling - ApiError is available through crate::models::ApiError in handlers |  | ||||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone