Implement comprehensive recurring event support
- Add RRULE parsing for DAILY, WEEKLY, MONTHLY, YEARLY frequencies - Support INTERVAL, COUNT, and BYDAY recurrence parameters - Generate event occurrences across 30 days past to 365 days future - Update event refresh to regenerate all recurring occurrences - Clean up unused imports for cleaner compilation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
31
src/app.rs
31
src/app.rs
@@ -138,16 +138,33 @@ fn CalendarView() -> Html {
|
||||
|
||||
match calendar_service.refresh_event(&token, &uid).await {
|
||||
Ok(Some(refreshed_event)) => {
|
||||
// Update the event in the existing events map
|
||||
// If this is a recurring event, we need to regenerate all occurrences
|
||||
let mut updated_events = (*events).clone();
|
||||
|
||||
// First, remove all existing occurrences of this event
|
||||
for (_, day_events) in updated_events.iter_mut() {
|
||||
for existing_event in day_events.iter_mut() {
|
||||
if existing_event.uid == uid {
|
||||
*existing_event = refreshed_event.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
day_events.retain(|e| e.uid != uid);
|
||||
}
|
||||
|
||||
// Then, if it's a recurring event, generate new occurrences
|
||||
if refreshed_event.recurrence_rule.is_some() {
|
||||
let new_occurrences = CalendarService::expand_recurring_events(vec![refreshed_event.clone()]);
|
||||
|
||||
// Add all new occurrences to the appropriate dates
|
||||
for occurrence in new_occurrences {
|
||||
let date = occurrence.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(occurrence);
|
||||
}
|
||||
} else {
|
||||
// Non-recurring event, just add it to the appropriate date
|
||||
let date = refreshed_event.get_date();
|
||||
updated_events.entry(date)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(refreshed_event);
|
||||
}
|
||||
|
||||
events.set(updated_events);
|
||||
}
|
||||
Ok(None) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{DateTime, Utc, NaiveDate};
|
||||
use chrono::{DateTime, Utc, NaiveDate, Datelike, Weekday, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
@@ -183,7 +183,10 @@ impl CalendarService {
|
||||
pub fn group_events_by_date(events: Vec<CalendarEvent>) -> HashMap<NaiveDate, Vec<CalendarEvent>> {
|
||||
let mut grouped = HashMap::new();
|
||||
|
||||
for event in events {
|
||||
// Expand recurring events first
|
||||
let expanded_events = Self::expand_recurring_events(events);
|
||||
|
||||
for event in expanded_events {
|
||||
let date = event.get_date();
|
||||
|
||||
grouped.entry(date)
|
||||
@@ -194,6 +197,215 @@ impl CalendarService {
|
||||
grouped
|
||||
}
|
||||
|
||||
/// Expand recurring events to show on all occurrence dates within a reasonable range
|
||||
pub fn expand_recurring_events(events: Vec<CalendarEvent>) -> Vec<CalendarEvent> {
|
||||
let mut expanded_events = Vec::new();
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let start_range = today - Duration::days(30); // Show past 30 days
|
||||
let end_range = today + Duration::days(365); // Show next 365 days
|
||||
|
||||
for event in events {
|
||||
if let Some(ref rrule) = event.recurrence_rule {
|
||||
// Generate occurrences for recurring events
|
||||
let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range);
|
||||
expanded_events.extend(occurrences);
|
||||
} else {
|
||||
// Non-recurring event - add as-is
|
||||
expanded_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
expanded_events
|
||||
}
|
||||
|
||||
/// Generate occurrence dates for a recurring event based on RRULE
|
||||
fn generate_occurrences(
|
||||
base_event: &CalendarEvent,
|
||||
rrule: &str,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
) -> Vec<CalendarEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
|
||||
// Parse RRULE components
|
||||
let rrule_upper = rrule.to_uppercase();
|
||||
let components: HashMap<String, String> = rrule_upper
|
||||
.split(';')
|
||||
.filter_map(|part| {
|
||||
let mut split = part.split('=');
|
||||
if let (Some(key), Some(value)) = (split.next(), split.next()) {
|
||||
Some((key.to_string(), value.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get frequency
|
||||
let freq = components.get("FREQ").map(|s| s.as_str()).unwrap_or("DAILY");
|
||||
|
||||
// Get interval (default 1)
|
||||
let interval: i32 = components.get("INTERVAL")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
// Get count limit (default 100 to prevent infinite loops)
|
||||
let count: usize = components.get("COUNT")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(100)
|
||||
.min(365); // Cap at 365 occurrences for performance
|
||||
|
||||
let start_date = base_event.start.date_naive();
|
||||
let mut current_date = start_date;
|
||||
let mut occurrence_count = 0;
|
||||
|
||||
// Generate occurrences based on frequency
|
||||
while current_date <= end_range && occurrence_count < count {
|
||||
if current_date >= start_range {
|
||||
// Create occurrence event
|
||||
let mut occurrence_event = base_event.clone();
|
||||
|
||||
// Adjust dates
|
||||
let days_diff = current_date.signed_duration_since(start_date).num_days();
|
||||
occurrence_event.start = base_event.start + Duration::days(days_diff);
|
||||
|
||||
if let Some(end) = base_event.end {
|
||||
occurrence_event.end = Some(end + Duration::days(days_diff));
|
||||
}
|
||||
|
||||
occurrences.push(occurrence_event);
|
||||
}
|
||||
|
||||
// Calculate next occurrence date
|
||||
match freq {
|
||||
"DAILY" => {
|
||||
current_date = current_date + Duration::days(interval as i64);
|
||||
}
|
||||
"WEEKLY" => {
|
||||
if let Some(byday) = components.get("BYDAY") {
|
||||
// Handle specific days of week
|
||||
current_date = Self::next_weekday_occurrence(current_date, byday, interval);
|
||||
} else {
|
||||
current_date = current_date + Duration::weeks(interval as i64);
|
||||
}
|
||||
}
|
||||
"MONTHLY" => {
|
||||
// Simple monthly increment (same day of month)
|
||||
if let Some(next_month) = Self::add_months(current_date, interval) {
|
||||
current_date = next_month;
|
||||
} else {
|
||||
break; // Invalid date
|
||||
}
|
||||
}
|
||||
"YEARLY" => {
|
||||
if let Some(next_year) = Self::add_years(current_date, interval) {
|
||||
current_date = next_year;
|
||||
} else {
|
||||
break; // Invalid date
|
||||
}
|
||||
}
|
||||
_ => break, // Unsupported frequency
|
||||
}
|
||||
|
||||
occurrence_count += 1;
|
||||
}
|
||||
|
||||
occurrences
|
||||
}
|
||||
|
||||
/// Calculate next weekday occurrence for WEEKLY frequency with BYDAY
|
||||
fn next_weekday_occurrence(current_date: NaiveDate, byday: &str, interval: i32) -> NaiveDate {
|
||||
let weekdays = Self::parse_byday(byday);
|
||||
if weekdays.is_empty() {
|
||||
return current_date + Duration::weeks(interval as i64);
|
||||
}
|
||||
|
||||
let current_weekday = current_date.weekday();
|
||||
|
||||
// Find next occurrence within current week
|
||||
for &target_weekday in &weekdays {
|
||||
let days_until = Self::days_until_weekday(current_weekday, target_weekday);
|
||||
if days_until > 0 {
|
||||
return current_date + Duration::days(days_until as i64);
|
||||
}
|
||||
}
|
||||
|
||||
// No more occurrences this week, move to next interval
|
||||
let next_week_start = current_date + Duration::weeks(interval as i64) - Duration::days(current_weekday.num_days_from_monday() as i64);
|
||||
next_week_start + Duration::days(weekdays[0].num_days_from_monday() as i64)
|
||||
}
|
||||
|
||||
/// Parse BYDAY parameter (e.g., "MO,WE,FR" -> [Monday, Wednesday, Friday])
|
||||
fn parse_byday(byday: &str) -> Vec<Weekday> {
|
||||
byday
|
||||
.split(',')
|
||||
.filter_map(|day| match day {
|
||||
"MO" => Some(Weekday::Mon),
|
||||
"TU" => Some(Weekday::Tue),
|
||||
"WE" => Some(Weekday::Wed),
|
||||
"TH" => Some(Weekday::Thu),
|
||||
"FR" => Some(Weekday::Fri),
|
||||
"SA" => Some(Weekday::Sat),
|
||||
"SU" => Some(Weekday::Sun),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculate days until target weekday
|
||||
fn days_until_weekday(from: Weekday, to: Weekday) -> i32 {
|
||||
let from_num = from.num_days_from_monday();
|
||||
let to_num = to.num_days_from_monday();
|
||||
|
||||
if to_num > from_num {
|
||||
(to_num - from_num) as i32
|
||||
} else if to_num < from_num {
|
||||
(7 + to_num - from_num) as i32
|
||||
} else {
|
||||
0 // Same day
|
||||
}
|
||||
}
|
||||
|
||||
/// Add months to a date (handling month boundary issues)
|
||||
fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
|
||||
let mut year = date.year();
|
||||
let mut month = date.month() as i32 + months;
|
||||
|
||||
while month > 12 {
|
||||
year += 1;
|
||||
month -= 12;
|
||||
}
|
||||
while month < 1 {
|
||||
year -= 1;
|
||||
month += 12;
|
||||
}
|
||||
|
||||
// Handle day overflow (e.g., Jan 31 -> Feb 28)
|
||||
let day = date.day().min(Self::days_in_month(year, month as u32));
|
||||
|
||||
NaiveDate::from_ymd_opt(year, month as u32, day)
|
||||
}
|
||||
|
||||
/// Add years to a date
|
||||
fn add_years(date: NaiveDate, years: i32) -> Option<NaiveDate> {
|
||||
NaiveDate::from_ymd_opt(date.year() + years, date.month(), date.day())
|
||||
}
|
||||
|
||||
/// Get number of days in a month
|
||||
fn days_in_month(year: i32, month: u32) -> u32 {
|
||||
match month {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
2 => if Self::is_leap_year(year) { 29 } else { 28 },
|
||||
_ => 30,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if year is leap year
|
||||
fn is_leap_year(year: i32) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
|
||||
}
|
||||
|
||||
/// Refresh a single event by UID from the CalDAV server
|
||||
pub async fn refresh_event(&self, token: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod calendar_service;
|
||||
|
||||
pub use calendar_service::{CalendarService, CalendarEvent, EventStatus, EventClass, EventReminder, ReminderAction};
|
||||
pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction};
|
||||
Reference in New Issue
Block a user