diff --git a/backend/src/handlers/series.rs b/backend/src/handlers/series.rs index d8911eb..c123a26 100644 --- a/backend/src/handlers/series.rs +++ b/backend/src/handlers/series.rs @@ -262,8 +262,8 @@ pub async fn update_event_series( Json(request): Json, ) -> Result, ApiError> { println!( - "🔄 Update event series request received: series_uid='{}', update_scope='{}'", - request.series_uid, request.update_scope + "🔄 Update event series request received: series_uid='{}', update_scope='{}', recurrence_count={:?}, recurrence_end_date={:?}", + request.series_uid, request.update_scope, request.recurrence_count, request.recurrence_end_date ); // Extract and verify token @@ -765,9 +765,36 @@ fn update_entire_series( updated_event.last_modified = Some(now); // Keep original created timestamp to preserve event history - // For simple updates (like drag operations), preserve the existing RRULE - // For more complex updates, we might need to regenerate it, but for now keep it simple - // updated_event.rrule remains unchanged from the clone + // Update RRULE if recurrence parameters are provided + if let Some(ref existing_rrule) = updated_event.rrule { + let mut new_rrule = existing_rrule.clone(); + println!("🔄 Original RRULE: {}", existing_rrule); + + // Update COUNT if provided + if let Some(count) = request.recurrence_count { + println!("🔄 Updating RRULE with new COUNT: {}", count); + // Remove old COUNT or UNTIL parameters + new_rrule = new_rrule.split(';') + .filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL=")) + .collect::>() + .join(";"); + // Add new COUNT + new_rrule = format!("{};COUNT={}", new_rrule, count); + } else if let Some(ref end_date) = request.recurrence_end_date { + println!("🔄 Updating RRULE with new UNTIL: {}", end_date); + // Remove old COUNT or UNTIL parameters + new_rrule = new_rrule.split(';') + .filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL=")) + .collect::>() + .join(";"); + // Add new UNTIL (convert YYYY-MM-DD to YYYYMMDD format) + let until_date = end_date.replace("-", ""); + new_rrule = format!("{};UNTIL={}", new_rrule, until_date); + } + + println!("🔄 Updated RRULE: {}", new_rrule); + updated_event.rrule = Some(new_rrule); + } // Copy the updated event back to existing_event for the main handler *existing_event = updated_event.clone(); diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 0f21bbe..a628517 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -478,7 +478,10 @@ pub fn App() -> Html { params.13, // categories params.14, // reminder params.15, // recurrence - params.17, // calendar_path (skipping recurrence_days) + params.16, // recurrence_days + params.18, // recurrence_count + params.19, // recurrence_until + params.17, // calendar_path scope, event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date ) @@ -759,6 +762,9 @@ pub fn App() -> Html { original_event.categories.join(","), reminder_str.clone(), recurrence_str.clone(), + vec![false; 7], + None, + None, original_event.calendar_path.clone(), scope.clone(), occurrence_date, diff --git a/frontend/src/components/create_event_modal.rs b/frontend/src/components/create_event_modal.rs index aa28bbd..1335ff9 100644 --- a/frontend/src/components/create_event_modal.rs +++ b/frontend/src/components/create_event_modal.rs @@ -290,16 +290,32 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend // Reminders - TODO: Parse alarm from VEvent if needed reminder: ReminderType::None, - // Recurrence - TODO: Parse RRULE if needed for advanced editing - recurrence: if event.rrule.is_some() { - RecurrenceType::Daily // Default, could be enhanced to parse actual RRULE + // Recurrence - Parse RRULE if present + recurrence: if let Some(ref rrule_str) = event.rrule { + parse_rrule_frequency(rrule_str) } else { RecurrenceType::None }, - recurrence_interval: 1, - recurrence_until: None, - recurrence_count: None, - recurrence_days: vec![false; 7], + recurrence_interval: if let Some(ref rrule_str) = event.rrule { + parse_rrule_interval(rrule_str) + } else { + 1 + }, + recurrence_until: if let Some(ref rrule_str) = event.rrule { + parse_rrule_until(rrule_str) + } else { + None + }, + recurrence_count: if let Some(ref rrule_str) = event.rrule { + parse_rrule_count(rrule_str) + } else { + None + }, + recurrence_days: if let Some(ref rrule_str) = event.rrule { + parse_rrule_days(rrule_str) + } else { + vec![false; 7] + }, // Advanced recurrence monthly_by_day: None, @@ -327,4 +343,113 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend original_uid: Some(event.uid.clone()), // Preserve original UID for editing occurrence_date: Some(start_local.date()), // The occurrence date being edited } +} + +// Parse RRULE frequency component +fn parse_rrule_frequency(rrule: &str) -> RecurrenceType { + if rrule.contains("FREQ=DAILY") { + RecurrenceType::Daily + } else if rrule.contains("FREQ=WEEKLY") { + RecurrenceType::Weekly + } else if rrule.contains("FREQ=MONTHLY") { + RecurrenceType::Monthly + } else if rrule.contains("FREQ=YEARLY") { + RecurrenceType::Yearly + } else { + RecurrenceType::None + } +} + +// Parse RRULE interval component +fn parse_rrule_interval(rrule: &str) -> u32 { + if let Some(start) = rrule.find("INTERVAL=") { + let interval_part = &rrule[start + 9..]; + if let Some(end) = interval_part.find(';') { + interval_part[..end].parse().unwrap_or(1) + } else { + interval_part.parse().unwrap_or(1) + } + } else { + 1 + } +} + +// Parse RRULE count component +fn parse_rrule_count(rrule: &str) -> Option { + if let Some(start) = rrule.find("COUNT=") { + let count_part = &rrule[start + 6..]; + if let Some(end) = count_part.find(';') { + count_part[..end].parse().ok() + } else { + count_part.parse().ok() + } + } else { + None + } +} + +// Parse RRULE until component +fn parse_rrule_until(rrule: &str) -> Option { + if let Some(start) = rrule.find("UNTIL=") { + let until_part = &rrule[start + 6..]; + let until_str = if let Some(end) = until_part.find(';') { + &until_part[..end] + } else { + until_part + }; + + // UNTIL can be in format YYYYMMDD or YYYYMMDDTHHMMSSZ + let date_part = if until_str.len() >= 8 { + &until_str[..8] + } else { + until_str + }; + + // Parse YYYYMMDD format + if date_part.len() == 8 { + if let (Ok(year), Ok(month), Ok(day)) = ( + date_part[0..4].parse::(), + date_part[4..6].parse::(), + date_part[6..8].parse::(), + ) { + chrono::NaiveDate::from_ymd_opt(year, month, day) + } else { + None + } + } else { + None + } + } else { + None + } +} + +// Parse RRULE BYDAY component for weekly recurrence +fn parse_rrule_days(rrule: &str) -> Vec { + let mut days = vec![false; 7]; // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] + + if let Some(start) = rrule.find("BYDAY=") { + let byday_part = &rrule[start + 6..]; + let byday_str = if let Some(end) = byday_part.find(';') { + &byday_part[..end] + } else { + byday_part + }; + + // Parse comma-separated day codes: SU,MO,TU,WE,TH,FR,SA + for day_code in byday_str.split(',') { + match day_code.trim() { + "SU" => days[0] = true, // Sunday + "MO" => days[1] = true, // Monday + "TU" => days[2] = true, // Tuesday + "WE" => days[3] = true, // Wednesday + "TH" => days[4] = true, // Thursday + "FR" => days[5] = true, // Friday + "SA" => days[6] = true, // Saturday + _ => {} // Ignore unknown day codes + } + } + } + + days } \ No newline at end of file diff --git a/frontend/src/services/calendar_service.rs b/frontend/src/services/calendar_service.rs index f0b64c1..5e850a1 100644 --- a/frontend/src/services/calendar_service.rs +++ b/frontend/src/services/calendar_service.rs @@ -1678,6 +1678,9 @@ impl CalendarService { categories: String, reminder: String, recurrence: String, + recurrence_days: Vec, + recurrence_count: Option, + recurrence_until: Option, calendar_path: Option, update_scope: String, occurrence_date: Option, @@ -1706,10 +1709,10 @@ impl CalendarService { "categories": categories, "reminder": reminder, "recurrence": recurrence, - "recurrence_days": vec![false; 7], // Default - could be enhanced - "recurrence_interval": 1_u32, // Default interval - "recurrence_end_date": None as Option, // No end date by default - "recurrence_count": None as Option, // No count limit by default + "recurrence_days": recurrence_days, + "recurrence_interval": 1_u32, // Default interval - could be enhanced to be a parameter + "recurrence_end_date": recurrence_until, + "recurrence_count": recurrence_count, "calendar_path": calendar_path, "update_scope": update_scope, "occurrence_date": occurrence_date