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
|
||||
let rrule = match request.recurrence.to_uppercase().as_str() {
|
||||
"DAILY" => Some("FREQ=DAILY".to_string()),
|
||||
"WEEKLY" => {
|
||||
// Handle weekly recurrence with optional BYDAY parameter
|
||||
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||
|
||||
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||
if request.recurrence_days.len() == 7 {
|
||||
let selected_days: Vec<&str> = request.recurrence_days
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &selected)| {
|
||||
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,
|
||||
// Check if recurrence is already a full RRULE or just a simple type
|
||||
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||
// Frontend sent a complete RRULE string, use it directly
|
||||
if request.recurrence.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(request.recurrence.clone())
|
||||
}
|
||||
} else {
|
||||
// Legacy path: Parse recurrence with BYDAY support for weekly recurrence
|
||||
match request.recurrence.to_uppercase().as_str() {
|
||||
"DAILY" => Some("FREQ=DAILY".to_string()),
|
||||
"WEEKLY" => {
|
||||
// Handle weekly recurrence with optional BYDAY parameter
|
||||
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||
|
||||
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||
if request.recurrence_days.len() == 7 {
|
||||
let selected_days: Vec<&str> = request.recurrence_days
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &selected)| {
|
||||
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 {
|
||||
None
|
||||
@@ -419,11 +428,12 @@ pub async fn create_event(
|
||||
}
|
||||
}
|
||||
|
||||
Some(rrule)
|
||||
},
|
||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
Some(rrule)
|
||||
},
|
||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// Create the VEvent struct (RFC 5545 compliant)
|
||||
|
||||
@@ -161,11 +161,16 @@ pub async fn create_event_series(
|
||||
// Set priority
|
||||
event.priority = request.priority;
|
||||
|
||||
// Generate the RRULE for recurrence
|
||||
let rrule = build_series_rrule_with_freq(&request, recurrence_freq)?;
|
||||
// Check if recurrence is already a full RRULE or just a simple type
|
||||
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);
|
||||
|
||||
println!("🔁 Generated RRULE: {:?}", event.rrule);
|
||||
|
||||
// Create the event on the CalDAV server
|
||||
let event_href = client.create_event(&calendar_path, &event)
|
||||
@@ -455,34 +460,6 @@ pub async fn delete_event_series(
|
||||
|
||||
// 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> {
|
||||
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 create_event_modal_open = use_state(|| false);
|
||||
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
||||
let recurring_edit_modal_open = use_state(|| false);
|
||||
let recurring_edit_event = use_state(|| -> Option<VEvent> { None });
|
||||
let recurring_edit_data = use_state(|| -> Option<EventCreationData> { None });
|
||||
let _recurring_edit_modal_open = use_state(|| false);
|
||||
let _recurring_edit_event = use_state(|| -> Option<VEvent> { None });
|
||||
let _recurring_edit_data = use_state(|| -> Option<EventCreationData> { None });
|
||||
|
||||
// Calendar view state - load from localStorage if available
|
||||
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
|
||||
/// 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
|
||||
|
||||
let rrule = data.build_rrule();
|
||||
println!("YEARLY RRULE: {}", rrule);
|
||||
assert!(rrule.contains("FREQ=YEARLY"));
|
||||
assert!(rrule.contains("BYMONTH=3,5"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[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();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
|
||||
@@ -398,6 +398,7 @@ impl CalendarService {
|
||||
}
|
||||
|
||||
occurrences.push(occurrence_event);
|
||||
occurrence_count += 1; // Only count when we actually add an occurrence
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,24 +425,61 @@ impl CalendarService {
|
||||
}
|
||||
}
|
||||
"MONTHLY" => {
|
||||
// Simple monthly increment (same day of month)
|
||||
if let Some(next_month) = Self::add_months(current_date, interval) {
|
||||
current_date = next_month;
|
||||
// Handle MONTHLY with BYMONTHDAY or BYDAY
|
||||
if let Some(bymonthday) = components.get("BYMONTHDAY") {
|
||||
// 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 {
|
||||
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" => {
|
||||
if let Some(next_year) = Self::add_years(current_date, interval) {
|
||||
current_date = next_year;
|
||||
if let Some(bymonth) = components.get("BYMONTH") {
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
occurrence_count += 1;
|
||||
}
|
||||
|
||||
occurrences
|
||||
@@ -464,14 +502,28 @@ impl CalendarService {
|
||||
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 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
|
||||
while current_week_start <= end_range && total_occurrences < count {
|
||||
// Continue generating until we reach count or date limits
|
||||
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
|
||||
for &weekday in &weekdays {
|
||||
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 {
|
||||
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
|
||||
@@ -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
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
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));
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generated weekly occurrence on {}", occurrence_date).into());
|
||||
total_events_generated += 1;
|
||||
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
|
||||
current_week_start = current_week_start + Duration::weeks(interval as i64);
|
||||
// Move to the next interval week
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/// 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])
|
||||
fn parse_byday(byday: &str) -> Vec<Weekday> {
|
||||
@@ -590,19 +884,6 @@ impl CalendarService {
|
||||
.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> {
|
||||
@@ -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