Compare commits
3 Commits
890940fe31
...
ac1164fd81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac1164fd81 | ||
|
|
a6092d13ce | ||
|
|
acc5ced551 |
@@ -359,19 +359,20 @@ impl CalDAVClient {
|
||||
.clone();
|
||||
|
||||
// Parse start time (required)
|
||||
let start = properties
|
||||
let start_prop = properties
|
||||
.get("DTSTART")
|
||||
.ok_or_else(|| CalDAVError::ParseError("Missing DTSTART field".to_string()))?;
|
||||
let start = self.parse_datetime(start, full_properties.get("DTSTART"))?;
|
||||
let (start_naive, start_tzid) = self.parse_datetime_with_tz(start_prop, full_properties.get("DTSTART"))?;
|
||||
|
||||
// Parse end time (optional - use start time if not present)
|
||||
let end = if let Some(dtend) = properties.get("DTEND") {
|
||||
Some(self.parse_datetime(dtend, full_properties.get("DTEND"))?)
|
||||
let (end_naive, end_tzid) = if let Some(dtend) = properties.get("DTEND") {
|
||||
let (end_dt, end_tz) = self.parse_datetime_with_tz(dtend, full_properties.get("DTEND"))?;
|
||||
(Some(end_dt), end_tz)
|
||||
} else if let Some(_duration) = properties.get("DURATION") {
|
||||
// TODO: Parse duration and add to start time
|
||||
Some(start)
|
||||
(Some(start_naive), start_tzid.clone())
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Determine if it's an all-day event by checking for VALUE=DATE parameter
|
||||
@@ -411,23 +412,35 @@ impl CalDAVClient {
|
||||
.map(|s| s.split(',').map(|c| c.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse dates
|
||||
let created = properties
|
||||
.get("CREATED")
|
||||
.and_then(|s| self.parse_datetime(s, None).ok());
|
||||
// Parse dates with timezone information
|
||||
let (created_naive, created_tzid) = if let Some(created_str) = properties.get("CREATED") {
|
||||
match self.parse_datetime_with_tz(created_str, None) {
|
||||
Ok((dt, tz)) => (Some(dt), tz),
|
||||
Err(_) => (None, None)
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let last_modified = properties
|
||||
.get("LAST-MODIFIED")
|
||||
.and_then(|s| self.parse_datetime(s, None).ok());
|
||||
let (last_modified_naive, last_modified_tzid) = if let Some(modified_str) = properties.get("LAST-MODIFIED") {
|
||||
match self.parse_datetime_with_tz(modified_str, None) {
|
||||
Ok((dt, tz)) => (Some(dt), tz),
|
||||
Err(_) => (None, None)
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Parse exception dates (EXDATE)
|
||||
let exdate = self.parse_exdate(&event);
|
||||
|
||||
// Create VEvent with required fields
|
||||
let mut vevent = VEvent::new(uid, start);
|
||||
// Create VEvent with parsed naive datetime and timezone info
|
||||
let mut vevent = VEvent::new(uid, start_naive);
|
||||
|
||||
// Set optional fields
|
||||
vevent.dtend = end;
|
||||
// Set optional fields with timezone information
|
||||
vevent.dtend = end_naive;
|
||||
vevent.dtstart_tzid = start_tzid;
|
||||
vevent.dtend_tzid = end_tzid;
|
||||
vevent.summary = properties.get("SUMMARY").cloned();
|
||||
vevent.description = properties.get("DESCRIPTION").cloned();
|
||||
vevent.location = properties.get("LOCATION").cloned();
|
||||
@@ -450,10 +463,13 @@ impl CalDAVClient {
|
||||
vevent.attendees = Vec::new();
|
||||
|
||||
vevent.categories = categories;
|
||||
vevent.created = created;
|
||||
vevent.last_modified = last_modified;
|
||||
vevent.created = created_naive;
|
||||
vevent.created_tzid = created_tzid;
|
||||
vevent.last_modified = last_modified_naive;
|
||||
vevent.last_modified_tzid = last_modified_tzid;
|
||||
vevent.rrule = properties.get("RRULE").cloned();
|
||||
vevent.exdate = exdate;
|
||||
vevent.exdate = exdate.into_iter().map(|dt| dt.naive_utc()).collect();
|
||||
vevent.exdate_tzid = None; // TODO: Parse timezone info for EXDATE
|
||||
vevent.all_day = all_day;
|
||||
|
||||
// Parse alarms
|
||||
@@ -704,6 +720,89 @@ impl CalDAVClient {
|
||||
Ok(calendar_paths)
|
||||
}
|
||||
|
||||
/// Parse iCal datetime format and return NaiveDateTime + timezone info
|
||||
/// According to RFC 5545: if no TZID parameter is provided, treat as UTC
|
||||
fn parse_datetime_with_tz(
|
||||
&self,
|
||||
datetime_str: &str,
|
||||
original_property: Option<&String>,
|
||||
) -> Result<(chrono::NaiveDateTime, Option<String>), CalDAVError> {
|
||||
// Extract timezone information from the original property if available
|
||||
let mut timezone_id: Option<String> = None;
|
||||
if let Some(prop) = original_property {
|
||||
// Look for TZID parameter in the property
|
||||
// Format: DTSTART;TZID=America/Denver:20231225T090000
|
||||
if let Some(tzid_start) = prop.find("TZID=") {
|
||||
let tzid_part = &prop[tzid_start + 5..];
|
||||
if let Some(tzid_end) = tzid_part.find(':') {
|
||||
timezone_id = Some(tzid_part[..tzid_end].to_string());
|
||||
} else if let Some(tzid_end) = tzid_part.find(';') {
|
||||
timezone_id = Some(tzid_part[..tzid_end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the datetime string - remove any TZID prefix if present
|
||||
let cleaned = datetime_str.replace("TZID=", "").trim().to_string();
|
||||
|
||||
// Split on colon to separate TZID from datetime if format is "TZID=America/Denver:20231225T090000"
|
||||
let datetime_part = if let Some(colon_pos) = cleaned.find(':') {
|
||||
&cleaned[colon_pos + 1..]
|
||||
} else {
|
||||
&cleaned
|
||||
};
|
||||
|
||||
// Try different parsing formats
|
||||
let formats = [
|
||||
"%Y%m%dT%H%M%SZ", // UTC format: 20231225T120000Z
|
||||
"%Y%m%dT%H%M%S", // Local format: 20231225T120000
|
||||
"%Y%m%d", // Date only: 20231225
|
||||
];
|
||||
|
||||
for format in &formats {
|
||||
// Try parsing as UTC format (with Z suffix)
|
||||
if datetime_part.ends_with('Z') {
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&datetime_part[..datetime_part.len()-1], "%Y%m%dT%H%M%S") {
|
||||
// Z suffix means UTC, ignore any TZID parameter
|
||||
return Ok((dt, Some("UTC".to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing with timezone offset (e.g., 20231225T120000-0500)
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_str(datetime_part, "%Y%m%dT%H%M%S%z") {
|
||||
// Convert to naive UTC time and return UTC timezone
|
||||
return Ok((dt.naive_utc(), Some("UTC".to_string())));
|
||||
}
|
||||
|
||||
// Try ISO format with timezone offset (e.g., 2023-12-25T12:00:00-05:00)
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_str(datetime_part, "%Y-%m-%dT%H:%M:%S%z") {
|
||||
// Convert to naive UTC time and return UTC timezone
|
||||
return Ok((dt.naive_utc(), Some("UTC".to_string())));
|
||||
}
|
||||
|
||||
// Try ISO format with Z suffix (e.g., 2023-12-25T12:00:00Z)
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_str(datetime_part, "%Y-%m-%dT%H:%M:%SZ") {
|
||||
// Z suffix means UTC
|
||||
return Ok((dt.naive_utc(), Some("UTC".to_string())));
|
||||
}
|
||||
|
||||
// Try parsing as naive datetime
|
||||
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(datetime_part, format) {
|
||||
// Per RFC 5545: if no TZID parameter is provided, treat as UTC
|
||||
let tz = timezone_id.unwrap_or_else(|| "UTC".to_string());
|
||||
|
||||
// If it's UTC, the naive time is already correct
|
||||
// If it's a local timezone, we store the naive time and the timezone ID
|
||||
return Ok((naive_dt, Some(tz)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(CalDAVError::ParseError(format!(
|
||||
"Could not parse datetime: {}",
|
||||
datetime_str
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse iCal datetime format with timezone support
|
||||
fn parse_datetime(
|
||||
&self,
|
||||
@@ -1207,8 +1306,19 @@ impl CalDAVClient {
|
||||
// Format datetime for iCal (YYYYMMDDTHHMMSSZ format)
|
||||
let format_datetime =
|
||||
|dt: &DateTime<Utc>| -> String { dt.format("%Y%m%dT%H%M%SZ").to_string() };
|
||||
let format_datetime_naive =
|
||||
|dt: &chrono::NaiveDateTime| -> String { dt.format("%Y%m%dT%H%M%S").to_string() };
|
||||
|
||||
let format_date = |dt: &DateTime<Utc>| -> String { dt.format("%Y%m%d").to_string() };
|
||||
let _format_date = |dt: &DateTime<Utc>| -> String { dt.format("%Y%m%d").to_string() };
|
||||
|
||||
// Format NaiveDateTime for iCal (local time without Z suffix)
|
||||
let format_naive_datetime = |dt: &chrono::NaiveDateTime| -> String {
|
||||
dt.format("%Y%m%dT%H%M%S").to_string()
|
||||
};
|
||||
|
||||
let format_naive_date = |dt: &chrono::NaiveDateTime| -> String {
|
||||
dt.format("%Y%m%d").to_string()
|
||||
};
|
||||
|
||||
// Start building the iCal event
|
||||
let mut ical = String::new();
|
||||
@@ -1225,15 +1335,77 @@ impl CalDAVClient {
|
||||
if event.all_day {
|
||||
ical.push_str(&format!(
|
||||
"DTSTART;VALUE=DATE:{}\r\n",
|
||||
format_date(&event.dtstart)
|
||||
format_naive_date(&event.dtstart)
|
||||
));
|
||||
if let Some(end) = &event.dtend {
|
||||
ical.push_str(&format!("DTEND;VALUE=DATE:{}\r\n", format_date(end)));
|
||||
ical.push_str(&format!("DTEND;VALUE=DATE:{}\r\n", format_naive_date(end)));
|
||||
}
|
||||
} else {
|
||||
ical.push_str(&format!("DTSTART:{}\r\n", format_datetime(&event.dtstart)));
|
||||
// Include timezone information for non-all-day events per RFC 5545
|
||||
if let Some(ref start_tzid) = event.dtstart_tzid {
|
||||
if start_tzid == "UTC" {
|
||||
// UTC events should use Z suffix format
|
||||
ical.push_str(&format!("DTSTART:{}Z\r\n", format_naive_datetime(&event.dtstart)));
|
||||
} else if start_tzid.starts_with('+') || start_tzid.starts_with('-') {
|
||||
// Timezone offset format (e.g., "+05:00", "-04:00")
|
||||
// Convert local time to UTC using the offset and use Z format
|
||||
if let Ok(offset_hours) = start_tzid[1..3].parse::<i32>() {
|
||||
let offset_minutes = start_tzid[4..6].parse::<i32>().unwrap_or(0);
|
||||
let total_offset_minutes = if start_tzid.starts_with('+') {
|
||||
offset_hours * 60 + offset_minutes
|
||||
} else {
|
||||
-(offset_hours * 60 + offset_minutes)
|
||||
};
|
||||
|
||||
// Convert local time to UTC by applying the inverse offset
|
||||
// If timezone is +04:00 (local ahead of UTC), subtract to get UTC
|
||||
// If timezone is -04:00 (local behind UTC), add to get UTC
|
||||
let utc_time = event.dtstart - chrono::Duration::minutes(total_offset_minutes as i64);
|
||||
ical.push_str(&format!("DTSTART:{}Z\r\n", format_naive_datetime(&utc_time)));
|
||||
} else {
|
||||
// Fallback to floating time if offset parsing fails
|
||||
ical.push_str(&format!("DTSTART:{}\r\n", format_naive_datetime(&event.dtstart)));
|
||||
}
|
||||
} else {
|
||||
// Named timezone (e.g., "America/New_York") - use TZID parameter per RFC 5545
|
||||
ical.push_str(&format!("DTSTART;TZID={}:{}\r\n", start_tzid, format_naive_datetime(&event.dtstart)));
|
||||
}
|
||||
} else {
|
||||
// No timezone info - treat as floating local time per RFC 5545
|
||||
ical.push_str(&format!("DTSTART:{}\r\n", format_naive_datetime(&event.dtstart)));
|
||||
}
|
||||
|
||||
if let Some(end) = &event.dtend {
|
||||
ical.push_str(&format!("DTEND:{}\r\n", format_datetime(end)));
|
||||
if let Some(ref end_tzid) = event.dtend_tzid {
|
||||
if end_tzid == "UTC" {
|
||||
// UTC events should use Z suffix format
|
||||
ical.push_str(&format!("DTEND:{}Z\r\n", format_naive_datetime(end)));
|
||||
} else if end_tzid.starts_with('+') || end_tzid.starts_with('-') {
|
||||
// Timezone offset format (e.g., "+05:00", "-04:00")
|
||||
// Convert local time to UTC using the offset and use Z format
|
||||
if let Ok(offset_hours) = end_tzid[1..3].parse::<i32>() {
|
||||
let offset_minutes = end_tzid[4..6].parse::<i32>().unwrap_or(0);
|
||||
let total_offset_minutes = if end_tzid.starts_with('+') {
|
||||
offset_hours * 60 + offset_minutes
|
||||
} else {
|
||||
-(offset_hours * 60 + offset_minutes)
|
||||
};
|
||||
|
||||
// Convert local time to UTC by subtracting the offset
|
||||
let utc_time = *end - chrono::Duration::minutes(total_offset_minutes as i64);
|
||||
ical.push_str(&format!("DTEND:{}Z\r\n", format_naive_datetime(&utc_time)));
|
||||
} else {
|
||||
// Fallback to floating time if offset parsing fails
|
||||
ical.push_str(&format!("DTEND:{}\r\n", format_naive_datetime(end)));
|
||||
}
|
||||
} else {
|
||||
// Named timezone (e.g., "America/New_York") - use TZID parameter per RFC 5545
|
||||
ical.push_str(&format!("DTEND;TZID={}:{}\r\n", end_tzid, format_naive_datetime(end)));
|
||||
}
|
||||
} else {
|
||||
// No timezone info - treat as floating local time per RFC 5545
|
||||
ical.push_str(&format!("DTEND:{}\r\n", format_naive_datetime(end)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1289,7 +1461,18 @@ impl CalDAVClient {
|
||||
|
||||
// Creation and modification times
|
||||
if let Some(created) = &event.created {
|
||||
ical.push_str(&format!("CREATED:{}\r\n", format_datetime(created)));
|
||||
if let Some(ref created_tzid) = event.created_tzid {
|
||||
if created_tzid == "UTC" {
|
||||
ical.push_str(&format!("CREATED:{}Z\r\n", format_datetime_naive(created)));
|
||||
} else {
|
||||
// Per RFC 5545, CREATED typically should be in UTC or floating time
|
||||
// Treat non-UTC as floating time
|
||||
ical.push_str(&format!("CREATED:{}\r\n", format_datetime_naive(created)));
|
||||
}
|
||||
} else {
|
||||
// No timezone info - output as floating time per RFC 5545
|
||||
ical.push_str(&format!("CREATED:{}\r\n", format_datetime_naive(created)));
|
||||
}
|
||||
}
|
||||
|
||||
ical.push_str(&format!("LAST-MODIFIED:{}\r\n", format_datetime(&now)));
|
||||
@@ -1346,10 +1529,10 @@ impl CalDAVClient {
|
||||
if event.all_day {
|
||||
ical.push_str(&format!(
|
||||
"EXDATE;VALUE=DATE:{}\r\n",
|
||||
format_date(exception_date)
|
||||
format_naive_date(exception_date)
|
||||
));
|
||||
} else {
|
||||
ical.push_str(&format!("EXDATE:{}\r\n", format_datetime(exception_date)));
|
||||
ical.push_str(&format!("EXDATE:{}\r\n", format_naive_datetime(exception_date)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ pub async fn get_calendar_events(
|
||||
} - chrono::Duration::days(1);
|
||||
|
||||
all_events.retain(|event| {
|
||||
let event_date = event.dtstart.date_naive();
|
||||
let event_date = event.dtstart.date();
|
||||
|
||||
// For non-recurring events, check if the event date is within the month
|
||||
if event.rrule.is_none() || event.rrule.as_ref().unwrap().is_empty() {
|
||||
@@ -234,26 +234,26 @@ pub async fn delete_event(
|
||||
if event.rrule.is_some() && !event.rrule.as_ref().unwrap().is_empty() {
|
||||
// Recurring event - add EXDATE for this occurrence
|
||||
if let Some(occurrence_date) = &request.occurrence_date {
|
||||
let exception_utc = if let Ok(date) =
|
||||
let exception_datetime = if let Ok(date) =
|
||||
chrono::DateTime::parse_from_rfc3339(occurrence_date)
|
||||
{
|
||||
// RFC3339 format (with time and timezone)
|
||||
date.with_timezone(&chrono::Utc)
|
||||
// RFC3339 format (with time and timezone) - convert to naive
|
||||
date.naive_utc()
|
||||
} else if let Ok(naive_date) =
|
||||
chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
|
||||
{
|
||||
// Simple date format (YYYY-MM-DD)
|
||||
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
|
||||
naive_date.and_hms_opt(0, 0, 0).unwrap()
|
||||
} else {
|
||||
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
|
||||
};
|
||||
|
||||
let mut updated_event = event;
|
||||
updated_event.exdate.push(exception_utc);
|
||||
updated_event.exdate.push(exception_datetime);
|
||||
|
||||
println!(
|
||||
"🔄 Adding EXDATE {} to recurring event {}",
|
||||
exception_utc.format("%Y%m%dT%H%M%SZ"),
|
||||
exception_datetime.format("%Y%m%dT%H%M%S"),
|
||||
updated_event.uid
|
||||
);
|
||||
|
||||
@@ -453,12 +453,12 @@ pub async fn create_event(
|
||||
calendar_paths[0].clone()
|
||||
};
|
||||
|
||||
// Parse dates and times
|
||||
// Parse dates and times as local times (no UTC conversion)
|
||||
let start_datetime =
|
||||
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
|
||||
parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
|
||||
|
||||
let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// For all-day events, add one day to end date for RFC-5545 compliance
|
||||
@@ -594,9 +594,13 @@ pub async fn create_event(
|
||||
}
|
||||
};
|
||||
|
||||
// Create the VEvent struct (RFC 5545 compliant)
|
||||
// Create the VEvent struct (RFC 5545 compliant) with local times
|
||||
let mut event = VEvent::new(uid, start_datetime);
|
||||
event.dtend = Some(end_datetime);
|
||||
|
||||
// Set timezone information from client
|
||||
event.dtstart_tzid = Some(request.timezone.clone());
|
||||
event.dtend_tzid = Some(request.timezone.clone());
|
||||
event.summary = if request.title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -757,12 +761,14 @@ pub async fn update_event(
|
||||
let (mut event, calendar_path, event_href) = found_event
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", request.uid)))?;
|
||||
|
||||
// Parse dates and times
|
||||
// Parse dates and times as local times (no UTC conversion)
|
||||
println!("🕐 UPDATE: Received start_date: '{}', start_time: '{}', timezone: '{}'",
|
||||
request.start_date, request.start_time, request.timezone);
|
||||
let start_datetime =
|
||||
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
|
||||
parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
|
||||
|
||||
let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// For all-day events, add one day to end date for RFC-5545 compliance
|
||||
@@ -786,9 +792,11 @@ pub async fn update_event(
|
||||
}
|
||||
}
|
||||
|
||||
// Update event properties
|
||||
// Update event properties with local times and timezone info
|
||||
event.dtstart = start_datetime;
|
||||
event.dtend = Some(end_datetime);
|
||||
event.dtstart_tzid = Some(request.timezone.clone());
|
||||
event.dtend_tzid = Some(request.timezone.clone());
|
||||
event.summary = if request.title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -822,6 +830,99 @@ pub async fn update_event(
|
||||
|
||||
event.priority = request.priority;
|
||||
|
||||
// Process recurrence information to set RRULE
|
||||
println!("🔄 Processing recurrence: '{}'", request.recurrence);
|
||||
println!("🔄 Recurrence days: {:?}", request.recurrence_days);
|
||||
println!("🔄 Recurrence interval: {:?}", request.recurrence_interval);
|
||||
println!("🔄 Recurrence count: {:?}", request.recurrence_count);
|
||||
println!("🔄 Recurrence end date: {:?}", request.recurrence_end_date);
|
||||
|
||||
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||
// Frontend sent a complete RRULE string, use it directly
|
||||
if request.recurrence.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(request.recurrence.clone())
|
||||
}
|
||||
} else {
|
||||
// Parse recurrence type and build RRULE with all parameters
|
||||
let base_rrule = match request.recurrence.to_uppercase().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 = format!("{};BYDAY={}", rrule, selected_days.join(","));
|
||||
}
|
||||
}
|
||||
|
||||
Some(rrule)
|
||||
}
|
||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||
"NONE" | "" => None, // Clear any existing recurrence
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Add INTERVAL, COUNT, and UNTIL parameters if specified
|
||||
if let Some(mut rrule_string) = base_rrule {
|
||||
// Add INTERVAL parameter (every N days/weeks/months/years)
|
||||
if let Some(interval) = request.recurrence_interval {
|
||||
if interval > 1 {
|
||||
rrule_string = format!("{};INTERVAL={}", rrule_string, interval);
|
||||
}
|
||||
}
|
||||
|
||||
// Add COUNT or UNTIL parameter (but not both - COUNT takes precedence)
|
||||
if let Some(count) = request.recurrence_count {
|
||||
rrule_string = format!("{};COUNT={}", rrule_string, count);
|
||||
} else if let Some(end_date) = &request.recurrence_end_date {
|
||||
// Convert YYYY-MM-DD to YYYYMMDD format for UNTIL
|
||||
if let Ok(date) = chrono::NaiveDate::parse_from_str(end_date, "%Y-%m-%d") {
|
||||
rrule_string = format!("{};UNTIL={}", rrule_string, date.format("%Y%m%d"));
|
||||
}
|
||||
}
|
||||
|
||||
Some(rrule_string)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
event.rrule = rrule.clone();
|
||||
println!("🔄 Set event RRULE to: {:?}", rrule);
|
||||
|
||||
if rrule.is_some() {
|
||||
println!("✨ Converting singleton event to recurring series with RRULE: {}", rrule.as_ref().unwrap());
|
||||
} else {
|
||||
println!("📝 Event remains non-recurring (no RRULE set)");
|
||||
}
|
||||
|
||||
// Update the event on the CalDAV server
|
||||
println!(
|
||||
"📝 Updating event {} at calendar_path: {}, event_href: {}",
|
||||
@@ -840,33 +941,29 @@ pub async fn update_event(
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_event_datetime(
|
||||
fn parse_event_datetime_local(
|
||||
date_str: &str,
|
||||
time_str: &str,
|
||||
all_day: bool,
|
||||
) -> Result<chrono::DateTime<chrono::Utc>, String> {
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||
) -> Result<chrono::NaiveDateTime, String> {
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
|
||||
// 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 noon UTC to avoid timezone boundary issues
|
||||
// This ensures the date remains correct when converted to any local timezone
|
||||
// For all-day events, use start of day
|
||||
let datetime = date
|
||||
.and_hms_opt(12, 0, 0)
|
||||
.ok_or_else(|| "Failed to create noon datetime".to_string())?;
|
||||
Ok(Utc.from_utc_datetime(&datetime))
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| "Failed to create start-of-day datetime".to_string())?;
|
||||
Ok(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);
|
||||
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
Ok(Utc.from_utc_datetime(&datetime))
|
||||
// Combine date and time - now keeping as local time
|
||||
Ok(NaiveDateTime::new(date, time))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,17 +285,25 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result<VEvent, Box<dyn std::
|
||||
|
||||
let vevent = VEvent {
|
||||
uid: uid.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
||||
dtstart,
|
||||
dtend,
|
||||
dtstart: dtstart.naive_utc(),
|
||||
dtstart_tzid: None, // TODO: Parse timezone from ICS
|
||||
dtend: dtend.map(|dt| dt.naive_utc()),
|
||||
dtend_tzid: None, // TODO: Parse timezone from ICS
|
||||
summary,
|
||||
description,
|
||||
location,
|
||||
all_day,
|
||||
rrule,
|
||||
rdate: Vec::new(),
|
||||
rdate_tzid: None,
|
||||
exdate: Vec::new(), // External calendars don't need exception handling
|
||||
exdate_tzid: None,
|
||||
recurrence_id: None,
|
||||
recurrence_id_tzid: None,
|
||||
created: None,
|
||||
created_tzid: None,
|
||||
last_modified: None,
|
||||
last_modified_tzid: None,
|
||||
dtstamp: Utc::now(),
|
||||
sequence: Some(0),
|
||||
status: None,
|
||||
@@ -313,7 +321,6 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result<VEvent, Box<dyn std::
|
||||
class: None,
|
||||
contact: None,
|
||||
comment: None,
|
||||
rdate: Vec::new(),
|
||||
alarms: Vec::new(),
|
||||
etag: None,
|
||||
href: None,
|
||||
@@ -834,7 +841,7 @@ fn would_event_be_generated_by_rrule(recurring_event: &VEvent, single_event: &VE
|
||||
if rrule.contains("FREQ=DAILY") {
|
||||
// Daily recurrence
|
||||
let interval = extract_interval_from_rrule(rrule).unwrap_or(1);
|
||||
let days_diff = (single_event.dtstart.date_naive() - recurring_event.dtstart.date_naive()).num_days();
|
||||
let days_diff = (single_event.dtstart.date() - recurring_event.dtstart.date()).num_days();
|
||||
|
||||
if days_diff >= 0 && days_diff % interval as i64 == 0 {
|
||||
// Check if times match (allowing for timezone differences within same day)
|
||||
@@ -845,7 +852,7 @@ fn would_event_be_generated_by_rrule(recurring_event: &VEvent, single_event: &VE
|
||||
} else if rrule.contains("FREQ=WEEKLY") {
|
||||
// Weekly recurrence
|
||||
let interval = extract_interval_from_rrule(rrule).unwrap_or(1);
|
||||
let days_diff = (single_event.dtstart.date_naive() - recurring_event.dtstart.date_naive()).num_days();
|
||||
let days_diff = (single_event.dtstart.date() - recurring_event.dtstart.date()).num_days();
|
||||
|
||||
// First check if it's the same day of week and time
|
||||
let recurring_weekday = recurring_event.dtstart.weekday();
|
||||
|
||||
@@ -14,6 +14,33 @@ use calendar_models::{EventClass, EventStatus, VEvent};
|
||||
|
||||
use super::auth::{extract_bearer_token, extract_password_header};
|
||||
|
||||
fn parse_event_datetime_local(
|
||||
date_str: &str,
|
||||
time_str: &str,
|
||||
all_day: bool,
|
||||
) -> Result<chrono::NaiveDateTime, String> {
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
|
||||
// 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 start of day
|
||||
let datetime = date
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| "Failed to create start-of-day datetime".to_string())?;
|
||||
Ok(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 - now keeping as local time
|
||||
Ok(NaiveDateTime::new(date, time))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new recurring event series
|
||||
pub async fn create_event_series(
|
||||
State(state): State<Arc<AppState>>,
|
||||
@@ -106,84 +133,29 @@ pub async fn create_event_series(
|
||||
|
||||
println!("📅 Using calendar path: {}", calendar_path);
|
||||
|
||||
// Parse datetime components
|
||||
let start_date =
|
||||
chrono::NaiveDate::parse_from_str(&request.start_date, "%Y-%m-%d").map_err(|_| {
|
||||
ApiError::BadRequest("Invalid start_date format. Expected YYYY-MM-DD".to_string())
|
||||
})?;
|
||||
// Parse dates and times as local times (no UTC conversion)
|
||||
let start_datetime = parse_event_datetime_local(&request.start_date, &request.start_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
|
||||
|
||||
let (start_datetime, end_datetime) = if request.all_day {
|
||||
// For all-day events, use the dates as-is
|
||||
let start_dt = start_date
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?;
|
||||
let mut end_datetime = parse_event_datetime_local(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
let end_date = if !request.end_date.is_empty() {
|
||||
chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d").map_err(|_| {
|
||||
ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string())
|
||||
})?
|
||||
} else {
|
||||
start_date
|
||||
};
|
||||
|
||||
let end_dt = end_date
|
||||
.and_hms_opt(23, 59, 59)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
end_local.with_timezone(&chrono::Utc),
|
||||
)
|
||||
} else {
|
||||
// Parse times for timed events
|
||||
let start_time = if !request.start_time.is_empty() {
|
||||
chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| {
|
||||
ApiError::BadRequest("Invalid start_time format. Expected HH:MM".to_string())
|
||||
})?
|
||||
} else {
|
||||
chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap() // Default to 9 AM
|
||||
};
|
||||
|
||||
let end_time = if !request.end_time.is_empty() {
|
||||
chrono::NaiveTime::parse_from_str(&request.end_time, "%H:%M").map_err(|_| {
|
||||
ApiError::BadRequest("Invalid end_time format. Expected HH:MM".to_string())
|
||||
})?
|
||||
} else {
|
||||
chrono::NaiveTime::from_hms_opt(10, 0, 0).unwrap() // Default to 1 hour duration
|
||||
};
|
||||
|
||||
let start_dt = start_date.and_time(start_time);
|
||||
let end_dt = if !request.end_date.is_empty() {
|
||||
let end_date = chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d")
|
||||
.map_err(|_| {
|
||||
ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string())
|
||||
})?;
|
||||
end_date.and_time(end_time)
|
||||
} else {
|
||||
start_date.and_time(end_time)
|
||||
};
|
||||
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
end_local.with_timezone(&chrono::Utc),
|
||||
)
|
||||
};
|
||||
// For all-day events, add one day to end date for RFC-5545 compliance
|
||||
if request.all_day {
|
||||
end_datetime = end_datetime + chrono::Duration::days(1);
|
||||
}
|
||||
|
||||
// Generate a unique UID for the series
|
||||
let uid = format!("series-{}", uuid::Uuid::new_v4().to_string());
|
||||
|
||||
// Create the VEvent for the series
|
||||
// Create the VEvent for the series with local times
|
||||
let mut event = VEvent::new(uid.clone(), start_datetime);
|
||||
event.dtend = Some(end_datetime);
|
||||
event.all_day = request.all_day; // Set the all_day flag properly
|
||||
|
||||
// Set timezone information from client
|
||||
event.dtstart_tzid = Some(request.timezone.clone());
|
||||
event.dtend_tzid = Some(request.timezone.clone());
|
||||
event.summary = if request.title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -257,6 +229,8 @@ pub async fn update_event_series(
|
||||
"🔄 Update event series request received: series_uid='{}', update_scope='{}', recurrence_count={:?}, recurrence_end_date={:?}",
|
||||
request.series_uid, request.update_scope, request.recurrence_count, request.recurrence_end_date
|
||||
);
|
||||
println!("🕐 SERIES: Received start_date: '{}', start_time: '{}', timezone: '{}'",
|
||||
request.start_date, request.start_time, request.timezone);
|
||||
|
||||
// Extract and verify token
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
@@ -372,7 +346,7 @@ pub async fn update_event_series(
|
||||
);
|
||||
|
||||
// Parse datetime components for the update
|
||||
let original_start_date = existing_event.dtstart.date_naive();
|
||||
let original_start_date = existing_event.dtstart.date();
|
||||
|
||||
// For "this_and_future" and "this_only" updates, use the occurrence date for the modified event
|
||||
// For "all_in_series" updates, preserve the original series start date
|
||||
@@ -399,7 +373,7 @@ pub async fn update_event_series(
|
||||
// Calculate the duration from the original event
|
||||
let original_duration_days = existing_event
|
||||
.dtend
|
||||
.map(|end| (end.date_naive() - existing_event.dtstart.date_naive()).num_days())
|
||||
.map(|end| (end.date() - existing_event.dtstart.date()).num_days())
|
||||
.unwrap_or(0);
|
||||
start_date + chrono::Duration::days(original_duration_days)
|
||||
} else {
|
||||
@@ -410,11 +384,8 @@ pub async fn update_event_series(
|
||||
.and_hms_opt(12, 0, 0)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||
|
||||
// For all-day events, use UTC directly (no local conversion needed)
|
||||
(
|
||||
chrono::Utc.from_utc_datetime(&start_dt),
|
||||
chrono::Utc.from_utc_datetime(&end_dt),
|
||||
)
|
||||
// For all-day events, use local times directly
|
||||
(start_dt, end_dt)
|
||||
} else {
|
||||
let start_time = if !request.start_time.is_empty() {
|
||||
chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| {
|
||||
@@ -445,17 +416,11 @@ pub async fn update_event_series(
|
||||
.dtend
|
||||
.map(|end| end - existing_event.dtstart)
|
||||
.unwrap_or_else(|| chrono::Duration::hours(1));
|
||||
(chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc()
|
||||
start_dt + original_duration
|
||||
};
|
||||
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
end_local.with_timezone(&chrono::Utc),
|
||||
)
|
||||
// Frontend now sends local times, so use them directly
|
||||
(start_dt, end_dt)
|
||||
};
|
||||
|
||||
// Handle different update scopes
|
||||
@@ -702,8 +667,8 @@ fn build_series_rrule_with_freq(
|
||||
fn update_entire_series(
|
||||
existing_event: &mut VEvent,
|
||||
request: &UpdateEventSeriesRequest,
|
||||
start_datetime: chrono::DateTime<chrono::Utc>,
|
||||
end_datetime: chrono::DateTime<chrono::Utc>,
|
||||
start_datetime: chrono::NaiveDateTime,
|
||||
end_datetime: chrono::NaiveDateTime,
|
||||
) -> Result<(VEvent, u32), ApiError> {
|
||||
// Clone the existing event to preserve all metadata
|
||||
let mut updated_event = existing_event.clone();
|
||||
@@ -711,6 +676,8 @@ fn update_entire_series(
|
||||
// Update only the modified properties from the request
|
||||
updated_event.dtstart = start_datetime;
|
||||
updated_event.dtend = Some(end_datetime);
|
||||
updated_event.dtstart_tzid = Some(request.timezone.clone());
|
||||
updated_event.dtend_tzid = Some(request.timezone.clone());
|
||||
updated_event.summary = if request.title.trim().is_empty() {
|
||||
existing_event.summary.clone() // Keep original if empty
|
||||
} else {
|
||||
@@ -743,8 +710,9 @@ fn update_entire_series(
|
||||
|
||||
// Update timestamps
|
||||
let now = chrono::Utc::now();
|
||||
let now_naive = now.naive_utc();
|
||||
updated_event.dtstamp = now;
|
||||
updated_event.last_modified = Some(now);
|
||||
updated_event.last_modified = Some(now_naive);
|
||||
// Keep original created timestamp to preserve event history
|
||||
|
||||
// Update RRULE if recurrence parameters are provided
|
||||
@@ -832,8 +800,8 @@ fn update_entire_series(
|
||||
async fn update_this_and_future(
|
||||
existing_event: &mut VEvent,
|
||||
request: &UpdateEventSeriesRequest,
|
||||
start_datetime: chrono::DateTime<chrono::Utc>,
|
||||
end_datetime: chrono::DateTime<chrono::Utc>,
|
||||
start_datetime: chrono::NaiveDateTime,
|
||||
end_datetime: chrono::NaiveDateTime,
|
||||
client: &CalDAVClient,
|
||||
calendar_path: &str,
|
||||
) -> Result<(VEvent, u32), ApiError> {
|
||||
@@ -881,6 +849,8 @@ async fn update_this_and_future(
|
||||
new_series.uid = new_series_uid.clone();
|
||||
new_series.dtstart = start_datetime;
|
||||
new_series.dtend = Some(end_datetime);
|
||||
new_series.dtstart_tzid = Some(request.timezone.clone());
|
||||
new_series.dtend_tzid = Some(request.timezone.clone());
|
||||
new_series.summary = if request.title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -913,9 +883,10 @@ async fn update_this_and_future(
|
||||
|
||||
// Update timestamps
|
||||
let now = chrono::Utc::now();
|
||||
let now_naive = now.naive_utc();
|
||||
new_series.dtstamp = now;
|
||||
new_series.created = Some(now);
|
||||
new_series.last_modified = Some(now);
|
||||
new_series.created = Some(now_naive);
|
||||
new_series.last_modified = Some(now_naive);
|
||||
new_series.href = None; // Will be set when created
|
||||
|
||||
println!(
|
||||
@@ -943,8 +914,8 @@ async fn update_this_and_future(
|
||||
async fn update_single_occurrence(
|
||||
existing_event: &mut VEvent,
|
||||
request: &UpdateEventSeriesRequest,
|
||||
start_datetime: chrono::DateTime<chrono::Utc>,
|
||||
end_datetime: chrono::DateTime<chrono::Utc>,
|
||||
start_datetime: chrono::NaiveDateTime,
|
||||
end_datetime: chrono::NaiveDateTime,
|
||||
client: &CalDAVClient,
|
||||
calendar_path: &str,
|
||||
_original_event_href: &str,
|
||||
@@ -969,21 +940,20 @@ async fn update_single_occurrence(
|
||||
// Create the EXDATE datetime using the original event's time
|
||||
let original_time = existing_event.dtstart.time();
|
||||
let exception_datetime = exception_date.and_time(original_time);
|
||||
let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime);
|
||||
|
||||
// Add the exception date to the original series
|
||||
println!(
|
||||
"📝 BEFORE adding EXDATE: existing_event.exdate = {:?}",
|
||||
existing_event.exdate
|
||||
);
|
||||
existing_event.exdate.push(exception_utc);
|
||||
existing_event.exdate.push(exception_datetime);
|
||||
println!(
|
||||
"📝 AFTER adding EXDATE: existing_event.exdate = {:?}",
|
||||
existing_event.exdate
|
||||
);
|
||||
println!(
|
||||
"🚫 Added EXDATE for single occurrence modification: {}",
|
||||
exception_utc.format("%Y-%m-%d %H:%M:%S")
|
||||
exception_datetime.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
|
||||
// Create exception event by cloning the existing event to preserve all metadata
|
||||
@@ -995,6 +965,8 @@ async fn update_single_occurrence(
|
||||
// Update the modified properties from the request
|
||||
exception_event.dtstart = start_datetime;
|
||||
exception_event.dtend = Some(end_datetime);
|
||||
exception_event.dtstart_tzid = Some(request.timezone.clone());
|
||||
exception_event.dtend_tzid = Some(request.timezone.clone());
|
||||
exception_event.summary = if request.title.trim().is_empty() {
|
||||
existing_event.summary.clone() // Keep original if empty
|
||||
} else {
|
||||
@@ -1027,8 +999,9 @@ async fn update_single_occurrence(
|
||||
|
||||
// Update timestamps for the exception event
|
||||
let now = chrono::Utc::now();
|
||||
let now_naive = now.naive_utc();
|
||||
exception_event.dtstamp = now;
|
||||
exception_event.last_modified = Some(now);
|
||||
exception_event.last_modified = Some(now_naive);
|
||||
// Keep original created timestamp to preserve event history
|
||||
|
||||
// Set RECURRENCE-ID to point to the original occurrence
|
||||
@@ -1044,7 +1017,7 @@ async fn update_single_occurrence(
|
||||
|
||||
println!(
|
||||
"✨ Created exception event with RECURRENCE-ID: {}",
|
||||
exception_utc.format("%Y-%m-%d %H:%M:%S")
|
||||
exception_datetime.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
|
||||
// Create the exception event as a new event (original series will be updated by main handler)
|
||||
@@ -1172,15 +1145,14 @@ async fn delete_single_occurrence(
|
||||
// Create the EXDATE datetime (use the same time as the original event)
|
||||
let original_time = existing_event.dtstart.time();
|
||||
let exception_datetime = exception_date.and_time(original_time);
|
||||
let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime);
|
||||
|
||||
// Add the exception date to the event's EXDATE list
|
||||
let mut updated_event = existing_event;
|
||||
updated_event.exdate.push(exception_utc);
|
||||
updated_event.exdate.push(exception_datetime);
|
||||
|
||||
println!(
|
||||
"🗑️ Added EXDATE for single occurrence deletion: {}",
|
||||
exception_utc.format("%Y%m%dT%H%M%SZ")
|
||||
exception_datetime.format("%Y%m%dT%H%M%S")
|
||||
);
|
||||
|
||||
// Update the event on the CalDAV server
|
||||
|
||||
@@ -117,6 +117,7 @@ pub struct CreateEventRequest {
|
||||
pub recurrence: String, // recurrence type
|
||||
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
||||
pub timezone: String, // Client timezone (e.g., "+05:00", "-04:00")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -146,8 +147,12 @@ pub struct UpdateEventRequest {
|
||||
pub reminder: String, // reminder type
|
||||
pub recurrence: String, // recurrence type
|
||||
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
|
||||
pub recurrence_count: Option<u32>, // Number of occurrences
|
||||
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
||||
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
||||
pub update_action: Option<String>, // "update_series" for recurring events
|
||||
pub timezone: String, // Client timezone (e.g., "+05:00", "-04:00")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause
|
||||
}
|
||||
@@ -185,6 +190,7 @@ pub struct CreateEventSeriesRequest {
|
||||
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
||||
pub recurrence_count: Option<u32>, // Number of occurrences
|
||||
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
||||
pub timezone: String, // Client timezone (e.g., "+05:00", "-04:00")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -227,6 +233,7 @@ pub struct UpdateEventSeriesRequest {
|
||||
pub update_scope: String, // "this_only", "this_and_future", "all_in_series"
|
||||
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being updated
|
||||
pub changed_fields: Option<Vec<String>>, // List of field names that were changed (for optimization)
|
||||
pub timezone: String, // Client timezone (e.g., "+05:00", "-04:00")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! VEvent - RFC 5545 compliant calendar event structure
|
||||
|
||||
use crate::common::*;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ==================== VEVENT COMPONENT ====================
|
||||
@@ -9,12 +9,14 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VEvent {
|
||||
// Required properties
|
||||
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED
|
||||
pub uid: String, // Unique identifier (UID) - REQUIRED
|
||||
pub dtstart: DateTime<Utc>, // Start date-time (DTSTART) - REQUIRED
|
||||
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED (always UTC)
|
||||
pub uid: String, // Unique identifier (UID) - REQUIRED
|
||||
pub dtstart: NaiveDateTime, // Start date-time (DTSTART) - REQUIRED (local time)
|
||||
pub dtstart_tzid: Option<String>, // Timezone ID for DTSTART (TZID parameter)
|
||||
|
||||
// Optional properties (commonly used)
|
||||
pub dtend: Option<DateTime<Utc>>, // End date-time (DTEND)
|
||||
pub dtend: Option<NaiveDateTime>, // End date-time (DTEND) (local time)
|
||||
pub dtend_tzid: Option<String>, // Timezone ID for DTEND (TZID parameter)
|
||||
pub duration: Option<Duration>, // Duration (DURATION) - alternative to DTEND
|
||||
pub summary: Option<String>, // Summary/title (SUMMARY)
|
||||
pub description: Option<String>, // Description (DESCRIPTION)
|
||||
@@ -43,14 +45,19 @@ pub struct VEvent {
|
||||
|
||||
// Versioning and modification
|
||||
pub sequence: Option<u32>, // Sequence number (SEQUENCE)
|
||||
pub created: Option<DateTime<Utc>>, // Creation time (CREATED)
|
||||
pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED)
|
||||
pub created: Option<NaiveDateTime>, // Creation time (CREATED) (local time)
|
||||
pub created_tzid: Option<String>, // Timezone ID for CREATED
|
||||
pub last_modified: Option<NaiveDateTime>, // Last modified (LAST-MODIFIED) (local time)
|
||||
pub last_modified_tzid: Option<String>, // Timezone ID for LAST-MODIFIED
|
||||
|
||||
// Recurrence
|
||||
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
||||
pub rdate: Vec<DateTime<Utc>>, // Recurrence dates (RDATE)
|
||||
pub exdate: Vec<DateTime<Utc>>, // Exception dates (EXDATE)
|
||||
pub recurrence_id: Option<DateTime<Utc>>, // Recurrence ID (RECURRENCE-ID)
|
||||
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
||||
pub rdate: Vec<NaiveDateTime>, // Recurrence dates (RDATE) (local time)
|
||||
pub rdate_tzid: Option<String>, // Timezone ID for RDATE
|
||||
pub exdate: Vec<NaiveDateTime>, // Exception dates (EXDATE) (local time)
|
||||
pub exdate_tzid: Option<String>, // Timezone ID for EXDATE
|
||||
pub recurrence_id: Option<NaiveDateTime>, // Recurrence ID (RECURRENCE-ID) (local time)
|
||||
pub recurrence_id_tzid: Option<String>, // Timezone ID for RECURRENCE-ID
|
||||
|
||||
// Alarms and attachments
|
||||
pub alarms: Vec<VAlarm>, // VALARM components
|
||||
@@ -64,13 +71,15 @@ pub struct VEvent {
|
||||
}
|
||||
|
||||
impl VEvent {
|
||||
/// Create a new VEvent with required fields
|
||||
pub fn new(uid: String, dtstart: DateTime<Utc>) -> Self {
|
||||
/// Create a new VEvent with required fields (local time)
|
||||
pub fn new(uid: String, dtstart: NaiveDateTime) -> Self {
|
||||
Self {
|
||||
dtstamp: Utc::now(),
|
||||
uid,
|
||||
dtstart,
|
||||
dtstart_tzid: None,
|
||||
dtend: None,
|
||||
dtend_tzid: None,
|
||||
duration: None,
|
||||
summary: None,
|
||||
description: None,
|
||||
@@ -89,12 +98,17 @@ impl VEvent {
|
||||
url: None,
|
||||
geo: None,
|
||||
sequence: None,
|
||||
created: Some(Utc::now()),
|
||||
last_modified: Some(Utc::now()),
|
||||
created: Some(chrono::Local::now().naive_local()),
|
||||
created_tzid: None,
|
||||
last_modified: Some(chrono::Local::now().naive_local()),
|
||||
last_modified_tzid: None,
|
||||
rrule: None,
|
||||
rdate: Vec::new(),
|
||||
rdate_tzid: None,
|
||||
exdate: Vec::new(),
|
||||
exdate_tzid: None,
|
||||
recurrence_id: None,
|
||||
recurrence_id_tzid: None,
|
||||
alarms: Vec::new(),
|
||||
attachments: Vec::new(),
|
||||
etag: None,
|
||||
@@ -105,7 +119,7 @@ impl VEvent {
|
||||
}
|
||||
|
||||
/// Helper method to get effective end time (dtend or dtstart + duration)
|
||||
pub fn get_end_time(&self) -> DateTime<Utc> {
|
||||
pub fn get_end_time(&self) -> NaiveDateTime {
|
||||
if let Some(dtend) = self.dtend {
|
||||
dtend
|
||||
} else if let Some(duration) = self.duration {
|
||||
@@ -136,7 +150,7 @@ impl VEvent {
|
||||
|
||||
/// Helper method to get start date for UI compatibility
|
||||
pub fn get_date(&self) -> chrono::NaiveDate {
|
||||
self.dtstart.date_naive()
|
||||
self.dtstart.date()
|
||||
}
|
||||
|
||||
/// Check if event is recurring
|
||||
|
||||
@@ -723,8 +723,12 @@ pub fn App() -> Html {
|
||||
crate::components::event_form::RecurrenceType::Monthly |
|
||||
crate::components::event_form::RecurrenceType::Yearly);
|
||||
|
||||
web_sys::console::log_1(&format!("🐛 FRONTEND DEBUG: is_recurring={}, edit_scope={:?}, original_uid={:?}",
|
||||
is_recurring, event_data_for_update.edit_scope, event_data_for_update.original_uid).into());
|
||||
|
||||
let update_result = if is_recurring && event_data_for_update.edit_scope.is_some() {
|
||||
// Use series update endpoint for recurring events
|
||||
// Only use series endpoint for existing recurring events being edited
|
||||
// Singleton→series conversion should use regular update_event endpoint
|
||||
let edit_action = event_data_for_update.edit_scope.unwrap();
|
||||
let scope = match edit_action {
|
||||
crate::components::EditAction::EditAll => "all_in_series".to_string(),
|
||||
@@ -754,11 +758,13 @@ pub fn App() -> Html {
|
||||
params.14, // reminder
|
||||
params.15, // recurrence
|
||||
params.16, // recurrence_days
|
||||
params.17, // recurrence_interval
|
||||
params.18, // recurrence_count
|
||||
params.19, // recurrence_until
|
||||
params.17, // calendar_path
|
||||
params.20, // calendar_path
|
||||
scope,
|
||||
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
|
||||
params.21, // timezone
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
@@ -785,10 +791,11 @@ pub fn App() -> Html {
|
||||
params.14, // reminder
|
||||
params.15, // recurrence
|
||||
params.16, // recurrence_days
|
||||
params.17, // calendar_path
|
||||
vec![], // exception_dates - empty for simple updates
|
||||
None, // update_action - None for regular updates
|
||||
None, // until_date - None for regular updates
|
||||
params.17, // recurrence_interval
|
||||
params.18, // recurrence_count
|
||||
params.19, // recurrence_until
|
||||
params.20, // calendar_path
|
||||
params.21, // timezone
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -872,9 +879,11 @@ pub fn App() -> Html {
|
||||
params.14, // reminder
|
||||
params.15, // recurrence
|
||||
params.16, // recurrence_days
|
||||
params.17, // recurrence_interval
|
||||
params.18, // recurrence_count
|
||||
params.19, // recurrence_until
|
||||
params.17, // calendar_path
|
||||
params.20, // calendar_path
|
||||
params.21, // timezone
|
||||
)
|
||||
.await;
|
||||
match create_result {
|
||||
@@ -915,7 +924,7 @@ pub fn App() -> Html {
|
||||
chrono::NaiveDateTime,
|
||||
chrono::NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)| {
|
||||
@@ -954,30 +963,13 @@ pub fn App() -> Html {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Convert local naive datetime to UTC before sending to backend
|
||||
use chrono::TimeZone;
|
||||
let local_tz = chrono::Local;
|
||||
|
||||
let start_utc = local_tz.from_local_datetime(&new_start)
|
||||
.single()
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback for ambiguous times (DST transitions)
|
||||
local_tz.from_local_datetime(&new_start).earliest().unwrap()
|
||||
})
|
||||
.with_timezone(&chrono::Utc);
|
||||
|
||||
let end_utc = local_tz.from_local_datetime(&new_end)
|
||||
.single()
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback for ambiguous times (DST transitions)
|
||||
local_tz.from_local_datetime(&new_end).earliest().unwrap()
|
||||
})
|
||||
.with_timezone(&chrono::Utc);
|
||||
|
||||
let start_date = start_utc.format("%Y-%m-%d").to_string();
|
||||
let start_time = start_utc.format("%H:%M").to_string();
|
||||
let end_date = end_utc.format("%Y-%m-%d").to_string();
|
||||
let end_time = end_utc.format("%H:%M").to_string();
|
||||
// Send local times to backend, which will handle timezone conversion
|
||||
let start_date = new_start.format("%Y-%m-%d").to_string();
|
||||
let start_time = new_start.format("%H:%M").to_string();
|
||||
let end_date = new_end.format("%Y-%m-%d").to_string();
|
||||
let end_time = new_end.format("%H:%M").to_string();
|
||||
|
||||
// Convert existing event data to string formats for the API
|
||||
let status_str = match original_event.status {
|
||||
@@ -1056,12 +1048,21 @@ pub fn App() -> Html {
|
||||
original_event.categories.join(","),
|
||||
reminder_str.clone(),
|
||||
recurrence_str.clone(),
|
||||
vec![false; 7],
|
||||
None,
|
||||
None,
|
||||
original_event.calendar_path.clone(),
|
||||
scope.clone(),
|
||||
occurrence_date,
|
||||
vec![false; 7], // recurrence_days
|
||||
1, // recurrence_interval - default for drag-and-drop
|
||||
None, // recurrence_count
|
||||
None, // recurrence_until
|
||||
original_event.calendar_path.clone(), // calendar_path
|
||||
scope.clone(), // update_scope
|
||||
occurrence_date, // occurrence_date
|
||||
{
|
||||
// Get timezone offset
|
||||
let date = js_sys::Date::new_0();
|
||||
let timezone_offset = date.get_timezone_offset(); // Minutes from UTC
|
||||
let hours = -(timezone_offset as i32) / 60; // Convert to hours, negate for proper sign
|
||||
let minutes = (timezone_offset as i32).abs() % 60;
|
||||
format!("{:+03}:{:02}", hours, minutes) // Format as +05:00 or -04:00
|
||||
}, // timezone
|
||||
)
|
||||
.await,
|
||||
)
|
||||
@@ -1105,14 +1106,18 @@ pub fn App() -> Html {
|
||||
reminder_str,
|
||||
recurrence_str,
|
||||
recurrence_days,
|
||||
1, // recurrence_interval - default to 1 for drag-and-drop
|
||||
None, // recurrence_count - preserve existing
|
||||
None, // recurrence_until - preserve existing
|
||||
original_event.calendar_path,
|
||||
original_event.exdate.clone(),
|
||||
if preserve_rrule {
|
||||
Some("update_series".to_string())
|
||||
} else {
|
||||
Some("this_and_future".to_string())
|
||||
{
|
||||
// Get timezone offset
|
||||
let date = js_sys::Date::new_0();
|
||||
let timezone_offset = date.get_timezone_offset(); // Minutes from UTC
|
||||
let hours = -(timezone_offset as i32) / 60; // Convert to hours, negate for proper sign
|
||||
let minutes = (timezone_offset as i32).abs() % 60;
|
||||
format!("{:+03}:{:02}", hours, minutes) // Format as +05:00 or -04:00
|
||||
},
|
||||
until_date,
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -1597,7 +1602,7 @@ pub fn App() -> Html {
|
||||
};
|
||||
|
||||
// Get the occurrence date from the clicked event
|
||||
let occurrence_date = Some(event.dtstart.date_naive().format("%Y-%m-%d").to_string());
|
||||
let occurrence_date = Some(event.dtstart.date().format("%Y-%m-%d").to_string());
|
||||
|
||||
web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into());
|
||||
web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into());
|
||||
@@ -1637,6 +1642,19 @@ pub fn App() -> Html {
|
||||
}
|
||||
}
|
||||
})}
|
||||
on_edit_singleton={Callback::from({
|
||||
let event_context_menu_event = event_context_menu_event.clone();
|
||||
let event_context_menu_open = event_context_menu_open.clone();
|
||||
let create_event_modal_open = create_event_modal_open.clone();
|
||||
let event_edit_scope = event_edit_scope.clone();
|
||||
move |event: VEvent| {
|
||||
// For singleton events, open edit modal WITHOUT setting edit_scope
|
||||
event_context_menu_event.set(Some(event));
|
||||
event_edit_scope.set(None); // Explicitly set to None for singleton edits
|
||||
event_context_menu_open.set(false);
|
||||
create_event_modal_open.set(true);
|
||||
}
|
||||
})}
|
||||
on_view_details={Callback::from({
|
||||
let event_context_menu_open = event_context_menu_open.clone();
|
||||
let view_event_modal_open = view_event_modal_open.clone();
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct CalendarProps {
|
||||
chrono::NaiveDateTime,
|
||||
chrono::NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
@@ -437,7 +437,7 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
chrono::NaiveDateTime,
|
||||
chrono::NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)| {
|
||||
|
||||
@@ -195,7 +195,7 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
let on_external_success = on_external_success.clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let calendar_service = CalendarService::new();
|
||||
let _calendar_service = CalendarService::new();
|
||||
|
||||
match CalendarService::create_external_calendar(&name, &url, &color).await {
|
||||
Ok(calendar) => {
|
||||
|
||||
@@ -238,12 +238,11 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
||||
|
||||
// Convert VEvent to EventCreationData for editing
|
||||
fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calendars: &[CalendarInfo]) -> EventCreationData {
|
||||
use chrono::Local;
|
||||
|
||||
// Convert start datetime from UTC to local
|
||||
let start_local = event.dtstart.with_timezone(&Local).naive_local();
|
||||
// VEvent fields are already local time (NaiveDateTime)
|
||||
let start_local = event.dtstart;
|
||||
let end_local = if let Some(dtend) = event.dtend {
|
||||
dtend.with_timezone(&Local).naive_local()
|
||||
dtend
|
||||
} else {
|
||||
// Default to 1 hour after start if no end time
|
||||
start_local + chrono::Duration::hours(1)
|
||||
@@ -292,8 +291,10 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend
|
||||
|
||||
// Recurrence - Parse RRULE if present
|
||||
recurrence: if let Some(ref rrule_str) = event.rrule {
|
||||
web_sys::console::log_1(&format!("🐛 MODAL DEBUG: Event has RRULE: {}", rrule_str).into());
|
||||
parse_rrule_frequency(rrule_str)
|
||||
} else {
|
||||
web_sys::console::log_1(&"🐛 MODAL DEBUG: Event has no RRULE (singleton)".into());
|
||||
RecurrenceType::None
|
||||
},
|
||||
recurrence_interval: if let Some(ref rrule_str) = event.rrule {
|
||||
@@ -338,7 +339,10 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend
|
||||
},
|
||||
|
||||
// Edit tracking
|
||||
edit_scope: None, // Will be set by the modal after creation
|
||||
edit_scope: {
|
||||
web_sys::console::log_1(&"🐛 MODAL DEBUG: Setting edit_scope to None for vevent_to_creation_data".into());
|
||||
None // Will be set by the modal after creation
|
||||
},
|
||||
changed_fields: vec![],
|
||||
original_uid: Some(event.uid.clone()), // Preserve original UID for editing
|
||||
occurrence_date: Some(start_local.date()), // The occurrence date being edited
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct EventContextMenuProps {
|
||||
pub on_delete: Callback<DeleteAction>,
|
||||
pub on_view_details: Callback<VEvent>,
|
||||
pub on_close: Callback<()>,
|
||||
pub on_edit_singleton: Callback<VEvent>, // New callback for editing singleton events without scope
|
||||
}
|
||||
|
||||
#[function_component(EventContextMenu)]
|
||||
@@ -109,6 +110,18 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let create_singleton_edit_callback = {
|
||||
let on_edit_singleton = props.on_edit_singleton.clone();
|
||||
let on_close = props.on_close.clone();
|
||||
let event = props.event.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
if let Some(event) = &event {
|
||||
on_edit_singleton.emit(event.clone());
|
||||
}
|
||||
on_close.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
let create_delete_callback = |action: DeleteAction| {
|
||||
let on_delete = props.on_delete.clone();
|
||||
let on_close = props.on_close.clone();
|
||||
@@ -160,9 +173,9 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html {
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
// Regular single events - show edit option
|
||||
// Regular single events - show edit option without setting edit scope
|
||||
html! {
|
||||
<div class="context-menu-item" onclick={create_edit_callback(EditAction::EditThis)}>
|
||||
<div class="context-menu-item" onclick={create_singleton_edit_callback}>
|
||||
{"Edit Event"}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -148,57 +148,45 @@ impl EventCreationData {
|
||||
String, // reminder
|
||||
String, // recurrence
|
||||
Vec<bool>, // recurrence_days
|
||||
Option<String>, // calendar_path
|
||||
u32, // recurrence_interval
|
||||
Option<u32>, // recurrence_count
|
||||
Option<String>, // recurrence_until
|
||||
Option<String>, // calendar_path
|
||||
String, // timezone
|
||||
) {
|
||||
use chrono::{Local, TimeZone};
|
||||
|
||||
// Convert local date/time to UTC for backend
|
||||
let (utc_start_date, utc_start_time, utc_end_date, utc_end_time) = if self.all_day {
|
||||
// For all-day events, just use the dates as-is (no time conversion needed)
|
||||
(
|
||||
self.start_date.format("%Y-%m-%d").to_string(),
|
||||
self.start_time.format("%H:%M").to_string(),
|
||||
self.end_date.format("%Y-%m-%d").to_string(),
|
||||
self.end_time.format("%H:%M").to_string(),
|
||||
)
|
||||
// Use local date/times and timezone - no UTC conversion
|
||||
let effective_end_date = if self.end_time == NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
|
||||
// If end time is midnight (00:00), treat it as beginning of next day
|
||||
self.end_date + chrono::Duration::days(1)
|
||||
} else {
|
||||
// Convert local date/time to UTC, but preserve original local dates
|
||||
let start_local = Local.from_local_datetime(&self.start_date.and_time(self.start_time)).single();
|
||||
let end_local = Local.from_local_datetime(&self.end_date.and_time(self.end_time)).single();
|
||||
|
||||
if let (Some(start_dt), Some(end_dt)) = (start_local, end_local) {
|
||||
let start_utc = start_dt.with_timezone(&chrono::Utc);
|
||||
let end_utc = end_dt.with_timezone(&chrono::Utc);
|
||||
|
||||
// IMPORTANT: Use original local dates, not UTC dates!
|
||||
// This ensures events display on the correct day regardless of timezone conversion
|
||||
(
|
||||
self.start_date.format("%Y-%m-%d").to_string(),
|
||||
start_utc.format("%H:%M").to_string(),
|
||||
self.end_date.format("%Y-%m-%d").to_string(),
|
||||
end_utc.format("%H:%M").to_string(),
|
||||
)
|
||||
} else {
|
||||
// Fallback if timezone conversion fails - use local time as-is
|
||||
web_sys::console::warn_1(&"⚠️ Failed to convert local time to UTC, using local time".into());
|
||||
(
|
||||
self.start_date.format("%Y-%m-%d").to_string(),
|
||||
self.start_time.format("%H:%M").to_string(),
|
||||
self.end_date.format("%Y-%m-%d").to_string(),
|
||||
self.end_time.format("%H:%M").to_string(),
|
||||
)
|
||||
}
|
||||
self.end_date
|
||||
};
|
||||
|
||||
// Get the local timezone
|
||||
let timezone = {
|
||||
use js_sys::Date;
|
||||
let date = Date::new_0();
|
||||
let timezone_offset = date.get_timezone_offset(); // Minutes from UTC
|
||||
let hours = -(timezone_offset as i32) / 60; // Convert to hours, negate for proper sign
|
||||
let minutes = (timezone_offset as i32).abs() % 60;
|
||||
format!("{:+03}:{:02}", hours, minutes) // Format as +05:00 or -04:00
|
||||
};
|
||||
|
||||
let (start_date, start_time, end_date, end_time) = (
|
||||
self.start_date.format("%Y-%m-%d").to_string(),
|
||||
self.start_time.format("%H:%M").to_string(),
|
||||
effective_end_date.format("%Y-%m-%d").to_string(),
|
||||
self.end_time.format("%H:%M").to_string(),
|
||||
);
|
||||
|
||||
(
|
||||
self.title.clone(),
|
||||
self.description.clone(),
|
||||
utc_start_date,
|
||||
utc_start_time,
|
||||
utc_end_date,
|
||||
utc_end_time,
|
||||
start_date,
|
||||
start_time,
|
||||
end_date,
|
||||
end_time,
|
||||
self.location.clone(),
|
||||
self.all_day,
|
||||
format!("{:?}", self.status).to_uppercase(),
|
||||
@@ -210,9 +198,11 @@ impl EventCreationData {
|
||||
format!("{:?}", self.reminder),
|
||||
format!("{:?}", self.recurrence),
|
||||
self.recurrence_days.clone(),
|
||||
self.selected_calendar.clone(),
|
||||
self.recurrence_interval,
|
||||
self.recurrence_count,
|
||||
self.recurrence_until.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||
self.selected_calendar.clone(),
|
||||
timezone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::models::ical::VEvent;
|
||||
use chrono::{DateTime, Utc};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
@@ -213,7 +212,7 @@ pub fn EventModal(props: &EventModalProps) -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_datetime(dt: &DateTime<Utc>, all_day: bool) -> String {
|
||||
fn format_datetime(dt: &chrono::NaiveDateTime, all_day: bool) -> String {
|
||||
if all_day {
|
||||
dt.format("%B %d, %Y").to_string()
|
||||
} else {
|
||||
@@ -221,7 +220,7 @@ fn format_datetime(dt: &DateTime<Utc>, all_day: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_datetime_end(dt: &DateTime<Utc>, all_day: bool) -> String {
|
||||
fn format_datetime_end(dt: &chrono::NaiveDateTime, all_day: bool) -> String {
|
||||
if all_day {
|
||||
// For all-day events, subtract one day from end date for display
|
||||
// RFC-5545 uses exclusive end dates, but users expect inclusive display
|
||||
|
||||
@@ -38,7 +38,7 @@ pub struct RouteHandlerProps {
|
||||
chrono::NaiveDateTime,
|
||||
chrono::NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
@@ -136,7 +136,7 @@ pub struct CalendarViewProps {
|
||||
chrono::NaiveDateTime,
|
||||
chrono::NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct WeekViewProps {
|
||||
NaiveDateTime,
|
||||
NaiveDateTime,
|
||||
bool,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::NaiveDateTime>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
@@ -285,18 +285,14 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
|
||||
// Calculate the day before this occurrence for UNTIL clause
|
||||
let until_date =
|
||||
edit.event.dtstart.date_naive() - chrono::Duration::days(1);
|
||||
edit.event.dtstart.date() - chrono::Duration::days(1);
|
||||
let until_datetime = until_date
|
||||
.and_time(chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap());
|
||||
let until_utc =
|
||||
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||
until_datetime,
|
||||
chrono::Utc,
|
||||
);
|
||||
let until_naive = until_datetime; // Use local time directly
|
||||
|
||||
web_sys::console::log_1(&format!("🔄 Will set UNTIL {} for original series to end before occurrence {}",
|
||||
until_utc.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
edit.event.dtstart.format("%Y-%m-%d %H:%M:%S UTC")).into());
|
||||
until_naive.format("%Y-%m-%d %H:%M:%S"),
|
||||
edit.event.dtstart.format("%Y-%m-%d %H:%M:%S")).into());
|
||||
|
||||
// Critical: Use the dragged times (new_start/new_end) not the original series times
|
||||
// This ensures the new series reflects the user's drag operation
|
||||
@@ -317,7 +313,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
new_start, // Dragged start time for new series
|
||||
new_end, // Dragged end time for new series
|
||||
true, // preserve_rrule = true
|
||||
Some(until_utc), // UNTIL date for original series
|
||||
Some(until_naive), // UNTIL date for original series
|
||||
Some("this_and_future".to_string()), // Update scope
|
||||
Some(occurrence_date), // Date of occurrence being modified
|
||||
));
|
||||
@@ -617,10 +613,9 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
|
||||
// Keep the original end time
|
||||
let original_end = if let Some(end) = event.dtend {
|
||||
end.with_timezone(&chrono::Local).naive_local()
|
||||
} else {
|
||||
end } else {
|
||||
// If no end time, use start time + 1 hour as default
|
||||
event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1)
|
||||
event.dtstart + chrono::Duration::hours(1)
|
||||
};
|
||||
|
||||
let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time);
|
||||
@@ -651,8 +646,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
// Calculate new end time based on drag position
|
||||
let new_end_time = pixels_to_time(current_drag.current_y, time_increment);
|
||||
|
||||
// Keep the original start time
|
||||
let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local();
|
||||
// Keep the original start time (already local)
|
||||
let original_start = event.dtstart;
|
||||
|
||||
let new_end_datetime = NaiveDateTime::new(current_drag.start_date, new_end_time);
|
||||
|
||||
@@ -827,9 +822,9 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let time_display = if event.all_day {
|
||||
"All Day".to_string()
|
||||
} else {
|
||||
let local_start = event.dtstart.with_timezone(&Local);
|
||||
let local_start = event.dtstart;
|
||||
if let Some(end) = event.dtend {
|
||||
let local_end = end.with_timezone(&Local);
|
||||
let local_end = end;
|
||||
|
||||
// Check if both times are in same AM/PM period to avoid redundancy
|
||||
let start_is_am = local_start.hour() < 12;
|
||||
@@ -1056,14 +1051,13 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
// Show the event being resized from the start
|
||||
let new_start_time = pixels_to_time(drag.current_y, props.time_increment);
|
||||
let original_end = if let Some(end) = event.dtend {
|
||||
end.with_timezone(&chrono::Local).naive_local()
|
||||
} else {
|
||||
event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1)
|
||||
end } else {
|
||||
event.dtstart + chrono::Duration::hours(1)
|
||||
};
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour);
|
||||
let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local());
|
||||
let original_duration = original_end.signed_duration_since(event.dtstart);
|
||||
let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32);
|
||||
|
||||
let new_start_pixels = drag.current_y;
|
||||
@@ -1089,7 +1083,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
DragType::ResizeEventEnd(event) => {
|
||||
// Show the event being resized from the end
|
||||
let new_end_time = pixels_to_time(drag.current_y, props.time_increment);
|
||||
let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local();
|
||||
let original_start = event.dtstart;
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour);
|
||||
@@ -1227,12 +1221,12 @@ fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime {
|
||||
}
|
||||
|
||||
fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32, print_pixels_per_hour: Option<f64>, print_start_hour: Option<u32>) -> (f32, f32, bool) {
|
||||
// Convert UTC times to local time for display
|
||||
let local_start = event.dtstart.with_timezone(&Local);
|
||||
// Events are already in local time
|
||||
let local_start = event.dtstart;
|
||||
|
||||
// Events should display based on their stored date (which now preserves the original local date)
|
||||
// not the calculated local date from UTC conversion, since we fixed the creation logic
|
||||
let event_date = event.dtstart.date_naive(); // Use the stored date, not the converted local date
|
||||
// Events should display based on their local date, since we now store proper UTC times
|
||||
// Convert the UTC stored time back to local time to determine display date
|
||||
let event_date = local_start.date();
|
||||
|
||||
if event_date != date {
|
||||
return (0.0, 0.0, false); // Event not on this date
|
||||
@@ -1263,8 +1257,8 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32
|
||||
|
||||
// Calculate duration and height
|
||||
let duration_pixels = if let Some(end) = event.dtend {
|
||||
let local_end = end.with_timezone(&Local);
|
||||
let end_date = local_end.date_naive();
|
||||
let local_end = end;
|
||||
let end_date = local_end.date();
|
||||
|
||||
// Handle events that span multiple days by capping at midnight
|
||||
if end_date > date {
|
||||
@@ -1291,16 +1285,16 @@ fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
let start1 = event1.dtstart.with_timezone(&Local).naive_local();
|
||||
let start1 = event1.dtstart;
|
||||
let end1 = if let Some(end) = event1.dtend {
|
||||
end.with_timezone(&Local).naive_local()
|
||||
end
|
||||
} else {
|
||||
start1 + chrono::Duration::hours(1) // Default 1 hour duration
|
||||
};
|
||||
|
||||
let start2 = event2.dtstart.with_timezone(&Local).naive_local();
|
||||
let start2 = event2.dtstart;
|
||||
let end2 = if let Some(end) = event2.dtend {
|
||||
end.with_timezone(&Local).naive_local()
|
||||
end
|
||||
} else {
|
||||
start2 + chrono::Duration::hours(1) // Default 1 hour duration
|
||||
};
|
||||
@@ -1322,8 +1316,8 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
||||
}
|
||||
|
||||
let (_, _, _) = calculate_event_position(event, date, time_increment, None, None);
|
||||
let local_start = event.dtstart.with_timezone(&Local);
|
||||
let event_date = local_start.date_naive();
|
||||
let local_start = event.dtstart;
|
||||
let event_date = local_start.date();
|
||||
if event_date == date ||
|
||||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20) {
|
||||
Some((idx, event))
|
||||
@@ -1334,7 +1328,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
||||
.collect();
|
||||
|
||||
// Sort by start time
|
||||
day_events.sort_by_key(|(_, event)| event.dtstart.with_timezone(&Local).naive_local());
|
||||
day_events.sort_by_key(|(_, event)| event.dtstart);
|
||||
|
||||
// For each event, find all events it overlaps with
|
||||
let mut event_columns = vec![(0, 1); events.len()]; // (column_idx, total_columns)
|
||||
@@ -1359,7 +1353,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
||||
} else {
|
||||
// This event overlaps - we need to calculate column layout
|
||||
// Sort the overlapping group by start time
|
||||
overlapping_events.sort_by_key(|&idx| day_events[idx].1.dtstart.with_timezone(&Local).naive_local());
|
||||
overlapping_events.sort_by_key(|&idx| day_events[idx].1.dtstart);
|
||||
|
||||
// Assign columns using a greedy algorithm
|
||||
let mut columns: Vec<Vec<usize>> = Vec::new();
|
||||
@@ -1407,19 +1401,19 @@ fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool {
|
||||
let start_date = if event.all_day {
|
||||
// For all-day events, extract date directly from UTC without timezone conversion
|
||||
// since all-day events are stored at noon UTC to avoid timezone boundary issues
|
||||
event.dtstart.date_naive()
|
||||
event.dtstart.date()
|
||||
} else {
|
||||
event.dtstart.with_timezone(&Local).date_naive()
|
||||
event.dtstart.date()
|
||||
};
|
||||
|
||||
let end_date = if let Some(dtend) = event.dtend {
|
||||
if event.all_day {
|
||||
// For all-day events, dtend is set to the day after the last day (RFC 5545)
|
||||
// Extract date directly from UTC and subtract a day to get actual last day
|
||||
dtend.date_naive() - chrono::Duration::days(1)
|
||||
dtend.date() - chrono::Duration::days(1)
|
||||
} else {
|
||||
// For timed events, use timezone conversion
|
||||
dtend.with_timezone(&Local).date_naive()
|
||||
dtend.date()
|
||||
}
|
||||
} else {
|
||||
// Single day event
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveDate, TimeZone, Utc, Weekday};
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveDate, Utc, Weekday};
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -275,14 +275,60 @@ impl CalendarService {
|
||||
grouped
|
||||
}
|
||||
|
||||
/// Convert UTC events to local timezone for display
|
||||
fn convert_utc_to_local(mut event: VEvent) -> VEvent {
|
||||
// Check if event times are in UTC (legacy events from before timezone migration)
|
||||
let is_utc_event = event.dtstart_tzid.as_ref().map_or(true, |tz| tz == "UTC");
|
||||
|
||||
if is_utc_event {
|
||||
web_sys::console::log_1(&format!(
|
||||
"🕐 Converting UTC event '{}' to local time",
|
||||
event.summary.as_deref().unwrap_or("Untitled")
|
||||
).into());
|
||||
|
||||
// Get current timezone offset (convert from UTC to local)
|
||||
let date = js_sys::Date::new_0();
|
||||
let timezone_offset_minutes = date.get_timezone_offset() as i32;
|
||||
|
||||
// Convert start time from UTC to local
|
||||
// getTimezoneOffset() returns minutes UTC is ahead of local time
|
||||
// To convert UTC to local, we subtract the offset (add negative offset)
|
||||
let local_start = event.dtstart + chrono::Duration::minutes(-timezone_offset_minutes as i64);
|
||||
event.dtstart = local_start;
|
||||
event.dtstart_tzid = None; // Clear UTC timezone indicator
|
||||
|
||||
// Convert end time if present
|
||||
if let Some(end_utc) = event.dtend {
|
||||
let local_end = end_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64);
|
||||
event.dtend = Some(local_end);
|
||||
event.dtend_tzid = None; // Clear UTC timezone indicator
|
||||
}
|
||||
|
||||
// Convert created/modified times if present
|
||||
if let Some(created_utc) = event.created {
|
||||
event.created = Some(created_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64));
|
||||
event.created_tzid = None;
|
||||
}
|
||||
|
||||
if let Some(modified_utc) = event.last_modified {
|
||||
event.last_modified = Some(modified_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64));
|
||||
event.last_modified_tzid = None;
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
/// Expand recurring events using VEvent (RFC 5545 compliant)
|
||||
pub fn expand_recurring_events(events: Vec<VEvent>) -> Vec<VEvent> {
|
||||
let mut expanded_events = Vec::new();
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let today = chrono::Local::now().date_naive();
|
||||
let start_range = today - Duration::days(36500); // Show past 100 years (to catch any historical yearly events)
|
||||
let end_range = today + Duration::days(36500); // Show next 100 years
|
||||
|
||||
for event in events {
|
||||
// Convert UTC events to local time for proper display
|
||||
let event = Self::convert_utc_to_local(event);
|
||||
if let Some(ref rrule) = event.rrule {
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
@@ -372,17 +418,18 @@ impl CalendarService {
|
||||
|
||||
// Get UNTIL date if specified
|
||||
let until_date = components.get("UNTIL").and_then(|until_str| {
|
||||
// Parse UNTIL date in YYYYMMDDTHHMMSSZ format
|
||||
// Parse UNTIL date in YYYYMMDDTHHMMSSZ format (treat as local time)
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(
|
||||
until_str.trim_end_matches('Z'),
|
||||
"%Y%m%dT%H%M%S",
|
||||
) {
|
||||
Some(chrono::Utc.from_utc_datetime(&dt))
|
||||
Some(dt)
|
||||
} else if let Ok(dt) = chrono::DateTime::parse_from_str(until_str, "%Y%m%dT%H%M%SZ") {
|
||||
Some(dt.with_timezone(&chrono::Utc))
|
||||
// Convert UTC to local (naive) time for consistency
|
||||
Some(dt.naive_utc())
|
||||
} else if let Ok(date) = chrono::NaiveDate::parse_from_str(until_str, "%Y%m%d") {
|
||||
// Handle date-only UNTIL
|
||||
Some(chrono::Utc.from_utc_datetime(&date.and_hms_opt(23, 59, 59).unwrap()))
|
||||
Some(date.and_hms_opt(23, 59, 59).unwrap())
|
||||
} else {
|
||||
web_sys::console::log_1(
|
||||
&format!("⚠️ Failed to parse UNTIL date: {}", until_str).into(),
|
||||
@@ -395,7 +442,7 @@ impl CalendarService {
|
||||
web_sys::console::log_1(&format!("📅 RRULE has UNTIL: {}", until).into());
|
||||
}
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let start_date = base_event.dtstart.date();
|
||||
let mut current_date = start_date;
|
||||
let mut occurrence_count = 0;
|
||||
|
||||
@@ -422,8 +469,8 @@ impl CalendarService {
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
// Compare dates ignoring sub-second precision
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let exception_naive = exception_date.and_utc();
|
||||
let occurrence_naive = occurrence_datetime.and_utc();
|
||||
|
||||
// Check if dates match (within a minute to handle minor time differences)
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
@@ -555,7 +602,7 @@ impl CalendarService {
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
until_date: Option<chrono::NaiveDateTime>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
@@ -565,7 +612,7 @@ impl CalendarService {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let start_date = base_event.dtstart.date();
|
||||
|
||||
// Find the Monday of the week containing the start_date (reference week)
|
||||
let reference_week_start =
|
||||
@@ -623,8 +670,8 @@ impl CalendarService {
|
||||
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let exception_naive = exception_date.and_utc();
|
||||
let occurrence_naive = occurrence_datetime.and_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let matches = diff.num_seconds().abs() < 60;
|
||||
|
||||
@@ -675,7 +722,7 @@ impl CalendarService {
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
until_date: Option<chrono::NaiveDateTime>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
@@ -691,7 +738,7 @@ impl CalendarService {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let start_date = base_event.dtstart.date();
|
||||
let mut current_month_start =
|
||||
NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||
let mut total_occurrences = 0;
|
||||
@@ -749,9 +796,7 @@ impl CalendarService {
|
||||
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let diff = occurrence_datetime - *exception_date;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
@@ -792,14 +837,14 @@ impl CalendarService {
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
until_date: Option<chrono::NaiveDateTime>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
|
||||
// Parse BYDAY for monthly (e.g., "1MO" = first Monday, "-1FR" = last Friday)
|
||||
if let Some((position, weekday)) = Self::parse_monthly_byday(byday) {
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let start_date = base_event.dtstart.date();
|
||||
let mut current_month_start =
|
||||
NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||
let mut total_occurrences = 0;
|
||||
@@ -830,9 +875,7 @@ impl CalendarService {
|
||||
|
||||
// Check EXDATE
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let diff = occurrence_datetime - *exception_date;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
@@ -871,7 +914,7 @@ impl CalendarService {
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
until_date: Option<chrono::NaiveDateTime>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
@@ -887,7 +930,7 @@ impl CalendarService {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let start_date = base_event.dtstart.date();
|
||||
let mut current_year = start_date.year();
|
||||
let mut total_occurrences = 0;
|
||||
|
||||
@@ -930,9 +973,7 @@ impl CalendarService {
|
||||
|
||||
// Check EXDATE
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let diff = occurrence_datetime - *exception_date;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
@@ -1257,9 +1298,11 @@ impl CalendarService {
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
recurrence_interval: u32,
|
||||
recurrence_count: Option<u32>,
|
||||
recurrence_until: Option<String>,
|
||||
calendar_path: Option<String>,
|
||||
timezone: String,
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -1290,10 +1333,11 @@ impl CalendarService {
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"recurrence_interval": 1_u32, // Default interval
|
||||
"recurrence_interval": recurrence_interval,
|
||||
"recurrence_end_date": recurrence_until,
|
||||
"recurrence_count": recurrence_count,
|
||||
"calendar_path": calendar_path
|
||||
"calendar_path": calendar_path,
|
||||
"timezone": timezone
|
||||
});
|
||||
let url = format!("{}/calendar/events/series/create", self.base_url);
|
||||
(body, url)
|
||||
@@ -1317,7 +1361,8 @@ impl CalendarService {
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"calendar_path": calendar_path
|
||||
"calendar_path": calendar_path,
|
||||
"timezone": timezone
|
||||
});
|
||||
let url = format!("{}/calendar/events/create", self.base_url);
|
||||
(body, url)
|
||||
@@ -1394,10 +1439,11 @@ impl CalendarService {
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
recurrence_interval: u32,
|
||||
recurrence_count: Option<u32>,
|
||||
recurrence_until: Option<String>,
|
||||
calendar_path: Option<String>,
|
||||
exception_dates: Vec<DateTime<Utc>>,
|
||||
update_action: Option<String>,
|
||||
until_date: Option<DateTime<Utc>>,
|
||||
timezone: String,
|
||||
) -> Result<(), String> {
|
||||
// Forward to update_event_with_scope with default scope
|
||||
self.update_event_with_scope(
|
||||
@@ -1421,10 +1467,11 @@ impl CalendarService {
|
||||
reminder,
|
||||
recurrence,
|
||||
recurrence_days,
|
||||
recurrence_interval,
|
||||
recurrence_count,
|
||||
recurrence_until,
|
||||
calendar_path,
|
||||
exception_dates,
|
||||
update_action,
|
||||
until_date,
|
||||
timezone,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1451,10 +1498,11 @@ impl CalendarService {
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
recurrence_interval: u32,
|
||||
recurrence_count: Option<u32>,
|
||||
recurrence_until: Option<String>,
|
||||
calendar_path: Option<String>,
|
||||
exception_dates: Vec<DateTime<Utc>>,
|
||||
update_action: Option<String>,
|
||||
until_date: Option<DateTime<Utc>>,
|
||||
timezone: String,
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -1482,11 +1530,11 @@ impl CalendarService {
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"recurrence_interval": recurrence_interval,
|
||||
"recurrence_count": recurrence_count,
|
||||
"recurrence_end_date": recurrence_until,
|
||||
"calendar_path": calendar_path,
|
||||
"update_action": update_action,
|
||||
"occurrence_date": null,
|
||||
"exception_dates": exception_dates.iter().map(|dt| dt.to_rfc3339()).collect::<Vec<String>>(),
|
||||
"until_date": until_date.as_ref().map(|dt| dt.to_rfc3339())
|
||||
"timezone": timezone
|
||||
});
|
||||
let url = format!("{}/calendar/events/update", self.base_url);
|
||||
|
||||
@@ -1687,11 +1735,13 @@ impl CalendarService {
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
recurrence_interval: u32,
|
||||
recurrence_count: Option<u32>,
|
||||
recurrence_until: Option<String>,
|
||||
calendar_path: Option<String>,
|
||||
update_scope: String,
|
||||
occurrence_date: Option<String>,
|
||||
timezone: String,
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -1718,12 +1768,13 @@ impl CalendarService {
|
||||
"reminder": reminder,
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"recurrence_interval": 1_u32, // Default interval - could be enhanced to be a parameter
|
||||
"recurrence_interval": recurrence_interval,
|
||||
"recurrence_end_date": recurrence_until,
|
||||
"recurrence_count": recurrence_count,
|
||||
"calendar_path": calendar_path,
|
||||
"update_scope": update_scope,
|
||||
"occurrence_date": occurrence_date
|
||||
"occurrence_date": occurrence_date,
|
||||
"timezone": timezone
|
||||
});
|
||||
|
||||
let url = format!("{}/calendar/events/series/update", self.base_url);
|
||||
|
||||
Reference in New Issue
Block a user