diff --git a/frontend/src/components/week_view.rs b/frontend/src/components/week_view.rs index aea5f4a..772356c 100644 --- a/frontend/src/components/week_view.rs +++ b/frontend/src/components/week_view.rs @@ -353,6 +353,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { 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(); + let event_layouts = calculate_event_layout(&day_events, *date); // Drag event handlers let drag_state_clone = drag_state.clone(); @@ -589,7 +590,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Events positioned absolutely based on their actual times
{ - day_events.iter().filter_map(|event| { + day_events.iter().enumerate().filter_map(|(event_idx, 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 @@ -782,12 +783,28 @@ pub fn week_view(props: &WeekViewProps) -> Html { if is_refreshing { Some("refreshing") } else { None }, if is_all_day { Some("all-day") } else { None } )} - style={format!( - "background-color: {}; top: {}px; height: {}px;", - event_color, - start_pixels, - duration_pixels - )} + style={ + let (column_idx, total_columns) = event_layouts[event_idx]; + let column_width = if total_columns > 1 { + format!("calc((100% - 8px) / {})", total_columns) // Account for 4px margins on each side + } else { + "calc(100% - 8px)".to_string() + }; + let left_offset = if total_columns > 1 { + format!("calc(4px + {} * (100% - 8px) / {})", column_idx, total_columns) + } else { + "4px".to_string() + }; + + format!( + "background-color: {}; top: {}px; height: {}px; left: {}; width: {}; right: auto;", + event_color, + start_pixels, + duration_pixels, + left_offset, + column_width + ) + } {onclick} {oncontextmenu} onmousedown={onmousedown_event} @@ -1065,3 +1082,82 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool) (start_pixels, duration_pixels, false) // is_all_day = false } + +// Check if two events overlap in time +fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool { + let start1 = event1.dtstart.with_timezone(&Local).naive_local(); + let end1 = if let Some(end) = event1.dtend { + end.with_timezone(&Local).naive_local() + } else { + start1 + chrono::Duration::hours(1) // Default 1 hour duration + }; + + let start2 = event2.dtstart.with_timezone(&Local).naive_local(); + let end2 = if let Some(end) = event2.dtend { + end.with_timezone(&Local).naive_local() + } else { + start2 + chrono::Duration::hours(1) // Default 1 hour duration + }; + + // Events overlap if one starts before the other ends + start1 < end2 && start2 < end1 +} + +// Calculate layout columns for overlapping events +fn calculate_event_layout(events: &[VEvent], date: NaiveDate) -> Vec<(usize, usize)> { + // Filter events that should appear on this date and sort by start time + let mut day_events: Vec<_> = events.iter() + .filter(|event| { + let (_, _, _) = calculate_event_position(event, date); + // Only include events that would be positioned (non-zero dimensions or all-day) + let local_start = event.dtstart.with_timezone(&Local); + let event_date = local_start.date_naive(); + event_date == date || + (event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20) + }) + .collect(); + + // Sort by start time + day_events.sort_by_key(|event| event.dtstart.with_timezone(&Local).naive_local()); + + // Calculate layout: (column_index, total_columns) + let mut layout = Vec::with_capacity(day_events.len()); + let mut columns: Vec> = Vec::new(); + + for event in &day_events { + // Find the first column where this event doesn't overlap with any existing event + let mut placed = false; + for (col_idx, column) in columns.iter_mut().enumerate() { + if !column.iter().any(|existing_event| events_overlap(event, existing_event)) { + column.push(event); + layout.push((col_idx, 0)); // total_columns will be set later + placed = true; + break; + } + } + + if !placed { + // Create new column + columns.push(vec![event]); + layout.push((columns.len() - 1, 0)); // total_columns will be set later + } + } + + // Update total_columns for all events + let total_columns = columns.len(); + for (_, total_cols) in layout.iter_mut() { + *total_cols = total_columns; + } + + // Create result mapping original events to their layout + let mut result = Vec::with_capacity(events.len()); + for event in events { + if let Some(pos) = day_events.iter().position(|e| e.uid == event.uid) { + result.push(layout[pos]); + } else { + result.push((0, 1)); // Default: single column + } + } + + result +} diff --git a/frontend/styles.css b/frontend/styles.css index c7353c2..c517ca3 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -782,8 +782,7 @@ body { /* Week Events */ .week-event { position: absolute !important; - left: 4px; - right: 4px; + /* left and width are now set inline for overlap handling */ min-height: 20px; background: #3B82F6; color: white;