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:
Connor Johnstone
2025-09-03 12:21:46 -04:00
parent cb8cc7258c
commit e56253b9c2
5 changed files with 43 additions and 10 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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") {

View File

@@ -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);