- Add calendar selection dropdown to event creation modal - Update EventCreationData to include selected_calendar field - Pass available calendars from user info to modal component - Initialize dropdown with first available calendar as default - Fix backend to fetch events from ALL calendars, not just the first - Update refresh_event to search across all calendars - Events created in any calendar now properly display in UI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
633 lines
22 KiB
Rust
633 lines
22 KiB
Rust
use axum::{
|
|
extract::{State, Query, Path},
|
|
http::HeaderMap,
|
|
response::Json,
|
|
};
|
|
use serde::Deserialize;
|
|
use std::sync::Arc;
|
|
use chrono::Datelike;
|
|
|
|
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse}};
|
|
use crate::calendar::{CalDAVClient, CalendarEvent};
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CalendarQuery {
|
|
pub year: Option<i32>,
|
|
pub month: Option<u32>,
|
|
}
|
|
|
|
pub async fn get_calendar_events(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(params): Query<CalendarQuery>,
|
|
headers: HeaderMap,
|
|
) -> Result<Json<Vec<CalendarEvent>>, 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<Arc<AppState>>,
|
|
Path(uid): Path<String>,
|
|
headers: HeaderMap,
|
|
) -> Result<Json<Option<CalendarEvent>>, 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<Arc<AppState>>,
|
|
Json(request): Json<CalDAVLoginRequest>,
|
|
) -> Result<Json<AuthResponse>, 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<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
) -> Result<Json<serde_json::Value>, 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<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
) -> Result<Json<UserInfo>, 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<CalendarInfo> = 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::<String>() + chars.as_str(),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.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<String, ApiError> {
|
|
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<String, ApiError> {
|
|
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<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
Json(request): Json<CreateCalendarRequest>,
|
|
) -> Result<Json<CreateCalendarResponse>, 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<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
Json(request): Json<DeleteCalendarRequest>,
|
|
) -> Result<Json<DeleteCalendarResponse>, 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(),
|
|
}))
|
|
}
|
|
|
|
pub async fn delete_event(
|
|
State(state): State<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
Json(request): Json<DeleteEventRequest>,
|
|
) -> Result<Json<DeleteEventResponse>, ApiError> {
|
|
println!("🗑️ Delete event request received: calendar_path='{}', event_href='{}'", request.calendar_path, request.event_href);
|
|
|
|
// 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);
|
|
|
|
// Delete the event
|
|
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(),
|
|
}))
|
|
}
|
|
|
|
pub async fn create_event(
|
|
State(state): State<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
Json(request): Json<CreateEventRequest>,
|
|
) -> Result<Json<CreateEventResponse>, 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<String> = 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<String> = 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<crate::calendar::EventReminder> = 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,
|
|
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),
|
|
}))
|
|
}
|
|
|
|
/// Parse date and time strings into a UTC DateTime
|
|
fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result<chrono::DateTime<chrono::Utc>, 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))
|
|
}
|
|
} |