Implement complete event creation functionality with CalDAV backend

- Add CalDAV create_event method with proper iCalendar generation
- Add comprehensive backend API for event creation with validation
- Add event creation models and handlers with date/time parsing
- Add frontend service method for creating events via API
- Update frontend to call backend API instead of placeholder
- Fix CalDAV URL construction to avoid duplicate /dav.php paths
- Support all event fields: title, description, dates, location, all-day

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-28 22:29:06 -04:00
parent 5c966b2571
commit 3440403bed
6 changed files with 452 additions and 5 deletions

View File

@@ -7,7 +7,7 @@ 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}};
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse}};
use crate::calendar::{CalDAVClient, CalendarEvent};
#[derive(Deserialize)]
@@ -357,4 +357,130 @@ pub async fn delete_event(
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());
// 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: crate::calendar::EventStatus::Confirmed,
class: crate::calendar::EventClass::Public,
priority: None,
organizer: None,
attendees: Vec::new(),
categories: Vec::new(),
created: Some(chrono::Utc::now()),
last_modified: Some(chrono::Utc::now()),
recurrence_rule: None,
all_day: request.all_day,
reminders: Vec::new(),
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))
}
}