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;