use crate::components::event_form::*; use crate::components::EditAction; use crate::models::ical::VEvent; use crate::services::calendar_service::CalendarInfo; use gloo_storage::{LocalStorage, Storage}; use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct CreateEventModalProps { pub is_open: bool, pub on_close: Callback<()>, pub on_create: Callback, pub available_calendars: Vec, pub selected_date: Option, pub initial_start_time: Option, pub initial_end_time: Option, #[prop_or_default] pub event_to_edit: Option, #[prop_or_default] pub edit_scope: Option, } #[function_component(CreateEventModal)] pub fn create_event_modal(props: &CreateEventModalProps) -> Html { let active_tab = use_state(|| ModalTab::default()); let event_data = use_state(|| EventCreationData::default()); // 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 { // Convert VEvent to EventCreationData for editing vevent_to_creation_data(event, &available_calendars) } 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; } if let Some(end_time) = initial_end_time { data.end_time = end_time; } data } else { EventCreationData::default() }; // Set default calendar if data.selected_calendar.is_none() && !available_calendars.is_empty() { // For new events, try to use the last used calendar if event_to_edit.is_none() { // Try to get last used calendar from localStorage if let Ok(last_used_calendar) = LocalStorage::get::("last_used_calendar") { // Check if the last used calendar is still available if available_calendars.iter().any(|cal| cal.path == last_used_calendar) { data.selected_calendar = Some(last_used_calendar); } else { // Fall back to first available calendar data.selected_calendar = Some(available_calendars[0].path.clone()); } } else { // No last used calendar, use first available data.selected_calendar = Some(available_calendars[0].path.clone()); } } else { // For editing existing events, keep the current calendar as default 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(()); } }) }; let switch_to_tab = { let active_tab = active_tab.clone(); Callback::from(move |tab: ModalTab| { active_tab.set(tab); }) }; let on_save = { let event_data = event_data.clone(); let on_create = props.on_create.clone(); let event_to_edit = props.event_to_edit.clone(); Callback::from(move |_: MouseEvent| { let mut data = (*event_data).clone(); // If we're editing an existing event, mark it as an update operation if let Some(ref original_event) = event_to_edit { // Set the original UID so the backend knows to update instead of create data.original_uid = Some(original_event.uid.clone()); } on_create.emit(data); }) }; 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! { } } // Convert VEvent to EventCreationData for editing fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calendars: &[CalendarInfo]) -> EventCreationData { use chrono::Local; // Convert start datetime from UTC to local let start_local = event.dtstart.with_timezone(&Local).naive_local(); let end_local = if let Some(dtend) = event.dtend { dtend.with_timezone(&Local).naive_local() } else { // Default to 1 hour after start if no end time start_local + chrono::Duration::hours(1) }; EventCreationData { // Basic event info title: event.summary.clone().unwrap_or_default(), description: event.description.clone().unwrap_or_default(), location: event.location.clone().unwrap_or_default(), all_day: event.all_day, // Timing start_date: start_local.date(), end_date: end_local.date(), start_time: start_local.time(), end_time: end_local.time(), // Classification status: match event.status { Some(crate::models::ical::EventStatus::Tentative) => EventStatus::Tentative, Some(crate::models::ical::EventStatus::Confirmed) => EventStatus::Confirmed, Some(crate::models::ical::EventStatus::Cancelled) => EventStatus::Cancelled, None => EventStatus::Confirmed, }, class: match event.class { Some(crate::models::ical::EventClass::Public) => EventClass::Public, Some(crate::models::ical::EventClass::Private) => EventClass::Private, Some(crate::models::ical::EventClass::Confidential) => EventClass::Confidential, None => EventClass::Public, }, priority: event.priority, // People 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(","), // Categorization categories: event.categories.join(","), // Reminders - TODO: Parse alarm from VEvent if needed reminder: ReminderType::None, // Recurrence - Parse RRULE if present recurrence: if let Some(ref rrule_str) = event.rrule { parse_rrule_frequency(rrule_str) } else { RecurrenceType::None }, 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, monthly_by_monthday: None, yearly_by_month: vec![false; 12], // Calendar selection - try to find the calendar this event belongs to selected_calendar: if let Some(ref calendar_path) = event.calendar_path { if available_calendars.iter().any(|cal| cal.path == *calendar_path) { Some(calendar_path.clone()) } else if let Some(first_calendar) = available_calendars.first() { Some(first_calendar.path.clone()) } else { None } } else if let Some(first_calendar) = available_calendars.first() { Some(first_calendar.path.clone()) } else { None }, // Edit tracking edit_scope: None, // Will be set by the modal after creation changed_fields: vec![], 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 }