Compare commits
2 Commits
235dcf8e1d
...
289284a532
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
289284a532 | ||
|
|
089f4ce105 |
@@ -852,10 +852,11 @@ fn parse_event_datetime(
|
|||||||
.map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?;
|
.map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?;
|
||||||
|
|
||||||
if all_day {
|
if all_day {
|
||||||
// For all-day events, use midnight UTC
|
// For all-day events, use noon UTC to avoid timezone boundary issues
|
||||||
|
// This ensures the date remains correct when converted to any local timezone
|
||||||
let datetime = date
|
let datetime = date
|
||||||
.and_hms_opt(0, 0, 0)
|
.and_hms_opt(12, 0, 0)
|
||||||
.ok_or_else(|| "Failed to create midnight datetime".to_string())?;
|
.ok_or_else(|| "Failed to create noon datetime".to_string())?;
|
||||||
Ok(Utc.from_utc_datetime(&datetime))
|
Ok(Utc.from_utc_datetime(&datetime))
|
||||||
} else {
|
} else {
|
||||||
// Parse the time
|
// Parse the time
|
||||||
|
|||||||
@@ -262,8 +262,8 @@ pub async fn update_event_series(
|
|||||||
Json(request): Json<UpdateEventSeriesRequest>,
|
Json(request): Json<UpdateEventSeriesRequest>,
|
||||||
) -> Result<Json<UpdateEventSeriesResponse>, ApiError> {
|
) -> Result<Json<UpdateEventSeriesResponse>, ApiError> {
|
||||||
println!(
|
println!(
|
||||||
"🔄 Update event series request received: series_uid='{}', update_scope='{}'",
|
"🔄 Update event series request received: series_uid='{}', update_scope='{}', recurrence_count={:?}, recurrence_end_date={:?}",
|
||||||
request.series_uid, request.update_scope
|
request.series_uid, request.update_scope, request.recurrence_count, request.recurrence_end_date
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extract and verify token
|
// Extract and verify token
|
||||||
@@ -397,8 +397,9 @@ pub async fn update_event_series(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (start_datetime, end_datetime) = if request.all_day {
|
let (start_datetime, end_datetime) = if request.all_day {
|
||||||
|
// For all-day events, use noon UTC to avoid timezone boundary issues
|
||||||
let start_dt = start_date
|
let start_dt = start_date
|
||||||
.and_hms_opt(0, 0, 0)
|
.and_hms_opt(12, 0, 0)
|
||||||
.ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?;
|
.ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?;
|
||||||
|
|
||||||
// For all-day events, also preserve the original date pattern
|
// For all-day events, also preserve the original date pattern
|
||||||
@@ -414,20 +415,13 @@ pub async fn update_event_series(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let end_dt = end_date
|
let end_dt = end_date
|
||||||
.and_hms_opt(23, 59, 59)
|
.and_hms_opt(12, 0, 0)
|
||||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||||
|
|
||||||
// Convert from local time to UTC
|
// For all-day events, use UTC directly (no local conversion needed)
|
||||||
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
|
||||||
.single()
|
|
||||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
|
||||||
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
|
||||||
.single()
|
|
||||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
|
||||||
|
|
||||||
(
|
(
|
||||||
start_local.with_timezone(&chrono::Utc),
|
chrono::Utc.from_utc_datetime(&start_dt),
|
||||||
end_local.with_timezone(&chrono::Utc),
|
chrono::Utc.from_utc_datetime(&end_dt),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let start_time = if !request.start_time.is_empty() {
|
let start_time = if !request.start_time.is_empty() {
|
||||||
@@ -765,9 +759,36 @@ fn update_entire_series(
|
|||||||
updated_event.last_modified = Some(now);
|
updated_event.last_modified = Some(now);
|
||||||
// Keep original created timestamp to preserve event history
|
// Keep original created timestamp to preserve event history
|
||||||
|
|
||||||
// For simple updates (like drag operations), preserve the existing RRULE
|
// Update RRULE if recurrence parameters are provided
|
||||||
// For more complex updates, we might need to regenerate it, but for now keep it simple
|
if let Some(ref existing_rrule) = updated_event.rrule {
|
||||||
// updated_event.rrule remains unchanged from the clone
|
let mut new_rrule = existing_rrule.clone();
|
||||||
|
println!("🔄 Original RRULE: {}", existing_rrule);
|
||||||
|
|
||||||
|
// Update COUNT if provided
|
||||||
|
if let Some(count) = request.recurrence_count {
|
||||||
|
println!("🔄 Updating RRULE with new COUNT: {}", count);
|
||||||
|
// Remove old COUNT or UNTIL parameters
|
||||||
|
new_rrule = new_rrule.split(';')
|
||||||
|
.filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL="))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";");
|
||||||
|
// Add new COUNT
|
||||||
|
new_rrule = format!("{};COUNT={}", new_rrule, count);
|
||||||
|
} else if let Some(ref end_date) = request.recurrence_end_date {
|
||||||
|
println!("🔄 Updating RRULE with new UNTIL: {}", end_date);
|
||||||
|
// Remove old COUNT or UNTIL parameters
|
||||||
|
new_rrule = new_rrule.split(';')
|
||||||
|
.filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL="))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";");
|
||||||
|
// Add new UNTIL (convert YYYY-MM-DD to YYYYMMDD format)
|
||||||
|
let until_date = end_date.replace("-", "");
|
||||||
|
new_rrule = format!("{};UNTIL={}", new_rrule, until_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("🔄 Updated RRULE: {}", new_rrule);
|
||||||
|
updated_event.rrule = Some(new_rrule);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy the updated event back to existing_event for the main handler
|
// Copy the updated event back to existing_event for the main handler
|
||||||
*existing_event = updated_event.clone();
|
*existing_event = updated_event.clone();
|
||||||
|
|||||||
@@ -478,7 +478,10 @@ pub fn App() -> Html {
|
|||||||
params.13, // categories
|
params.13, // categories
|
||||||
params.14, // reminder
|
params.14, // reminder
|
||||||
params.15, // recurrence
|
params.15, // recurrence
|
||||||
params.17, // calendar_path (skipping recurrence_days)
|
params.16, // recurrence_days
|
||||||
|
params.18, // recurrence_count
|
||||||
|
params.19, // recurrence_until
|
||||||
|
params.17, // calendar_path
|
||||||
scope,
|
scope,
|
||||||
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
|
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
|
||||||
)
|
)
|
||||||
@@ -759,6 +762,9 @@ pub fn App() -> Html {
|
|||||||
original_event.categories.join(","),
|
original_event.categories.join(","),
|
||||||
reminder_str.clone(),
|
reminder_str.clone(),
|
||||||
recurrence_str.clone(),
|
recurrence_str.clone(),
|
||||||
|
vec![false; 7],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
original_event.calendar_path.clone(),
|
original_event.calendar_path.clone(),
|
||||||
scope.clone(),
|
scope.clone(),
|
||||||
occurrence_date,
|
occurrence_date,
|
||||||
|
|||||||
@@ -290,16 +290,32 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend
|
|||||||
// Reminders - TODO: Parse alarm from VEvent if needed
|
// Reminders - TODO: Parse alarm from VEvent if needed
|
||||||
reminder: ReminderType::None,
|
reminder: ReminderType::None,
|
||||||
|
|
||||||
// Recurrence - TODO: Parse RRULE if needed for advanced editing
|
// Recurrence - Parse RRULE if present
|
||||||
recurrence: if event.rrule.is_some() {
|
recurrence: if let Some(ref rrule_str) = event.rrule {
|
||||||
RecurrenceType::Daily // Default, could be enhanced to parse actual RRULE
|
parse_rrule_frequency(rrule_str)
|
||||||
} else {
|
} else {
|
||||||
RecurrenceType::None
|
RecurrenceType::None
|
||||||
},
|
},
|
||||||
recurrence_interval: 1,
|
recurrence_interval: if let Some(ref rrule_str) = event.rrule {
|
||||||
recurrence_until: None,
|
parse_rrule_interval(rrule_str)
|
||||||
recurrence_count: None,
|
} else {
|
||||||
recurrence_days: vec![false; 7],
|
1
|
||||||
|
},
|
||||||
|
recurrence_until: if let Some(ref rrule_str) = event.rrule {
|
||||||
|
parse_rrule_until(rrule_str)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
recurrence_count: if let Some(ref rrule_str) = event.rrule {
|
||||||
|
parse_rrule_count(rrule_str)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
recurrence_days: if let Some(ref rrule_str) = event.rrule {
|
||||||
|
parse_rrule_days(rrule_str)
|
||||||
|
} else {
|
||||||
|
vec![false; 7]
|
||||||
|
},
|
||||||
|
|
||||||
// Advanced recurrence
|
// Advanced recurrence
|
||||||
monthly_by_day: None,
|
monthly_by_day: None,
|
||||||
@@ -327,4 +343,113 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend
|
|||||||
original_uid: Some(event.uid.clone()), // Preserve original UID for editing
|
original_uid: Some(event.uid.clone()), // Preserve original UID for editing
|
||||||
occurrence_date: Some(start_local.date()), // The occurrence date being edited
|
occurrence_date: Some(start_local.date()), // The occurrence date being edited
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RRULE frequency component
|
||||||
|
fn parse_rrule_frequency(rrule: &str) -> RecurrenceType {
|
||||||
|
if rrule.contains("FREQ=DAILY") {
|
||||||
|
RecurrenceType::Daily
|
||||||
|
} else if rrule.contains("FREQ=WEEKLY") {
|
||||||
|
RecurrenceType::Weekly
|
||||||
|
} else if rrule.contains("FREQ=MONTHLY") {
|
||||||
|
RecurrenceType::Monthly
|
||||||
|
} else if rrule.contains("FREQ=YEARLY") {
|
||||||
|
RecurrenceType::Yearly
|
||||||
|
} else {
|
||||||
|
RecurrenceType::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RRULE interval component
|
||||||
|
fn parse_rrule_interval(rrule: &str) -> u32 {
|
||||||
|
if let Some(start) = rrule.find("INTERVAL=") {
|
||||||
|
let interval_part = &rrule[start + 9..];
|
||||||
|
if let Some(end) = interval_part.find(';') {
|
||||||
|
interval_part[..end].parse().unwrap_or(1)
|
||||||
|
} else {
|
||||||
|
interval_part.parse().unwrap_or(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RRULE count component
|
||||||
|
fn parse_rrule_count(rrule: &str) -> Option<u32> {
|
||||||
|
if let Some(start) = rrule.find("COUNT=") {
|
||||||
|
let count_part = &rrule[start + 6..];
|
||||||
|
if let Some(end) = count_part.find(';') {
|
||||||
|
count_part[..end].parse().ok()
|
||||||
|
} else {
|
||||||
|
count_part.parse().ok()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RRULE until component
|
||||||
|
fn parse_rrule_until(rrule: &str) -> Option<chrono::NaiveDate> {
|
||||||
|
if let Some(start) = rrule.find("UNTIL=") {
|
||||||
|
let until_part = &rrule[start + 6..];
|
||||||
|
let until_str = if let Some(end) = until_part.find(';') {
|
||||||
|
&until_part[..end]
|
||||||
|
} else {
|
||||||
|
until_part
|
||||||
|
};
|
||||||
|
|
||||||
|
// UNTIL can be in format YYYYMMDD or YYYYMMDDTHHMMSSZ
|
||||||
|
let date_part = if until_str.len() >= 8 {
|
||||||
|
&until_str[..8]
|
||||||
|
} else {
|
||||||
|
until_str
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse YYYYMMDD format
|
||||||
|
if date_part.len() == 8 {
|
||||||
|
if let (Ok(year), Ok(month), Ok(day)) = (
|
||||||
|
date_part[0..4].parse::<i32>(),
|
||||||
|
date_part[4..6].parse::<u32>(),
|
||||||
|
date_part[6..8].parse::<u32>(),
|
||||||
|
) {
|
||||||
|
chrono::NaiveDate::from_ymd_opt(year, month, day)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse RRULE BYDAY component for weekly recurrence
|
||||||
|
fn parse_rrule_days(rrule: &str) -> Vec<bool> {
|
||||||
|
let mut days = vec![false; 7]; // [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
|
||||||
|
|
||||||
|
if let Some(start) = rrule.find("BYDAY=") {
|
||||||
|
let byday_part = &rrule[start + 6..];
|
||||||
|
let byday_str = if let Some(end) = byday_part.find(';') {
|
||||||
|
&byday_part[..end]
|
||||||
|
} else {
|
||||||
|
byday_part
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse comma-separated day codes: SU,MO,TU,WE,TH,FR,SA
|
||||||
|
for day_code in byday_str.split(',') {
|
||||||
|
match day_code.trim() {
|
||||||
|
"SU" => days[0] = true, // Sunday
|
||||||
|
"MO" => days[1] = true, // Monday
|
||||||
|
"TU" => days[2] = true, // Tuesday
|
||||||
|
"WE" => days[3] = true, // Wednesday
|
||||||
|
"TH" => days[4] = true, // Thursday
|
||||||
|
"FR" => days[5] = true, // Friday
|
||||||
|
"SA" => days[6] = true, // Saturday
|
||||||
|
_ => {} // Ignore unknown day codes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
days
|
||||||
}
|
}
|
||||||
@@ -1320,11 +1320,23 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
|||||||
|
|
||||||
// Check if an all-day event spans the given date
|
// Check if an all-day event spans the given date
|
||||||
fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool {
|
fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool {
|
||||||
let start_date = event.dtstart.with_timezone(&Local).date_naive();
|
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()
|
||||||
|
} else {
|
||||||
|
event.dtstart.with_timezone(&Local).date_naive()
|
||||||
|
};
|
||||||
|
|
||||||
let end_date = if let Some(dtend) = event.dtend {
|
let end_date = if let Some(dtend) = event.dtend {
|
||||||
// For all-day events, dtend is often set to the day after the last day
|
if event.all_day {
|
||||||
// So we need to subtract a day to get the actual last day of the event
|
// For all-day events, dtend is set to the day after the last day (RFC 5545)
|
||||||
dtend.with_timezone(&Local).date_naive() - chrono::Duration::days(1)
|
// Extract date directly from UTC and subtract a day to get actual last day
|
||||||
|
dtend.date_naive() - chrono::Duration::days(1)
|
||||||
|
} else {
|
||||||
|
// For timed events, use timezone conversion
|
||||||
|
dtend.with_timezone(&Local).date_naive()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single day event
|
// Single day event
|
||||||
start_date
|
start_date
|
||||||
|
|||||||
@@ -1678,6 +1678,9 @@ impl CalendarService {
|
|||||||
categories: String,
|
categories: String,
|
||||||
reminder: String,
|
reminder: String,
|
||||||
recurrence: String,
|
recurrence: String,
|
||||||
|
recurrence_days: Vec<bool>,
|
||||||
|
recurrence_count: Option<u32>,
|
||||||
|
recurrence_until: Option<String>,
|
||||||
calendar_path: Option<String>,
|
calendar_path: Option<String>,
|
||||||
update_scope: String,
|
update_scope: String,
|
||||||
occurrence_date: Option<String>,
|
occurrence_date: Option<String>,
|
||||||
@@ -1706,10 +1709,10 @@ impl CalendarService {
|
|||||||
"categories": categories,
|
"categories": categories,
|
||||||
"reminder": reminder,
|
"reminder": reminder,
|
||||||
"recurrence": recurrence,
|
"recurrence": recurrence,
|
||||||
"recurrence_days": vec![false; 7], // Default - could be enhanced
|
"recurrence_days": recurrence_days,
|
||||||
"recurrence_interval": 1_u32, // Default interval
|
"recurrence_interval": 1_u32, // Default interval - could be enhanced to be a parameter
|
||||||
"recurrence_end_date": None as Option<String>, // No end date by default
|
"recurrence_end_date": recurrence_until,
|
||||||
"recurrence_count": None as Option<u32>, // No count limit by default
|
"recurrence_count": recurrence_count,
|
||||||
"calendar_path": calendar_path,
|
"calendar_path": calendar_path,
|
||||||
"update_scope": update_scope,
|
"update_scope": update_scope,
|
||||||
"occurrence_date": occurrence_date
|
"occurrence_date": occurrence_date
|
||||||
|
|||||||
Reference in New Issue
Block a user