Compare commits
3 Commits
d0aa6fda08
...
63968280b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63968280b8 | ||
|
|
3ccf31f479 | ||
|
|
c599598390 |
@@ -384,29 +384,38 @@ pub async fn create_event(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse recurrence with BYDAY support for weekly recurrence
|
// Check if recurrence is already a full RRULE or just a simple type
|
||||||
let rrule = match request.recurrence.to_uppercase().as_str() {
|
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||||
"DAILY" => Some("FREQ=DAILY".to_string()),
|
// Frontend sent a complete RRULE string, use it directly
|
||||||
"WEEKLY" => {
|
if request.recurrence.is_empty() {
|
||||||
// Handle weekly recurrence with optional BYDAY parameter
|
None
|
||||||
let mut rrule = "FREQ=WEEKLY".to_string();
|
} else {
|
||||||
|
Some(request.recurrence.clone())
|
||||||
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
}
|
||||||
if request.recurrence_days.len() == 7 {
|
} else {
|
||||||
let selected_days: Vec<&str> = request.recurrence_days
|
// Legacy path: Parse recurrence with BYDAY support for weekly recurrence
|
||||||
.iter()
|
match request.recurrence.to_uppercase().as_str() {
|
||||||
.enumerate()
|
"DAILY" => Some("FREQ=DAILY".to_string()),
|
||||||
.filter_map(|(i, &selected)| {
|
"WEEKLY" => {
|
||||||
if selected {
|
// Handle weekly recurrence with optional BYDAY parameter
|
||||||
Some(match i {
|
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||||
0 => "SU", // Sunday
|
|
||||||
1 => "MO", // Monday
|
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||||
2 => "TU", // Tuesday
|
if request.recurrence_days.len() == 7 {
|
||||||
3 => "WE", // Wednesday
|
let selected_days: Vec<&str> = request.recurrence_days
|
||||||
4 => "TH", // Thursday
|
.iter()
|
||||||
5 => "FR", // Friday
|
.enumerate()
|
||||||
6 => "SA", // Saturday
|
.filter_map(|(i, &selected)| {
|
||||||
_ => return None,
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -419,11 +428,12 @@ pub async fn create_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(rrule)
|
Some(rrule)
|
||||||
},
|
},
|
||||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the VEvent struct (RFC 5545 compliant)
|
// Create the VEvent struct (RFC 5545 compliant)
|
||||||
|
|||||||
@@ -161,11 +161,16 @@ pub async fn create_event_series(
|
|||||||
// Set priority
|
// Set priority
|
||||||
event.priority = request.priority;
|
event.priority = request.priority;
|
||||||
|
|
||||||
// Generate the RRULE for recurrence
|
// Check if recurrence is already a full RRULE or just a simple type
|
||||||
let rrule = build_series_rrule_with_freq(&request, recurrence_freq)?;
|
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||||
|
// Frontend sent a complete RRULE string, use it directly
|
||||||
|
request.recurrence.clone()
|
||||||
|
} else {
|
||||||
|
// Legacy path: Generate the RRULE for recurrence
|
||||||
|
build_series_rrule_with_freq(&request, recurrence_freq)?
|
||||||
|
};
|
||||||
event.rrule = Some(rrule);
|
event.rrule = Some(rrule);
|
||||||
|
|
||||||
println!("🔁 Generated RRULE: {:?}", event.rrule);
|
|
||||||
|
|
||||||
// Create the event on the CalDAV server
|
// Create the event on the CalDAV server
|
||||||
let event_href = client.create_event(&calendar_path, &event)
|
let event_href = client.create_event(&calendar_path, &event)
|
||||||
@@ -455,34 +460,6 @@ pub async fn delete_event_series(
|
|||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
fn build_series_rrule(request: &CreateEventSeriesRequest) -> Result<String, ApiError> {
|
|
||||||
// Extract frequency from request
|
|
||||||
let recurrence_freq = if request.recurrence.contains("FREQ=") {
|
|
||||||
// Parse RRULE to extract frequency
|
|
||||||
if request.recurrence.contains("FREQ=DAILY") {
|
|
||||||
"daily"
|
|
||||||
} else if request.recurrence.contains("FREQ=WEEKLY") {
|
|
||||||
"weekly"
|
|
||||||
} else if request.recurrence.contains("FREQ=MONTHLY") {
|
|
||||||
"monthly"
|
|
||||||
} else if request.recurrence.contains("FREQ=YEARLY") {
|
|
||||||
"yearly"
|
|
||||||
} else {
|
|
||||||
return Err(ApiError::BadRequest("Invalid RRULE frequency".to_string()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let lower = request.recurrence.to_lowercase();
|
|
||||||
match lower.as_str() {
|
|
||||||
"daily" => "daily",
|
|
||||||
"weekly" => "weekly",
|
|
||||||
"monthly" => "monthly",
|
|
||||||
"yearly" => "yearly",
|
|
||||||
_ => return Err(ApiError::BadRequest("Invalid recurrence type".to_string())),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
build_series_rrule_with_freq(request, recurrence_freq)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_series_rrule_with_freq(request: &CreateEventSeriesRequest, freq: &str) -> Result<String, ApiError> {
|
fn build_series_rrule_with_freq(request: &CreateEventSeriesRequest, freq: &str) -> Result<String, ApiError> {
|
||||||
let mut rrule_parts = Vec::new();
|
let mut rrule_parts = Vec::new();
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ pub fn App() -> Html {
|
|||||||
let calendar_context_menu_date = use_state(|| -> Option<NaiveDate> { None });
|
let calendar_context_menu_date = use_state(|| -> Option<NaiveDate> { None });
|
||||||
let create_event_modal_open = use_state(|| false);
|
let create_event_modal_open = use_state(|| false);
|
||||||
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
||||||
let recurring_edit_modal_open = use_state(|| false);
|
let _recurring_edit_modal_open = use_state(|| false);
|
||||||
let recurring_edit_event = use_state(|| -> Option<VEvent> { None });
|
let _recurring_edit_event = use_state(|| -> Option<VEvent> { None });
|
||||||
let recurring_edit_data = use_state(|| -> Option<EventCreationData> { None });
|
let _recurring_edit_data = use_state(|| -> Option<EventCreationData> { None });
|
||||||
|
|
||||||
// Calendar view state - load from localStorage if available
|
// Calendar view state - load from localStorage if available
|
||||||
let current_view = use_state(|| {
|
let current_view = use_state(|| {
|
||||||
|
|||||||
@@ -81,17 +81,6 @@ impl Default for RecurrenceType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecurrenceType {
|
|
||||||
pub fn from_rrule(rrule: Option<&str>) -> Self {
|
|
||||||
match rrule {
|
|
||||||
Some(rule) if rule.contains("FREQ=DAILY") => RecurrenceType::Daily,
|
|
||||||
Some(rule) if rule.contains("FREQ=WEEKLY") => RecurrenceType::Weekly,
|
|
||||||
Some(rule) if rule.contains("FREQ=MONTHLY") => RecurrenceType::Monthly,
|
|
||||||
Some(rule) if rule.contains("FREQ=YEARLY") => RecurrenceType::Yearly,
|
|
||||||
_ => RecurrenceType::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse RRULE string into recurrence components
|
/// Parse RRULE string into recurrence components
|
||||||
/// Example RRULE: "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;UNTIL=20231215T000000Z"
|
/// Example RRULE: "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;UNTIL=20231215T000000Z"
|
||||||
@@ -306,9 +295,11 @@ mod rrule_tests {
|
|||||||
data.yearly_by_month = vec![false, false, true, false, true, false, false, false, false, false, false, false]; // March, May
|
data.yearly_by_month = vec![false, false, true, false, true, false, false, false, false, false, false, false]; // March, May
|
||||||
|
|
||||||
let rrule = data.build_rrule();
|
let rrule = data.build_rrule();
|
||||||
|
println!("YEARLY RRULE: {}", rrule);
|
||||||
assert!(rrule.contains("FREQ=YEARLY"));
|
assert!(rrule.contains("FREQ=YEARLY"));
|
||||||
assert!(rrule.contains("BYMONTH=3,5"));
|
assert!(rrule.contains("BYMONTH=3,5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
@@ -761,7 +752,7 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_priority_input = {
|
let _on_priority_input = {
|
||||||
let event_data = event_data.clone();
|
let event_data = event_data.clone();
|
||||||
Callback::from(move |e: InputEvent| {
|
Callback::from(move |e: InputEvent| {
|
||||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
occurrences.push(occurrence_event);
|
occurrences.push(occurrence_event);
|
||||||
|
occurrence_count += 1; // Only count when we actually add an occurrence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,24 +425,61 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"MONTHLY" => {
|
"MONTHLY" => {
|
||||||
// Simple monthly increment (same day of month)
|
// Handle MONTHLY with BYMONTHDAY or BYDAY
|
||||||
if let Some(next_month) = Self::add_months(current_date, interval) {
|
if let Some(bymonthday) = components.get("BYMONTHDAY") {
|
||||||
current_date = next_month;
|
// Monthly by day of month (e.g., 15th of every month)
|
||||||
|
return Self::generate_monthly_bymonthday_occurrences(
|
||||||
|
base_event,
|
||||||
|
bymonthday,
|
||||||
|
interval,
|
||||||
|
start_range,
|
||||||
|
end_range,
|
||||||
|
until_date,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
} else if let Some(byday) = components.get("BYDAY") {
|
||||||
|
// Monthly by weekday position (e.g., first Monday)
|
||||||
|
return Self::generate_monthly_byday_occurrences(
|
||||||
|
base_event,
|
||||||
|
byday,
|
||||||
|
interval,
|
||||||
|
start_range,
|
||||||
|
end_range,
|
||||||
|
until_date,
|
||||||
|
count
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
break; // Invalid date
|
// 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" => {
|
"YEARLY" => {
|
||||||
if let Some(next_year) = Self::add_years(current_date, interval) {
|
if let Some(bymonth) = components.get("BYMONTH") {
|
||||||
current_date = next_year;
|
// Yearly with specific months (e.g., January, March, June)
|
||||||
|
return Self::generate_yearly_bymonth_occurrences(
|
||||||
|
base_event,
|
||||||
|
bymonth,
|
||||||
|
interval,
|
||||||
|
start_range,
|
||||||
|
end_range,
|
||||||
|
until_date,
|
||||||
|
count
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
break; // Invalid date
|
// Simple yearly increment (same date each year)
|
||||||
|
if let Some(next_year) = Self::add_years(current_date, interval) {
|
||||||
|
current_date = next_year;
|
||||||
|
} else {
|
||||||
|
break; // Invalid date
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => break, // Unsupported frequency
|
_ => break, // Unsupported frequency
|
||||||
}
|
}
|
||||||
|
|
||||||
occurrence_count += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
occurrences
|
occurrences
|
||||||
@@ -464,14 +502,28 @@ impl CalendarService {
|
|||||||
return occurrences;
|
return occurrences;
|
||||||
}
|
}
|
||||||
|
|
||||||
web_sys::console::log_1(&format!("🗓️ Generating WEEKLY BYDAY occurrences for days: {:?}", weekdays).into());
|
|
||||||
|
|
||||||
let start_date = base_event.dtstart.date_naive();
|
let start_date = base_event.dtstart.date_naive();
|
||||||
let mut current_week_start = start_date - Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
|
||||||
let mut total_occurrences = 0;
|
// Find the Monday of the week containing the start_date (reference week)
|
||||||
|
let reference_week_start = start_date - Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
||||||
|
|
||||||
|
let mut total_events_generated = 0; // Count of actual events generated (matches RFC 5545 COUNT)
|
||||||
|
let mut week_interval_number = 0; // Which interval week we're processing (0, 1, 2, ...)
|
||||||
|
let max_weeks = (count * 10).min(520); // Prevent infinite loops - max 10 years
|
||||||
|
|
||||||
|
|
||||||
// Generate occurrences week by week
|
// Continue generating until we reach count or date limits
|
||||||
while current_week_start <= end_range && total_occurrences < count {
|
while total_events_generated < count && week_interval_number < max_weeks {
|
||||||
|
// Calculate the actual week we're processing: original + (interval_number * interval) weeks
|
||||||
|
let current_week_start = reference_week_start + Duration::weeks((week_interval_number as i32 * interval) as i64);
|
||||||
|
|
||||||
|
// Stop if we've gone past the end range
|
||||||
|
if current_week_start > end_range {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Generate occurrences for all matching weekdays in this week
|
// Generate occurrences for all matching weekdays in this week
|
||||||
for &weekday in &weekdays {
|
for &weekday in &weekdays {
|
||||||
let occurrence_date = current_week_start + Duration::days(weekday.num_days_from_monday() as i64);
|
let occurrence_date = current_week_start + Duration::days(weekday.num_days_from_monday() as i64);
|
||||||
@@ -480,10 +532,10 @@ impl CalendarService {
|
|||||||
if occurrence_date < start_range || occurrence_date > end_range {
|
if occurrence_date < start_range || occurrence_date > end_range {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if we've reached the count limit
|
// Skip if this occurrence is before the original event date
|
||||||
if total_occurrences >= count {
|
if occurrence_date < start_date {
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check UNTIL constraint
|
// Check UNTIL constraint
|
||||||
@@ -496,11 +548,6 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if this occurrence is before the original event date
|
|
||||||
if occurrence_date < start_date {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the occurrence datetime
|
// Calculate the occurrence datetime
|
||||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
@@ -529,49 +576,296 @@ impl CalendarService {
|
|||||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||||
}
|
}
|
||||||
|
|
||||||
web_sys::console::log_1(&format!("📅 Generated weekly occurrence on {}", occurrence_date).into());
|
total_events_generated += 1;
|
||||||
occurrences.push(occurrence_event);
|
occurrences.push(occurrence_event);
|
||||||
total_occurrences += 1;
|
|
||||||
|
// Stop if we've reached the count limit
|
||||||
|
if total_events_generated >= count {
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to next week interval
|
// Move to the next interval week
|
||||||
current_week_start = current_week_start + Duration::weeks(interval as i64);
|
week_interval_number += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate occurrences for MONTHLY frequency with BYMONTHDAY
|
||||||
|
fn generate_monthly_bymonthday_occurrences(
|
||||||
|
base_event: &VEvent,
|
||||||
|
bymonthday: &str,
|
||||||
|
interval: i32,
|
||||||
|
start_range: NaiveDate,
|
||||||
|
end_range: NaiveDate,
|
||||||
|
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
count: usize,
|
||||||
|
) -> Vec<VEvent> {
|
||||||
|
let mut occurrences = Vec::new();
|
||||||
|
|
||||||
|
// Parse BYMONTHDAY (e.g., "15" or "1,15,31")
|
||||||
|
let monthdays: Vec<u32> = bymonthday
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|day| day.trim().parse().ok())
|
||||||
|
.filter(|&day| day >= 1 && day <= 31)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if monthdays.is_empty() {
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let start_date = base_event.dtstart.date_naive();
|
||||||
|
let mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||||
|
let mut total_occurrences = 0;
|
||||||
|
let mut months_processed = 0;
|
||||||
|
let max_months = (count * 12).min(120); // Prevent infinite loops - max 10 years
|
||||||
|
|
||||||
|
|
||||||
|
// Generate occurrences month by month
|
||||||
|
while current_month_start <= end_range && total_occurrences < count && months_processed < max_months {
|
||||||
|
// Generate occurrences for all matching days in this month
|
||||||
|
for &day in &monthdays {
|
||||||
|
// Try to create the date, skip if invalid (e.g., Feb 31)
|
||||||
|
if let Some(occurrence_date) = NaiveDate::from_ymd_opt(current_month_start.year(), current_month_start.month(), day) {
|
||||||
|
// Skip if occurrence is before start_range or after end_range
|
||||||
|
if occurrence_date < start_range || occurrence_date > end_range {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we've reached the count limit
|
||||||
|
if total_occurrences >= count {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if this occurrence is before the original event date
|
||||||
|
if occurrence_date < start_date {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UNTIL constraint
|
||||||
|
if let Some(until) = until_date {
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
if occurrence_datetime > until {
|
||||||
|
web_sys::console::log_1(&format!("🛑 Stopping at {} due to UNTIL {}", occurrence_datetime, until).into());
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the occurrence datetime
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
diff.num_seconds().abs() < 60
|
||||||
|
});
|
||||||
|
|
||||||
|
if !is_exception {
|
||||||
|
// Create occurrence event
|
||||||
|
let mut occurrence_event = base_event.clone();
|
||||||
|
occurrence_event.dtstart = occurrence_datetime;
|
||||||
|
occurrence_event.dtstamp = chrono::Utc::now();
|
||||||
|
|
||||||
|
if let Some(end) = base_event.dtend {
|
||||||
|
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences.push(occurrence_event);
|
||||||
|
total_occurrences += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next month interval
|
||||||
|
months_processed += 1;
|
||||||
|
if let Some(next_month_start) = Self::add_months(current_month_start, interval) {
|
||||||
|
current_month_start = NaiveDate::from_ymd_opt(next_month_start.year(), next_month_start.month(), 1).unwrap();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate occurrences for MONTHLY frequency with BYDAY (e.g., "1MO" = first Monday)
|
||||||
|
fn generate_monthly_byday_occurrences(
|
||||||
|
base_event: &VEvent,
|
||||||
|
byday: &str,
|
||||||
|
interval: i32,
|
||||||
|
start_range: NaiveDate,
|
||||||
|
end_range: NaiveDate,
|
||||||
|
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
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 mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||||
|
let mut total_occurrences = 0;
|
||||||
|
|
||||||
|
// Generate occurrences month by month
|
||||||
|
while current_month_start <= end_range && total_occurrences < count {
|
||||||
|
if let Some(occurrence_date) = Self::find_nth_weekday_in_month(current_month_start, position, weekday) {
|
||||||
|
// Skip if occurrence is before start_range or after end_range
|
||||||
|
if occurrence_date < start_range || occurrence_date > end_range {
|
||||||
|
} else if occurrence_date >= start_date && total_occurrences < count {
|
||||||
|
// Check UNTIL constraint
|
||||||
|
if let Some(until) = until_date {
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
if occurrence_datetime > until {
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the occurrence datetime
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
diff.num_seconds().abs() < 60
|
||||||
|
});
|
||||||
|
|
||||||
|
if !is_exception {
|
||||||
|
let mut occurrence_event = base_event.clone();
|
||||||
|
occurrence_event.dtstart = occurrence_datetime;
|
||||||
|
occurrence_event.dtstamp = chrono::Utc::now();
|
||||||
|
|
||||||
|
if let Some(end) = base_event.dtend {
|
||||||
|
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences.push(occurrence_event);
|
||||||
|
total_occurrences += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next month interval
|
||||||
|
if let Some(next_month) = Self::add_months(current_month_start, interval) {
|
||||||
|
current_month_start = NaiveDate::from_ymd_opt(next_month.year(), next_month.month(), 1).unwrap();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate occurrences for YEARLY frequency with BYMONTH
|
||||||
|
fn generate_yearly_bymonth_occurrences(
|
||||||
|
base_event: &VEvent,
|
||||||
|
bymonth: &str,
|
||||||
|
interval: i32,
|
||||||
|
start_range: NaiveDate,
|
||||||
|
end_range: NaiveDate,
|
||||||
|
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
count: usize,
|
||||||
|
) -> Vec<VEvent> {
|
||||||
|
let mut occurrences = Vec::new();
|
||||||
|
|
||||||
|
// Parse BYMONTH (e.g., "1,3,6" -> [January, March, June])
|
||||||
|
let months: Vec<u32> = bymonth
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|month| month.trim().parse().ok())
|
||||||
|
.filter(|&month| month >= 1 && month <= 12)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if months.is_empty() {
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let start_date = base_event.dtstart.date_naive();
|
||||||
|
let mut current_year = start_date.year();
|
||||||
|
let mut total_occurrences = 0;
|
||||||
|
|
||||||
|
// Generate occurrences year by year
|
||||||
|
while total_occurrences < count {
|
||||||
|
// Generate occurrences for all matching months in this year
|
||||||
|
for &month in &months {
|
||||||
|
// Create the date for this year/month/day
|
||||||
|
if let Some(occurrence_date) = NaiveDate::from_ymd_opt(current_year, month, start_date.day()) {
|
||||||
|
// Skip if occurrence is before start_range or after end_range
|
||||||
|
if occurrence_date < start_range || occurrence_date > end_range {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we've reached the count limit
|
||||||
|
if total_occurrences >= count {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if this occurrence is before the original event date
|
||||||
|
if occurrence_date < start_date {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UNTIL constraint
|
||||||
|
if let Some(until) = until_date {
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
if occurrence_datetime > until {
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the occurrence datetime
|
||||||
|
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||||
|
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
diff.num_seconds().abs() < 60
|
||||||
|
});
|
||||||
|
|
||||||
|
if !is_exception {
|
||||||
|
let mut occurrence_event = base_event.clone();
|
||||||
|
occurrence_event.dtstart = occurrence_datetime;
|
||||||
|
occurrence_event.dtstamp = chrono::Utc::now();
|
||||||
|
|
||||||
|
if let Some(end) = base_event.dtend {
|
||||||
|
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences.push(occurrence_event);
|
||||||
|
total_occurrences += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next year interval
|
||||||
|
current_year += interval;
|
||||||
|
|
||||||
|
// Stop if we've gone beyond reasonable range
|
||||||
|
if current_year > end_range.year() + 10 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
web_sys::console::log_1(&format!("✅ Generated {} total weekly BYDAY occurrences", occurrences.len()).into());
|
|
||||||
occurrences
|
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
|
|
||||||
let mut next_occurrences = Vec::new();
|
|
||||||
for &target_weekday in &weekdays {
|
|
||||||
let days_until = Self::days_until_weekday(current_weekday, target_weekday);
|
|
||||||
if days_until > 0 {
|
|
||||||
next_occurrences.push((days_until, target_weekday));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by days_until and return the closest one
|
|
||||||
if !next_occurrences.is_empty() {
|
|
||||||
next_occurrences.sort_by_key(|(days, _)| *days);
|
|
||||||
return current_date + Duration::days(next_occurrences[0].0 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])
|
/// Parse BYDAY parameter (e.g., "MO,WE,FR" -> [Monday, Wednesday, Friday])
|
||||||
fn parse_byday(byday: &str) -> Vec<Weekday> {
|
fn parse_byday(byday: &str) -> Vec<Weekday> {
|
||||||
@@ -590,19 +884,6 @@ impl CalendarService {
|
|||||||
.collect()
|
.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)
|
/// Add months to a date (handling month boundary issues)
|
||||||
fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
|
fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
|
||||||
@@ -1275,5 +1556,76 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse monthly BYDAY (e.g., "1MO" -> (1, Monday), "-1FR" -> (-1, Friday))
|
||||||
|
fn parse_monthly_byday(byday: &str) -> Option<(i8, chrono::Weekday)> {
|
||||||
|
if byday.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract position and weekday
|
||||||
|
let (position_str, weekday_str) = if byday.starts_with('-') {
|
||||||
|
(&byday[..2], &byday[2..])
|
||||||
|
} else {
|
||||||
|
(&byday[..1], &byday[1..])
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = position_str.parse::<i8>().ok()?;
|
||||||
|
let weekday = match weekday_str {
|
||||||
|
"MO" => chrono::Weekday::Mon,
|
||||||
|
"TU" => chrono::Weekday::Tue,
|
||||||
|
"WE" => chrono::Weekday::Wed,
|
||||||
|
"TH" => chrono::Weekday::Thu,
|
||||||
|
"FR" => chrono::Weekday::Fri,
|
||||||
|
"SA" => chrono::Weekday::Sat,
|
||||||
|
"SU" => chrono::Weekday::Sun,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((position, weekday))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find nth weekday in a month (e.g., 1st Monday, 2nd Tuesday, -1 = last)
|
||||||
|
fn find_nth_weekday_in_month(month_start: NaiveDate, position: i8, weekday: chrono::Weekday) -> Option<NaiveDate> {
|
||||||
|
let year = month_start.year();
|
||||||
|
let month = month_start.month();
|
||||||
|
|
||||||
|
if position > 0 {
|
||||||
|
// Find nth occurrence from beginning of month
|
||||||
|
let mut current = NaiveDate::from_ymd_opt(year, month, 1)?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
while current.month() == month {
|
||||||
|
if current.weekday() == weekday {
|
||||||
|
count += 1;
|
||||||
|
if count == position as u8 {
|
||||||
|
return Some(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.succ_opt()?;
|
||||||
|
}
|
||||||
|
} else if position < 0 {
|
||||||
|
// Find nth occurrence from end of month
|
||||||
|
let next_month = if month == 12 {
|
||||||
|
NaiveDate::from_ymd_opt(year + 1, 1, 1)?
|
||||||
|
} else {
|
||||||
|
NaiveDate::from_ymd_opt(year, month + 1, 1)?
|
||||||
|
};
|
||||||
|
let mut current = next_month.pred_opt()?; // Last day of current month
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
while current.month() == month {
|
||||||
|
if current.weekday() == weekday {
|
||||||
|
count += 1;
|
||||||
|
if count == (-position) as u8 {
|
||||||
|
return Some(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.pred_opt()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user