Fix all-day events: validation and proper header positioning
Backend fixes: - Fix all-day event creation validation error - Allow same start/end date for all-day events (single-day events) - Maintain strict validation for timed events (end must be after start) Frontend improvements: - Move all-day events from time grid to day headers - Add dedicated all-day events container that stacks vertically - Filter all-day events out of main time-based events area - Add proper CSS styling for all-day event display and interaction - Maintain event click handling and color themes All-day events now appear in the correct location at the top of each day column and properly stack when multiple events exist. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -417,11 +417,19 @@ pub async fn create_event(
|
||||
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// Validate that end is after start
|
||||
if end_datetime <= start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date/time must be after start date/time".to_string(),
|
||||
));
|
||||
// Validate that end is after start (allow equal times for all-day events)
|
||||
if request.all_day {
|
||||
if end_datetime < start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date must be on or after start date for all-day events".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if end_datetime <= start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date/time must be after start date/time".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique UID for the event
|
||||
@@ -707,11 +715,19 @@ pub async fn update_event(
|
||||
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
|
||||
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
|
||||
|
||||
// Validate that end is after start
|
||||
if end_datetime <= start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date/time must be after start date/time".to_string(),
|
||||
));
|
||||
// Validate that end is after start (allow equal times for all-day events)
|
||||
if request.all_day {
|
||||
if end_datetime < start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date must be on or after start date for all-day events".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if end_datetime <= start_datetime {
|
||||
return Err(ApiError::BadRequest(
|
||||
"End date/time must be after start date/time".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Update event properties
|
||||
|
||||
@@ -319,11 +319,52 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
week_days.iter().map(|date| {
|
||||
let is_today = *date == props.today;
|
||||
let weekday_name = get_weekday_name(date.weekday());
|
||||
let day_events = props.events.get(date).cloned().unwrap_or_default();
|
||||
|
||||
// Filter for all-day events only
|
||||
let all_day_events: Vec<_> = day_events.iter().filter(|event| event.all_day).collect();
|
||||
|
||||
html! {
|
||||
<div class={classes!("week-day-header", if is_today { Some("today") } else { None })}>
|
||||
<div class="weekday-name">{weekday_name}</div>
|
||||
<div class="day-number">{date.day()}</div>
|
||||
<div class="day-header-content">
|
||||
<div class="weekday-name">{weekday_name}</div>
|
||||
<div class="day-number">{date.day()}</div>
|
||||
</div>
|
||||
|
||||
// All-day events section
|
||||
{if !all_day_events.is_empty() {
|
||||
html! {
|
||||
<div class="all-day-events">
|
||||
{
|
||||
all_day_events.iter().map(|event| {
|
||||
let event_color = get_event_color(event);
|
||||
let onclick = {
|
||||
let on_event_click = props.on_event_click.clone();
|
||||
let event = (*event).clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
on_event_click.emit(event.clone());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="all-day-event"
|
||||
style={format!("background-color: {}", event_color)}
|
||||
{onclick}
|
||||
>
|
||||
<span class="all-day-event-title">
|
||||
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
@@ -611,8 +652,13 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
day_events.iter().enumerate().filter_map(|(event_idx, event)| {
|
||||
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date);
|
||||
|
||||
// Skip all-day events (they're rendered in the header)
|
||||
if is_all_day {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if start_pixels == 0.0 && duration_pixels == 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -650,11 +650,14 @@ body {
|
||||
}
|
||||
|
||||
.week-day-header {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||
background: var(--weekday-header-bg, #f8f9fa);
|
||||
color: var(--weekday-header-text, inherit);
|
||||
min-height: 70px; /* Ensure space for all-day events */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.week-day-header.today {
|
||||
@@ -680,6 +683,45 @@ body {
|
||||
color: var(--calendar-today-text, #1976d2);
|
||||
}
|
||||
|
||||
/* All-day events in header */
|
||||
.day-header-content {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.all-day-events {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-top: 0.5rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.all-day-event {
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.75rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
min-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.all-day-event:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.all-day-event-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Week Content */
|
||||
.week-content {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user