Fix all-day recurring events RFC-5545 compliance
- Set all_day flag properly when creating VEvent in series handler - Improve all-day event detection using VALUE=DATE parameter - Add RFC-5545 compliance for exclusive end dates (backend adds 1 day) - Fix end date display in event modal (frontend subtracts 1 day for display) - Fix recurring all-day event expansion to maintain proper end date pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -361,11 +361,12 @@ impl CalDAVClient {
|
||||
None
|
||||
};
|
||||
|
||||
// Determine if it's an all-day event
|
||||
let all_day = properties
|
||||
.get("DTSTART")
|
||||
.map(|s| !s.contains("T"))
|
||||
.unwrap_or(false);
|
||||
// Determine if it's an all-day event by checking for VALUE=DATE parameter
|
||||
let empty_string = String::new();
|
||||
let dtstart_raw = properties.get("DTSTART").unwrap_or(&empty_string);
|
||||
let all_day = dtstart_raw.contains("VALUE=DATE") || (!dtstart_raw.contains("T") && dtstart_raw.len() == 8);
|
||||
|
||||
eprintln!("🔍 DTSTART parsing: '{}' -> all_day: {}", dtstart_raw, all_day);
|
||||
|
||||
// Parse status
|
||||
let status = properties
|
||||
|
||||
@@ -458,9 +458,15 @@ pub async fn create_event(
|
||||
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
|
||||
|
||||
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// For all-day events, add one day to end date for RFC-5545 compliance
|
||||
// RFC-5545 uses exclusive end dates for all-day events
|
||||
if request.all_day {
|
||||
end_datetime = end_datetime + chrono::Duration::days(1);
|
||||
}
|
||||
|
||||
// Validate that end is after start (allow equal times for all-day events)
|
||||
if request.all_day {
|
||||
if end_datetime < start_datetime {
|
||||
@@ -756,9 +762,15 @@ pub async fn update_event(
|
||||
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
|
||||
|
||||
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// For all-day events, add one day to end date for RFC-5545 compliance
|
||||
// RFC-5545 uses exclusive end dates for all-day events
|
||||
if request.all_day {
|
||||
end_datetime = end_datetime + chrono::Duration::days(1);
|
||||
}
|
||||
|
||||
// Validate that end is after start (allow equal times for all-day events)
|
||||
if request.all_day {
|
||||
if end_datetime < start_datetime {
|
||||
|
||||
@@ -175,6 +175,7 @@ pub async fn create_event_series(
|
||||
// Create the VEvent for the series
|
||||
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
|
||||
event.summary = if request.title.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn EventModal(props: &EventModalProps) -> Html {
|
||||
html! {
|
||||
<div class="event-detail">
|
||||
<strong>{"End:"}</strong>
|
||||
<span>{format_datetime(end, event.all_day)}</span>
|
||||
<span>{format_datetime_end(end, event.all_day)}</span>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
@@ -221,6 +221,17 @@ fn format_datetime(dt: &DateTime<Utc>, all_day: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_datetime_end(dt: &DateTime<Utc>, 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
|
||||
let display_date = *dt - chrono::Duration::days(1);
|
||||
display_date.format("%B %d, %Y").to_string()
|
||||
} else {
|
||||
dt.format("%B %d, %Y at %I:%M %p").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_recurrence_rule(rrule: &str) -> String {
|
||||
// Basic parsing of RRULE to display user-friendly text
|
||||
if rrule.contains("FREQ=DAILY") {
|
||||
|
||||
@@ -439,9 +439,17 @@ impl CalendarService {
|
||||
let mut occurrence_event = base_event.clone();
|
||||
occurrence_event.dtstart = occurrence_datetime;
|
||||
occurrence_event.dtstamp = chrono::Utc::now(); // Update DTSTAMP for each occurrence
|
||||
|
||||
|
||||
if let Some(end) = base_event.dtend {
|
||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||
if let Some(base_end) = base_event.dtend {
|
||||
if base_event.all_day {
|
||||
// For all-day events, maintain the RFC-5545 end date pattern
|
||||
// End date should always be exactly one day after start date
|
||||
occurrence_event.dtend = Some(occurrence_datetime + Duration::days(1));
|
||||
} else {
|
||||
// For timed events, preserve the original duration
|
||||
occurrence_event.dtend = Some(base_end + Duration::days(days_diff));
|
||||
}
|
||||
}
|
||||
|
||||
occurrences.push(occurrence_event);
|
||||
|
||||
Reference in New Issue
Block a user