diff --git a/frontend/src/app.rs b/frontend/src/app.rs index f4c87a7..be11bba 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,7 +1,6 @@ use crate::components::{ - CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModalV2, DeleteAction, - EditAction, EventClass, EventContextMenu, EventCreationData, EventStatus, RecurrenceType, - ReminderType, RouteHandler, Sidebar, Theme, ViewMode, + CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModal, DeleteAction, + EditAction, EventContextMenu, EventCreationData, RouteHandler, Sidebar, Theme, ViewMode, }; use crate::components::sidebar::{Style}; use crate::models::ical::VEvent; @@ -1031,7 +1030,7 @@ pub fn App() -> Html { on_create_event={on_create_event_click} /> - Html { /> // Create event modal - , - 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, + pub selected_date: Option, + pub initial_start_time: Option, + pub initial_end_time: Option, #[prop_or_default] - pub initial_start_time: Option, - #[prop_or_default] - pub initial_end_time: Option, + pub event_to_edit: 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()); + let event_data = use_state(|| EventCreationData::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()); + // Initialize data when modal opens + { + let event_data = event_data.clone(); + let is_open = props.is_open; + let event_to_edit = props.event_to_edit.clone(); + let selected_date = props.selected_date; + let initial_start_time = props.initial_start_time; + let initial_end_time = props.initial_end_time; + let edit_scope = props.edit_scope.clone(); + let available_calendars = props.available_calendars.clone(); + + use_effect_with(is_open, move |&is_open| { + if is_open { + let mut data = if let Some(_event) = &event_to_edit { + // TODO: Convert VEvent to EventCreationData + EventCreationData::default() + } else if let Some(date) = selected_date { + let mut data = EventCreationData::default(); + data.start_date = date; + data.end_date = date; + if let Some(start_time) = initial_start_time { + data.start_time = start_time; } - - // Set edit scope if provided - if let Some(scope) = edit_scope { - data.edit_scope = Some(scope.clone()); + if let Some(end_time) = initial_end_time { + data.end_time = end_time; } + data + } else { + EventCreationData::default() + }; - event_data.set(data); + // Set default calendar + 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! {}; @@ -741,388 +84,6 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { }) }; - // 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| { @@ -1130,1186 +91,119 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { }) }; - let data = &*event_data; + let on_save = { + let event_data = event_data.clone(); + let on_create = props.on_create.clone(); + Callback::from(move |_: MouseEvent| { + on_create.emit((*event_data).clone()); + }) + }; + + let on_close = props.on_close.clone(); + let on_close_header = on_close.clone(); + + let tab_props = TabProps { + data: event_data.clone(), + available_calendars: props.available_calendars.clone(), + }; html! {