use crate::components::EditAction; use crate::models::ical::VEvent; use crate::services::calendar_service::CalendarInfo; use chrono::{Datelike, Local, NaiveDate, NaiveTime, TimeZone, Utc}; use wasm_bindgen::JsCast; use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement}; use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct CreateEventModalProps { pub is_open: bool, pub selected_date: Option, pub event_to_edit: Option, pub on_close: Callback<()>, pub on_create: Callback, pub on_update: Callback<(VEvent, EventCreationData)>, // (original_event, updated_data) pub available_calendars: Vec, #[prop_or_default] pub initial_start_time: Option, #[prop_or_default] pub initial_end_time: Option, #[prop_or_default] pub edit_scope: Option, } #[derive(Clone, PartialEq, Debug)] pub enum EventStatus { Tentative, Confirmed, Cancelled, } impl Default for EventStatus { fn default() -> Self { EventStatus::Confirmed } } #[derive(Clone, PartialEq, Debug)] pub enum EventClass { Public, Private, Confidential, } impl Default for EventClass { fn default() -> Self { EventClass::Public } } #[derive(Clone, PartialEq, Debug)] pub enum ReminderType { None, Minutes15, Minutes30, Hour1, Hours2, Day1, Days2, Week1, } impl Default for ReminderType { fn default() -> Self { ReminderType::None } } #[derive(Clone, PartialEq, Debug)] pub enum RecurrenceType { None, Daily, Weekly, Monthly, Yearly, } impl Default for RecurrenceType { fn default() -> Self { RecurrenceType::None } } /// Parse RRULE string into recurrence components /// Example RRULE: "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;UNTIL=20231215T000000Z" #[derive(Debug, Default, Clone)] struct ParsedRrule { pub freq: RecurrenceType, pub interval: u32, pub until: Option, pub count: Option, pub byday: Vec, pub bymonthday: Option, pub bymonth: Vec, } fn parse_rrule(rrule: Option<&str>) -> ParsedRrule { let mut parsed = ParsedRrule { interval: 1, // Default interval is 1 ..Default::default() }; let Some(rrule_str) = rrule else { return parsed; }; // Split RRULE into parts: "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE" for part in rrule_str.split(';') { let mut key_value = part.split('='); let key = key_value.next().unwrap_or(""); let value = key_value.next().unwrap_or(""); match key { "FREQ" => { parsed.freq = match value { "DAILY" => RecurrenceType::Daily, "WEEKLY" => RecurrenceType::Weekly, "MONTHLY" => RecurrenceType::Monthly, "YEARLY" => RecurrenceType::Yearly, _ => RecurrenceType::None, }; } "INTERVAL" => { if let Ok(interval) = value.parse::() { parsed.interval = interval.max(1); } } "UNTIL" => { // Parse UNTIL date: "20231215T000000Z" -> NaiveDate if value.len() >= 8 { let date_part = &value[..8]; // Extract YYYYMMDD if let Ok(until_date) = NaiveDate::parse_from_str(date_part, "%Y%m%d") { parsed.until = Some(until_date); } } } "COUNT" => { if let Ok(count) = value.parse::() { parsed.count = Some(count); } } "BYDAY" => { // Parse BYDAY: "MO,WE,FR" or "1MO,-1SU" (with position) parsed.byday = value.split(',').map(|s| s.trim().to_uppercase()).collect(); } "BYMONTHDAY" => { // Parse BYMONTHDAY: "15" or "1,15,31" if let Some(first_day) = value.split(',').next() { if let Ok(day) = first_day.parse::() { if day >= 1 && day <= 31 { parsed.bymonthday = Some(day); } } } } "BYMONTH" => { // Parse BYMONTH: "1,3,5" (January, March, May) parsed.bymonth = value .split(',') .filter_map(|m| m.trim().parse::().ok()) .filter(|&m| m >= 1 && m <= 12) .collect(); } _ => {} // Ignore unknown parameters } } parsed } /// Convert BYDAY values to weekday boolean array /// Maps RFC 5545 day codes to [Sun, Mon, Tue, Wed, Thu, Fri, Sat] fn byday_to_weekday_array(byday: &[String]) -> Vec { let mut weekdays = vec![false; 7]; for day_spec in byday { // Handle both simple days (MO, TU) and positioned days (1MO, -1SU) let day_code = if day_spec.len() > 2 { // Extract last 2 characters for positioned days like "1MO" -> "MO" &day_spec[day_spec.len() - 2..] } else { day_spec }; let index = match day_code { "SU" => 0, // Sunday "MO" => 1, // Monday "TU" => 2, // Tuesday "WE" => 3, // Wednesday "TH" => 4, // Thursday "FR" => 5, // Friday "SA" => 6, // Saturday _ => continue, }; weekdays[index] = true; } weekdays } /// Convert BYMONTH values to monthly boolean array /// Maps month numbers to [Jan, Feb, Mar, ..., Dec] fn bymonth_to_monthly_array(bymonth: &[u8]) -> Vec { let mut months = vec![false; 12]; for &month in bymonth { if month >= 1 && month <= 12 { months[(month - 1) as usize] = true; } } months } /// Extract positioned weekday from BYDAY for monthly recurrence /// Examples: "1MO" -> Some("1MO"), "2TU" -> Some("2TU"), "-1SU" -> Some("-1SU") fn extract_monthly_byday(byday: &[String]) -> Option { byday .iter() .find(|day| day.len() > 2) // Positioned days have length > 2 .cloned() } #[cfg(test)] mod rrule_tests { use super::*; #[test] fn test_parse_simple_weekly() { let parsed = parse_rrule(Some("FREQ=WEEKLY;BYDAY=MO,WE,FR")); assert_eq!(parsed.freq, RecurrenceType::Weekly); assert_eq!(parsed.interval, 1); assert_eq!(parsed.byday, vec!["MO", "WE", "FR"]); } #[test] fn test_parse_complex_monthly() { let parsed = parse_rrule(Some( "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO;UNTIL=20241231T000000Z", )); assert_eq!(parsed.freq, RecurrenceType::Monthly); assert_eq!(parsed.interval, 2); assert_eq!(parsed.byday, vec!["1MO"]); assert!(parsed.until.is_some()); } #[test] fn test_byday_to_weekday_array() { let weekdays = byday_to_weekday_array(&["MO".to_string(), "WE".to_string(), "FR".to_string()]); // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] assert_eq!(weekdays, vec![false, true, false, true, false, true, false]); } #[test] fn test_extract_monthly_byday() { let byday = vec!["1MO".to_string(), "WE".to_string()]; assert_eq!(extract_monthly_byday(&byday), Some("1MO".to_string())); let byday_simple = vec!["MO".to_string(), "TU".to_string()]; assert_eq!(extract_monthly_byday(&byday_simple), None); } #[test] fn test_build_rrule_weekly() { let mut data = EventCreationData::default(); data.recurrence = RecurrenceType::Weekly; data.recurrence_interval = 2; data.recurrence_days = vec![false, true, false, true, false, true, false]; // Mon, Wed, Fri data.recurrence_count = Some(10); let rrule = data.build_rrule(); assert!(rrule.contains("FREQ=WEEKLY")); assert!(rrule.contains("INTERVAL=2")); assert!(rrule.contains("BYDAY=MO,WE,FR")); assert!(rrule.contains("COUNT=10")); } #[test] fn test_build_rrule_monthly_by_day() { let mut data = EventCreationData::default(); data.recurrence = RecurrenceType::Monthly; data.monthly_by_day = Some("1MO".to_string()); data.recurrence_until = Some(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()); let rrule = data.build_rrule(); assert!(rrule.contains("FREQ=MONTHLY")); assert!(rrule.contains("BYDAY=1MO")); assert!(rrule.contains("UNTIL=20241231T000000Z")); } #[test] fn test_build_rrule_yearly() { let mut data = EventCreationData::default(); data.recurrence = RecurrenceType::Yearly; 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)] pub struct EventCreationData { pub title: String, pub description: String, pub start_date: NaiveDate, pub start_time: NaiveTime, pub end_date: NaiveDate, pub end_time: NaiveTime, pub location: String, pub all_day: bool, pub status: EventStatus, pub class: EventClass, pub priority: Option, pub organizer: String, pub attendees: String, // Comma-separated list pub categories: String, // Comma-separated list pub reminder: ReminderType, pub recurrence: RecurrenceType, pub recurrence_days: Vec, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence pub selected_calendar: Option, // Calendar path // Advanced recurrence fields pub recurrence_interval: u32, // INTERVAL - every N (days/weeks/months/years) pub recurrence_until: Option, // UNTIL date pub recurrence_count: Option, // COUNT - number of occurrences pub monthly_by_day: Option, // For monthly: "1MO" = first Monday, "2TU" = second Tuesday, etc. pub monthly_by_monthday: Option, // For monthly: day of month (1-31) pub yearly_by_month: Vec, // For yearly: [Jan, Feb, Mar, ..., Dec] // Edit scope and tracking fields pub edit_scope: Option, pub changed_fields: Vec, // List of field names that were changed } impl Default for EventCreationData { fn default() -> Self { let now = chrono::Local::now().naive_local(); let start_time = NaiveTime::from_hms_opt(9, 0, 0).unwrap_or_default(); let end_time = NaiveTime::from_hms_opt(10, 0, 0).unwrap_or_default(); Self { title: String::new(), description: String::new(), start_date: now.date(), start_time, end_date: now.date(), end_time, location: String::new(), all_day: false, status: EventStatus::default(), class: EventClass::default(), priority: None, organizer: String::new(), attendees: String::new(), categories: String::new(), reminder: ReminderType::default(), recurrence: RecurrenceType::default(), recurrence_days: vec![false; 7], // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] - all false by default selected_calendar: None, // Advanced recurrence defaults recurrence_interval: 1, recurrence_until: None, recurrence_count: None, monthly_by_day: None, monthly_by_monthday: None, yearly_by_month: vec![false; 12], // [Jan, Feb, ..., Dec] - all false by default // Edit scope and tracking defaults edit_scope: None, changed_fields: vec![], } } } impl EventCreationData { /// Build a complete RRULE string from recurrence fields fn build_rrule(&self) -> String { if matches!(self.recurrence, RecurrenceType::None) { return String::new(); } let mut parts = Vec::new(); // Add frequency (required) match self.recurrence { RecurrenceType::Daily => parts.push("FREQ=DAILY".to_string()), RecurrenceType::Weekly => parts.push("FREQ=WEEKLY".to_string()), RecurrenceType::Monthly => parts.push("FREQ=MONTHLY".to_string()), RecurrenceType::Yearly => parts.push("FREQ=YEARLY".to_string()), RecurrenceType::None => return String::new(), } // Add interval if not 1 if self.recurrence_interval > 1 { parts.push(format!("INTERVAL={}", self.recurrence_interval)); } // Add frequency-specific rules match self.recurrence { RecurrenceType::Weekly => { // Add BYDAY for weekly recurrence let selected_days: Vec<&str> = self .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 _ => "", }) } else { None } }) .filter(|s| !s.is_empty()) .collect(); if !selected_days.is_empty() { parts.push(format!("BYDAY={}", selected_days.join(","))); } } RecurrenceType::Monthly => { // Add BYDAY or BYMONTHDAY for monthly recurrence if let Some(ref by_day) = self.monthly_by_day { parts.push(format!("BYDAY={}", by_day)); } else if let Some(by_monthday) = self.monthly_by_monthday { parts.push(format!("BYMONTHDAY={}", by_monthday)); } } RecurrenceType::Yearly => { // Add BYMONTH for yearly recurrence let selected_months: Vec = self .yearly_by_month .iter() .enumerate() .filter_map(|(i, &selected)| { if selected { Some((i + 1).to_string()) // Convert 0-based index to 1-based month } else { None } }) .collect(); if !selected_months.is_empty() { parts.push(format!("BYMONTH={}", selected_months.join(","))); } } _ => {} } // Add end condition (UNTIL or COUNT) if let Some(until_date) = self.recurrence_until { // Format as UNTIL=YYYYMMDDTHHMMSSZ parts.push(format!("UNTIL={}T000000Z", until_date.format("%Y%m%d"))); } else if let Some(count) = self.recurrence_count { parts.push(format!("COUNT={}", count)); } parts.join(";") } pub fn to_create_event_params( &self, ) -> ( String, String, String, String, String, String, String, bool, String, String, Option, String, String, String, String, String, Vec, Option, ) { // Convert local date/time to UTC let start_local = Local .from_local_datetime(&self.start_date.and_time(self.start_time)) .single() .unwrap_or_else(|| Local::now()); let end_local = Local .from_local_datetime(&self.end_date.and_time(self.end_time)) .single() .unwrap_or_else(|| Local::now()); let start_utc = start_local.with_timezone(&Utc); let end_utc = end_local.with_timezone(&Utc); ( self.title.clone(), self.description.clone(), start_utc.format("%Y-%m-%d").to_string(), start_utc.format("%H:%M").to_string(), end_utc.format("%Y-%m-%d").to_string(), end_utc.format("%H:%M").to_string(), self.location.clone(), self.all_day, match self.status { EventStatus::Tentative => "TENTATIVE".to_string(), EventStatus::Confirmed => "CONFIRMED".to_string(), EventStatus::Cancelled => "CANCELLED".to_string(), }, match self.class { EventClass::Public => "PUBLIC".to_string(), EventClass::Private => "PRIVATE".to_string(), EventClass::Confidential => "CONFIDENTIAL".to_string(), }, self.priority, self.organizer.clone(), self.attendees.clone(), self.categories.clone(), match self.reminder { ReminderType::None => "".to_string(), ReminderType::Minutes15 => "15".to_string(), ReminderType::Minutes30 => "30".to_string(), ReminderType::Hour1 => "60".to_string(), ReminderType::Hours2 => "120".to_string(), ReminderType::Day1 => "1440".to_string(), ReminderType::Days2 => "2880".to_string(), ReminderType::Week1 => "10080".to_string(), }, self.build_rrule(), // Use the comprehensive RRULE builder self.recurrence_days.clone(), self.selected_calendar.clone(), ) } } impl EventCreationData { pub fn from_calendar_event(event: &VEvent) -> Self { // Convert VEvent to EventCreationData for editing // All events (including temporary drag events) now have proper UTC times // Convert to local time for display in the modal // Parse RRULE once for efficiency let parsed_rrule = parse_rrule(event.rrule.as_deref()); Self { title: event.summary.clone().unwrap_or_default(), description: event.description.clone().unwrap_or_default(), start_date: event.dtstart.with_timezone(&chrono::Local).date_naive(), start_time: event.dtstart.with_timezone(&chrono::Local).time(), end_date: event .dtend .as_ref() .map(|e| e.with_timezone(&chrono::Local).date_naive()) .unwrap_or(event.dtstart.with_timezone(&chrono::Local).date_naive()), end_time: event .dtend .as_ref() .map(|e| e.with_timezone(&chrono::Local).time()) .unwrap_or(event.dtstart.with_timezone(&chrono::Local).time()), location: event.location.clone().unwrap_or_default(), all_day: event.all_day, status: event .status .as_ref() .map(|s| match s { crate::models::ical::EventStatus::Tentative => EventStatus::Tentative, crate::models::ical::EventStatus::Confirmed => EventStatus::Confirmed, crate::models::ical::EventStatus::Cancelled => EventStatus::Cancelled, }) .unwrap_or(EventStatus::Confirmed), class: event .class .as_ref() .map(|c| match c { crate::models::ical::EventClass::Public => EventClass::Public, crate::models::ical::EventClass::Private => EventClass::Private, crate::models::ical::EventClass::Confidential => EventClass::Confidential, }) .unwrap_or(EventClass::Public), priority: event.priority, organizer: event .organizer .as_ref() .map(|o| o.cal_address.clone()) .unwrap_or_default(), attendees: event .attendees .iter() .map(|a| a.cal_address.clone()) .collect::>() .join(", "), categories: event.categories.join(", "), reminder: ReminderType::default(), // TODO: Convert from event reminders recurrence: parsed_rrule.freq.clone(), recurrence_days: if parsed_rrule.freq == RecurrenceType::Weekly { byday_to_weekday_array(&parsed_rrule.byday) } else { vec![false; 7] }, selected_calendar: event.calendar_path.clone(), // Advanced recurrence fields from parsed RRULE recurrence_interval: parsed_rrule.interval, recurrence_until: parsed_rrule.until, recurrence_count: parsed_rrule.count, monthly_by_day: if parsed_rrule.freq == RecurrenceType::Monthly { extract_monthly_byday(&parsed_rrule.byday) } else { None }, monthly_by_monthday: if parsed_rrule.freq == RecurrenceType::Monthly { parsed_rrule.bymonthday } else { None }, yearly_by_month: if parsed_rrule.freq == RecurrenceType::Yearly { bymonth_to_monthly_array(&parsed_rrule.bymonth) } else { vec![false; 12] }, // Edit scope and tracking defaults (will be set later if needed) edit_scope: None, changed_fields: vec![], } } } #[derive(Clone, PartialEq)] enum ModalTab { BasicDetails, Advanced, People, Categories, Location, Reminders, } impl Default for ModalTab { fn default() -> Self { ModalTab::BasicDetails } } #[function_component(CreateEventModal)] pub fn create_event_modal(props: &CreateEventModalProps) -> Html { let event_data = use_state(|| EventCreationData::default()); let active_tab = use_state(|| ModalTab::default()); // Initialize with selected date or event data if provided use_effect_with( ( props.selected_date, props.event_to_edit.clone(), props.is_open, props.available_calendars.clone(), props.initial_start_time, props.initial_end_time, props.edit_scope.clone(), ), { let event_data = event_data.clone(); move |( selected_date, event_to_edit, is_open, available_calendars, initial_start_time, initial_end_time, edit_scope, )| { if *is_open { let mut data = if let Some(event) = event_to_edit { // Pre-populate with event data for editing EventCreationData::from_calendar_event(event) } else if let Some(date) = selected_date { // Initialize with selected date for new event let mut data = EventCreationData::default(); data.start_date = *date; data.end_date = *date; // Use initial times if provided (from drag-to-create) if let Some(start_time) = initial_start_time { data.start_time = *start_time; } if let Some(end_time) = initial_end_time { data.end_time = *end_time; } data } else { // Default initialization EventCreationData::default() }; // Set default calendar to the first available one if none selected if data.selected_calendar.is_none() && !available_calendars.is_empty() { data.selected_calendar = Some(available_calendars[0].path.clone()); } // Set edit scope if provided if let Some(scope) = edit_scope { data.edit_scope = Some(scope.clone()); } event_data.set(data); } || () } }, ); if !props.is_open { return html! {}; } let on_backdrop_click = { let on_close = props.on_close.clone(); Callback::from(move |e: MouseEvent| { if e.target() == e.current_target() { on_close.emit(()); } }) }; // Helper function to track field changes let _track_field_change = |data: &mut EventCreationData, field_name: &str| { if !data.changed_fields.contains(&field_name.to_string()) { data.changed_fields.push(field_name.to_string()); } }; let on_title_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); let new_value = input.value(); if data.title != new_value { data.title = new_value; if !data.changed_fields.contains(&"title".to_string()) { data.changed_fields.push("title".to_string()); } } event_data.set(data); } }) }; let on_calendar_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); let value = select.value(); let new_calendar = if value.is_empty() { None } else { Some(value) }; if data.selected_calendar != new_calendar { data.selected_calendar = new_calendar; if !data .changed_fields .contains(&"selected_calendar".to_string()) { data.changed_fields.push("selected_calendar".to_string()); } } event_data.set(data); } }) }; let on_description_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(textarea) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.description = textarea.value(); event_data.set(data); } }) }; let on_location_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.location = input.value(); event_data.set(data); } }) }; let on_organizer_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.organizer = input.value(); event_data.set(data); } }) }; let on_attendees_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(textarea) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.attendees = textarea.value(); event_data.set(data); } }) }; let on_categories_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.categories = input.value(); event_data.set(data); } }) }; let on_status_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.status = match select.value().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }; event_data.set(data); } }) }; let on_class_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.class = match select.value().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }; event_data.set(data); } }) }; let _on_priority_input = { let event_data = event_data.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.priority = input.value().parse::().ok().filter(|&p| p <= 9); event_data.set(data); } }) }; let on_reminder_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.reminder = match select.value().as_str() { "15min" => ReminderType::Minutes15, "30min" => ReminderType::Minutes30, "1hour" => ReminderType::Hour1, "2hours" => ReminderType::Hours2, "1day" => ReminderType::Day1, "2days" => ReminderType::Days2, "1week" => ReminderType::Week1, _ => ReminderType::None, }; event_data.set(data); } }) }; let on_recurrence_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.recurrence = match select.value().as_str() { "daily" => RecurrenceType::Daily, "weekly" => RecurrenceType::Weekly, "monthly" => RecurrenceType::Monthly, "yearly" => RecurrenceType::Yearly, _ => RecurrenceType::None, }; // Reset recurrence-related fields when changing recurrence type data.recurrence_days = vec![false; 7]; data.recurrence_interval = 1; data.recurrence_until = None; data.recurrence_count = None; data.monthly_by_day = None; data.monthly_by_monthday = None; data.yearly_by_month = vec![false; 12]; event_data.set(data); } }) }; let on_recurrence_interval_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { if let Ok(interval) = input.value().parse::() { let mut data = (*event_data).clone(); data.recurrence_interval = interval.max(1); event_data.set(data); } } }) }; let on_recurrence_until_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if input.value().is_empty() { data.recurrence_until = None; } else if let Ok(date) = NaiveDate::parse_from_str(&input.value(), "%Y-%m-%d") { data.recurrence_until = Some(date); } event_data.set(data); } }) }; let on_recurrence_count_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if input.value().is_empty() { data.recurrence_count = None; } else if let Ok(count) = input.value().parse::() { data.recurrence_count = Some(count.max(1)); } event_data.set(data); } }) }; let on_monthly_by_monthday_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if input.value().is_empty() { data.monthly_by_monthday = None; } else if let Ok(day) = input.value().parse::() { if day >= 1 && day <= 31 { data.monthly_by_monthday = Some(day); data.monthly_by_day = None; // Clear the other option } } event_data.set(data); } }) }; let on_monthly_by_day_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(select) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if select.value().is_empty() || select.value() == "none" { data.monthly_by_day = None; } else { data.monthly_by_day = Some(select.value()); data.monthly_by_monthday = None; // Clear the other option } event_data.set(data); } }) }; let on_yearly_month_change = { let event_data = event_data.clone(); move |month_index: usize| { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if month_index < data.yearly_by_month.len() { data.yearly_by_month[month_index] = input.checked(); event_data.set(data); } } }) } }; let on_weekday_change = { let event_data = event_data.clone(); move |day_index: usize| { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); if day_index < data.recurrence_days.len() { data.recurrence_days[day_index] = input.checked(); event_data.set(data); } } }) } }; let on_start_date_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { if let Ok(date) = NaiveDate::parse_from_str(&input.value(), "%Y-%m-%d") { let mut data = (*event_data).clone(); data.start_date = date; event_data.set(data); } } }) }; let on_start_time_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { if let Ok(time) = NaiveTime::parse_from_str(&input.value(), "%H:%M") { let mut data = (*event_data).clone(); data.start_time = time; event_data.set(data); } } }) }; let on_end_date_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { if let Ok(date) = NaiveDate::parse_from_str(&input.value(), "%Y-%m-%d") { let mut data = (*event_data).clone(); data.end_date = date; event_data.set(data); } } }) }; let on_end_time_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { if let Ok(time) = NaiveTime::parse_from_str(&input.value(), "%H:%M") { let mut data = (*event_data).clone(); data.end_time = time; event_data.set(data); } } }) }; let on_all_day_change = { let event_data = event_data.clone(); Callback::from(move |e: Event| { if let Some(input) = e.target_dyn_into::() { let mut data = (*event_data).clone(); data.all_day = input.checked(); event_data.set(data); } }) }; let on_submit_click = { let event_data = event_data.clone(); let on_create = props.on_create.clone(); let on_update = props.on_update.clone(); let event_to_edit = props.event_to_edit.clone(); Callback::from(move |_: MouseEvent| { if let Some(original_event) = &event_to_edit { // We're editing - call on_update with original event and new data on_update.emit((original_event.clone(), (*event_data).clone())); } else { // We're creating - call on_create with new data on_create.emit((*event_data).clone()); } }) }; let on_cancel_click = { let on_close = props.on_close.clone(); Callback::from(move |_: MouseEvent| { on_close.emit(()); }) }; // Tab switching callbacks let switch_to_tab = { let active_tab = active_tab.clone(); Callback::from(move |tab: ModalTab| { active_tab.set(tab); }) }; let data = &*event_data; html! { } }