diff --git a/src/app.rs b/src/app.rs index 63e44dd..5c3edc7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -345,6 +345,102 @@ pub fn App() -> Html { }) }; + let on_event_update = { + let auth_token = auth_token.clone(); + Callback::from(move |(original_event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| { + web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}", + original_event.uid, + new_start.format("%Y-%m-%d %H:%M"), + new_end.format("%Y-%m-%d %H:%M")).into()); + + if let Some(token) = (*auth_token).clone() { + let original_event = original_event.clone(); + wasm_bindgen_futures::spawn_local(async move { + let calendar_service = CalendarService::new(); + + // Get CalDAV password from storage + let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { + if let Ok(credentials) = serde_json::from_str::(&credentials_str) { + credentials["password"].as_str().unwrap_or("").to_string() + } else { + String::new() + } + } else { + String::new() + }; + + // Convert local times to UTC for backend storage + let start_utc = new_start.and_local_timezone(chrono::Local).unwrap().to_utc(); + let end_utc = new_end.and_local_timezone(chrono::Local).unwrap().to_utc(); + + // Format UTC date and time strings for backend + let start_date = start_utc.format("%Y-%m-%d").to_string(); + let start_time = start_utc.format("%H:%M").to_string(); + let end_date = end_utc.format("%Y-%m-%d").to_string(); + let end_time = end_utc.format("%H:%M").to_string(); + + // Convert existing event data to string formats for the API + let status_str = match original_event.status { + crate::services::calendar_service::EventStatus::Tentative => "TENTATIVE".to_string(), + crate::services::calendar_service::EventStatus::Confirmed => "CONFIRMED".to_string(), + crate::services::calendar_service::EventStatus::Cancelled => "CANCELLED".to_string(), + }; + + let class_str = match original_event.class { + crate::services::calendar_service::EventClass::Public => "PUBLIC".to_string(), + crate::services::calendar_service::EventClass::Private => "PRIVATE".to_string(), + crate::services::calendar_service::EventClass::Confidential => "CONFIDENTIAL".to_string(), + }; + + // Convert reminders to string format + let reminder_str = if !original_event.reminders.is_empty() { + format!("{}", original_event.reminders[0].minutes_before) + } else { + "".to_string() + }; + + // Handle recurrence (keep existing) + let recurrence_str = original_event.recurrence_rule.unwrap_or_default(); + let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence + + match calendar_service.update_event( + &token, + &password, + original_event.uid, + original_event.summary.unwrap_or_default(), + original_event.description.unwrap_or_default(), + start_date, + start_time, + end_date, + end_time, + original_event.location.unwrap_or_default(), + original_event.all_day, + status_str, + class_str, + original_event.priority, + original_event.organizer.unwrap_or_default(), + original_event.attendees.join(","), + original_event.categories.join(","), + reminder_str, + recurrence_str, + recurrence_days, + original_event.calendar_path + ).await { + Ok(_) => { + web_sys::console::log_1(&"Event updated successfully".into()); + // Trigger a page reload to refresh events from all calendars + web_sys::window().unwrap().location().reload().unwrap(); + } + Err(err) => { + web_sys::console::error_1(&format!("Failed to update event: {}", err).into()); + web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap(); + } + } + }); + } + }) + }; + let refresh_calendars = { let auth_token = auth_token.clone(); let user_info = user_info.clone(); @@ -420,6 +516,7 @@ pub fn App() -> Html { on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} view={(*current_view).clone()} on_create_event_request={Some(on_event_create.clone())} + on_event_update_request={Some(on_event_update.clone())} context_menus_open={any_context_menu_open} /> @@ -434,6 +531,7 @@ pub fn App() -> Html { on_login={on_login.clone()} on_event_context_menu={Some(on_event_context_menu.clone())} on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} + on_event_update_request={Some(on_event_update.clone())} on_create_event_request={Some(on_event_create.clone())} context_menus_open={any_context_menu_open} /> diff --git a/src/components/calendar.rs b/src/components/calendar.rs index e6869f6..6c1a54b 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -24,6 +24,8 @@ pub struct CalendarProps { #[prop_or_default] pub on_create_event_request: Option>, #[prop_or_default] + pub on_event_update_request: Option>, + #[prop_or_default] pub context_menus_open: bool, } @@ -188,6 +190,16 @@ pub fn Calendar(props: &CalendarProps) -> Html { show_create_modal.set(true); }) }; + + // Handle drag-to-move event + let on_event_update = { + let on_event_update_request = props.on_event_update_request.clone(); + Callback::from(move |(event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| { + if let Some(callback) = &on_event_update_request { + callback.emit((event, new_start, new_end)); + } + }) + }; html! {
Some("week-view"), _ => None })}> @@ -238,6 +250,7 @@ pub fn Calendar(props: &CalendarProps) -> Html { on_event_context_menu={props.on_event_context_menu.clone()} on_calendar_context_menu={props.on_calendar_context_menu.clone()} on_create_event={Some(on_create_event)} + on_event_update={Some(on_event_update)} context_menus_open={props.context_menus_open} time_increment={*time_increment} /> diff --git a/src/components/route_handler.rs b/src/components/route_handler.rs index a94e5fe..5a7b14a 100644 --- a/src/components/route_handler.rs +++ b/src/components/route_handler.rs @@ -27,6 +27,8 @@ pub struct RouteHandlerProps { #[prop_or_default] pub on_create_event_request: Option>, #[prop_or_default] + pub on_event_update_request: Option>, + #[prop_or_default] pub context_menus_open: bool, } @@ -39,6 +41,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { let on_calendar_context_menu = props.on_calendar_context_menu.clone(); let view = props.view.clone(); let on_create_event_request = props.on_create_event_request.clone(); + let on_event_update_request = props.on_event_update_request.clone(); let context_menus_open = props.context_menus_open; html! { @@ -50,6 +53,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { let on_calendar_context_menu = on_calendar_context_menu.clone(); let view = view.clone(); let on_create_event_request = on_create_event_request.clone(); + let on_event_update_request = on_event_update_request.clone(); let context_menus_open = context_menus_open; match route { @@ -76,6 +80,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { on_calendar_context_menu={on_calendar_context_menu} view={view} on_create_event_request={on_create_event_request} + on_event_update_request={on_event_update_request} context_menus_open={context_menus_open} /> } @@ -100,6 +105,8 @@ pub struct CalendarViewProps { #[prop_or_default] pub on_create_event_request: Option>, #[prop_or_default] + pub on_event_update_request: Option>, + #[prop_or_default] pub context_menus_open: bool, } @@ -261,6 +268,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { on_calendar_context_menu={props.on_calendar_context_menu.clone()} view={props.view.clone()} on_create_event_request={props.on_create_event_request.clone()} + on_event_update_request={props.on_event_update_request.clone()} context_menus_open={props.context_menus_open} />
@@ -276,6 +284,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { on_calendar_context_menu={props.on_calendar_context_menu.clone()} view={props.view.clone()} on_create_event_request={props.on_create_event_request.clone()} + on_event_update_request={props.on_event_update_request.clone()} context_menus_open={props.context_menus_open} /> } diff --git a/src/components/week_view.rs b/src/components/week_view.rs index e5bab7e..326de39 100644 --- a/src/components/week_view.rs +++ b/src/components/week_view.rs @@ -21,17 +21,27 @@ pub struct WeekViewProps { #[prop_or_default] pub on_create_event: Option>, #[prop_or_default] + pub on_event_update: Option>, + #[prop_or_default] pub context_menus_open: bool, #[prop_or_default] pub time_increment: u32, } +#[derive(Clone, PartialEq)] +enum DragType { + CreateEvent, + MoveEvent(CalendarEvent), +} + #[derive(Clone, PartialEq)] struct DragState { is_dragging: bool, + drag_type: DragType, start_date: NaiveDate, start_y: f64, current_y: f64, + offset_y: f64, // For event moves, this is the offset from the event's top } #[function_component(WeekView)] @@ -146,9 +156,11 @@ pub fn week_view(props: &WeekViewProps) -> Html { drag_state.set(Some(DragState { is_dragging: true, + drag_type: DragType::CreateEvent, start_date: date_for_drag, start_y: snapped_y, current_y: snapped_y, + offset_y: 0.0, })); e.prevent_default(); }) @@ -177,32 +189,55 @@ pub fn week_view(props: &WeekViewProps) -> Html { let onmouseup = { let drag_state = drag_state_clone.clone(); let on_create_event = props.on_create_event.clone(); + let on_event_update = props.on_event_update.clone(); Callback::from(move |_e: MouseEvent| { if let Some(current_drag) = (*drag_state).clone() { if current_drag.is_dragging { - // Calculate start and end times - let start_time = pixels_to_time(current_drag.start_y); - let end_time = pixels_to_time(current_drag.current_y); - - // Ensure start is before end - let (actual_start, actual_end) = if start_time <= end_time { - (start_time, end_time) - } else { - (end_time, start_time) - }; - - // Ensure minimum duration (15 minutes) - let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 { - actual_start + chrono::Duration::minutes(15) - } else { - actual_end - }; - - let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start); - let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end); - - if let Some(callback) = &on_create_event { - callback.emit((current_drag.start_date, start_datetime, end_datetime)); + match ¤t_drag.drag_type { + DragType::CreateEvent => { + // Calculate start and end times + let start_time = pixels_to_time(current_drag.start_y); + let end_time = pixels_to_time(current_drag.current_y); + + // Ensure start is before end + let (actual_start, actual_end) = if start_time <= end_time { + (start_time, end_time) + } else { + (end_time, start_time) + }; + + // Ensure minimum duration (15 minutes) + let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 { + actual_start + chrono::Duration::minutes(15) + } else { + actual_end + }; + + let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start); + let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end); + + if let Some(callback) = &on_create_event { + callback.emit((current_drag.start_date, start_datetime, end_datetime)); + } + }, + DragType::MoveEvent(event) => { + // Calculate new start time based on drag position + let new_start_time = pixels_to_time(current_drag.current_y); + + // Calculate duration from original event + let original_duration = if let Some(end) = event.end { + end.signed_duration_since(event.start) + } else { + chrono::Duration::hours(1) // Default 1 hour + }; + + let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); + let new_end_datetime = new_start_datetime + original_duration; + + if let Some(callback) = &on_event_update { + callback.emit((event.clone(), new_start_datetime, new_end_datetime)); + } + } } drag_state.set(None); @@ -259,8 +294,41 @@ pub fn week_view(props: &WeekViewProps) -> Html { }; let onmousedown_event = { + let drag_state = drag_state.clone(); + let event_for_drag = event.clone(); + let date_for_drag = *date; + let time_increment = props.time_increment; Callback::from(move |e: MouseEvent| { e.stop_propagation(); // Prevent drag-to-create from starting on event clicks + + // Only handle left-click (button 0) + if e.button() != 0 { + return; + } + + // Calculate Y position relative to the day column + let relative_y = e.layer_y() as f64; + let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + + // Get event's current position + let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag); + let event_start_pixels = event_start_pixels as f64; + + // Calculate offset from the top of the event + let offset_y = relative_y - event_start_pixels; + + // Snap to increment + let snapped_y = snap_to_increment(relative_y, time_increment); + + drag_state.set(Some(DragState { + is_dragging: true, + drag_type: DragType::MoveEvent(event_for_drag.clone()), + start_date: date_for_drag, + start_y: snapped_y, + current_y: snapped_y, + offset_y, + })); + e.prevent_default(); }) }; @@ -309,31 +377,47 @@ pub fn week_view(props: &WeekViewProps) -> Html { } }; - Some(html! { -
-
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
- {if !is_all_day { - html! {
{time_display}
} - } else { - html! {} - }} -
- }) + // Check if this event is currently being dragged + let is_being_dragged = if let Some(drag) = (*drag_state).clone() { + if let DragType::MoveEvent(dragged_event) = &drag.drag_type { + dragged_event.uid == event.uid && drag.is_dragging + } else { + false + } + } else { + false + }; + + if is_being_dragged { + // Hide the original event while being dragged + Some(html! {}) + } else { + Some(html! { +
+
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+ {if !is_all_day { + html! {
{time_display}
} + } else { + html! {} + }} +
+ }) + } }).collect::() } @@ -342,21 +426,48 @@ pub fn week_view(props: &WeekViewProps) -> Html { { if let Some(drag) = (*drag_state).clone() { if drag.is_dragging && drag.start_date == *date { - let start_y = drag.start_y.min(drag.current_y); - let end_y = drag.start_y.max(drag.current_y); - let height = (drag.current_y - drag.start_y).abs().max(20.0); - - // Convert pixels to times for display - let start_time = pixels_to_time(start_y); - let end_time = pixels_to_time(end_y); - - html! { -
- {format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))} -
+ match &drag.drag_type { + DragType::CreateEvent => { + let start_y = drag.start_y.min(drag.current_y); + let end_y = drag.start_y.max(drag.current_y); + let height = (drag.current_y - drag.start_y).abs().max(20.0); + + // Convert pixels to times for display + let start_time = pixels_to_time(start_y); + let end_time = pixels_to_time(end_y); + + html! { +
+ {format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))} +
+ } + }, + DragType::MoveEvent(event) => { + // Show the event being moved at its new position + let new_start_time = pixels_to_time(drag.current_y); + let original_duration = if let Some(end) = event.end { + end.signed_duration_since(event.start) + } else { + chrono::Duration::hours(1) + }; + let duration_pixels = (original_duration.num_minutes() as f64).max(20.0); + let new_end_time = new_start_time + original_duration; + + let event_color = get_event_color(event); + + html! { +
+
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+
{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}
+
+ } + } } } else { html! {} diff --git a/styles.css b/styles.css index 119e3e6..073128b 100644 --- a/styles.css +++ b/styles.css @@ -696,6 +696,37 @@ body { user-select: none; } +/* Moving event during drag */ +.temp-event-box.moving-event { + background: inherit; /* Use the event's actual color */ + border: 2px solid rgba(255, 255, 255, 0.8); + color: white; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + text-align: left; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transform: scale(1.02); +} + +.temp-event-box.moving-event .event-title { + font-weight: 600; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.temp-event-box.moving-event .event-time { + font-size: 0.65rem; + opacity: 0.9; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + .week-event .event-title { font-weight: 600; margin-bottom: 2px;