Clean up extensive console logging that was added during RRULE debugging. Removed debug logs from: - Frontend RRULE generation in create event modal - Frontend RRULE parsing in calendar service - Weekly/monthly/yearly occurrence generation functions - Backend RRULE processing in events and series handlers The core functionality remains unchanged - this is purely a cleanup of temporary debugging output that is no longer needed. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2207 lines
124 KiB
Rust
2207 lines
124 KiB
Rust
use yew::prelude::*;
|
||
use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement};
|
||
use wasm_bindgen::JsCast;
|
||
use chrono::{NaiveDate, NaiveTime, Local, TimeZone, Utc, Datelike};
|
||
use crate::services::calendar_service::CalendarInfo;
|
||
use crate::models::ical::VEvent;
|
||
|
||
#[derive(Properties, PartialEq)]
|
||
pub struct CreateEventModalProps {
|
||
pub is_open: bool,
|
||
pub selected_date: Option<NaiveDate>,
|
||
pub event_to_edit: Option<VEvent>,
|
||
pub on_close: Callback<()>,
|
||
pub on_create: Callback<EventCreationData>,
|
||
pub on_update: Callback<(VEvent, EventCreationData)>, // (original_event, updated_data)
|
||
pub available_calendars: Vec<CalendarInfo>,
|
||
#[prop_or_default]
|
||
pub initial_start_time: Option<NaiveTime>,
|
||
#[prop_or_default]
|
||
pub initial_end_time: Option<NaiveTime>,
|
||
}
|
||
|
||
#[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
|
||
}
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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<NaiveDate>,
|
||
pub count: Option<u32>,
|
||
pub byday: Vec<String>,
|
||
pub bymonthday: Option<u8>,
|
||
pub bymonth: Vec<u8>,
|
||
}
|
||
|
||
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::<u32>() {
|
||
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::<u32>() {
|
||
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::<u8>() {
|
||
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::<u8>().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<bool> {
|
||
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<bool> {
|
||
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<String> {
|
||
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<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
|
||
|
||
// Advanced recurrence fields
|
||
pub recurrence_interval: u32, // INTERVAL - every N (days/weeks/months/years)
|
||
pub recurrence_until: Option<NaiveDate>, // UNTIL date
|
||
pub recurrence_count: Option<u32>, // COUNT - number of occurrences
|
||
pub monthly_by_day: Option<String>, // For monthly: "1MO" = first Monday, "2TU" = second Tuesday, etc.
|
||
pub monthly_by_monthday: Option<u8>, // For monthly: day of month (1-31)
|
||
pub yearly_by_month: Vec<bool>, // For yearly: [Jan, Feb, Mar, ..., Dec]
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|
||
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<String> = 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<u8>, String, String, String, String, String, Vec<bool>, Option<String>) {
|
||
// 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::<Vec<_>>().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]
|
||
},
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
#[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), {
|
||
let event_data = event_data.clone();
|
||
move |(selected_date, event_to_edit, is_open, available_calendars, initial_start_time, initial_end_time)| {
|
||
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());
|
||
}
|
||
|
||
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-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::<HtmlInputElement>() {
|
||
if let Ok(interval) = input.value().parse::<u32>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<HtmlInputElement>() {
|
||
let mut data = (*event_data).clone();
|
||
if input.value().is_empty() {
|
||
data.recurrence_count = None;
|
||
} else if let Ok(count) = input.value().parse::<u32>() {
|
||
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::<HtmlInputElement>() {
|
||
let mut data = (*event_data).clone();
|
||
if input.value().is_empty() {
|
||
data.monthly_by_monthday = None;
|
||
} else if let Ok(day) = input.value().parse::<u8>() {
|
||
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::<HtmlSelectElement>() {
|
||
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::<HtmlInputElement>() {
|
||
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::<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(());
|
||
})
|
||
};
|
||
|
||
// 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! {
|
||
<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">
|
||
// Tab navigation
|
||
<div class="tab-navigation">
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::BasicDetails { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::BasicDetails))
|
||
}
|
||
>
|
||
{"Basic Details"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::Advanced { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Advanced))
|
||
}
|
||
>
|
||
{"Advanced"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::People { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::People))
|
||
}
|
||
>
|
||
{"People"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::Categories { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Categories))
|
||
}
|
||
>
|
||
{"Categories"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::Location { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Location))
|
||
}
|
||
>
|
||
{"Location"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class={if *active_tab == ModalTab::Reminders { "tab-button active" } else { "tab-button" }}
|
||
onclick={
|
||
let switch_to_tab = switch_to_tab.clone();
|
||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Reminders))
|
||
}
|
||
>
|
||
{"Reminders"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
// Tab Content
|
||
<div class="tab-content">
|
||
{
|
||
match *active_tab {
|
||
ModalTab::BasicDetails => html! {
|
||
<div class="tab-panel">
|
||
<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="event-recurrence-basic">{"Repeat"}</label>
|
||
<select
|
||
id="event-recurrence-basic"
|
||
class="form-input"
|
||
onchange={on_recurrence_change}
|
||
>
|
||
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"Does not repeat"}</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 class="form-group">
|
||
<label for="event-reminder-basic">{"Reminder"}</label>
|
||
<select
|
||
id="event-reminder-basic"
|
||
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 before"}</option>
|
||
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes before"}</option>
|
||
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour before"}</option>
|
||
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day before"}</option>
|
||
</select>
|
||
</div>
|
||
</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>
|
||
|
||
// 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>
|
||
}
|
||
|
||
// Show additional recurrence options for all recurring events
|
||
if !matches!(data.recurrence, RecurrenceType::None) {
|
||
<div class="recurrence-options">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="recurrence-interval">{"Every"}</label>
|
||
<div class="interval-input">
|
||
<input
|
||
id="recurrence-interval"
|
||
type="number"
|
||
class="form-input"
|
||
value={data.recurrence_interval.to_string()}
|
||
min="1"
|
||
max="999"
|
||
onchange={on_recurrence_interval_change}
|
||
/>
|
||
<span class="interval-unit">
|
||
{match data.recurrence {
|
||
RecurrenceType::Daily => if data.recurrence_interval == 1 { "day" } else { "days" },
|
||
RecurrenceType::Weekly => if data.recurrence_interval == 1 { "week" } else { "weeks" },
|
||
RecurrenceType::Monthly => if data.recurrence_interval == 1 { "month" } else { "months" },
|
||
RecurrenceType::Yearly => if data.recurrence_interval == 1 { "year" } else { "years" },
|
||
RecurrenceType::None => "",
|
||
}}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>{"Ends"}</label>
|
||
<div class="end-options">
|
||
<div class="end-option">
|
||
<label class="radio-label">
|
||
<input
|
||
type="radio"
|
||
name="recurrence-end"
|
||
value="never"
|
||
checked={data.recurrence_until.is_none() && data.recurrence_count.is_none()}
|
||
onchange={{
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut new_data = (*event_data).clone();
|
||
new_data.recurrence_until = None;
|
||
new_data.recurrence_count = None;
|
||
event_data.set(new_data);
|
||
})
|
||
}}
|
||
/>
|
||
{"Never"}
|
||
</label>
|
||
</div>
|
||
|
||
<div class="end-option">
|
||
<label class="radio-label">
|
||
<input
|
||
type="radio"
|
||
name="recurrence-end"
|
||
value="until"
|
||
checked={data.recurrence_until.is_some()}
|
||
onchange={{
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut new_data = (*event_data).clone();
|
||
new_data.recurrence_count = None;
|
||
new_data.recurrence_until = Some(new_data.start_date);
|
||
event_data.set(new_data);
|
||
})
|
||
}}
|
||
/>
|
||
{"Until"}
|
||
</label>
|
||
<input
|
||
type="date"
|
||
class="form-input"
|
||
value={data.recurrence_until.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or_default()}
|
||
onchange={on_recurrence_until_change.clone()}
|
||
/>
|
||
</div>
|
||
|
||
<div class="end-option">
|
||
<label class="radio-label">
|
||
<input
|
||
type="radio"
|
||
name="recurrence-end"
|
||
value="count"
|
||
checked={data.recurrence_count.is_some()}
|
||
onchange={{
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut new_data = (*event_data).clone();
|
||
new_data.recurrence_until = None;
|
||
new_data.recurrence_count = Some(10); // Default count
|
||
event_data.set(new_data);
|
||
})
|
||
}}
|
||
/>
|
||
{"After"}
|
||
</label>
|
||
<input
|
||
type="number"
|
||
class="form-input count-input"
|
||
value={data.recurrence_count.map(|c| c.to_string()).unwrap_or_default()}
|
||
min="1"
|
||
max="999"
|
||
placeholder="1"
|
||
onchange={on_recurrence_count_change.clone()}
|
||
/>
|
||
<span class="count-unit">{"occurrences"}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
// Monthly-specific options
|
||
if matches!(data.recurrence, RecurrenceType::Monthly) {
|
||
<div class="form-group">
|
||
<label>{"Repeat by"}</label>
|
||
<div class="monthly-options">
|
||
<div class="monthly-option">
|
||
<label class="radio-label">
|
||
<input
|
||
type="radio"
|
||
name="monthly-type"
|
||
checked={data.monthly_by_monthday.is_some()}
|
||
onchange={{
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut new_data = (*event_data).clone();
|
||
new_data.monthly_by_day = None;
|
||
new_data.monthly_by_monthday = Some(new_data.start_date.day() as u8);
|
||
event_data.set(new_data);
|
||
})
|
||
}}
|
||
/>
|
||
{"Day of month:"}
|
||
</label>
|
||
<input
|
||
type="number"
|
||
class="form-input day-input"
|
||
value={data.monthly_by_monthday.map(|d| d.to_string()).unwrap_or_else(|| data.start_date.day().to_string())}
|
||
min="1"
|
||
max="31"
|
||
onchange={on_monthly_by_monthday_change.clone()}
|
||
/>
|
||
</div>
|
||
|
||
<div class="monthly-option">
|
||
<label class="radio-label">
|
||
<input
|
||
type="radio"
|
||
name="monthly-type"
|
||
checked={data.monthly_by_day.is_some()}
|
||
onchange={{
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut new_data = (*event_data).clone();
|
||
new_data.monthly_by_monthday = None;
|
||
new_data.monthly_by_day = Some("1MO".to_string()); // Default to first Monday
|
||
event_data.set(new_data);
|
||
})
|
||
}}
|
||
/>
|
||
{"Day of week:"}
|
||
</label>
|
||
<select
|
||
class="form-input"
|
||
value={data.monthly_by_day.clone().unwrap_or_default()}
|
||
onchange={on_monthly_by_day_change.clone()}
|
||
>
|
||
<option value="none">{"Select..."}</option>
|
||
<option value="1MO">{"First Monday"}</option>
|
||
<option value="1TU">{"First Tuesday"}</option>
|
||
<option value="1WE">{"First Wednesday"}</option>
|
||
<option value="1TH">{"First Thursday"}</option>
|
||
<option value="1FR">{"First Friday"}</option>
|
||
<option value="1SA">{"First Saturday"}</option>
|
||
<option value="1SU">{"First Sunday"}</option>
|
||
<option value="2MO">{"Second Monday"}</option>
|
||
<option value="2TU">{"Second Tuesday"}</option>
|
||
<option value="2WE">{"Second Wednesday"}</option>
|
||
<option value="2TH">{"Second Thursday"}</option>
|
||
<option value="2FR">{"Second Friday"}</option>
|
||
<option value="2SA">{"Second Saturday"}</option>
|
||
<option value="2SU">{"Second Sunday"}</option>
|
||
<option value="3MO">{"Third Monday"}</option>
|
||
<option value="3TU">{"Third Tuesday"}</option>
|
||
<option value="3WE">{"Third Wednesday"}</option>
|
||
<option value="3TH">{"Third Thursday"}</option>
|
||
<option value="3FR">{"Third Friday"}</option>
|
||
<option value="3SA">{"Third Saturday"}</option>
|
||
<option value="3SU">{"Third Sunday"}</option>
|
||
<option value="4MO">{"Fourth Monday"}</option>
|
||
<option value="4TU">{"Fourth Tuesday"}</option>
|
||
<option value="4WE">{"Fourth Wednesday"}</option>
|
||
<option value="4TH">{"Fourth Thursday"}</option>
|
||
<option value="4FR">{"Fourth Friday"}</option>
|
||
<option value="4SA">{"Fourth Saturday"}</option>
|
||
<option value="4SU">{"Fourth Sunday"}</option>
|
||
<option value="-1MO">{"Last Monday"}</option>
|
||
<option value="-1TU">{"Last Tuesday"}</option>
|
||
<option value="-1WE">{"Last Wednesday"}</option>
|
||
<option value="-1TH">{"Last Thursday"}</option>
|
||
<option value="-1FR">{"Last Friday"}</option>
|
||
<option value="-1SA">{"Last Saturday"}</option>
|
||
<option value="-1SU">{"Last Sunday"}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
// Yearly-specific options
|
||
if matches!(data.recurrence, RecurrenceType::Yearly) {
|
||
<div class="form-group">
|
||
<label>{"Repeat in months"}</label>
|
||
<div class="yearly-months">
|
||
{
|
||
["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, month)| {
|
||
let month_checked = data.yearly_by_month.get(i).cloned().unwrap_or(false);
|
||
let on_change = on_yearly_month_change(i);
|
||
html! {
|
||
<label key={i} class="month-checkbox">
|
||
<input
|
||
type="checkbox"
|
||
checked={month_checked}
|
||
onchange={on_change}
|
||
/>
|
||
<span class="month-label">{month}</span>
|
||
</label>
|
||
}
|
||
})
|
||
.collect::<Html>()
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
},
|
||
ModalTab::Advanced => html! {
|
||
<div class="tab-panel">
|
||
<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"}</label>
|
||
<select
|
||
id="event-priority"
|
||
class="form-input"
|
||
onchange={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |e: Event| {
|
||
if let Some(target) = e.target() {
|
||
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
|
||
let mut data = (*event_data).clone();
|
||
let value = select.value();
|
||
data.priority = if value.is_empty() {
|
||
None
|
||
} else {
|
||
value.parse::<u8>().ok().filter(|&p| p <= 9)
|
||
};
|
||
event_data.set(data);
|
||
}
|
||
}
|
||
})
|
||
}
|
||
>
|
||
<option value="" selected={data.priority.is_none()}>{"Not set"}</option>
|
||
<option value="1" selected={data.priority == Some(1)}>{"High (1)"}</option>
|
||
<option value="2" selected={data.priority == Some(2)}>{"High (2)"}</option>
|
||
<option value="3" selected={data.priority == Some(3)}>{"High (3)"}</option>
|
||
<option value="4" selected={data.priority == Some(4)}>{"High (4)"}</option>
|
||
<option value="5" selected={data.priority == Some(5)}>{"Medium (5)"}</option>
|
||
<option value="6" selected={data.priority == Some(6)}>{"Low (6)"}</option>
|
||
<option value="7" selected={data.priority == Some(7)}>{"Low (7)"}</option>
|
||
<option value="8" selected={data.priority == Some(8)}>{"Low (8)"}</option>
|
||
<option value="9" selected={data.priority == Some(9)}>{"Low (9)"}</option>
|
||
</select>
|
||
<p class="form-help-text">{"RFC 5545 priority scale: 1-4 = High, 5 = Medium, 6-9 = Low"}</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="advanced-reminder">{"Advanced Reminder Options"}</label>
|
||
<select
|
||
id="advanced-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 before"}</option>
|
||
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes before"}</option>
|
||
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour before"}</option>
|
||
<option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours before"}</option>
|
||
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day before"}</option>
|
||
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days before"}</option>
|
||
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week before"}</option>
|
||
</select>
|
||
<p class="form-help-text">{"More reminder options available in the Reminders & Attachments tab"}</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="advanced-recurrence">{"Advanced Recurrence"}</label>
|
||
<select
|
||
id="advanced-recurrence"
|
||
class="form-input"
|
||
onchange={on_recurrence_change}
|
||
>
|
||
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"Does not repeat"}</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>
|
||
<p class="form-help-text">{"Custom recurrence rules and exceptions can be configured after event creation"}</p>
|
||
</div>
|
||
|
||
// Show advanced weekday selection when weekly recurrence is selected
|
||
if matches!(data.recurrence, RecurrenceType::Weekly) {
|
||
<div class="form-group">
|
||
<label>{"Repeat on days"}</label>
|
||
<div class="weekday-selection">
|
||
{
|
||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||
.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[0..3]}</span>
|
||
</label>
|
||
}
|
||
})
|
||
.collect::<Html>()
|
||
}
|
||
</div>
|
||
<p class="form-help-text">{"Select which days of the week to repeat this event"}</p>
|
||
</div>
|
||
}
|
||
|
||
<div class="advanced-info">
|
||
<h5>{"Advanced Features"}</h5>
|
||
<ul>
|
||
<li>{"Time transparency and free/busy status"}</li>
|
||
<li>{"Complex recurrence rules with exceptions"}</li>
|
||
<li>{"Multiple alarm configurations"}</li>
|
||
<li>{"Custom properties and metadata"}</li>
|
||
</ul>
|
||
<p class="form-help-text">{"These features follow RFC 5545 iCalendar standards"}</p>
|
||
</div>
|
||
</div>
|
||
},
|
||
ModalTab::People => html! {
|
||
<div class="tab-panel">
|
||
<div class="form-group">
|
||
<label for="event-organizer">{"Organizer"}</label>
|
||
<input
|
||
type="email"
|
||
id="event-organizer"
|
||
class="form-input"
|
||
value={data.organizer.clone()}
|
||
oninput={on_organizer_input}
|
||
placeholder="organizer@example.com"
|
||
/>
|
||
<p class="form-help-text">{"Email address of the person organizing this event"}</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="event-attendees">{"Attendees"}</label>
|
||
<textarea
|
||
id="event-attendees"
|
||
class="form-input"
|
||
value={data.attendees.clone()}
|
||
oninput={on_attendees_input}
|
||
placeholder="attendee1@example.com, attendee2@example.com, attendee3@example.com"
|
||
rows="4"
|
||
></textarea>
|
||
<p class="form-help-text">{"Enter attendee email addresses separated by commas"}</p>
|
||
</div>
|
||
|
||
<div class="people-info">
|
||
<h5>{"Invitation & Response Management"}</h5>
|
||
<ul>
|
||
<li>{"Invitations are sent automatically when the event is saved"}</li>
|
||
<li>{"Attendees can respond with Accept, Decline, or Tentative"}</li>
|
||
<li>{"Response tracking follows RFC 5545 PARTSTAT standards"}</li>
|
||
<li>{"Delegation and role management available after event creation"}</li>
|
||
</ul>
|
||
|
||
<div class="people-validation">
|
||
<h6>{"Email Validation"}</h6>
|
||
<p>{"Email addresses will be validated when you save the event. Invalid emails will be highlighted and must be corrected before proceeding."}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="attendee-roles-preview">
|
||
<h5>{"Advanced Attendee Features"}</h5>
|
||
<div class="role-examples">
|
||
<div class="role-item">
|
||
<strong>{"Chair:"}</strong>
|
||
<span>{"Meeting leader or event host"}</span>
|
||
</div>
|
||
<div class="role-item">
|
||
<strong>{"Required Participant:"}</strong>
|
||
<span>{"Attendance is required"}</span>
|
||
</div>
|
||
<div class="role-item">
|
||
<strong>{"Optional Participant:"}</strong>
|
||
<span>{"Attendance is optional"}</span>
|
||
</div>
|
||
<div class="role-item">
|
||
<strong>{"Non-Participant:"}</strong>
|
||
<span>{"For information only"}</span>
|
||
</div>
|
||
</div>
|
||
<p class="form-help-text">{"Advanced role assignment and RSVP management will be available in future versions"}</p>
|
||
</div>
|
||
</div>
|
||
},
|
||
ModalTab::Categories => html! {
|
||
<div class="tab-panel">
|
||
<div class="form-group">
|
||
<label for="event-categories">{"Categories"}</label>
|
||
<input
|
||
type="text"
|
||
id="event-categories"
|
||
class="form-input"
|
||
value={data.categories.clone()}
|
||
oninput={on_categories_input}
|
||
placeholder="work, meeting, personal, project, urgent"
|
||
/>
|
||
<p class="form-help-text">{"Enter categories separated by commas to help organize and filter your events"}</p>
|
||
</div>
|
||
|
||
<div class="categories-suggestions">
|
||
<h5>{"Common Categories"}</h5>
|
||
<div class="category-tags">
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "work".to_string();
|
||
} else {
|
||
data.categories = format!("{}, work", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"work"}</button>
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "meeting".to_string();
|
||
} else {
|
||
data.categories = format!("{}, meeting", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"meeting"}</button>
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "personal".to_string();
|
||
} else {
|
||
data.categories = format!("{}, personal", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"personal"}</button>
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "project".to_string();
|
||
} else {
|
||
data.categories = format!("{}, project", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"project"}</button>
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "urgent".to_string();
|
||
} else {
|
||
data.categories = format!("{}, urgent", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"urgent"}</button>
|
||
<button type="button" class="category-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
if data.categories.is_empty() {
|
||
data.categories = "social".to_string();
|
||
} else {
|
||
data.categories = format!("{}, social", data.categories);
|
||
}
|
||
event_data.set(data);
|
||
})
|
||
}>{"social"}</button>
|
||
</div>
|
||
<p class="form-help-text">{"Click to add these common categories to your event"}</p>
|
||
</div>
|
||
|
||
<div class="categories-info">
|
||
<h5>{"Event Organization & Filtering"}</h5>
|
||
<ul>
|
||
<li>{"Categories help organize events in calendar views"}</li>
|
||
<li>{"Filter events by category to focus on specific types"}</li>
|
||
<li>{"Categories are searchable and can be used for reporting"}</li>
|
||
<li>{"Multiple categories per event are fully supported"}</li>
|
||
</ul>
|
||
|
||
<div class="resources-section">
|
||
<h6>{"Resources & Related Events"}</h6>
|
||
<p>{"Advanced resource management features will include:"}</p>
|
||
<div class="resource-features">
|
||
<div class="feature-item">
|
||
<strong>{"Equipment Resources:"}</strong>
|
||
<span>{"Projectors, rooms, vehicles"}</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<strong>{"Human Resources:"}</strong>
|
||
<span>{"Required staff, specialists"}</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<strong>{"Related Events:"}</strong>
|
||
<span>{"Link dependencies and sequences"}</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<strong>{"Comments & Notes:"}</strong>
|
||
<span>{"Internal notes and documentation"}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quick-actions">
|
||
<h5>{"Quick Actions"}</h5>
|
||
<div class="action-buttons">
|
||
<button type="button" class="action-btn" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.categories = String::new();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Clear Categories"}</button>
|
||
</div>
|
||
<p class="form-help-text">{"Remove all categories from this event"}</p>
|
||
</div>
|
||
</div>
|
||
},
|
||
ModalTab::Location => html! {
|
||
<div class="tab-panel">
|
||
<div class="form-group">
|
||
<label for="event-location-detailed">{"Event Location"}</label>
|
||
<input
|
||
type="text"
|
||
id="event-location-detailed"
|
||
class="form-input"
|
||
value={data.location.clone()}
|
||
oninput={on_location_input}
|
||
placeholder="Conference Room A, 123 Main St, City, State 12345"
|
||
/>
|
||
<p class="form-help-text">{"Enter the full address or location description for the event"}</p>
|
||
</div>
|
||
|
||
<div class="location-suggestions">
|
||
<h5>{"Common Locations"}</h5>
|
||
<div class="location-tags">
|
||
<button type="button" class="location-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = "Conference Room".to_string();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Conference Room"}</button>
|
||
<button type="button" class="location-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = "Online Meeting".to_string();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Online Meeting"}</button>
|
||
<button type="button" class="location-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = "Main Office".to_string();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Main Office"}</button>
|
||
<button type="button" class="location-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = "Client Site".to_string();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Client Site"}</button>
|
||
<button type="button" class="location-tag" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = "Home Office".to_string();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Home Office"}</button>
|
||
</div>
|
||
<p class="form-help-text">{"Click to quickly set common location types"}</p>
|
||
</div>
|
||
|
||
<div class="location-info">
|
||
<h5>{"Location Features & Integration"}</h5>
|
||
<ul>
|
||
<li>{"Location information is included in calendar invitations"}</li>
|
||
<li>{"Supports both physical addresses and virtual meeting links"}</li>
|
||
<li>{"Compatible with mapping and navigation applications"}</li>
|
||
<li>{"Room booking integration available for enterprise setups"}</li>
|
||
</ul>
|
||
|
||
<div class="geo-section">
|
||
<h6>{"Geographic Coordinates (Advanced)"}</h6>
|
||
<p>{"Future versions will support:"}</p>
|
||
<div class="geo-features">
|
||
<div class="geo-item">
|
||
<strong>{"GPS Coordinates:"}</strong>
|
||
<span>{"Precise latitude/longitude positioning"}</span>
|
||
</div>
|
||
<div class="geo-item">
|
||
<strong>{"Map Integration:"}</strong>
|
||
<span>{"Embedded maps in event details"}</span>
|
||
</div>
|
||
<div class="geo-item">
|
||
<strong>{"Travel Time:"}</strong>
|
||
<span>{"Automatic travel time calculation"}</span>
|
||
</div>
|
||
<div class="geo-item">
|
||
<strong>{"Location History:"}</strong>
|
||
<span>{"Smart suggestions based on past events"}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="location-types">
|
||
<h5>{"Location Type Examples"}</h5>
|
||
<div class="type-examples">
|
||
<div class="type-category">
|
||
<strong>{"Physical Locations:"}</strong>
|
||
<ul>
|
||
<li>{"123 Business Ave, Suite 400, City, State 12345"}</li>
|
||
<li>{"Conference Room B, 2nd Floor, Main Building"}</li>
|
||
<li>{"Central Park, 5th Avenue entrance"}</li>
|
||
</ul>
|
||
</div>
|
||
<div class="type-category">
|
||
<strong>{"Virtual Locations:"}</strong>
|
||
<ul>
|
||
<li>{"Zoom Meeting ID: 123-456-7890"}</li>
|
||
<li>{"Microsoft Teams: team.microsoft.com/meeting/..."}</li>
|
||
<li>{"Google Meet: meet.google.com/abc-defg-hij"}</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<p class="form-help-text">{"Both physical addresses and virtual meeting information are fully supported"}</p>
|
||
</div>
|
||
|
||
<div class="quick-actions">
|
||
<h5>{"Quick Actions"}</h5>
|
||
<div class="action-buttons">
|
||
<button type="button" class="action-btn secondary" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.location = String::new();
|
||
event_data.set(data);
|
||
})
|
||
}>{"Clear Location"}</button>
|
||
</div>
|
||
<p class="form-help-text">{"Remove location information from this event"}</p>
|
||
</div>
|
||
</div>
|
||
},
|
||
ModalTab::Reminders => html! {
|
||
<div class="tab-panel">
|
||
<div class="form-group">
|
||
<label for="event-reminder-main">{"Primary Reminder"}</label>
|
||
<select
|
||
id="event-reminder-main"
|
||
class="form-input"
|
||
onchange={on_reminder_change}
|
||
>
|
||
<option value="none" selected={matches!(data.reminder, ReminderType::None)}>{"No reminder"}</option>
|
||
<option value="15min" selected={matches!(data.reminder, ReminderType::Minutes15)}>{"15 minutes before"}</option>
|
||
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes before"}</option>
|
||
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour before"}</option>
|
||
<option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours before"}</option>
|
||
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day before"}</option>
|
||
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days before"}</option>
|
||
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week before"}</option>
|
||
</select>
|
||
<p class="form-help-text">{"Choose when you'd like to be reminded about this event"}</p>
|
||
</div>
|
||
|
||
<div class="reminder-types">
|
||
<h5>{"Reminder & Alarm Types"}</h5>
|
||
<div class="alarm-examples">
|
||
<div class="alarm-type">
|
||
<strong>{"Display Alarm"}</strong>
|
||
<p>{"Pop-up notification on your device"}</p>
|
||
</div>
|
||
<div class="alarm-type">
|
||
<strong>{"Email Reminder"}</strong>
|
||
<p>{"Email notification sent to your address"}</p>
|
||
</div>
|
||
<div class="alarm-type">
|
||
<strong>{"Audio Alert"}</strong>
|
||
<p>{"Sound notification with custom audio"}</p>
|
||
</div>
|
||
<div class="alarm-type">
|
||
<strong>{"SMS/Text"}</strong>
|
||
<p>{"Text message reminder (enterprise feature)"}</p>
|
||
</div>
|
||
</div>
|
||
<p class="form-help-text">{"Multiple alarm types follow RFC 5545 VALARM standards"}</p>
|
||
</div>
|
||
|
||
<div class="reminder-info">
|
||
<h5>{"Advanced Reminder Features"}</h5>
|
||
<ul>
|
||
<li>{"Multiple reminders per event with different timing"}</li>
|
||
<li>{"Custom reminder messages and descriptions"}</li>
|
||
<li>{"Recurring reminders for recurring events"}</li>
|
||
<li>{"Snooze and dismiss functionality"}</li>
|
||
<li>{"Integration with system notifications"}</li>
|
||
</ul>
|
||
|
||
<div class="attachments-section">
|
||
<h6>{"File Attachments & Documents"}</h6>
|
||
<p>{"Future attachment features will include:"}</p>
|
||
<div class="attachment-features">
|
||
<div class="attachment-type">
|
||
<strong>{"File Uploads:"}</strong>
|
||
<span>{"Documents, images, presentations"}</span>
|
||
</div>
|
||
<div class="attachment-type">
|
||
<strong>{"URL Links:"}</strong>
|
||
<span>{"Web resources and reference materials"}</span>
|
||
</div>
|
||
<div class="attachment-type">
|
||
<strong>{"Cloud Storage:"}</strong>
|
||
<span>{"Google Drive, Dropbox, OneDrive integration"}</span>
|
||
</div>
|
||
<div class="attachment-type">
|
||
<strong>{"Meeting Notes:"}</strong>
|
||
<span>{"Collaborative note-taking and agenda items"}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reminder-patterns">
|
||
<h5>{"Common Reminder Patterns"}</h5>
|
||
<div class="pattern-examples">
|
||
<div class="pattern-item">
|
||
<strong>{"Meetings:"}</strong>
|
||
<span>{"15 minutes before (preparation time)"}</span>
|
||
</div>
|
||
<div class="pattern-item">
|
||
<strong>{"Travel Events:"}</strong>
|
||
<span>{"2 hours before (traffic and check-in)"}</span>
|
||
</div>
|
||
<div class="pattern-item">
|
||
<strong>{"Personal Events:"}</strong>
|
||
<span>{"1 day before (preparation and gifts)"}</span>
|
||
</div>
|
||
<div class="pattern-item">
|
||
<strong>{"Deadlines:"}</strong>
|
||
<span>{"1 week before (completion buffer)"}</span>
|
||
</div>
|
||
</div>
|
||
<p class="form-help-text">{"Suggested timing based on common event types"}</p>
|
||
</div>
|
||
|
||
<div class="quick-actions">
|
||
<h5>{"Quick Actions"}</h5>
|
||
<div class="action-buttons">
|
||
<button type="button" class="action-btn tertiary" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.reminder = ReminderType::Minutes15;
|
||
event_data.set(data);
|
||
})
|
||
}>{"Set 15min Reminder"}</button>
|
||
<button type="button" class="action-btn secondary" onclick={
|
||
let event_data = event_data.clone();
|
||
Callback::from(move |_| {
|
||
let mut data = (*event_data).clone();
|
||
data.reminder = ReminderType::None;
|
||
event_data.set(data);
|
||
})
|
||
}>{"No Reminder"}</button>
|
||
</div>
|
||
<p class="form-help-text">{"Quickly set or clear event reminders"}</p>
|
||
</div>
|
||
|
||
<div class="completion-status">
|
||
<h5>{"Modal Complete!"}</h5>
|
||
<p>{"You've reached the final tab of the comprehensive event creation interface. This modal now provides access to all major VEvent properties following RFC 5545 standards."}</p>
|
||
|
||
<div class="feature-summary">
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"Basic Details"}</span>
|
||
<span class="tab-desc">{"Title, calendar, dates, location, basic recurrence"}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"Advanced"}</span>
|
||
<span class="tab-desc">{"Status, priority, classification, advanced options"}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"People"}</span>
|
||
<span class="tab-desc">{"Organizer, attendees, invitation management"}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"Categories"}</span>
|
||
<span class="tab-desc">{"Event tagging and organizational features"}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"Location"}</span>
|
||
<span class="tab-desc">{"Physical and virtual location management"}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="tab-name">{"Reminders"}</span>
|
||
<span class="tab-desc">{"Alarm configuration and future attachments"}</span>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
}
|
||
} |