use axum::{ extract::{State, Query, Path}, http::HeaderMap, response::Json, }; use serde::Deserialize; use std::sync::Arc; use chrono::{Datelike, TimeZone}; 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::calendar::{CalDAVClient, CalendarEvent}; #[derive(Deserialize)] pub struct CalendarQuery { pub year: Option, pub month: Option, } pub async fn get_calendar_events( State(state): State>, Query(params): Query, headers: HeaderMap, ) -> Result>, ApiError> { // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; println!("🔑 API call with password length: {}", password.len()); // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Discover calendars if needed let calendar_paths = client.discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; if calendar_paths.is_empty() { return Ok(Json(vec![])); // No calendars found } // Fetch events from all calendars let mut all_events = Vec::new(); for calendar_path in &calendar_paths { match client.fetch_events(calendar_path).await { Ok(mut events) => { // Set calendar_path for each event to identify which calendar it belongs to for event in &mut events { event.calendar_path = Some(calendar_path.clone()); } all_events.extend(events); }, Err(e) => { // Log the error but continue with other calendars eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e); } } } let events = all_events; // Filter events by month if specified let filtered_events = if let (Some(year), Some(month)) = (params.year, params.month) { events.into_iter().filter(|event| { let event_date = event.start.date_naive(); event_date.year() == year && event_date.month() == month }).collect() } else { events }; Ok(Json(filtered_events)) } pub async fn refresh_event( State(state): State>, Path(uid): Path, headers: HeaderMap, ) -> Result>, ApiError> { // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Discover calendars if needed let calendar_paths = client.discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; if calendar_paths.is_empty() { return Ok(Json(None)); // No calendars found } // Search for the specific event by UID across all calendars let mut found_event = None; for calendar_path in &calendar_paths { match client.fetch_event_by_uid(calendar_path, &uid).await { Ok(Some(mut event)) => { event.calendar_path = Some(calendar_path.clone()); found_event = Some(event); break; }, Ok(None) => continue, // Event not found in this calendar Err(e) => { eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e); continue; } } } let event = found_event; Ok(Json(event)) } pub async fn login( State(state): State>, Json(request): Json, ) -> Result, ApiError> { println!("🔐 Login attempt:"); println!(" Server URL: {}", request.server_url); println!(" Username: {}", request.username); println!(" Password length: {}", request.password.len()); let response = state.auth_service.login(request).await?; Ok(Json(response)) } pub async fn verify_token( State(state): State>, headers: HeaderMap, ) -> Result, ApiError> { let token = extract_bearer_token(&headers)?; let claims = state.auth_service.verify_token(&token)?; Ok(Json(serde_json::json!({ "valid": true, "username": claims.username, "server_url": claims.server_url }))) } pub async fn get_user_info( State(state): State>, headers: HeaderMap, ) -> Result, ApiError> { // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; let claims = state.auth_service.verify_token(&token)?; // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Discover calendars let calendar_paths = client.discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; // Convert paths to CalendarInfo structs with display names, filtering out generic collections let calendars: Vec = calendar_paths.into_iter() .filter_map(|path| { let display_name = extract_calendar_name(&path); // Skip generic collection names if display_name.eq_ignore_ascii_case("calendar") || display_name.eq_ignore_ascii_case("calendars") || display_name.eq_ignore_ascii_case("collection") { None } else { Some(CalendarInfo { path: path.clone(), display_name, color: generate_calendar_color(&path), }) } }).collect(); Ok(Json(UserInfo { username: claims.username, server_url: claims.server_url, calendars, })) } // Helper function to generate a consistent color for a calendar based on its path fn generate_calendar_color(path: &str) -> String { // Predefined set of attractive, accessible colors for calendars let colors = [ "#3B82F6", // Blue "#10B981", // Emerald "#F59E0B", // Amber "#EF4444", // Red "#8B5CF6", // Violet "#06B6D4", // Cyan "#84CC16", // Lime "#F97316", // Orange "#EC4899", // Pink "#6366F1", // Indigo "#14B8A6", // Teal "#F3B806", // Yellow "#8B5A2B", // Brown "#6B7280", // Gray "#DC2626", // Red-600 "#7C3AED", // Violet-600 ]; // Create a simple hash from the path to ensure consistent color assignment let mut hash: u32 = 0; for byte in path.bytes() { hash = hash.wrapping_mul(31).wrapping_add(byte as u32); } // Use the hash to select a color from our palette let color_index = (hash as usize) % colors.len(); colors[color_index].to_string() } // Helper function to extract a readable calendar name from path fn extract_calendar_name(path: &str) -> String { // Extract the last meaningful part of the path // e.g., "/calendars/user/personal/" -> "personal" // or "/calendars/user/work-calendar/" -> "work-calendar" let parts: Vec<&str> = path.trim_end_matches('/').split('/').collect(); if let Some(last_part) = parts.last() { if !last_part.is_empty() && *last_part != "calendars" { // Convert kebab-case or snake_case to title case last_part .replace('-', " ") .replace('_', " ") .split_whitespace() .map(|word| { let mut chars = word.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::() + chars.as_str(), } }) .collect::>() .join(" ") } else if parts.len() > 1 { // If the last part is empty or "calendars", try the second to last extract_calendar_name(&parts[..parts.len()-1].join("/")) } else { "Calendar".to_string() } } else { "Calendar".to_string() } } // Helper functions fn extract_bearer_token(headers: &HeaderMap) -> Result { if let Some(auth_header) = headers.get("authorization") { let auth_str = auth_header .to_str() .map_err(|_| ApiError::Unauthorized("Invalid authorization header".to_string()))?; if let Some(token) = auth_str.strip_prefix("Bearer ") { Ok(token.to_string()) } else { Err(ApiError::Unauthorized("Authorization header must start with 'Bearer '".to_string())) } } else { Err(ApiError::Unauthorized("Authorization header required".to_string())) } } fn extract_password_header(headers: &HeaderMap) -> Result { if let Some(password_header) = headers.get("x-caldav-password") { let password = password_header .to_str() .map_err(|_| ApiError::BadRequest("Invalid password header".to_string()))?; Ok(password.to_string()) } else { Err(ApiError::BadRequest("X-CalDAV-Password header required".to_string())) } } pub async fn create_calendar( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("📝 Create calendar request received: name='{}', description={:?}, color={:?}", request.name, request.description, request.color); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.name.trim().is_empty() { return Err(ApiError::BadRequest("Calendar name is required".to_string())); } if request.name.len() > 100 { return Err(ApiError::BadRequest("Calendar name too long (max 100 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); // Create the calendar client.create_calendar( &request.name, request.description.as_deref(), request.color.as_deref() ) .await .map_err(|e| ApiError::Internal(format!("Failed to create calendar: {}", e)))?; Ok(Json(CreateCalendarResponse { success: true, message: "Calendar created successfully".to_string(), })) } pub async fn delete_calendar( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("🗑️ Delete calendar request received: path='{}'", request.path); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.path.trim().is_empty() { return Err(ApiError::BadRequest("Calendar path 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); // Delete the calendar client.delete_calendar(&request.path) .await .map_err(|e| ApiError::Internal(format!("Failed to delete calendar: {}", e)))?; Ok(Json(DeleteCalendarResponse { success: true, message: "Calendar deleted successfully".to_string(), })) } /// 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) } /// Delete event using v2 API with enum-based delete actions pub async fn delete_event_v2( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, 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.recurrence_rule.is_some() { // 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.date_naive().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(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.recurrence_rule { // Calculate the datetime for the occurrence we want to stop before let original_time = event.start.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.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(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( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { 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)?; 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.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 series deleted successfully".to_string(), })) } } } /// Create event using v2 API with direct DateTime support (no string parsing) pub async fn create_event_v2( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, 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 => crate::calendar::EventStatus::Tentative, crate::models_v2::EventStatusV2::Cancelled => crate::calendar::EventStatus::Cancelled, crate::models_v2::EventStatusV2::Confirmed => crate::calendar::EventStatus::Confirmed, }; let class = match request.class.unwrap_or_default() { crate::models_v2::EventClassV2::Private => crate::calendar::EventClass::Private, crate::models_v2::EventClassV2::Confidential => crate::calendar::EventClass::Confidential, crate::models_v2::EventClassV2::Public => crate::calendar::EventClass::Public, }; // Convert attendees from V2 to simple email list (for now) let attendees: Vec = request.attendees.into_iter() .map(|att| att.email) .collect(); // Convert alarms to reminders let reminders: Vec = 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! let event = crate::calendar::CalendarEvent { uid, summary: Some(request.summary.clone()), description: request.description, start: request.dtstart, end: request.dtend, location: request.location, status, class, priority: request.priority, organizer: request.organizer, attendees, categories: request.categories, created: Some(chrono::Utc::now()), last_modified: Some(chrono::Utc::now()), recurrence_rule: request.rrule, exception_dates: Vec::new(), // No exception dates for new events all_day: request.all_day, reminders, etag: None, href: None, 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.start, dtend: e.end, 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( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("📝 Create event request received: title='{}', all_day={}, calendar_path={:?}", request.title, 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.title.trim().is_empty() { return Err(ApiError::BadRequest("Event title is required".to_string())); } if request.title.len() > 200 { return Err(ApiError::BadRequest("Event title 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() }; // Parse dates and times let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; // Validate that end is after start if end_datetime <= start_datetime { 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()); // Parse status let status = match request.status.to_lowercase().as_str() { "tentative" => crate::calendar::EventStatus::Tentative, "cancelled" => crate::calendar::EventStatus::Cancelled, _ => crate::calendar::EventStatus::Confirmed, }; // Parse class let class = match request.class.to_lowercase().as_str() { "private" => crate::calendar::EventClass::Private, "confidential" => crate::calendar::EventClass::Confidential, _ => crate::calendar::EventClass::Public, }; // Parse attendees (comma-separated email list) let attendees: Vec = if request.attendees.trim().is_empty() { Vec::new() } else { request.attendees .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse categories (comma-separated list) let categories: Vec = if request.categories.trim().is_empty() { Vec::new() } else { request.categories .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse reminders and convert to EventReminder structs let reminders: Vec = match request.reminder.to_lowercase().as_str() { "15min" => vec![crate::calendar::EventReminder { minutes_before: 15, action: crate::calendar::ReminderAction::Display, description: None, }], "30min" => vec![crate::calendar::EventReminder { minutes_before: 30, action: crate::calendar::ReminderAction::Display, description: None, }], "1hour" => vec![crate::calendar::EventReminder { minutes_before: 60, action: crate::calendar::ReminderAction::Display, description: None, }], "2hours" => vec![crate::calendar::EventReminder { minutes_before: 120, action: crate::calendar::ReminderAction::Display, description: None, }], "1day" => vec![crate::calendar::EventReminder { minutes_before: 1440, // 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "2days" => vec![crate::calendar::EventReminder { minutes_before: 2880, // 48 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "1week" => vec![crate::calendar::EventReminder { minutes_before: 10080, // 7 * 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], _ => Vec::new(), }; // Parse recurrence with BYDAY support for weekly recurrence let recurrence_rule = match request.recurrence.to_lowercase().as_str() { "daily" => Some("FREQ=DAILY".to_string()), "weekly" => { // Handle weekly recurrence with optional BYDAY parameter let mut rrule = "FREQ=WEEKLY".to_string(); // Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]) if request.recurrence_days.len() == 7 { let selected_days: Vec<&str> = request.recurrence_days .iter() .enumerate() .filter_map(|(i, &selected)| { if selected { Some(match i { 0 => "SU", // Sunday 1 => "MO", // Monday 2 => "TU", // Tuesday 3 => "WE", // Wednesday 4 => "TH", // Thursday 5 => "FR", // Friday 6 => "SA", // Saturday _ => return None, }) } else { None } }) .collect(); if !selected_days.is_empty() { rrule.push_str(&format!(";BYDAY={}", selected_days.join(","))); } } Some(rrule) }, "monthly" => Some("FREQ=MONTHLY".to_string()), "yearly" => Some("FREQ=YEARLY".to_string()), _ => None, }; // Create the CalendarEvent struct let event = crate::calendar::CalendarEvent { uid, summary: Some(request.title.clone()), description: if request.description.trim().is_empty() { None } else { Some(request.description.clone()) }, start: start_datetime, end: Some(end_datetime), location: if request.location.trim().is_empty() { None } else { Some(request.location.clone()) }, status, class, priority: request.priority, organizer: if request.organizer.trim().is_empty() { None } else { Some(request.organizer.clone()) }, attendees, categories, 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, href: None, 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)))?; Ok(Json(CreateEventResponse { success: true, message: "Event created successfully".to_string(), event_href: Some(event_href), })) } /// Update event using v2 API with direct DateTime support (no string parsing) pub async fn update_event_v2( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, 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 => crate::calendar::EventStatus::Tentative, crate::models_v2::EventStatusV2::Cancelled => crate::calendar::EventStatus::Cancelled, crate::models_v2::EventStatusV2::Confirmed => crate::calendar::EventStatus::Confirmed, }; let class = match request.class.unwrap_or_default() { crate::models_v2::EventClassV2::Private => crate::calendar::EventClass::Private, crate::models_v2::EventClassV2::Confidential => crate::calendar::EventClass::Confidential, crate::models_v2::EventClassV2::Public => crate::calendar::EventClass::Public, }; // Convert attendees from V2 to simple email list (for now) let attendees: Vec = request.attendees.into_iter() .map(|att| att.email) .collect(); // Convert alarms to reminders let reminders: Vec = 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.start.date_naive(); let original_end_date = event.end.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.start = updated_start; event.end = Some(updated_end); } else { // For regular updates, update both date and time event.start = request.dtstart; event.end = request.dtend; } event.location = request.location; event.status = status; event.class = class; event.priority = request.priority; event.organizer = request.organizer; event.attendees = attendees; event.categories = request.categories; event.last_modified = Some(chrono::Utc::now()); event.all_day = request.all_day; event.reminders = reminders; // 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(exception_dates) = request.exception_dates { let mut new_exception_dates = Vec::new(); for date in exception_dates { new_exception_dates.push(date); } // Merge with existing exception dates (avoid duplicates) for new_date in new_exception_dates { if !event.exception_dates.contains(&new_date) { event.exception_dates.push(new_date); } } println!("🔄 Updated exception dates: {} total", event.exception_dates.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.recurrence_rule { // Remove existing UNTIL if present and add new one let rrule_without_until = rrule.split(';') .filter(|part| !part.starts_with("UNTIL=")) .collect::>() .join(";"); let until_formatted = until_date.format("%Y%m%dT%H%M%SZ").to_string(); event.recurrence_rule = Some(format!("{};UNTIL={}", rrule_without_until, until_formatted)); println!("🔄 Modified RRULE: {}", event.recurrence_rule.as_ref().unwrap()); // Clear exception dates since we're using UNTIL instead event.exception_dates.clear(); println!("🔄 Cleared exception dates for UNTIL approach"); } } } else { // For regular updates, use the new recurrence rule event.recurrence_rule = 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.start, dtend: e.end, 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( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { // Handle update request // 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.title.trim().is_empty() { return Err(ApiError::BadRequest("Event title is required".to_string())); } if request.title.len() > 200 { return Err(ApiError::BadRequest("Event title 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())); } // 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 // For recurring events, we might need to find by base UID pattern if exact match fails 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) => { // Look for any event whose UID starts with the search_uid for event in events { if let Some(href) = &event.href { // Check if this event's UID starts with our search UID (base pattern) 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)))?; // Parse dates and times for the updated event let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; // Validate that end is after start if end_datetime <= start_datetime { return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); } // Parse status let status = match request.status.to_lowercase().as_str() { "tentative" => crate::calendar::EventStatus::Tentative, "cancelled" => crate::calendar::EventStatus::Cancelled, _ => crate::calendar::EventStatus::Confirmed, }; // Parse class let class = match request.class.to_lowercase().as_str() { "private" => crate::calendar::EventClass::Private, "confidential" => crate::calendar::EventClass::Confidential, _ => crate::calendar::EventClass::Public, }; // Parse attendees (comma-separated email list) let attendees: Vec = if request.attendees.trim().is_empty() { Vec::new() } else { request.attendees .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse categories (comma-separated list) let categories: Vec = if request.categories.trim().is_empty() { Vec::new() } else { request.categories .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse reminders and convert to EventReminder structs let reminders: Vec = match request.reminder.to_lowercase().as_str() { "15min" => vec![crate::calendar::EventReminder { minutes_before: 15, action: crate::calendar::ReminderAction::Display, description: None, }], "30min" => vec![crate::calendar::EventReminder { minutes_before: 30, action: crate::calendar::ReminderAction::Display, description: None, }], "1hour" => vec![crate::calendar::EventReminder { minutes_before: 60, action: crate::calendar::ReminderAction::Display, description: None, }], "2hours" => vec![crate::calendar::EventReminder { minutes_before: 120, action: crate::calendar::ReminderAction::Display, description: None, }], "1day" => vec![crate::calendar::EventReminder { minutes_before: 1440, // 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "2days" => vec![crate::calendar::EventReminder { minutes_before: 2880, // 48 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "1week" => vec![crate::calendar::EventReminder { minutes_before: 10080, // 7 * 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], _ => Vec::new(), }; // Parse recurrence with BYDAY support for weekly recurrence let recurrence_rule = match request.recurrence.to_lowercase().as_str() { "daily" => Some("FREQ=DAILY".to_string()), "weekly" => { // Handle weekly recurrence with optional BYDAY parameter let mut rrule = "FREQ=WEEKLY".to_string(); // Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]) if request.recurrence_days.len() == 7 { let selected_days: Vec<&str> = request.recurrence_days .iter() .enumerate() .filter_map(|(i, &selected)| { if selected { Some(match i { 0 => "SU", // Sunday 1 => "MO", // Monday 2 => "TU", // Tuesday 3 => "WE", // Wednesday 4 => "TH", // Thursday 5 => "FR", // Friday 6 => "SA", // Saturday _ => return None, }) } else { None } }) .collect(); if !selected_days.is_empty() { rrule.push_str(&format!(";BYDAY={}", selected_days.join(","))); } } Some(rrule) }, "monthly" => Some("FREQ=MONTHLY".to_string()), "yearly" => Some("FREQ=YEARLY".to_string()), _ => None, }; // Update the event fields with new data event.summary = Some(request.title.clone()); event.description = if request.description.trim().is_empty() { None } else { Some(request.description.clone()) }; // 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.start.date_naive(); let original_end_date = event.end.map(|e| e.date_naive()).unwrap_or(original_start_date); let new_start_time = start_datetime.time(); let new_end_time = end_datetime.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(); // Preserve original date with new time event.start = updated_start; event.end = Some(updated_end); } else { // For regular updates, update both date and time event.start = start_datetime; event.end = Some(end_datetime); } event.location = if request.location.trim().is_empty() { None } else { Some(request.location.clone()) }; event.status = status; event.class = class; event.priority = request.priority; event.organizer = if request.organizer.trim().is_empty() { None } else { Some(request.organizer.clone()) }; event.attendees = attendees; event.categories = categories; event.last_modified = Some(chrono::Utc::now()); event.all_day = request.all_day; event.reminders = reminders; // 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; } } // Keep existing recurrence rule (don't overwrite with recurrence_rule variable) // event.recurrence_rule stays as-is from the original event // However, allow exception_dates to be updated - this is needed for "This and Future" events if let Some(exception_dates_str) = &request.exception_dates { // Parse the ISO datetime strings into DateTime let mut new_exception_dates = Vec::new(); for date_str in exception_dates_str { if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(date_str) { new_exception_dates.push(parsed_date.with_timezone(&chrono::Utc)); } else if let Ok(parsed_date) = chrono::DateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S UTC") { new_exception_dates.push(parsed_date.with_timezone(&chrono::Utc)); } else { eprintln!("Failed to parse exception date: {}", date_str); } } // Merge with existing exception dates (avoid duplicates) for new_date in new_exception_dates { if !event.exception_dates.contains(&new_date) { event.exception_dates.push(new_date); } } println!("🔄 Updated exception dates: {} total", event.exception_dates.len()); } // Handle UNTIL date modification for "This and Future Events" if let Some(until_date_str) = &request.until_date { println!("🔄 Adding UNTIL clause to RRULE: {}", until_date_str); if let Some(ref rrule) = event.recurrence_rule { // Remove existing UNTIL if present and add new one let rrule_without_until = rrule.split(';') .filter(|part| !part.starts_with("UNTIL=")) .collect::>() .join(";"); // Parse the until_date and format for RRULE if let Ok(until_datetime) = chrono::DateTime::parse_from_rfc3339(until_date_str) { let until_utc = until_datetime.with_timezone(&chrono::Utc); let until_formatted = until_utc.format("%Y%m%dT%H%M%SZ").to_string(); event.recurrence_rule = Some(format!("{};UNTIL={}", rrule_without_until, until_formatted)); println!("🔄 Modified RRULE: {}", event.recurrence_rule.as_ref().unwrap()); // Clear exception dates since we're using UNTIL instead event.exception_dates.clear(); println!("🔄 Cleared exception dates for UNTIL approach"); } } } } else { // For regular updates, use the new recurrence rule event.recurrence_rule = recurrence_rule; } // 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)))?; Ok(Json(UpdateEventResponse { success: true, message: "Event updated successfully".to_string(), })) } /// Parse date and time strings into a UTC DateTime fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result, String> { use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone}; // Parse the date let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") .map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?; if all_day { // For all-day events, use midnight UTC let datetime = date.and_hms_opt(0, 0, 0) .ok_or_else(|| "Failed to create midnight datetime".to_string())?; Ok(Utc.from_utc_datetime(&datetime)) } else { // Parse the time let time = NaiveTime::parse_from_str(time_str, "%H:%M") .map_err(|_| format!("Invalid time format: {}. Expected HH:MM", time_str))?; // Combine date and time let datetime = NaiveDateTime::new(date, time); // Assume local time and convert to UTC (in a real app, you'd want timezone support) Ok(Utc.from_utc_datetime(&datetime)) } }