use yew::prelude::*; use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike, NaiveDateTime, NaiveTime}; use std::collections::HashMap; use web_sys::MouseEvent; use crate::services::calendar_service::UserInfo; use crate::models::ical::VEvent; use crate::components::{RecurringEditModal, RecurringEditAction, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; #[derive(Properties, PartialEq)] pub struct WeekViewProps { pub current_date: NaiveDate, pub today: NaiveDate, pub events: HashMap>, pub on_event_click: Callback, #[prop_or_default] pub refreshing_event_uid: Option, #[prop_or_default] pub user_info: Option, #[prop_or_default] pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, #[prop_or_default] pub on_create_event: Option>, #[prop_or_default] pub on_create_event_request: 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(VEvent), ResizeEventStart(VEvent), // Resizing from the top edge (start time) ResizeEventEnd(VEvent), // Resizing from the bottom edge (end time) } #[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 has_moved: bool, // Track if we've moved enough to constitute a real drag } #[function_component(WeekView)] pub fn week_view(props: &WeekViewProps) -> Html { let start_of_week = get_start_of_week(props.current_date); let week_days: Vec = (0..7) .map(|i| start_of_week + Duration::days(i)) .collect(); // Drag state for event creation let drag_state = use_state(|| None::); // State for recurring event edit modal #[derive(Clone, PartialEq)] struct PendingRecurringEdit { event: VEvent, new_start: NaiveDateTime, new_end: NaiveDateTime, } let pending_recurring_edit = use_state(|| None::); // Helper function to get calendar color for an event let get_event_color = |event: &VEvent| -> String { if let Some(user_info) = &props.user_info { if let Some(calendar_path) = &event.calendar_path { if let Some(calendar) = user_info.calendars.iter() .find(|cal| &cal.path == calendar_path) { return calendar.color.clone(); } } } "#3B82F6".to_string() }; // Generate time labels - 24 hours plus the final midnight boundary let mut time_labels: Vec = (0..24).map(|hour| { if hour == 0 { "12 AM".to_string() } else if hour < 12 { format!("{} AM", hour) } else if hour == 12 { "12 PM".to_string() } else { format!("{} PM", hour - 12) } }).collect(); // Add the final midnight boundary to show where the day ends time_labels.push("12 AM".to_string()); // Handlers for recurring event modification modal let on_recurring_choice = { let pending_recurring_edit = pending_recurring_edit.clone(); let on_event_update = props.on_event_update.clone(); let _on_create_event = props.on_create_event.clone(); let on_create_event_request = props.on_create_event_request.clone(); let events = props.events.clone(); Callback::from(move |action: RecurringEditAction| { if let Some(edit) = (*pending_recurring_edit).clone() { match action { RecurringEditAction::ThisEvent => { // Create exception for this occurrence only // 1. First, add EXDATE to the original series to exclude this occurrence if let Some(update_callback) = &on_event_update { let mut updated_series = edit.event.clone(); updated_series.exdate.push(edit.event.dtstart); // Keep the original series times unchanged - we're only adding EXDATE let original_start = edit.event.dtstart.with_timezone(&chrono::Local).naive_local(); let original_end = edit.event.dtend.unwrap_or(edit.event.dtstart).with_timezone(&chrono::Local).naive_local(); web_sys::console::log_1(&format!("📅 Adding EXDATE {} to series '{}'", edit.event.dtstart.format("%Y-%m-%d %H:%M:%S UTC"), edit.event.summary.as_deref().unwrap_or("Untitled") ).into()); // Update the original series with the exception (times unchanged) update_callback.emit((updated_series, original_start, original_end, true, None)); // preserve_rrule = true for EXDATE, no until_date } // 2. Then create the new single event using the create callback if let Some(create_callback) = &on_create_event_request { // Convert to EventCreationData for single event let event_data = EventCreationData { title: edit.event.summary.clone().unwrap_or_default(), description: edit.event.description.clone().unwrap_or_default(), start_date: edit.new_start.date(), start_time: edit.new_start.time(), end_date: edit.new_end.date(), end_time: edit.new_end.time(), location: edit.event.location.clone().unwrap_or_default(), all_day: edit.event.all_day, status: EventStatus::Confirmed, class: EventClass::Public, priority: edit.event.priority, organizer: edit.event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(), attendees: edit.event.attendees.iter().map(|a| a.cal_address.clone()).collect::>().join(","), categories: edit.event.categories.join(","), reminder: ReminderType::None, recurrence: RecurrenceType::None, // Single event, no recurrence recurrence_days: vec![false; 7], selected_calendar: edit.event.calendar_path.clone(), }; // Create the single event create_callback.emit(event_data); } }, RecurringEditAction::FutureEvents => { // Split series and modify future events // 1. Update original series to set UNTIL to end before this occurrence if let Some(update_callback) = &on_event_update { // Find the original series event (not the occurrence) // UIDs like "uuid-timestamp" need to split on the last hyphen, not the first let base_uid = if let Some(last_hyphen_pos) = edit.event.uid.rfind('-') { let suffix = &edit.event.uid[last_hyphen_pos + 1..]; // Check if suffix is numeric (timestamp), if so remove it if suffix.chars().all(|c| c.is_numeric()) { edit.event.uid[..last_hyphen_pos].to_string() } else { edit.event.uid.clone() } } else { edit.event.uid.clone() }; web_sys::console::log_1(&format!("🔍 Looking for original series: '{}' from occurrence: '{}'", base_uid, edit.event.uid).into()); // Find the original series event by searching for the base UID let mut original_series = None; for events_list in events.values() { for event in events_list { if event.uid == base_uid { original_series = Some(event.clone()); break; } } if original_series.is_some() { break; } } let original_series = match original_series { Some(series) => { web_sys::console::log_1(&format!("✅ Found original series: '{}'", series.uid).into()); series }, None => { web_sys::console::log_1(&format!("⚠️ Could not find original series '{}', using occurrence but fixing UID", base_uid).into()); let mut fallback_event = edit.event.clone(); // Ensure the UID is the base UID, not the occurrence UID fallback_event.uid = base_uid.clone(); fallback_event } }; // Calculate the day before this occurrence for UNTIL clause let until_date = edit.event.dtstart.date_naive() - chrono::Duration::days(1); let until_datetime = until_date.and_time(chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap()); let until_utc = chrono::DateTime::::from_naive_utc_and_offset(until_datetime, chrono::Utc); web_sys::console::log_1(&format!("🔄 Will set UNTIL {} for original series to end before occurrence {}", until_utc.format("%Y-%m-%d %H:%M:%S UTC"), edit.event.dtstart.format("%Y-%m-%d %H:%M:%S UTC")).into()); // Use the original series start time (not the dragged occurrence time) let original_start = original_series.dtstart.with_timezone(&chrono::Local).naive_local(); let original_end = original_series.dtend.unwrap_or(original_series.dtstart).with_timezone(&chrono::Local).naive_local(); // Send until_date to backend instead of modifying RRULE on frontend update_callback.emit((original_series, original_start, original_end, true, Some(until_utc))); // preserve_rrule = true, backend will add UNTIL } // 2. Create new series starting from this occurrence with modified times if let Some(create_callback) = &on_create_event_request { // Convert the recurring event to EventCreationData for the create callback let event_data = EventCreationData { title: edit.event.summary.clone().unwrap_or_default(), description: edit.event.description.clone().unwrap_or_default(), start_date: edit.new_start.date(), start_time: edit.new_start.time(), end_date: edit.new_end.date(), end_time: edit.new_end.time(), location: edit.event.location.clone().unwrap_or_default(), all_day: edit.event.all_day, status: EventStatus::Confirmed, // Default status class: EventClass::Public, // Default class priority: edit.event.priority, organizer: edit.event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(), attendees: edit.event.attendees.iter().map(|a| a.cal_address.clone()).collect::>().join(","), categories: edit.event.categories.join(","), reminder: ReminderType::None, // Default reminder recurrence: if let Some(rrule) = &edit.event.rrule { if rrule.contains("FREQ=DAILY") { RecurrenceType::Daily } else if rrule.contains("FREQ=WEEKLY") { RecurrenceType::Weekly } else if rrule.contains("FREQ=MONTHLY") { RecurrenceType::Monthly } else if rrule.contains("FREQ=YEARLY") { RecurrenceType::Yearly } else { RecurrenceType::None } } else { RecurrenceType::None }, recurrence_days: vec![false; 7], // Default days selected_calendar: edit.event.calendar_path.clone(), }; // Create the new series create_callback.emit(event_data); } }, RecurringEditAction::AllEvents => { // Modify the entire series let series_event = edit.event.clone(); if let Some(callback) = &on_event_update { callback.emit((series_event, edit.new_start, edit.new_end, true, None)); // Regular drag operation - preserve RRULE, no until_date } }, } } pending_recurring_edit.set(None); }) }; let on_recurring_cancel = { let pending_recurring_edit = pending_recurring_edit.clone(); Callback::from(move |_| { pending_recurring_edit.set(None); }) }; html! {
// Header with weekday names and dates
{ week_days.iter().map(|date| { let is_today = *date == props.today; let weekday_name = get_weekday_name(date.weekday()); html! {
{weekday_name}
{date.day()}
} }).collect::() }
// Scrollable content area with time grid
// Time labels
{ time_labels.iter().enumerate().map(|(index, time)| { let is_final = index == time_labels.len() - 1; html! {
{time}
} }).collect::() }
// Day columns
{ week_days.iter().enumerate().map(|(_column_index, date)| { let is_today = *date == props.today; let day_events = props.events.get(date).cloned().unwrap_or_default(); // Drag event handlers let drag_state_clone = drag_state.clone(); let date_for_drag = *date; let onmousedown = { let drag_state = drag_state_clone.clone(); let context_menus_open = props.context_menus_open; let time_increment = props.time_increment; Callback::from(move |e: MouseEvent| { // Don't start drag if any context menu is open if context_menus_open { return; } // Only handle left-click (button 0) if e.button() != 0 { return; } // Calculate Y position relative to day column container // Use layer_y which gives coordinates relative to positioned ancestor let relative_y = e.layer_y() as f64; let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; // Snap to increment let snapped_y = snap_to_increment(relative_y, time_increment); 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, has_moved: false, })); e.prevent_default(); }) }; let onmousemove = { let drag_state = drag_state_clone.clone(); let time_increment = props.time_increment; Callback::from(move |e: MouseEvent| { if let Some(mut current_drag) = (*drag_state).clone() { if current_drag.is_dragging { // Use layer_y for consistent coordinate calculation let mouse_y = e.layer_y() as f64; let mouse_y = if mouse_y > 0.0 { mouse_y } else { e.offset_y() as f64 }; // For move operations, we now follow the mouse directly since we start at click position // For resize operations, we still use the mouse position directly let adjusted_y = mouse_y; // Snap to increment let snapped_y = snap_to_increment(adjusted_y, time_increment); // Check if we've moved enough to constitute a real drag (5 pixels minimum) let movement_distance = (snapped_y - current_drag.start_y).abs(); if movement_distance > 5.0 { current_drag.has_moved = true; } current_drag.current_y = snapped_y; drag_state.set(Some(current_drag)); } } }) }; 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(); let pending_recurring_edit = pending_recurring_edit.clone(); let time_increment = props.time_increment; Callback::from(move |_e: MouseEvent| { if let Some(current_drag) = (*drag_state).clone() { if current_drag.is_dragging && current_drag.has_moved { 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 (accounting for click offset) let unsnapped_position = current_drag.current_y - current_drag.offset_y; // Snap the final position to maintain time increment alignment let event_top_position = snap_to_increment(unsnapped_position, time_increment); let new_start_time = pixels_to_time(event_top_position); // Calculate duration from original event let original_duration = if let Some(end) = event.dtend { end.signed_duration_since(event.dtstart) } 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; // Check if this is a recurring event if event.rrule.is_some() { // Show modal for recurring event modification pending_recurring_edit.set(Some(PendingRecurringEdit { event: event.clone(), new_start: new_start_datetime, new_end: new_end_datetime, })); } else { // Regular event - proceed with update if let Some(callback) = &on_event_update { callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date } } }, DragType::ResizeEventStart(event) => { // Calculate new start time based on drag position let new_start_time = pixels_to_time(current_drag.current_y); // Keep the original end time let original_end = if let Some(end) = event.dtend { end.with_timezone(&chrono::Local).naive_local() } else { // If no end time, use start time + 1 hour as default event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) }; let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); // Ensure start is before end (minimum 15 minutes) let new_end_datetime = if new_start_datetime >= original_end { new_start_datetime + chrono::Duration::minutes(15) } else { original_end }; // Check if this is a recurring event if event.rrule.is_some() { // Show modal for recurring event modification pending_recurring_edit.set(Some(PendingRecurringEdit { event: event.clone(), new_start: new_start_datetime, new_end: new_end_datetime, })); } else { // Regular event - proceed with update if let Some(callback) = &on_event_update { callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date } } }, DragType::ResizeEventEnd(event) => { // Calculate new end time based on drag position let new_end_time = pixels_to_time(current_drag.current_y); // Keep the original start time let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); let new_end_datetime = NaiveDateTime::new(current_drag.start_date, new_end_time); // Ensure end is after start (minimum 15 minutes) let new_start_datetime = if new_end_datetime <= original_start { new_end_datetime - chrono::Duration::minutes(15) } else { original_start }; // Check if this is a recurring event if event.rrule.is_some() { // Show modal for recurring event modification pending_recurring_edit.set(Some(PendingRecurringEdit { event: event.clone(), new_start: new_start_datetime, new_end: new_end_datetime, })); } else { // Regular event - proceed with update if let Some(callback) = &on_event_update { callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date } } } } drag_state.set(None); } } }) }; html! {
// Time slot backgrounds - 24 hour slots to represent full day { (0..24).map(|_hour| { html! {
} }).collect::() } // Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots
// Events positioned absolutely based on their actual times
{ day_events.iter().filter_map(|event| { let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date); // Skip events that don't belong on this date or have invalid positioning if start_pixels == 0.0 && duration_pixels == 0.0 && !is_all_day { return None; } let event_color = get_event_color(event); let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); let onclick = { let on_event_click = props.on_event_click.clone(); let event = event.clone(); Callback::from(move |e: MouseEvent| { e.stop_propagation(); // Prevent calendar click events from also triggering on_event_click.emit(event.clone()); }) }; 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) for moving if e.button() != 0 { return; } // Calculate click position relative to event element let click_y_relative = e.layer_y() as f64; let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 }; // Get event's current position in day column coordinates let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag); let event_start_pixels = event_start_pixels as f64; // Convert click position to day column coordinates let click_y = event_start_pixels + click_y_relative; // Store the offset from the event's top where the user clicked // This will be used to maintain the relative click position let offset_y = click_y_relative; // Start drag tracking from where we clicked (in day column coordinates) drag_state.set(Some(DragState { is_dragging: true, drag_type: DragType::MoveEvent(event_for_drag.clone()), start_date: date_for_drag, start_y: click_y, current_y: click_y, offset_y, has_moved: false, })); e.prevent_default(); }) }; let oncontextmenu = { if let Some(callback) = &props.on_event_context_menu { let callback = callback.clone(); let event = event.clone(); let drag_state_for_context = drag_state.clone(); Some(Callback::from(move |e: web_sys::MouseEvent| { // Check if we're currently dragging - if so, prevent context menu if let Some(drag) = (*drag_state_for_context).clone() { if drag.is_dragging { e.prevent_default(); return; } } e.prevent_default(); e.stop_propagation(); // Prevent calendar context menu from also triggering callback.emit((e, event.clone())); })) } else { None } }; // Format time display for the event let time_display = if event.all_day { "All Day".to_string() } else { let local_start = event.dtstart.with_timezone(&Local); if let Some(end) = event.dtend { let local_end = end.with_timezone(&Local); // Check if both times are in same AM/PM period to avoid redundancy let start_is_am = local_start.hour() < 12; let end_is_am = local_end.hour() < 12; if start_is_am == end_is_am { // Same AM/PM period - show "9:00 - 10:30 AM" format!("{} - {}", local_start.format("%I:%M").to_string().trim_start_matches('0'), local_end.format("%I:%M %p") ) } else { // Different AM/PM periods - show "9:00 AM - 2:30 PM" format!("{} - {}", local_start.format("%I:%M %p"), local_end.format("%I:%M %p") ) } } else { // No end time, just show start time format!("{}", local_start.format("%I:%M %p")) } }; // Check if this event is currently being dragged or resized let is_being_dragged = if let Some(drag) = (*drag_state).clone() { match &drag.drag_type { DragType::MoveEvent(dragged_event) => dragged_event.uid == event.uid && drag.is_dragging, DragType::ResizeEventStart(dragged_event) => dragged_event.uid == event.uid && drag.is_dragging, DragType::ResizeEventEnd(dragged_event) => dragged_event.uid == event.uid && drag.is_dragging, _ => false, } } else { false }; if is_being_dragged { // Hide the original event while being dragged Some(html! {}) } else { // Create resize handles for left-click resize let resize_start_handler = { let drag_state = drag_state.clone(); let event_for_resize = event.clone(); let date_for_drag = *date; let time_increment = props.time_increment; Callback::from(move |e: web_sys::MouseEvent| { e.stop_propagation(); let relative_y = e.layer_y() as f64; let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; let snapped_y = snap_to_increment(relative_y, time_increment); drag_state.set(Some(DragState { is_dragging: true, drag_type: DragType::ResizeEventStart(event_for_resize.clone()), start_date: date_for_drag, start_y: snapped_y, current_y: snapped_y, offset_y: 0.0, has_moved: false, })); e.prevent_default(); }) }; let resize_end_handler = { let drag_state = drag_state.clone(); let event_for_resize = event.clone(); let date_for_drag = *date; let time_increment = props.time_increment; Callback::from(move |e: web_sys::MouseEvent| { e.stop_propagation(); let relative_y = e.layer_y() as f64; let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; let snapped_y = snap_to_increment(relative_y, time_increment); drag_state.set(Some(DragState { is_dragging: true, drag_type: DragType::ResizeEventEnd(event_for_resize.clone()), start_date: date_for_drag, start_y: snapped_y, current_y: snapped_y, offset_y: 0.0, has_moved: false, })); e.prevent_default(); }) }; Some(html! {
// Top resize handle {if !is_all_day { html! {
} } else { html! {} }} // Event content
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
{if !is_all_day { html! {
{time_display}
} } else { html! {} }}
// Bottom resize handle {if !is_all_day { html! {
} } else { html! {} }}
}) } }).collect::() }
// Temporary event box during drag { if let Some(drag) = (*drag_state).clone() { if drag.is_dragging && drag.start_date == *date { 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) => { // Calculate the event's new position accounting for click offset let unsnapped_position = drag.current_y - drag.offset_y; // Snap the final position to maintain time increment alignment let preview_position = snap_to_increment(unsnapped_position, props.time_increment); let new_start_time = pixels_to_time(preview_position); let original_duration = if let Some(end) = event.dtend { end.signed_duration_since(event.dtstart) } 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"))}
} }, DragType::ResizeEventStart(event) => { // Show the event being resized from the start let new_start_time = pixels_to_time(drag.current_y); let original_end = if let Some(end) = event.dtend { end.with_timezone(&chrono::Local).naive_local() } else { event.dtstart.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) }; // Calculate positions for the preview let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date); let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local()); let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32); let new_start_pixels = drag.current_y; let new_height = (original_end_pixels as f64 - new_start_pixels).max(20.0); 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"), original_end.time().format("%I:%M %p"))}
} }, DragType::ResizeEventEnd(event) => { // Show the event being resized from the end let new_end_time = pixels_to_time(drag.current_y); let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local(); // Calculate positions for the preview let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date); let new_end_pixels = drag.current_y; let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0); let event_color = get_event_color(event); html! {
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
{format!("{} - {}", original_start.time().format("%I:%M %p"), new_end_time.format("%I:%M %p"))}
} } } } else { html! {} } } else { html! {} } }
} }).collect::() }
// Recurring event modification modal if let Some(edit) = (*pending_recurring_edit).clone() { } else { <> }
} } fn get_start_of_week(date: NaiveDate) -> NaiveDate { let weekday = date.weekday(); let days_from_sunday = match weekday { Weekday::Sun => 0, Weekday::Mon => 1, Weekday::Tue => 2, Weekday::Wed => 3, Weekday::Thu => 4, Weekday::Fri => 5, Weekday::Sat => 6, }; date - Duration::days(days_from_sunday) } fn get_weekday_name(weekday: Weekday) -> &'static str { match weekday { Weekday::Sun => "Sun", Weekday::Mon => "Mon", Weekday::Tue => "Tue", Weekday::Wed => "Wed", Weekday::Thu => "Thu", Weekday::Fri => "Fri", Weekday::Sat => "Sat", } } // Calculate the pixel position of an event based on its time // Each hour is 60px, so we convert time to pixels // Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes) fn snap_to_increment(pixels: f64, increment: u32) -> f64 { let increment_px = increment as f64; // Convert to pixels (1px = 1 minute) (pixels / increment_px).round() * increment_px } // Convert pixel position to time (inverse of time to pixels) fn pixels_to_time(pixels: f64) -> NaiveTime { // Since 60px = 1 hour, pixels directly represent minutes let total_minutes = pixels; // 1px = 1 minute let hours = (total_minutes / 60.0) as u32; let minutes = (total_minutes % 60.0) as u32; // Handle midnight boundary - if we're at exactly 1440 pixels (24:00), return midnight if total_minutes >= 1440.0 { return NaiveTime::from_hms_opt(0, 0, 0).unwrap(); } // Clamp to valid time range for within-day times let hours = hours.min(23); let minutes = minutes.min(59); NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) } fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool) { // Convert UTC times to local time for display let local_start = event.dtstart.with_timezone(&Local); let event_date = local_start.date_naive(); // Only position events that are on this specific date if event_date != date { return (0.0, 0.0, false); // Event not on this date } // Handle all-day events - they appear at the top if event.all_day { return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true } // Calculate start position in pixels from midnight let start_hour = local_start.hour() as f32; let start_minute = local_start.minute() as f32; let start_pixels = (start_hour + start_minute / 60.0) * 60.0; // 60px per hour // Calculate duration and height let duration_pixels = if let Some(end) = event.dtend { let local_end = end.with_timezone(&Local); let end_date = local_end.date_naive(); // Handle events that span multiple days by capping at midnight if end_date > date { // Event continues past midnight, cap at 24:00 (1440px) 1440.0 - start_pixels } else { let end_hour = local_end.hour() as f32; let end_minute = local_end.minute() as f32; let end_pixels = (end_hour + end_minute / 60.0) * 60.0; (end_pixels - start_pixels).max(20.0) // Minimum 20px height } } else { 60.0 // Default 1 hour if no end time }; (start_pixels, duration_pixels, false) // is_all_day = false }