Frontend Changes: - Add edit context menu option to EventContextMenu with pencil icon - Enhance CreateEventModal to support both create and edit modes - Add event data conversion methods for pre-populating edit forms - Implement conditional submit logic (on_create vs on_update callbacks) - Add update_event method to CalendarService with POST /calendar/events/update Backend Changes: - Add UpdateEventRequest and UpdateEventResponse models - Implement update_event handler with event search by UID across calendars - Add POST /api/calendar/events/update route - Full validation and parsing of all event properties for updates - Integrate with existing CalDAV client update_event functionality Users can now right-click events, select "Edit Event", modify properties in the modal, and successfully update existing events instead of creating duplicates. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
792 lines
32 KiB
Rust
792 lines
32 KiB
Rust
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<NaiveDate>,
|
||
pub event_to_edit: Option<CalendarEvent>,
|
||
pub on_close: Callback<()>,
|
||
pub on_create: Callback<EventCreationData>,
|
||
pub on_update: Callback<(CalendarEvent, EventCreationData)>, // (original_event, updated_data)
|
||
pub available_calendars: Vec<CalendarInfo>,
|
||
}
|
||
|
||
#[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<u8>,
|
||
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<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||
pub selected_calendar: Option<String>, // 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::<HtmlInputElement>() {
|
||
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::<HtmlSelectElement>() {
|
||
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::<HtmlTextAreaElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlTextAreaElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlSelectElement>() {
|
||
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::<HtmlSelectElement>() {
|
||
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::<HtmlInputElement>() {
|
||
let mut data = (*event_data).clone();
|
||
data.priority = input.value().parse::<u8>().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::<HtmlSelectElement>() {
|
||
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::<HtmlSelectElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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! {
|
||
<div class="modal-backdrop" onclick={on_backdrop_click}>
|
||
<div class="modal-content create-event-modal" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}>
|
||
<div class="modal-header">
|
||
<h3>{if props.event_to_edit.is_some() { "Edit Event" } else { "Create New Event" }}</h3>
|
||
<button type="button" class="modal-close" onclick={Callback::from({
|
||
let on_close = props.on_close.clone();
|
||
move |_: MouseEvent| on_close.emit(())
|
||
})}>{"×"}</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label for="event-title">{"Title *"}</label>
|
||
<input
|
||
type="text"
|
||
id="event-title"
|
||
class="form-input"
|
||
value={data.title.clone()}
|
||
oninput={on_title_input}
|
||
placeholder="Enter event title"
|
||
required=true
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-calendar">{"Calendar"}</label>
|
||
<select
|
||
id="event-calendar"
|
||
class="form-input"
|
||
onchange={on_calendar_change}
|
||
>
|
||
<option value="" selected={data.selected_calendar.is_none()}>{"Select calendar..."}</option>
|
||
{
|
||
props.available_calendars.iter().map(|calendar| {
|
||
let is_selected = data.selected_calendar.as_ref() == Some(&calendar.path);
|
||
html! {
|
||
<option
|
||
key={calendar.path.clone()}
|
||
value={calendar.path.clone()}
|
||
selected={is_selected}
|
||
>
|
||
{&calendar.display_name}
|
||
</option>
|
||
}
|
||
}).collect::<Html>()
|
||
}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-description">{"Description"}</label>
|
||
<textarea
|
||
id="event-description"
|
||
class="form-input"
|
||
value={data.description.clone()}
|
||
oninput={on_description_input}
|
||
placeholder="Enter event description"
|
||
rows="3"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
checked={data.all_day}
|
||
onchange={on_all_day_change}
|
||
/>
|
||
{" All Day"}
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="start-date">{"Start Date *"}</label>
|
||
<input
|
||
type="date"
|
||
id="start-date"
|
||
class="form-input"
|
||
value={data.start_date.format("%Y-%m-%d").to_string()}
|
||
onchange={on_start_date_change}
|
||
required=true
|
||
/>
|
||
</div>
|
||
|
||
if !data.all_day {
|
||
<div class="form-group">
|
||
<label for="start-time">{"Start Time"}</label>
|
||
<input
|
||
type="time"
|
||
id="start-time"
|
||
class="form-input"
|
||
value={data.start_time.format("%H:%M").to_string()}
|
||
onchange={on_start_time_change}
|
||
/>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="end-date">{"End Date *"}</label>
|
||
<input
|
||
type="date"
|
||
id="end-date"
|
||
class="form-input"
|
||
value={data.end_date.format("%Y-%m-%d").to_string()}
|
||
onchange={on_end_date_change}
|
||
required=true
|
||
/>
|
||
</div>
|
||
|
||
if !data.all_day {
|
||
<div class="form-group">
|
||
<label for="end-time">{"End Time"}</label>
|
||
<input
|
||
type="time"
|
||
id="end-time"
|
||
class="form-input"
|
||
value={data.end_time.format("%H:%M").to_string()}
|
||
onchange={on_end_time_change}
|
||
/>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-location">{"Location"}</label>
|
||
<input
|
||
type="text"
|
||
id="event-location"
|
||
class="form-input"
|
||
value={data.location.clone()}
|
||
oninput={on_location_input}
|
||
placeholder="Enter event location"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="event-status">{"Status"}</label>
|
||
<select
|
||
id="event-status"
|
||
class="form-input"
|
||
onchange={on_status_change}
|
||
>
|
||
<option value="confirmed" selected={matches!(data.status, EventStatus::Confirmed)}>{"Confirmed"}</option>
|
||
<option value="tentative" selected={matches!(data.status, EventStatus::Tentative)}>{"Tentative"}</option>
|
||
<option value="cancelled" selected={matches!(data.status, EventStatus::Cancelled)}>{"Cancelled"}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-class">{"Privacy"}</label>
|
||
<select
|
||
id="event-class"
|
||
class="form-input"
|
||
onchange={on_class_change}
|
||
>
|
||
<option value="public" selected={matches!(data.class, EventClass::Public)}>{"Public"}</option>
|
||
<option value="private" selected={matches!(data.class, EventClass::Private)}>{"Private"}</option>
|
||
<option value="confidential" selected={matches!(data.class, EventClass::Confidential)}>{"Confidential"}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-priority">{"Priority (0-9, optional)"}</label>
|
||
<input
|
||
type="number"
|
||
id="event-priority"
|
||
class="form-input"
|
||
value={data.priority.map(|p| p.to_string()).unwrap_or_default()}
|
||
oninput={on_priority_input}
|
||
placeholder="0-9 priority level"
|
||
min="0"
|
||
max="9"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-organizer">{"Organizer Email"}</label>
|
||
<input
|
||
type="email"
|
||
id="event-organizer"
|
||
class="form-input"
|
||
value={data.organizer.clone()}
|
||
oninput={on_organizer_input}
|
||
placeholder="organizer@example.com"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-attendees">{"Attendees (comma-separated emails)"}</label>
|
||
<textarea
|
||
id="event-attendees"
|
||
class="form-input"
|
||
value={data.attendees.clone()}
|
||
oninput={on_attendees_input}
|
||
placeholder="attendee1@example.com, attendee2@example.com"
|
||
rows="2"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-categories">{"Categories (comma-separated)"}</label>
|
||
<input
|
||
type="text"
|
||
id="event-categories"
|
||
class="form-input"
|
||
value={data.categories.clone()}
|
||
oninput={on_categories_input}
|
||
placeholder="work, meeting, personal"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="event-reminder">{"Reminder"}</label>
|
||
<select
|
||
id="event-reminder"
|
||
class="form-input"
|
||
onchange={on_reminder_change}
|
||
>
|
||
<option value="none" selected={matches!(data.reminder, ReminderType::None)}>{"None"}</option>
|
||
<option value="15min" selected={matches!(data.reminder, ReminderType::Minutes15)}>{"15 minutes"}</option>
|
||
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes"}</option>
|
||
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour"}</option>
|
||
<option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours"}</option>
|
||
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day"}</option>
|
||
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days"}</option>
|
||
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week"}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-recurrence">{"Recurrence"}</label>
|
||
<select
|
||
id="event-recurrence"
|
||
class="form-input"
|
||
onchange={on_recurrence_change}
|
||
>
|
||
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"None"}</option>
|
||
<option value="daily" selected={matches!(data.recurrence, RecurrenceType::Daily)}>{"Daily"}</option>
|
||
<option value="weekly" selected={matches!(data.recurrence, RecurrenceType::Weekly)}>{"Weekly"}</option>
|
||
<option value="monthly" selected={matches!(data.recurrence, RecurrenceType::Monthly)}>{"Monthly"}</option>
|
||
<option value="yearly" selected={matches!(data.recurrence, RecurrenceType::Yearly)}>{"Yearly"}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
// Show weekday selection only when weekly recurrence is selected
|
||
if matches!(data.recurrence, RecurrenceType::Weekly) {
|
||
<div class="form-group">
|
||
<label>{"Repeat on"}</label>
|
||
<div class="weekday-selection">
|
||
{
|
||
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, day)| {
|
||
let day_checked = data.recurrence_days.get(i).cloned().unwrap_or(false);
|
||
let on_change = on_weekday_change(i);
|
||
html! {
|
||
<label key={i} class="weekday-checkbox">
|
||
<input
|
||
type="checkbox"
|
||
checked={day_checked}
|
||
onchange={on_change}
|
||
/>
|
||
<span class="weekday-label">{day}</span>
|
||
</label>
|
||
}
|
||
})
|
||
.collect::<Html>()
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick={on_cancel_click}>
|
||
{"Cancel"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-primary"
|
||
onclick={on_submit_click}
|
||
disabled={data.title.trim().is_empty()}
|
||
>
|
||
{if props.event_to_edit.is_some() { "Update Event" } else { "Create Event" }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
} |