use yew::prelude::*; use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement}; use chrono::{NaiveDate, NaiveTime}; use crate::services::calendar_service::{CalendarInfo, CalendarEvent}; #[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<(CalendarEvent, EventCreationData)>, // (original_event, updated_data) pub available_calendars: Vec, } #[derive(Clone, PartialEq, Debug)] pub enum EventStatus { Tentative, Confirmed, Cancelled, } impl Default for EventStatus { fn default() -> Self { EventStatus::Confirmed } } impl EventStatus { pub fn from_service_status(status: &crate::services::calendar_service::EventStatus) -> Self { match status { crate::services::calendar_service::EventStatus::Tentative => EventStatus::Tentative, crate::services::calendar_service::EventStatus::Confirmed => EventStatus::Confirmed, crate::services::calendar_service::EventStatus::Cancelled => EventStatus::Cancelled, } } } #[derive(Clone, PartialEq, Debug)] pub enum EventClass { Public, Private, Confidential, } impl Default for EventClass { fn default() -> Self { EventClass::Public } } impl EventClass { pub fn from_service_class(class: &crate::services::calendar_service::EventClass) -> Self { match class { crate::services::calendar_service::EventClass::Public => EventClass::Public, crate::services::calendar_service::EventClass::Private => EventClass::Private, crate::services::calendar_service::EventClass::Confidential => EventClass::Confidential, } } } #[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 } } 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, } } } #[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 } 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, } } } impl EventCreationData { pub fn from_calendar_event(event: &CalendarEvent) -> Self { // Convert CalendarEvent to EventCreationData for editing Self { title: event.summary.clone().unwrap_or_default(), description: event.description.clone().unwrap_or_default(), start_date: event.start.date_naive(), start_time: event.start.time(), end_date: event.end.as_ref().map(|e| e.date_naive()).unwrap_or(event.start.date_naive()), end_time: event.end.as_ref().map(|e| e.time()).unwrap_or(event.start.time()), location: event.location.clone().unwrap_or_default(), all_day: event.all_day, status: EventStatus::from_service_status(&event.status), class: EventClass::from_service_class(&event.class), priority: event.priority, organizer: event.organizer.clone().unwrap_or_default(), attendees: event.attendees.join(", "), categories: event.categories.join(", "), reminder: ReminderType::default(), // TODO: Convert from event reminders recurrence: RecurrenceType::from_rrule(event.recurrence_rule.as_deref()), recurrence_days: vec![false; 7], // TODO: Parse from RRULE selected_calendar: event.calendar_path.clone(), } } } #[function_component(CreateEventModal)] pub fn create_event_modal(props: &CreateEventModalProps) -> Html { 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()), { let event_data = event_data.clone(); move |(selected_date, event_to_edit, is_open, available_calendars)| { 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; 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()); } 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 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(); data.title = input.value(); 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(); data.selected_calendar = if value.is_empty() { None } else { Some(value) }; 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 days when changing recurrence type data.recurrence_days = vec![false; 7]; 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(()); }) }; let data = &*event_data; html! { } }