Compare commits
9 Commits
c0bdd3d8c2
...
235dcf8e1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235dcf8e1d | ||
|
|
8dd60a8ec1 | ||
|
|
20679b6b53 | ||
|
|
53c4a99697 | ||
|
|
5ea33b7d0a | ||
|
|
13a752a69c | ||
|
|
0609a99839 | ||
|
|
dce82d5f7d | ||
|
|
1e8a8ce5f2 |
2
backend/migrations/005_add_last_used_calendar.sql
Normal file
2
backend/migrations/005_add_last_used_calendar.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add last used calendar preference to user preferences
|
||||||
|
ALTER TABLE user_preferences ADD COLUMN last_used_calendar TEXT;
|
||||||
@@ -93,6 +93,7 @@ impl AuthService {
|
|||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
calendar_style: preferences.calendar_style,
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
|
last_used_calendar: preferences.last_used_calendar,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ pub struct UserPreferences {
|
|||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
pub calendar_style: Option<String>,
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>, // JSON string
|
pub calendar_colors: Option<String>, // JSON string
|
||||||
|
pub last_used_calendar: Option<String>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ impl UserPreferences {
|
|||||||
calendar_theme: Some("light".to_string()),
|
calendar_theme: Some("light".to_string()),
|
||||||
calendar_style: Some("default".to_string()),
|
calendar_style: Some("default".to_string()),
|
||||||
calendar_colors: None,
|
calendar_colors: None,
|
||||||
|
last_used_calendar: None,
|
||||||
updated_at: Utc::now(),
|
updated_at: Utc::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,8 +268,8 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO user_preferences
|
"INSERT INTO user_preferences
|
||||||
(user_id, calendar_selected_date, calendar_time_increment,
|
(user_id, calendar_selected_date, calendar_time_increment,
|
||||||
calendar_view_mode, calendar_theme, calendar_style, calendar_colors, updated_at)
|
calendar_view_mode, calendar_theme, calendar_style, calendar_colors, last_used_calendar, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&prefs.user_id)
|
.bind(&prefs.user_id)
|
||||||
.bind(&prefs.calendar_selected_date)
|
.bind(&prefs.calendar_selected_date)
|
||||||
@@ -276,6 +278,7 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
.bind(&prefs.calendar_theme)
|
.bind(&prefs.calendar_theme)
|
||||||
.bind(&prefs.calendar_style)
|
.bind(&prefs.calendar_style)
|
||||||
.bind(&prefs.calendar_colors)
|
.bind(&prefs.calendar_colors)
|
||||||
|
.bind(&prefs.last_used_calendar)
|
||||||
.bind(&prefs.updated_at)
|
.bind(&prefs.updated_at)
|
||||||
.execute(self.db.pool())
|
.execute(self.db.pool())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -290,7 +293,7 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
"UPDATE user_preferences
|
"UPDATE user_preferences
|
||||||
SET calendar_selected_date = ?, calendar_time_increment = ?,
|
SET calendar_selected_date = ?, calendar_time_increment = ?,
|
||||||
calendar_view_mode = ?, calendar_theme = ?, calendar_style = ?,
|
calendar_view_mode = ?, calendar_theme = ?, calendar_style = ?,
|
||||||
calendar_colors = ?, updated_at = ?
|
calendar_colors = ?, last_used_calendar = ?, updated_at = ?
|
||||||
WHERE user_id = ?",
|
WHERE user_id = ?",
|
||||||
)
|
)
|
||||||
.bind(&prefs.calendar_selected_date)
|
.bind(&prefs.calendar_selected_date)
|
||||||
@@ -299,6 +302,7 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
.bind(&prefs.calendar_theme)
|
.bind(&prefs.calendar_theme)
|
||||||
.bind(&prefs.calendar_style)
|
.bind(&prefs.calendar_style)
|
||||||
.bind(&prefs.calendar_colors)
|
.bind(&prefs.calendar_colors)
|
||||||
|
.bind(&prefs.last_used_calendar)
|
||||||
.bind(Utc::now())
|
.bind(Utc::now())
|
||||||
.bind(&prefs.user_id)
|
.bind(&prefs.user_id)
|
||||||
.execute(self.db.pool())
|
.execute(self.db.pool())
|
||||||
|
|||||||
@@ -845,7 +845,7 @@ fn parse_event_datetime(
|
|||||||
time_str: &str,
|
time_str: &str,
|
||||||
all_day: bool,
|
all_day: bool,
|
||||||
) -> Result<chrono::DateTime<chrono::Utc>, String> {
|
) -> Result<chrono::DateTime<chrono::Utc>, String> {
|
||||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||||
|
|
||||||
// Parse the date
|
// Parse the date
|
||||||
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
||||||
@@ -865,7 +865,11 @@ fn parse_event_datetime(
|
|||||||
// Combine date and time
|
// Combine date and time
|
||||||
let datetime = NaiveDateTime::new(date, time);
|
let datetime = NaiveDateTime::new(date, time);
|
||||||
|
|
||||||
// Assume local time and convert to UTC (in a real app, you'd want timezone support)
|
// Treat the datetime as local time and convert to UTC
|
||||||
Ok(Utc.from_utc_datetime(&datetime))
|
let local_datetime = Local.from_local_datetime(&datetime)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| "Ambiguous local datetime".to_string())?;
|
||||||
|
|
||||||
|
Ok(local_datetime.with_timezone(&Utc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub async fn get_preferences(
|
|||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
calendar_style: preferences.calendar_style,
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
|
last_used_calendar: preferences.last_used_calendar,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +86,9 @@ pub async fn update_preferences(
|
|||||||
if request.calendar_colors.is_some() {
|
if request.calendar_colors.is_some() {
|
||||||
preferences.calendar_colors = request.calendar_colors;
|
preferences.calendar_colors = request.calendar_colors;
|
||||||
}
|
}
|
||||||
|
if request.last_used_calendar.is_some() {
|
||||||
|
preferences.last_used_calendar = request.last_used_calendar;
|
||||||
|
}
|
||||||
|
|
||||||
prefs_repo
|
prefs_repo
|
||||||
.update(&preferences)
|
.update(&preferences)
|
||||||
@@ -100,6 +104,7 @@ pub async fn update_preferences(
|
|||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
calendar_style: preferences.calendar_style,
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
|
last_used_calendar: preferences.last_used_calendar,
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,17 @@ pub async fn create_event_series(
|
|||||||
.and_hms_opt(23, 59, 59)
|
.and_hms_opt(23, 59, 59)
|
||||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||||
|
|
||||||
|
// Convert from local time to UTC
|
||||||
|
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||||
|
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||||
|
|
||||||
(
|
(
|
||||||
chrono::Utc.from_utc_datetime(&start_dt),
|
start_local.with_timezone(&chrono::Utc),
|
||||||
chrono::Utc.from_utc_datetime(&end_dt),
|
end_local.with_timezone(&chrono::Utc),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Parse times for timed events
|
// Parse times for timed events
|
||||||
@@ -163,9 +171,17 @@ pub async fn create_event_series(
|
|||||||
start_date.and_time(end_time)
|
start_date.and_time(end_time)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert from local time to UTC
|
||||||
|
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||||
|
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||||
|
|
||||||
(
|
(
|
||||||
chrono::Utc.from_utc_datetime(&start_dt),
|
start_local.with_timezone(&chrono::Utc),
|
||||||
chrono::Utc.from_utc_datetime(&end_dt),
|
end_local.with_timezone(&chrono::Utc),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,9 +417,17 @@ pub async fn update_event_series(
|
|||||||
.and_hms_opt(23, 59, 59)
|
.and_hms_opt(23, 59, 59)
|
||||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||||
|
|
||||||
|
// Convert from local time to UTC
|
||||||
|
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||||
|
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||||
|
|
||||||
(
|
(
|
||||||
chrono::Utc.from_utc_datetime(&start_dt),
|
start_local.with_timezone(&chrono::Utc),
|
||||||
chrono::Utc.from_utc_datetime(&end_dt),
|
end_local.with_timezone(&chrono::Utc),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let start_time = if !request.start_time.is_empty() {
|
let start_time = if !request.start_time.is_empty() {
|
||||||
@@ -438,9 +462,17 @@ pub async fn update_event_series(
|
|||||||
(chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc()
|
(chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert from local time to UTC
|
||||||
|
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||||
|
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||||
|
|
||||||
(
|
(
|
||||||
chrono::Utc.from_utc_datetime(&start_dt),
|
start_local.with_timezone(&chrono::Utc),
|
||||||
chrono::Utc.from_utc_datetime(&end_dt),
|
end_local.with_timezone(&chrono::Utc),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub struct UserPreferencesResponse {
|
|||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
pub calendar_style: Option<String>,
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
|
pub last_used_calendar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -40,6 +41,7 @@ pub struct UpdatePreferencesRequest {
|
|||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
pub calendar_style: Option<String>,
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
|
pub last_used_calendar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::components::{
|
use crate::components::{
|
||||||
CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModalV2, DeleteAction,
|
CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModal, DeleteAction,
|
||||||
EditAction, EventClass, EventContextMenu, EventCreationData, EventStatus, RecurrenceType,
|
EditAction, EventContextMenu, EventCreationData, RouteHandler, Sidebar, Theme, ViewMode,
|
||||||
ReminderType, RouteHandler, Sidebar, Theme, ViewMode,
|
|
||||||
};
|
};
|
||||||
use crate::components::sidebar::{Style};
|
use crate::components::sidebar::{Style};
|
||||||
use crate::models::ical::VEvent;
|
use crate::models::ical::VEvent;
|
||||||
@@ -413,7 +412,148 @@ pub fn App() -> Html {
|
|||||||
let create_event_modal_open = create_event_modal_open.clone();
|
let create_event_modal_open = create_event_modal_open.clone();
|
||||||
let auth_token = auth_token.clone();
|
let auth_token = auth_token.clone();
|
||||||
Callback::from(move |event_data: EventCreationData| {
|
Callback::from(move |event_data: EventCreationData| {
|
||||||
|
// Check if this is an update operation (has original_uid) or a create operation
|
||||||
|
if let Some(original_uid) = event_data.original_uid.clone() {
|
||||||
|
web_sys::console::log_1(&format!("Updating event via modal: {:?}", event_data).into());
|
||||||
|
|
||||||
|
create_event_modal_open.set(false);
|
||||||
|
|
||||||
|
// Handle the update operation using the existing backend update logic
|
||||||
|
if let Some(token) = (*auth_token).clone() {
|
||||||
|
let event_data_for_update = event_data.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::<String>("caldav_credentials")
|
||||||
|
{
|
||||||
|
if let Ok(credentials) =
|
||||||
|
serde_json::from_str::<serde_json::Value>(&credentials_str)
|
||||||
|
{
|
||||||
|
credentials["password"].as_str().unwrap_or("").to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert EventCreationData to update parameters
|
||||||
|
let params = event_data_for_update.to_create_event_params();
|
||||||
|
|
||||||
|
// Determine if this is a recurring event update
|
||||||
|
let is_recurring = matches!(event_data_for_update.recurrence, crate::components::event_form::RecurrenceType::Daily |
|
||||||
|
crate::components::event_form::RecurrenceType::Weekly |
|
||||||
|
crate::components::event_form::RecurrenceType::Monthly |
|
||||||
|
crate::components::event_form::RecurrenceType::Yearly);
|
||||||
|
|
||||||
|
let update_result = if is_recurring && event_data_for_update.edit_scope.is_some() {
|
||||||
|
// Use series update endpoint for recurring events
|
||||||
|
let edit_action = event_data_for_update.edit_scope.unwrap();
|
||||||
|
let scope = match edit_action {
|
||||||
|
crate::components::EditAction::EditAll => "all_in_series".to_string(),
|
||||||
|
crate::components::EditAction::EditFuture => "this_and_future".to_string(),
|
||||||
|
crate::components::EditAction::EditThis => "this_only".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
calendar_service
|
||||||
|
.update_series(
|
||||||
|
&token,
|
||||||
|
&password,
|
||||||
|
original_uid.clone(),
|
||||||
|
params.0, // title
|
||||||
|
params.1, // description
|
||||||
|
params.2, // start_date
|
||||||
|
params.3, // start_time
|
||||||
|
params.4, // end_date
|
||||||
|
params.5, // end_time
|
||||||
|
params.6, // location
|
||||||
|
params.7, // all_day
|
||||||
|
params.8, // status
|
||||||
|
params.9, // class
|
||||||
|
params.10, // priority
|
||||||
|
params.11, // organizer
|
||||||
|
params.12, // attendees
|
||||||
|
params.13, // categories
|
||||||
|
params.14, // reminder
|
||||||
|
params.15, // recurrence
|
||||||
|
params.17, // calendar_path (skipping recurrence_days)
|
||||||
|
scope,
|
||||||
|
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
// Use regular update endpoint for single events
|
||||||
|
calendar_service
|
||||||
|
.update_event(
|
||||||
|
&token,
|
||||||
|
&password,
|
||||||
|
original_uid.clone(),
|
||||||
|
params.0, // title
|
||||||
|
params.1, // description
|
||||||
|
params.2, // start_date
|
||||||
|
params.3, // start_time
|
||||||
|
params.4, // end_date
|
||||||
|
params.5, // end_time
|
||||||
|
params.6, // location
|
||||||
|
params.7, // all_day
|
||||||
|
params.8, // status
|
||||||
|
params.9, // class
|
||||||
|
params.10, // priority
|
||||||
|
params.11, // organizer
|
||||||
|
params.12, // attendees
|
||||||
|
params.13, // categories
|
||||||
|
params.14, // reminder
|
||||||
|
params.15, // recurrence
|
||||||
|
params.16, // recurrence_days
|
||||||
|
params.17, // calendar_path
|
||||||
|
vec![], // exception_dates - empty for simple updates
|
||||||
|
None, // update_action - None for regular updates
|
||||||
|
None, // until_date - None for regular updates
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
match update_result {
|
||||||
|
Ok(_) => {
|
||||||
|
web_sys::console::log_1(&"Event updated successfully via modal".into());
|
||||||
|
// Trigger a page reload to refresh events from all calendars
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let _ = window.location().reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into());
|
web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into());
|
||||||
|
|
||||||
|
// Save the selected calendar as the last used calendar
|
||||||
|
if let Some(ref calendar_path) = event_data.selected_calendar {
|
||||||
|
let _ = LocalStorage::set("last_used_calendar", calendar_path);
|
||||||
|
|
||||||
|
// Also sync to backend asynchronously
|
||||||
|
let calendar_path_for_sync = calendar_path.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let preferences_service = crate::services::preferences::PreferencesService::new();
|
||||||
|
if let Err(e) = preferences_service.update_last_used_calendar(&calendar_path_for_sync).await {
|
||||||
|
web_sys::console::warn_1(&format!("Failed to sync last used calendar to backend: {}", e).into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
create_event_modal_open.set(false);
|
create_event_modal_open.set(false);
|
||||||
|
|
||||||
if let Some(_token) = (*auth_token).clone() {
|
if let Some(_token) = (*auth_token).clone() {
|
||||||
@@ -455,6 +595,8 @@ pub fn App() -> Html {
|
|||||||
params.14, // reminder
|
params.14, // reminder
|
||||||
params.15, // recurrence
|
params.15, // recurrence
|
||||||
params.16, // recurrence_days
|
params.16, // recurrence_days
|
||||||
|
params.18, // recurrence_count
|
||||||
|
params.19, // recurrence_until
|
||||||
params.17, // calendar_path
|
params.17, // calendar_path
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -534,18 +676,11 @@ pub fn App() -> Html {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert local times to UTC for backend storage
|
// Send local time directly to backend (backend will handle UTC conversion)
|
||||||
let start_utc = new_start
|
let start_date = new_start.format("%Y-%m-%d").to_string();
|
||||||
.and_local_timezone(chrono::Local)
|
let start_time = new_start.format("%H:%M").to_string();
|
||||||
.unwrap()
|
let end_date = new_end.format("%Y-%m-%d").to_string();
|
||||||
.to_utc();
|
let end_time = new_end.format("%H:%M").to_string();
|
||||||
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
|
// Convert existing event data to string formats for the API
|
||||||
let status_str = match original_event.status {
|
let status_str = match original_event.status {
|
||||||
@@ -1031,7 +1166,7 @@ pub fn App() -> Html {
|
|||||||
on_create_event={on_create_event_click}
|
on_create_event={on_create_event_click}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateEventModalV2
|
<CreateEventModal
|
||||||
is_open={*create_event_modal_open}
|
is_open={*create_event_modal_open}
|
||||||
selected_date={(*selected_date_for_event).clone()}
|
selected_date={(*selected_date_for_event).clone()}
|
||||||
initial_start_time={None}
|
initial_start_time={None}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::components::{
|
use crate::components::{
|
||||||
CalendarHeader, CreateEventModalV2, EventCreationData, EventModal, MonthView, ViewMode, WeekView,
|
CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, ViewMode, WeekView,
|
||||||
};
|
};
|
||||||
use crate::models::ical::VEvent;
|
use crate::models::ical::VEvent;
|
||||||
use crate::services::{calendar_service::UserInfo, CalendarService};
|
use crate::services::{calendar_service::UserInfo, CalendarService};
|
||||||
@@ -492,7 +492,7 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
// Create event modal
|
// Create event modal
|
||||||
<CreateEventModalV2
|
<CreateEventModal
|
||||||
is_open={*show_create_modal}
|
is_open={*show_create_modal}
|
||||||
selected_date={create_event_data.as_ref().map(|(date, _, _)| *date)}
|
selected_date={create_event_data.as_ref().map(|(date, _, _)| *date)}
|
||||||
event_to_edit={None}
|
event_to_edit={None}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
|||||||
use crate::components::event_form::*;
|
|
||||||
use crate::components::create_event_modal::{EventCreationData}; // Use the existing types
|
|
||||||
use crate::components::{EditAction};
|
|
||||||
use crate::models::ical::VEvent;
|
|
||||||
use crate::services::calendar_service::CalendarInfo;
|
|
||||||
use yew::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
|
||||||
pub struct CreateEventModalProps {
|
|
||||||
pub is_open: bool,
|
|
||||||
pub on_close: Callback<()>,
|
|
||||||
pub on_create: Callback<EventCreationData>,
|
|
||||||
pub available_calendars: Vec<CalendarInfo>,
|
|
||||||
pub selected_date: Option<chrono::NaiveDate>,
|
|
||||||
pub initial_start_time: Option<chrono::NaiveTime>,
|
|
||||||
pub initial_end_time: Option<chrono::NaiveTime>,
|
|
||||||
#[prop_or_default]
|
|
||||||
pub event_to_edit: Option<VEvent>,
|
|
||||||
#[prop_or_default]
|
|
||||||
pub edit_scope: Option<EditAction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(CreateEventModalV2)]
|
|
||||||
pub fn create_event_modal_v2(props: &CreateEventModalProps) -> Html {
|
|
||||||
let active_tab = use_state(|| ModalTab::default());
|
|
||||||
let event_data = use_state(|| EventCreationData::default());
|
|
||||||
|
|
||||||
// Initialize data when modal opens
|
|
||||||
{
|
|
||||||
let event_data = event_data.clone();
|
|
||||||
let is_open = props.is_open;
|
|
||||||
let event_to_edit = props.event_to_edit.clone();
|
|
||||||
let selected_date = props.selected_date;
|
|
||||||
let initial_start_time = props.initial_start_time;
|
|
||||||
let initial_end_time = props.initial_end_time;
|
|
||||||
let edit_scope = props.edit_scope.clone();
|
|
||||||
let available_calendars = props.available_calendars.clone();
|
|
||||||
|
|
||||||
use_effect_with(is_open, move |&is_open| {
|
|
||||||
if is_open {
|
|
||||||
let mut data = if let Some(_event) = &event_to_edit {
|
|
||||||
// TODO: Convert VEvent to EventCreationData
|
|
||||||
EventCreationData::default()
|
|
||||||
} else if let Some(date) = selected_date {
|
|
||||||
let mut data = EventCreationData::default();
|
|
||||||
data.start_date = date;
|
|
||||||
data.end_date = date;
|
|
||||||
if let Some(start_time) = initial_start_time {
|
|
||||||
data.start_time = start_time;
|
|
||||||
}
|
|
||||||
if let Some(end_time) = initial_end_time {
|
|
||||||
data.end_time = end_time;
|
|
||||||
}
|
|
||||||
data
|
|
||||||
} else {
|
|
||||||
EventCreationData::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set default calendar
|
|
||||||
if data.selected_calendar.is_none() && !available_calendars.is_empty() {
|
|
||||||
data.selected_calendar = Some(available_calendars[0].path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set edit scope if provided
|
|
||||||
if let Some(scope) = &edit_scope {
|
|
||||||
data.edit_scope = Some(scope.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
event_data.set(data);
|
|
||||||
}
|
|
||||||
|| ()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !props.is_open {
|
|
||||||
return html! {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let on_backdrop_click = {
|
|
||||||
let on_close = props.on_close.clone();
|
|
||||||
Callback::from(move |e: MouseEvent| {
|
|
||||||
if e.target() == e.current_target() {
|
|
||||||
on_close.emit(());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let switch_to_tab = {
|
|
||||||
let active_tab = active_tab.clone();
|
|
||||||
Callback::from(move |tab: ModalTab| {
|
|
||||||
active_tab.set(tab);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_save = {
|
|
||||||
let event_data = event_data.clone();
|
|
||||||
let on_create = props.on_create.clone();
|
|
||||||
Callback::from(move |_: MouseEvent| {
|
|
||||||
on_create.emit((*event_data).clone());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_close = props.on_close.clone();
|
|
||||||
let on_close_header = on_close.clone();
|
|
||||||
|
|
||||||
let tab_props = TabProps {
|
|
||||||
data: event_data.clone(),
|
|
||||||
available_calendars: props.available_calendars.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="modal-backdrop" onclick={on_backdrop_click}>
|
|
||||||
<div class="modal-content create-event-modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>
|
|
||||||
{if props.event_to_edit.is_some() { "Edit Event" } else { "Create Event" }}
|
|
||||||
</h3>
|
|
||||||
<button class="modal-close" onclick={Callback::from(move |_| on_close_header.emit(()))}>
|
|
||||||
{"×"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-tabs">
|
|
||||||
<div class="tab-navigation">
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::BasicDetails { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::BasicDetails))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Basic"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::Advanced { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Advanced))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Advanced"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::People { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::People))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"People"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::Categories { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Categories))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Categories"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::Location { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Location))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Location"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={if *active_tab == ModalTab::Reminders { "tab-button active" } else { "tab-button" }}
|
|
||||||
onclick={{
|
|
||||||
let switch_to_tab = switch_to_tab.clone();
|
|
||||||
Callback::from(move |_| switch_to_tab.emit(ModalTab::Reminders))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Reminders"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="tab-content">
|
|
||||||
{
|
|
||||||
match *active_tab {
|
|
||||||
ModalTab::BasicDetails => html! { <BasicDetailsTab ..tab_props /> },
|
|
||||||
ModalTab::Advanced => html! { <AdvancedTab ..tab_props /> },
|
|
||||||
ModalTab::People => html! { <PeopleTab ..tab_props /> },
|
|
||||||
ModalTab::Categories => html! { <CategoriesTab ..tab_props /> },
|
|
||||||
ModalTab::Location => html! { <LocationTab ..tab_props /> },
|
|
||||||
ModalTab::Reminders => html! { <RemindersTab ..tab_props /> },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-secondary" onclick={Callback::from(move |_| on_close.emit(()))}>
|
|
||||||
{"Cancel"}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" onclick={on_save}>
|
|
||||||
{if props.event_to_edit.is_some() { "Update Event" } else { "Create Event" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::types::*;
|
use super::types::*;
|
||||||
use crate::components::create_event_modal::{EventStatus, EventClass};
|
// Types are already imported from super::types::*
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{HtmlInputElement, HtmlSelectElement};
|
use web_sys::HtmlSelectElement;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[function_component(AdvancedTab)]
|
#[function_component(AdvancedTab)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::types::*;
|
use super::types::*;
|
||||||
use crate::components::create_event_modal::{EventStatus, EventClass, RecurrenceType, ReminderType};
|
// Types are already imported from super::types::*
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement};
|
use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::types::*;
|
use super::types::*;
|
||||||
use crate::components::create_event_modal::ReminderType;
|
// Types are already imported from super::types::*
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlSelectElement;
|
use web_sys::HtmlSelectElement;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use crate::models::ical::VEvent;
|
|
||||||
use crate::services::calendar_service::CalendarInfo;
|
use crate::services::calendar_service::CalendarInfo;
|
||||||
use chrono::{Datelike, Local, NaiveDate, NaiveTime, TimeZone, Utc};
|
use chrono::{Local, NaiveDate, NaiveTime};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
@@ -78,12 +76,7 @@ impl Default for ModalTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
// EditAction is now imported from event_context_menu - this duplicate removed
|
||||||
pub enum EditAction {
|
|
||||||
ThisOnly,
|
|
||||||
ThisAndFuture,
|
|
||||||
AllInSeries,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct EventCreationData {
|
pub struct EventCreationData {
|
||||||
@@ -130,8 +123,58 @@ pub struct EventCreationData {
|
|||||||
pub selected_calendar: Option<String>,
|
pub selected_calendar: Option<String>,
|
||||||
|
|
||||||
// Edit tracking (for recurring events)
|
// Edit tracking (for recurring events)
|
||||||
pub edit_scope: Option<EditAction>,
|
pub edit_scope: Option<crate::components::EditAction>,
|
||||||
pub changed_fields: Vec<String>,
|
pub changed_fields: Vec<String>,
|
||||||
|
pub original_uid: Option<String>, // Set when editing existing events
|
||||||
|
pub occurrence_date: Option<NaiveDate>, // The specific occurrence date being edited
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventCreationData {
|
||||||
|
pub fn to_create_event_params(&self) -> (
|
||||||
|
String, // title
|
||||||
|
String, // description
|
||||||
|
String, // start_date
|
||||||
|
String, // start_time
|
||||||
|
String, // end_date
|
||||||
|
String, // end_time
|
||||||
|
String, // location
|
||||||
|
bool, // all_day
|
||||||
|
String, // status
|
||||||
|
String, // class
|
||||||
|
Option<u8>, // priority
|
||||||
|
String, // organizer
|
||||||
|
String, // attendees
|
||||||
|
String, // categories
|
||||||
|
String, // reminder
|
||||||
|
String, // recurrence
|
||||||
|
Vec<bool>, // recurrence_days
|
||||||
|
Option<String>, // calendar_path
|
||||||
|
Option<u32>, // recurrence_count
|
||||||
|
Option<String>, // recurrence_until
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
self.title.clone(),
|
||||||
|
self.description.clone(),
|
||||||
|
self.start_date.format("%Y-%m-%d").to_string(),
|
||||||
|
self.start_time.format("%H:%M").to_string(),
|
||||||
|
self.end_date.format("%Y-%m-%d").to_string(),
|
||||||
|
self.end_time.format("%H:%M").to_string(),
|
||||||
|
self.location.clone(),
|
||||||
|
self.all_day,
|
||||||
|
format!("{:?}", self.status).to_uppercase(),
|
||||||
|
format!("{:?}", self.class).to_uppercase(),
|
||||||
|
self.priority,
|
||||||
|
self.organizer.clone(),
|
||||||
|
self.attendees.clone(),
|
||||||
|
self.categories.clone(),
|
||||||
|
format!("{:?}", self.reminder),
|
||||||
|
format!("{:?}", self.recurrence),
|
||||||
|
self.recurrence_days.clone(),
|
||||||
|
self.selected_calendar.clone(),
|
||||||
|
self.recurrence_count,
|
||||||
|
self.recurrence_until.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EventCreationData {
|
impl Default for EventCreationData {
|
||||||
@@ -168,6 +211,8 @@ impl Default for EventCreationData {
|
|||||||
selected_calendar: None,
|
selected_calendar: None,
|
||||||
edit_scope: None,
|
edit_scope: None,
|
||||||
changed_fields: vec![],
|
changed_fields: vec![],
|
||||||
|
original_uid: None,
|
||||||
|
occurrence_date: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +220,6 @@ impl Default for EventCreationData {
|
|||||||
// Common props for all tab components
|
// Common props for all tab components
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct TabProps {
|
pub struct TabProps {
|
||||||
pub data: UseStateHandle<crate::components::create_event_modal::EventCreationData>,
|
pub data: UseStateHandle<EventCreationData>,
|
||||||
pub available_calendars: Vec<CalendarInfo>,
|
pub available_calendars: Vec<CalendarInfo>,
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ pub mod calendar_list_item;
|
|||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod create_calendar_modal;
|
pub mod create_calendar_modal;
|
||||||
pub mod create_event_modal;
|
pub mod create_event_modal;
|
||||||
pub mod create_event_modal_v2;
|
|
||||||
pub mod event_context_menu;
|
pub mod event_context_menu;
|
||||||
pub mod event_form;
|
pub mod event_form;
|
||||||
pub mod event_modal;
|
pub mod event_modal;
|
||||||
@@ -22,14 +21,9 @@ pub use calendar_header::CalendarHeader;
|
|||||||
pub use calendar_list_item::CalendarListItem;
|
pub use calendar_list_item::CalendarListItem;
|
||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
pub use create_calendar_modal::CreateCalendarModal;
|
pub use create_calendar_modal::CreateCalendarModal;
|
||||||
pub use create_event_modal::{
|
pub use create_event_modal::CreateEventModal;
|
||||||
CreateEventModal, EventClass, EventCreationData, EventStatus, RecurrenceType, ReminderType,
|
// Re-export event form types for backwards compatibility
|
||||||
};
|
pub use event_form::EventCreationData;
|
||||||
pub use create_event_modal_v2::CreateEventModalV2;
|
|
||||||
pub use event_form::{
|
|
||||||
EventClass as EventFormClass, EventCreationData as EventFormData, EventStatus as EventFormStatus,
|
|
||||||
RecurrenceType as EventFormRecurrenceType, ReminderType as EventFormReminderType,
|
|
||||||
};
|
|
||||||
pub use event_context_menu::{DeleteAction, EditAction, EventContextMenu};
|
pub use event_context_menu::{DeleteAction, EditAction, EventContextMenu};
|
||||||
pub use event_modal::EventModal;
|
pub use event_modal::EventModal;
|
||||||
pub use login::Login;
|
pub use login::Login;
|
||||||
|
|||||||
@@ -316,10 +316,19 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
week_days.iter().map(|date| {
|
week_days.iter().map(|date| {
|
||||||
let is_today = *date == props.today;
|
let is_today = *date == props.today;
|
||||||
let weekday_name = get_weekday_name(date.weekday());
|
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
|
// Collect all-day events that span this date (from any day in the week)
|
||||||
let all_day_events: Vec<_> = day_events.iter().filter(|event| event.all_day).collect();
|
let mut all_day_events: Vec<&VEvent> = Vec::new();
|
||||||
|
for events_list in props.events.values() {
|
||||||
|
for event in events_list {
|
||||||
|
if event.all_day && event_spans_date(event, *date) {
|
||||||
|
all_day_events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove duplicates (same event might appear in multiple day buckets)
|
||||||
|
all_day_events.sort_by_key(|e| &e.uid);
|
||||||
|
all_day_events.dedup_by_key(|e| &e.uid);
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("week-day-header", if is_today { Some("today") } else { None })}>
|
<div class={classes!("week-day-header", if is_today { Some("today") } else { None })}>
|
||||||
@@ -1193,6 +1202,11 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32
|
|||||||
|
|
||||||
// Check if two events overlap in time
|
// Check if two events overlap in time
|
||||||
fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
|
fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
|
||||||
|
// All-day events don't overlap with timed events for width calculation purposes
|
||||||
|
if event1.all_day || event2.all_day {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let start1 = event1.dtstart.with_timezone(&Local).naive_local();
|
let start1 = event1.dtstart.with_timezone(&Local).naive_local();
|
||||||
let end1 = if let Some(end) = event1.dtend {
|
let end1 = if let Some(end) = event1.dtend {
|
||||||
end.with_timezone(&Local).naive_local()
|
end.with_timezone(&Local).naive_local()
|
||||||
@@ -1214,10 +1228,15 @@ fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
|
|||||||
// Calculate layout columns for overlapping events
|
// Calculate layout columns for overlapping events
|
||||||
fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u32) -> Vec<(usize, usize)> {
|
fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u32) -> Vec<(usize, usize)> {
|
||||||
|
|
||||||
// Filter and sort events that should appear on this date
|
// Filter and sort events that should appear on this date (excluding all-day events)
|
||||||
let mut day_events: Vec<_> = events.iter()
|
let mut day_events: Vec<_> = events.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(idx, event)| {
|
.filter_map(|(idx, event)| {
|
||||||
|
// Skip all-day events as they don't participate in timed event overlap calculations
|
||||||
|
if event.all_day {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let (_, _, _) = calculate_event_position(event, date, time_increment);
|
let (_, _, _) = calculate_event_position(event, date, time_increment);
|
||||||
let local_start = event.dtstart.with_timezone(&Local);
|
let local_start = event.dtstart.with_timezone(&Local);
|
||||||
let event_date = local_start.date_naive();
|
let event_date = local_start.date_naive();
|
||||||
@@ -1298,3 +1317,19 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
|||||||
|
|
||||||
event_columns
|
event_columns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an all-day event spans the given date
|
||||||
|
fn event_spans_date(event: &VEvent, date: NaiveDate) -> bool {
|
||||||
|
let start_date = event.dtstart.with_timezone(&Local).date_naive();
|
||||||
|
let end_date = if let Some(dtend) = event.dtend {
|
||||||
|
// For all-day events, dtend is often set to the day after the last day
|
||||||
|
// So we need to subtract a day to get the actual last day of the event
|
||||||
|
dtend.with_timezone(&Local).date_naive() - chrono::Duration::days(1)
|
||||||
|
} else {
|
||||||
|
// Single day event
|
||||||
|
start_date
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the given date falls within the event's date range
|
||||||
|
date >= start_date && date <= end_date
|
||||||
|
}
|
||||||
|
|||||||
@@ -1249,6 +1249,8 @@ impl CalendarService {
|
|||||||
reminder: String,
|
reminder: String,
|
||||||
recurrence: String,
|
recurrence: String,
|
||||||
recurrence_days: Vec<bool>,
|
recurrence_days: Vec<bool>,
|
||||||
|
recurrence_count: Option<u32>,
|
||||||
|
recurrence_until: Option<String>,
|
||||||
calendar_path: Option<String>,
|
calendar_path: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let window = web_sys::window().ok_or("No global window exists")?;
|
let window = web_sys::window().ok_or("No global window exists")?;
|
||||||
@@ -1281,8 +1283,8 @@ impl CalendarService {
|
|||||||
"recurrence": recurrence,
|
"recurrence": recurrence,
|
||||||
"recurrence_days": recurrence_days,
|
"recurrence_days": recurrence_days,
|
||||||
"recurrence_interval": 1_u32, // Default interval
|
"recurrence_interval": 1_u32, // Default interval
|
||||||
"recurrence_end_date": None as Option<String>, // No end date by default
|
"recurrence_end_date": recurrence_until,
|
||||||
"recurrence_count": None as Option<u32>, // No count limit by default
|
"recurrence_count": recurrence_count,
|
||||||
"calendar_path": calendar_path
|
"calendar_path": calendar_path
|
||||||
});
|
});
|
||||||
let url = format!("{}/calendar/events/series/create", self.base_url);
|
let url = format!("{}/calendar/events/series/create", self.base_url);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub struct UserPreferences {
|
|||||||
pub calendar_view_mode: Option<String>,
|
pub calendar_view_mode: Option<String>,
|
||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
|
pub last_used_calendar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -22,6 +23,7 @@ pub struct UpdatePreferencesRequest {
|
|||||||
pub calendar_view_mode: Option<String>,
|
pub calendar_view_mode: Option<String>,
|
||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
|
pub last_used_calendar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -61,6 +63,7 @@ impl PreferencesService {
|
|||||||
calendar_view_mode: None,
|
calendar_view_mode: None,
|
||||||
calendar_theme: None,
|
calendar_theme: None,
|
||||||
calendar_colors: None,
|
calendar_colors: None,
|
||||||
|
last_used_calendar: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the specific field
|
// Update the specific field
|
||||||
@@ -95,6 +98,7 @@ impl PreferencesService {
|
|||||||
calendar_view_mode: preferences.calendar_view_mode.clone(),
|
calendar_view_mode: preferences.calendar_view_mode.clone(),
|
||||||
calendar_theme: preferences.calendar_theme.clone(),
|
calendar_theme: preferences.calendar_theme.clone(),
|
||||||
calendar_colors: preferences.calendar_colors.clone(),
|
calendar_colors: preferences.calendar_colors.clone(),
|
||||||
|
last_used_calendar: preferences.last_used_calendar.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sync_preferences(&session_token, &request).await
|
self.sync_preferences(&session_token, &request).await
|
||||||
@@ -156,6 +160,7 @@ impl PreferencesService {
|
|||||||
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),
|
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),
|
||||||
calendar_theme: LocalStorage::get::<String>("calendar_theme").ok(),
|
calendar_theme: LocalStorage::get::<String>("calendar_theme").ok(),
|
||||||
calendar_colors: LocalStorage::get::<String>("calendar_colors").ok(),
|
calendar_colors: LocalStorage::get::<String>("calendar_colors").ok(),
|
||||||
|
last_used_calendar: LocalStorage::get::<String>("last_used_calendar").ok(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only migrate if we have some preferences to migrate
|
// Only migrate if we have some preferences to migrate
|
||||||
@@ -164,6 +169,7 @@ impl PreferencesService {
|
|||||||
|| request.calendar_view_mode.is_some()
|
|| request.calendar_view_mode.is_some()
|
||||||
|| request.calendar_theme.is_some()
|
|| request.calendar_theme.is_some()
|
||||||
|| request.calendar_colors.is_some()
|
|| request.calendar_colors.is_some()
|
||||||
|
|| request.last_used_calendar.is_some()
|
||||||
{
|
{
|
||||||
self.sync_preferences(&session_token, &request).await?;
|
self.sync_preferences(&session_token, &request).await?;
|
||||||
|
|
||||||
@@ -177,4 +183,24 @@ impl PreferencesService {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the last used calendar and sync with backend
|
||||||
|
pub async fn update_last_used_calendar(&self, calendar_path: &str) -> Result<(), String> {
|
||||||
|
// Get session token
|
||||||
|
let session_token = LocalStorage::get::<String>("session_token")
|
||||||
|
.map_err(|_| "No session token found".to_string())?;
|
||||||
|
|
||||||
|
// Create minimal update request with only the last used calendar
|
||||||
|
let request = UpdatePreferencesRequest {
|
||||||
|
calendar_selected_date: None,
|
||||||
|
calendar_time_increment: None,
|
||||||
|
calendar_view_mode: None,
|
||||||
|
calendar_theme: None,
|
||||||
|
calendar_colors: None,
|
||||||
|
last_used_calendar: Some(calendar_path.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync to backend
|
||||||
|
self.sync_preferences(&session_token, &request).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user