feat: Migrate calendar functionality to a database
- Replaced Redis-based calendar with a database-backed solution - Implemented database models for calendars and events - Improved error handling and logging for database interactions - Added new database functions for calendar management - Updated calendar views to reflect the database changes - Enhanced event creation and deletion processes - Refined date/time handling for better consistency
This commit is contained in:
parent
d815d9d365
commit
58d1cde1ce
@ -1,12 +1,16 @@
|
|||||||
use actix_web::{web, HttpResponse, Responder, Result};
|
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
|
use actix_web::{HttpResponse, Responder, Result, web};
|
||||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tera::Tera;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
use crate::db::calendar::{
|
||||||
|
add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar,
|
||||||
|
};
|
||||||
use crate::models::{CalendarEvent, CalendarViewMode};
|
use crate::models::{CalendarEvent, CalendarViewMode};
|
||||||
use crate::utils::{RedisCalendarService, render_template};
|
use crate::utils::render_template;
|
||||||
|
use heromodels_core::Model;
|
||||||
|
|
||||||
/// Controller for handling calendar-related routes
|
/// Controller for handling calendar-related routes
|
||||||
pub struct CalendarController;
|
pub struct CalendarController;
|
||||||
@ -14,9 +18,11 @@ pub struct CalendarController;
|
|||||||
impl CalendarController {
|
impl CalendarController {
|
||||||
/// Helper function to get user from session
|
/// Helper function to get user from session
|
||||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
session
|
||||||
serde_json::from_str(&user_json).ok()
|
.get::<String>("user")
|
||||||
})
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|user_json| serde_json::from_str(&user_json).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the calendar page route
|
/// Handles the calendar page route
|
||||||
@ -29,13 +35,16 @@ impl CalendarController {
|
|||||||
ctx.insert("active_page", "calendar");
|
ctx.insert("active_page", "calendar");
|
||||||
|
|
||||||
// Parse the view mode from the query parameters
|
// Parse the view mode from the query parameters
|
||||||
let view_mode = CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
|
let view_mode =
|
||||||
|
CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
|
||||||
ctx.insert("view_mode", &view_mode.to_str());
|
ctx.insert("view_mode", &view_mode.to_str());
|
||||||
|
|
||||||
// Parse the date from the query parameters or use the current date
|
// Parse the date from the query parameters or use the current date
|
||||||
let date = if let Some(date_str) = &query.date {
|
let date = if let Some(date_str) = &query.date {
|
||||||
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
||||||
Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
|
Ok(naive_date) => Utc
|
||||||
|
.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap())
|
||||||
|
.into(),
|
||||||
Err(_) => Utc::now(),
|
Err(_) => Utc::now(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -47,44 +56,109 @@ impl CalendarController {
|
|||||||
ctx.insert("current_month", &date.month());
|
ctx.insert("current_month", &date.month());
|
||||||
ctx.insert("current_day", &date.day());
|
ctx.insert("current_day", &date.day());
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available and ensure user has a calendar
|
||||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
|
|
||||||
|
// Get or create user calendar
|
||||||
|
if let (Some(user_id), Some(user_name)) = (
|
||||||
|
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||||
|
user.get("full_name").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
match get_or_create_user_calendar(user_id, user_name) {
|
||||||
|
Ok(calendar) => {
|
||||||
|
log::info!(
|
||||||
|
"User calendar ready: ID {}, Name: '{}'",
|
||||||
|
calendar.get_id(),
|
||||||
|
calendar.name
|
||||||
|
);
|
||||||
|
ctx.insert("user_calendar", &calendar);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get or create user calendar: {}", e);
|
||||||
|
// Continue without calendar - the app should still work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get events for the current view
|
// Get events for the current view
|
||||||
let (start_date, end_date) = match view_mode {
|
let (start_date, end_date) = match view_mode {
|
||||||
CalendarViewMode::Year => {
|
CalendarViewMode::Year => {
|
||||||
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
|
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
|
||||||
let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap();
|
let end = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59)
|
||||||
|
.unwrap();
|
||||||
(start, end)
|
(start, end)
|
||||||
},
|
}
|
||||||
CalendarViewMode::Month => {
|
CalendarViewMode::Month => {
|
||||||
let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
|
let start = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
|
||||||
|
.unwrap();
|
||||||
let last_day = Self::last_day_of_month(date.year(), date.month());
|
let last_day = Self::last_day_of_month(date.year(), date.month());
|
||||||
let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap();
|
let end = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59)
|
||||||
|
.unwrap();
|
||||||
(start, end)
|
(start, end)
|
||||||
},
|
}
|
||||||
CalendarViewMode::Week => {
|
CalendarViewMode::Week => {
|
||||||
// Calculate the start of the week (Sunday)
|
// Calculate the start of the week (Sunday)
|
||||||
let _weekday = date.weekday().num_days_from_sunday();
|
let _weekday = date.weekday().num_days_from_sunday();
|
||||||
let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
|
let start_date = date
|
||||||
|
.date_naive()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap()
|
||||||
|
.pred_opt()
|
||||||
|
.unwrap();
|
||||||
let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
|
let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
|
||||||
let end = start + chrono::Duration::days(7);
|
let end = start + chrono::Duration::days(7);
|
||||||
(start, end)
|
(start, end)
|
||||||
},
|
}
|
||||||
CalendarViewMode::Day => {
|
CalendarViewMode::Day => {
|
||||||
let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap();
|
let start = Utc
|
||||||
let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap();
|
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
|
||||||
|
.unwrap();
|
||||||
|
let end = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59)
|
||||||
|
.unwrap();
|
||||||
(start, end)
|
(start, end)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get events from Redis
|
// Get events from database
|
||||||
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
|
let events = match get_events() {
|
||||||
Ok(events) => events,
|
Ok(db_events) => {
|
||||||
|
// Filter events for the date range and convert to CalendarEvent format
|
||||||
|
db_events
|
||||||
|
.into_iter()
|
||||||
|
.filter(|event| {
|
||||||
|
// Event overlaps with the date range
|
||||||
|
event.start_time < end_date && event.end_time > start_date
|
||||||
|
})
|
||||||
|
.map(|event| CalendarEvent {
|
||||||
|
id: event.get_id().to_string(),
|
||||||
|
title: event.title.clone(),
|
||||||
|
description: event.description.clone().unwrap_or_default(),
|
||||||
|
start_time: event.start_time,
|
||||||
|
end_time: event.end_time,
|
||||||
|
color: event.color.clone().unwrap_or_else(|| "#4285F4".to_string()),
|
||||||
|
all_day: event.all_day,
|
||||||
|
user_id: event.created_by.map(|id| id.to_string()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get events from Redis: {}", e);
|
log::error!("Failed to get events from database: {}", e);
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -94,7 +168,8 @@ impl CalendarController {
|
|||||||
// Generate calendar data based on the view mode
|
// Generate calendar data based on the view mode
|
||||||
match view_mode {
|
match view_mode {
|
||||||
CalendarViewMode::Year => {
|
CalendarViewMode::Year => {
|
||||||
let months = (1..=12).map(|month| {
|
let months = (1..=12)
|
||||||
|
.map(|month| {
|
||||||
let month_name = match month {
|
let month_name = match month {
|
||||||
1 => "January",
|
1 => "January",
|
||||||
2 => "February",
|
2 => "February",
|
||||||
@ -111,7 +186,8 @@ impl CalendarController {
|
|||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let month_events = events.iter()
|
let month_events = events
|
||||||
|
.iter()
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
event.start_time.month() == month || event.end_time.month() == month
|
event.start_time.month() == month || event.end_time.month() == month
|
||||||
})
|
})
|
||||||
@ -123,13 +199,16 @@ impl CalendarController {
|
|||||||
name: month_name.to_string(),
|
name: month_name.to_string(),
|
||||||
events: month_events,
|
events: month_events,
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>();
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
ctx.insert("months", &months);
|
ctx.insert("months", &months);
|
||||||
},
|
}
|
||||||
CalendarViewMode::Month => {
|
CalendarViewMode::Month => {
|
||||||
let days_in_month = Self::last_day_of_month(date.year(), date.month());
|
let days_in_month = Self::last_day_of_month(date.year(), date.month());
|
||||||
let first_day = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
|
let first_day = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
|
||||||
|
.unwrap();
|
||||||
let first_weekday = first_day.weekday().num_days_from_sunday();
|
let first_weekday = first_day.weekday().num_days_from_sunday();
|
||||||
|
|
||||||
let mut calendar_days = Vec::new();
|
let mut calendar_days = Vec::new();
|
||||||
@ -145,13 +224,20 @@ impl CalendarController {
|
|||||||
|
|
||||||
// Add days for the current month
|
// Add days for the current month
|
||||||
for day in 1..=days_in_month {
|
for day in 1..=days_in_month {
|
||||||
let day_events = events.iter()
|
let day_events = events
|
||||||
|
.iter()
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap();
|
let day_start = Utc
|
||||||
let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap();
|
.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0)
|
||||||
|
.unwrap();
|
||||||
|
let day_end = Utc
|
||||||
|
.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
(event.start_time <= day_end && event.end_time >= day_start) ||
|
(event.start_time <= day_end && event.end_time >= day_start)
|
||||||
(event.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
|
|| (event.all_day
|
||||||
|
&& event.start_time.day() <= day
|
||||||
|
&& event.end_time.day() >= day)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -175,7 +261,7 @@ impl CalendarController {
|
|||||||
|
|
||||||
ctx.insert("calendar_days", &calendar_days);
|
ctx.insert("calendar_days", &calendar_days);
|
||||||
ctx.insert("month_name", &Self::month_name(date.month()));
|
ctx.insert("month_name", &Self::month_name(date.month()));
|
||||||
},
|
}
|
||||||
CalendarViewMode::Week => {
|
CalendarViewMode::Week => {
|
||||||
// Calculate the start of the week (Sunday)
|
// Calculate the start of the week (Sunday)
|
||||||
let weekday = date.weekday().num_days_from_sunday();
|
let weekday = date.weekday().num_days_from_sunday();
|
||||||
@ -184,13 +270,34 @@ impl CalendarController {
|
|||||||
let mut week_days = Vec::new();
|
let mut week_days = Vec::new();
|
||||||
for i in 0..7 {
|
for i in 0..7 {
|
||||||
let day_date = week_start + chrono::Duration::days(i);
|
let day_date = week_start + chrono::Duration::days(i);
|
||||||
let day_events = events.iter()
|
let day_events = events
|
||||||
|
.iter()
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
let day_start = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 0, 0, 0).unwrap();
|
let day_start = Utc
|
||||||
let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap();
|
.with_ymd_and_hms(
|
||||||
|
day_date.year(),
|
||||||
|
day_date.month(),
|
||||||
|
day_date.day(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let day_end = Utc
|
||||||
|
.with_ymd_and_hms(
|
||||||
|
day_date.year(),
|
||||||
|
day_date.month(),
|
||||||
|
day_date.day(),
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
(event.start_time <= day_end && event.end_time >= day_start) ||
|
(event.start_time <= day_end && event.end_time >= day_start)
|
||||||
(event.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day())
|
|| (event.all_day
|
||||||
|
&& event.start_time.day() <= day_date.day()
|
||||||
|
&& event.end_time.day() >= day_date.day())
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -203,16 +310,22 @@ impl CalendarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.insert("week_days", &week_days);
|
ctx.insert("week_days", &week_days);
|
||||||
},
|
}
|
||||||
CalendarViewMode::Day => {
|
CalendarViewMode::Day => {
|
||||||
log::info!("Day view selected");
|
log::info!("Day view selected");
|
||||||
ctx.insert("day_name", &Self::day_name(date.weekday().num_days_from_sunday()));
|
ctx.insert(
|
||||||
|
"day_name",
|
||||||
|
&Self::day_name(date.weekday().num_days_from_sunday()),
|
||||||
|
);
|
||||||
|
|
||||||
// Add debug info
|
// Add debug info
|
||||||
log::info!("Events count: {}", events.len());
|
log::info!("Events count: {}", events.len());
|
||||||
log::info!("Current date: {}", date.format("%Y-%m-%d"));
|
log::info!("Current date: {}", date.format("%Y-%m-%d"));
|
||||||
log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday()));
|
log::info!(
|
||||||
},
|
"Day name: {}",
|
||||||
|
Self::day_name(date.weekday().num_days_from_sunday())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_template(&tmpl, "calendar/index.html", &ctx)
|
render_template(&tmpl, "calendar/index.html", &ctx)
|
||||||
@ -223,9 +336,24 @@ impl CalendarController {
|
|||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "calendar");
|
ctx.insert("active_page", "calendar");
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available and ensure user has a calendar
|
||||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
|
|
||||||
|
// Get or create user calendar
|
||||||
|
if let (Some(user_id), Some(user_name)) = (
|
||||||
|
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||||
|
user.get("full_name").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
match get_or_create_user_calendar(user_id, user_name) {
|
||||||
|
Ok(calendar) => {
|
||||||
|
ctx.insert("user_calendar", &calendar);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get or create user calendar: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_template(&tmpl, "calendar/new_event.html", &ctx)
|
render_template(&tmpl, "calendar/new_event.html", &ctx)
|
||||||
@ -237,44 +365,91 @@ impl CalendarController {
|
|||||||
tmpl: web::Data<Tera>,
|
tmpl: web::Data<Tera>,
|
||||||
_session: Session,
|
_session: Session,
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
|
// Log the form data for debugging
|
||||||
|
log::info!(
|
||||||
|
"Creating event with form data: title='{}', start_time='{}', end_time='{}', all_day={}",
|
||||||
|
form.title,
|
||||||
|
form.start_time,
|
||||||
|
form.end_time,
|
||||||
|
form.all_day
|
||||||
|
);
|
||||||
|
|
||||||
// Parse the start and end times
|
// Parse the start and end times
|
||||||
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
|
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
|
||||||
Ok(dt) => dt.with_timezone(&Utc),
|
Ok(dt) => dt.with_timezone(&Utc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to parse start time: {}", e);
|
log::error!("Failed to parse start time '{}': {}", form.start_time, e);
|
||||||
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
|
return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
|
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
|
||||||
Ok(dt) => dt.with_timezone(&Utc),
|
Ok(dt) => dt.with_timezone(&Utc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to parse end time: {}", e);
|
log::error!("Failed to parse end time '{}': {}", form.end_time, e);
|
||||||
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
|
return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the event
|
// Get user information from session
|
||||||
let event = CalendarEvent::new(
|
let user_info = Self::get_user_from_session(&_session);
|
||||||
form.title.clone(),
|
let (user_id, user_name) = if let Some(user) = &user_info {
|
||||||
form.description.clone(),
|
let id = user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32);
|
||||||
|
let name = user
|
||||||
|
.get("full_name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Unknown User");
|
||||||
|
log::info!("User from session: id={:?}, name='{}'", id, name);
|
||||||
|
(id, name)
|
||||||
|
} else {
|
||||||
|
log::warn!("No user found in session");
|
||||||
|
(None, "Unknown User")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the event in the database
|
||||||
|
match create_new_event(
|
||||||
|
&form.title,
|
||||||
|
Some(&form.description),
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
Some(form.color.clone()),
|
None, // location
|
||||||
|
Some(&form.color),
|
||||||
form.all_day,
|
form.all_day,
|
||||||
None, // User ID would come from session in a real app
|
user_id,
|
||||||
);
|
None, // category
|
||||||
|
None, // reminder_minutes
|
||||||
|
) {
|
||||||
|
Ok((event_id, _saved_event)) => {
|
||||||
|
log::info!("Created event with ID: {}", event_id);
|
||||||
|
|
||||||
// Save the event to Redis
|
// If user is logged in, add the event to their calendar
|
||||||
match RedisCalendarService::save_event(&event) {
|
if let Some(user_id) = user_id {
|
||||||
|
match get_or_create_user_calendar(user_id, user_name) {
|
||||||
|
Ok(calendar) => match add_event_to_calendar(calendar.get_id(), event_id) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
log::info!(
|
||||||
|
"Added event {} to calendar {}",
|
||||||
|
event_id,
|
||||||
|
calendar.get_id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to add event to calendar: {}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get user calendar: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to the calendar page
|
// Redirect to the calendar page
|
||||||
Ok(HttpResponse::SeeOther()
|
Ok(HttpResponse::SeeOther()
|
||||||
.append_header(("Location", "/calendar"))
|
.append_header(("Location", "/calendar"))
|
||||||
.finish())
|
.finish())
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to save event to Redis: {}", e);
|
log::error!("Failed to save event to database: {}", e);
|
||||||
|
|
||||||
// Show an error message
|
// Show an error message
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
@ -282,13 +457,15 @@ impl CalendarController {
|
|||||||
ctx.insert("error", "Failed to save event");
|
ctx.insert("error", "Failed to save event");
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available
|
||||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
if let Some(user) = user_info {
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
|
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
|
||||||
|
|
||||||
Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
|
Ok(HttpResponse::InternalServerError()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(result.into_body()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -300,16 +477,26 @@ impl CalendarController {
|
|||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
// Delete the event from Redis
|
// Parse the event ID
|
||||||
match RedisCalendarService::delete_event(&id) {
|
let event_id = match id.parse::<u32>() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("Invalid event ID: {}", id);
|
||||||
|
return Ok(HttpResponse::BadRequest().body("Invalid event ID"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete the event from database
|
||||||
|
match delete_event(event_id) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
log::info!("Deleted event with ID: {}", event_id);
|
||||||
// Redirect to the calendar page
|
// Redirect to the calendar page
|
||||||
Ok(HttpResponse::SeeOther()
|
Ok(HttpResponse::SeeOther()
|
||||||
.append_header(("Location", "/calendar"))
|
.append_header(("Location", "/calendar"))
|
||||||
.finish())
|
.finish())
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to delete event from Redis: {}", e);
|
log::error!("Failed to delete event from database: {}", e);
|
||||||
Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
|
Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -326,7 +513,7 @@ impl CalendarController {
|
|||||||
} else {
|
} else {
|
||||||
28
|
28
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => 30, // Default to 30 days
|
_ => 30, // Default to 30 days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,360 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use heromodels::{
|
||||||
|
db::{Collection, Db},
|
||||||
|
models::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::db::get_db;
|
||||||
|
|
||||||
|
/// Creates a new calendar and saves it to the database. Returns the saved calendar and its ID.
|
||||||
|
pub fn create_new_calendar(
|
||||||
|
name: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
owner_id: Option<u32>,
|
||||||
|
is_public: bool,
|
||||||
|
color: Option<&str>,
|
||||||
|
) -> Result<(u32, Calendar), String> {
|
||||||
|
let db = get_db().expect("Can get DB");
|
||||||
|
|
||||||
|
// Create a new calendar (with auto-generated ID)
|
||||||
|
let mut calendar = Calendar::new(None, name);
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
calendar = calendar.description(desc);
|
||||||
|
}
|
||||||
|
if let Some(owner) = owner_id {
|
||||||
|
calendar = calendar.owner_id(owner);
|
||||||
|
}
|
||||||
|
if let Some(col) = color {
|
||||||
|
calendar = calendar.color(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar = calendar.is_public(is_public);
|
||||||
|
|
||||||
|
// Save the calendar to the database
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.expect("can open calendar collection");
|
||||||
|
let (calendar_id, saved_calendar) = collection.set(&calendar).expect("can save calendar");
|
||||||
|
|
||||||
|
Ok((calendar_id, saved_calendar))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new event and saves it to the database. Returns the saved event and its ID.
|
||||||
|
pub fn create_new_event(
|
||||||
|
title: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
start_time: DateTime<Utc>,
|
||||||
|
end_time: DateTime<Utc>,
|
||||||
|
location: Option<&str>,
|
||||||
|
color: Option<&str>,
|
||||||
|
all_day: bool,
|
||||||
|
created_by: Option<u32>,
|
||||||
|
category: Option<&str>,
|
||||||
|
reminder_minutes: Option<i32>,
|
||||||
|
) -> Result<(u32, Event), String> {
|
||||||
|
let db = get_db().expect("Can get DB");
|
||||||
|
|
||||||
|
// Create a new event (with auto-generated ID)
|
||||||
|
let mut event = Event::new(title, start_time, end_time);
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
event = event.description(desc);
|
||||||
|
}
|
||||||
|
if let Some(loc) = location {
|
||||||
|
event = event.location(loc);
|
||||||
|
}
|
||||||
|
if let Some(col) = color {
|
||||||
|
event = event.color(col);
|
||||||
|
}
|
||||||
|
if let Some(user_id) = created_by {
|
||||||
|
event = event.created_by(user_id);
|
||||||
|
}
|
||||||
|
if let Some(cat) = category {
|
||||||
|
event = event.category(cat);
|
||||||
|
}
|
||||||
|
if let Some(reminder) = reminder_minutes {
|
||||||
|
event = event.reminder_minutes(reminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
event = event.all_day(all_day);
|
||||||
|
|
||||||
|
// Save the event to the database
|
||||||
|
let collection = db.collection::<Event>().expect("can open event collection");
|
||||||
|
let (event_id, saved_event) = collection.set(&event).expect("can save event");
|
||||||
|
|
||||||
|
Ok((event_id, saved_event))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all calendars from the database and returns them as a Vec<Calendar>.
|
||||||
|
pub fn get_calendars() -> Result<Vec<Calendar>, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.expect("can open calendar collection");
|
||||||
|
|
||||||
|
// Try to load all calendars, but handle deserialization errors gracefully
|
||||||
|
let calendars = match collection.get_all() {
|
||||||
|
Ok(calendars) => calendars,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading calendars: {:?}", e);
|
||||||
|
vec![] // Return an empty vector if there's an error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(calendars)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all events from the database and returns them as a Vec<Event>.
|
||||||
|
pub fn get_events() -> Result<Vec<Event>, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db.collection::<Event>().expect("can open event collection");
|
||||||
|
|
||||||
|
// Try to load all events, but handle deserialization errors gracefully
|
||||||
|
let events = match collection.get_all() {
|
||||||
|
Ok(events) => events,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading events: {:?}", e);
|
||||||
|
vec![] // Return an empty vector if there's an error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a single calendar by its ID from the database.
|
||||||
|
pub fn get_calendar_by_id(calendar_id: u32) -> Result<Option<Calendar>, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
match collection.get_by_id(calendar_id) {
|
||||||
|
Ok(calendar) => Ok(calendar),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error fetching calendar by id {}: {:?}", calendar_id, e);
|
||||||
|
Err(format!("Failed to fetch calendar: {:?}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a single event by its ID from the database.
|
||||||
|
pub fn get_event_by_id(event_id: u32) -> Result<Option<Event>, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Event>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
match collection.get_by_id(event_id) {
|
||||||
|
Ok(event) => Ok(event),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error fetching event by id {}: {:?}", event_id, e);
|
||||||
|
Err(format!("Failed to fetch event: {:?}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new attendee and saves it to the database. Returns the saved attendee and its ID.
|
||||||
|
pub fn create_new_attendee(
|
||||||
|
contact_id: u32,
|
||||||
|
status: AttendanceStatus,
|
||||||
|
) -> Result<(u32, Attendee), String> {
|
||||||
|
let db = get_db().expect("Can get DB");
|
||||||
|
|
||||||
|
// Create a new attendee (with auto-generated ID)
|
||||||
|
let attendee = Attendee::new(contact_id).status(status);
|
||||||
|
|
||||||
|
// Save the attendee to the database
|
||||||
|
let collection = db
|
||||||
|
.collection::<Attendee>()
|
||||||
|
.expect("can open attendee collection");
|
||||||
|
let (attendee_id, saved_attendee) = collection.set(&attendee).expect("can save attendee");
|
||||||
|
|
||||||
|
Ok((attendee_id, saved_attendee))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a single attendee by its ID from the database.
|
||||||
|
pub fn get_attendee_by_id(attendee_id: u32) -> Result<Option<Attendee>, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Attendee>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
match collection.get_by_id(attendee_id) {
|
||||||
|
Ok(attendee) => Ok(attendee),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error fetching attendee by id {}: {:?}", attendee_id, e);
|
||||||
|
Err(format!("Failed to fetch attendee: {:?}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates attendee status in the database and returns the updated attendee.
|
||||||
|
pub fn update_attendee_status(
|
||||||
|
attendee_id: u32,
|
||||||
|
status: AttendanceStatus,
|
||||||
|
) -> Result<Attendee, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Attendee>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(mut attendee) = collection
|
||||||
|
.get_by_id(attendee_id)
|
||||||
|
.map_err(|e| format!("Failed to fetch attendee: {:?}", e))?
|
||||||
|
{
|
||||||
|
attendee = attendee.status(status);
|
||||||
|
let (_, updated_attendee) = collection
|
||||||
|
.set(&attendee)
|
||||||
|
.map_err(|e| format!("Failed to update attendee: {:?}", e))?;
|
||||||
|
Ok(updated_attendee)
|
||||||
|
} else {
|
||||||
|
Err("Attendee not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add attendee to event
|
||||||
|
pub fn add_attendee_to_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Event>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(mut event) = collection
|
||||||
|
.get_by_id(event_id)
|
||||||
|
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
|
||||||
|
{
|
||||||
|
event = event.add_attendee(attendee_id);
|
||||||
|
let (_, updated_event) = collection
|
||||||
|
.set(&event)
|
||||||
|
.map_err(|e| format!("Failed to update event: {:?}", e))?;
|
||||||
|
Ok(updated_event)
|
||||||
|
} else {
|
||||||
|
Err("Event not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove attendee from event
|
||||||
|
pub fn remove_attendee_from_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Event>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(mut event) = collection
|
||||||
|
.get_by_id(event_id)
|
||||||
|
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
|
||||||
|
{
|
||||||
|
event = event.remove_attendee(attendee_id);
|
||||||
|
let (_, updated_event) = collection
|
||||||
|
.set(&event)
|
||||||
|
.map_err(|e| format!("Failed to update event: {:?}", e))?;
|
||||||
|
Ok(updated_event)
|
||||||
|
} else {
|
||||||
|
Err("Event not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add event to calendar
|
||||||
|
pub fn add_event_to_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(mut calendar) = collection
|
||||||
|
.get_by_id(calendar_id)
|
||||||
|
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
|
||||||
|
{
|
||||||
|
calendar = calendar.add_event(event_id as i64);
|
||||||
|
let (_, updated_calendar) = collection
|
||||||
|
.set(&calendar)
|
||||||
|
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
|
||||||
|
Ok(updated_calendar)
|
||||||
|
} else {
|
||||||
|
Err("Calendar not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove event from calendar
|
||||||
|
pub fn remove_event_from_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(mut calendar) = collection
|
||||||
|
.get_by_id(calendar_id)
|
||||||
|
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
|
||||||
|
{
|
||||||
|
calendar = calendar.remove_event(event_id as i64);
|
||||||
|
let (_, updated_calendar) = collection
|
||||||
|
.set(&calendar)
|
||||||
|
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
|
||||||
|
Ok(updated_calendar)
|
||||||
|
} else {
|
||||||
|
Err("Calendar not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a calendar from the database.
|
||||||
|
pub fn delete_calendar(calendar_id: u32) -> Result<(), String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
collection
|
||||||
|
.delete_by_id(calendar_id)
|
||||||
|
.map_err(|e| format!("Failed to delete calendar: {:?}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes an event from the database.
|
||||||
|
pub fn delete_event(event_id: u32) -> Result<(), String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Event>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
collection
|
||||||
|
.delete_by_id(event_id)
|
||||||
|
.map_err(|e| format!("Failed to delete event: {:?}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets or creates a calendar for a user. If the user already has a calendar, returns it.
|
||||||
|
/// If not, creates a new calendar for the user and returns it.
|
||||||
|
pub fn get_or_create_user_calendar(user_id: u32, user_name: &str) -> Result<Calendar, String> {
|
||||||
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Calendar>()
|
||||||
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||||
|
|
||||||
|
// Try to find existing calendar for this user
|
||||||
|
let calendars = match collection.get_all() {
|
||||||
|
Ok(calendars) => calendars,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading calendars: {:?}", e);
|
||||||
|
vec![] // Return an empty vector if there's an error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look for a calendar owned by this user
|
||||||
|
for calendar in calendars {
|
||||||
|
if let Some(owner_id) = calendar.owner_id {
|
||||||
|
if owner_id == user_id {
|
||||||
|
return Ok(calendar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No calendar found for this user, create a new one
|
||||||
|
let calendar_name = format!("{}'s Calendar", user_name);
|
||||||
|
let (_, new_calendar) = create_new_calendar(
|
||||||
|
&calendar_name,
|
||||||
|
Some("Personal calendar"),
|
||||||
|
Some(user_id),
|
||||||
|
false, // Private calendar
|
||||||
|
Some("#4285F4"), // Default blue color
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(new_calendar)
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
pub mod calendar;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod governance;
|
pub mod governance;
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
<p class="text-muted mb-0">Manage your events and schedule</p>
|
<p class="text-muted mb-0">Manage your events and schedule</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
|
<button type="button" class="btn btn-primary" onclick="openEventModal()">
|
||||||
data-bs-target="#newEventModal">
|
|
||||||
<i class="bi bi-plus-circle"></i> Create Event
|
<i class="bi bi-plus-circle"></i> Create Event
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -245,6 +244,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<form id="newEventForm" action="/calendar/events" method="post">
|
<form id="newEventForm" action="/calendar/events" method="post">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<!-- Date locked info (hidden by default) -->
|
||||||
|
<div id="dateLockInfo" class="alert alert-info" style="display: none;">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<strong>Date Selected:</strong> <span id="selectedDateDisplay"></span>
|
||||||
|
<br>
|
||||||
|
<small>The date is pre-selected. You can only modify the time.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -308,7 +315,7 @@
|
|||||||
|
|
||||||
<!-- Floating Action Button (FAB) for mobile -->
|
<!-- Floating Action Button (FAB) for mobile -->
|
||||||
<button type="button" class="d-md-none position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow"
|
<button type="button" class="d-md-none position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow"
|
||||||
data-bs-toggle="modal" data-bs-target="#newEventModal"
|
onclick="openEventModal()"
|
||||||
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
|
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
|
||||||
<i class="bi bi-plus"></i>
|
<i class="bi bi-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -429,20 +436,42 @@
|
|||||||
timeInputs.style.display = 'none';
|
timeInputs.style.display = 'none';
|
||||||
startTime.removeAttribute('required');
|
startTime.removeAttribute('required');
|
||||||
endTime.removeAttribute('required');
|
endTime.removeAttribute('required');
|
||||||
|
// Clear the values to prevent validation issues
|
||||||
|
startTime.value = '';
|
||||||
|
endTime.value = '';
|
||||||
} else {
|
} else {
|
||||||
timeInputs.style.display = 'block';
|
timeInputs.style.display = 'block';
|
||||||
startTime.setAttribute('required', '');
|
startTime.setAttribute('required', '');
|
||||||
endTime.setAttribute('required', '');
|
endTime.setAttribute('required', '');
|
||||||
|
// Set default times if empty
|
||||||
|
if (!startTime.value || !endTime.value) {
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||||
|
startTime.value = now.toISOString().slice(0, 16);
|
||||||
|
endTime.value = oneHourLater.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission (ensure only one listener)
|
||||||
document.getElementById('newEventForm').addEventListener('submit', function (e) {
|
const eventForm = document.getElementById('newEventForm');
|
||||||
|
if (!eventForm.hasAttribute('data-listener-added')) {
|
||||||
|
eventForm.setAttribute('data-listener-added', 'true');
|
||||||
|
eventForm.addEventListener('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Prevent double submission
|
||||||
|
if (this.hasAttribute('data-submitting')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setAttribute('data-submitting', 'true');
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
const allDay = document.getElementById('allDayEvent').checked;
|
const allDay = document.getElementById('allDayEvent').checked;
|
||||||
|
|
||||||
|
// Ensure all_day field is always present (checkboxes don't send false values)
|
||||||
|
formData.set('all_day', allDay ? 'true' : 'false');
|
||||||
|
|
||||||
if (!allDay) {
|
if (!allDay) {
|
||||||
// Convert datetime-local to RFC3339 format
|
// Convert datetime-local to RFC3339 format
|
||||||
const startTime = document.getElementById('startTime').value;
|
const startTime = document.getElementById('startTime').value;
|
||||||
@ -456,29 +485,76 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For all-day events, set times to start and end of day
|
// For all-day events, set times to start and end of day
|
||||||
|
let selectedDate;
|
||||||
|
|
||||||
|
// Check if this is a date-specific event (from calendar click)
|
||||||
|
const modal = document.getElementById('newEventModal');
|
||||||
|
const selectedDateStr = modal.getAttribute('data-selected-date');
|
||||||
|
|
||||||
|
if (selectedDateStr) {
|
||||||
|
// Parse the date string and create in local timezone to preserve the selected date
|
||||||
|
// selectedDateStr is in format "YYYY-MM-DD"
|
||||||
|
const dateParts = selectedDateStr.split('-');
|
||||||
|
const year = parseInt(dateParts[0]);
|
||||||
|
const month = parseInt(dateParts[1]) - 1; // Month is 0-based
|
||||||
|
const day = parseInt(dateParts[2]);
|
||||||
|
|
||||||
|
// Create dates in local timezone at noon to avoid any date boundary issues
|
||||||
|
// This ensures the date stays consistent regardless of timezone when converted to UTC
|
||||||
|
const startOfDay = new Date(year, month, day, 12, 0, 0); // Noon local time
|
||||||
|
const endOfDay = new Date(year, month, day, 12, 0, 1); // Noon + 1 second local time
|
||||||
|
|
||||||
|
formData.set('start_time', startOfDay.toISOString());
|
||||||
|
formData.set('end_time', endOfDay.toISOString());
|
||||||
|
} else {
|
||||||
|
// Use today's date for general "Create Event" button
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
||||||
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);
|
// Create dates in local timezone at noon to avoid date boundary issues
|
||||||
|
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 0, 0);
|
||||||
|
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 0, 1);
|
||||||
|
|
||||||
formData.set('start_time', startOfDay.toISOString());
|
formData.set('start_time', startOfDay.toISOString());
|
||||||
formData.set('end_time', endOfDay.toISOString());
|
formData.set('end_time', endOfDay.toISOString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Submit the form
|
// Debug: Log form data
|
||||||
|
console.log('Submitting form data:');
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit the form with correct content type
|
||||||
fetch(this.action, {
|
fetch(this.action, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(formData)
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response ok:', response.ok);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert('Error creating event. Please try again.');
|
// Reset submitting flag on error
|
||||||
|
eventForm.removeAttribute('data-submitting');
|
||||||
|
// Get the response text to see the actual error
|
||||||
|
return response.text().then(text => {
|
||||||
|
console.error('Server response:', text);
|
||||||
|
alert(`Error creating event (${response.status}): ${text}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error:', error);
|
// Reset submitting flag on error
|
||||||
alert('Error creating event. Please try again.');
|
eventForm.removeAttribute('data-submitting');
|
||||||
|
console.error('Network error:', error);
|
||||||
|
alert('Network error creating event. Please try again.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Delete event function
|
// Delete event function
|
||||||
function deleteEvent(eventId) {
|
function deleteEvent(eventId) {
|
||||||
@ -543,8 +619,19 @@
|
|||||||
endTime.setHours(10, 0, 0, 0);
|
endTime.setHours(10, 0, 0, 0);
|
||||||
|
|
||||||
// Update the modal form with the selected date
|
// Update the modal form with the selected date
|
||||||
document.getElementById('startTime').value = startTime.toISOString().slice(0, 16);
|
const startTimeInput = document.getElementById('startTime');
|
||||||
document.getElementById('endTime').value = endTime.toISOString().slice(0, 16);
|
const endTimeInput = document.getElementById('endTime');
|
||||||
|
|
||||||
|
startTimeInput.value = startTime.toISOString().slice(0, 16);
|
||||||
|
endTimeInput.value = endTime.toISOString().slice(0, 16);
|
||||||
|
|
||||||
|
// Restrict date changes - set min and max to the selected date
|
||||||
|
const minDate = selectedDate.toISOString().split('T')[0] + 'T00:00';
|
||||||
|
const maxDate = selectedDate.toISOString().split('T')[0] + 'T23:59';
|
||||||
|
startTimeInput.min = minDate;
|
||||||
|
startTimeInput.max = maxDate;
|
||||||
|
endTimeInput.min = minDate;
|
||||||
|
endTimeInput.max = maxDate;
|
||||||
|
|
||||||
// Update modal title to show the selected date
|
// Update modal title to show the selected date
|
||||||
const modalTitle = document.getElementById('newEventModalLabel');
|
const modalTitle = document.getElementById('newEventModalLabel');
|
||||||
@ -556,12 +643,82 @@
|
|||||||
});
|
});
|
||||||
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create Event for ${dateStr}`;
|
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create Event for ${dateStr}`;
|
||||||
|
|
||||||
|
// Show date lock info
|
||||||
|
document.getElementById('dateLockInfo').style.display = 'block';
|
||||||
|
document.getElementById('selectedDateDisplay').textContent = dateStr;
|
||||||
|
|
||||||
|
// Add smart time validation for date-locked events
|
||||||
|
startTimeInput.addEventListener('change', function () {
|
||||||
|
const startTime = new Date(this.value);
|
||||||
|
const endTime = new Date(endTimeInput.value);
|
||||||
|
|
||||||
|
if (endTime <= startTime) {
|
||||||
|
// Set end time to 1 hour after start time
|
||||||
|
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||||
|
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update end time minimum to be after start time
|
||||||
|
endTimeInput.min = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
endTimeInput.addEventListener('change', function () {
|
||||||
|
const startTime = new Date(startTimeInput.value);
|
||||||
|
const endTime = new Date(this.value);
|
||||||
|
|
||||||
|
if (endTime <= startTime) {
|
||||||
|
// Reset to 1 hour after start time
|
||||||
|
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||||
|
this.value = newEndTime.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a flag to indicate this is a date-specific event
|
||||||
|
document.getElementById('newEventModal').setAttribute('data-date-locked', 'true');
|
||||||
|
document.getElementById('newEventModal').setAttribute('data-selected-date', selectedDate.toISOString().split('T')[0]);
|
||||||
|
|
||||||
// Show the modal
|
// Show the modal
|
||||||
const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
|
const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open event modal for general event creation (not date-specific)
|
||||||
|
function openEventModal() {
|
||||||
|
// Reset modal to allow full date/time selection
|
||||||
|
const modal = document.getElementById('newEventModal');
|
||||||
|
const startTimeInput = document.getElementById('startTime');
|
||||||
|
const endTimeInput = document.getElementById('endTime');
|
||||||
|
|
||||||
|
// Remove date restrictions
|
||||||
|
startTimeInput.removeAttribute('min');
|
||||||
|
startTimeInput.removeAttribute('max');
|
||||||
|
endTimeInput.removeAttribute('min');
|
||||||
|
endTimeInput.removeAttribute('max');
|
||||||
|
|
||||||
|
// Set default times to current time + 1 hour
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||||
|
|
||||||
|
startTimeInput.value = now.toISOString().slice(0, 16);
|
||||||
|
endTimeInput.value = oneHourLater.toISOString().slice(0, 16);
|
||||||
|
|
||||||
|
// Reset modal title
|
||||||
|
const modalTitle = document.getElementById('newEventModalLabel');
|
||||||
|
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create New Event`;
|
||||||
|
|
||||||
|
// Hide date lock info
|
||||||
|
document.getElementById('dateLockInfo').style.display = 'none';
|
||||||
|
|
||||||
|
// Remove date-locked flag
|
||||||
|
modal.removeAttribute('data-date-locked');
|
||||||
|
modal.removeAttribute('data-selected-date');
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
const bootstrapModal = new bootstrap.Modal(modal);
|
||||||
|
bootstrapModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize calendar features
|
// Initialize calendar features
|
||||||
function initializeCalendar() {
|
function initializeCalendar() {
|
||||||
// Highlight today's date
|
// Highlight today's date
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form action="/calendar/new" method="post">
|
<form action="/calendar/events" method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="title" class="form-label">Event Title</label>
|
<label for="title" class="form-label">Event Title</label>
|
||||||
<input type="text" class="form-control" id="title" name="title" required>
|
<input type="text" class="form-control" id="title" name="title" required>
|
||||||
@ -39,6 +39,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Show selected date info when coming from calendar date click -->
|
||||||
|
<div id="selected-date-info" class="alert alert-info" style="display: none;">
|
||||||
|
<strong>Selected Date:</strong> <span id="selected-date-display"></span>
|
||||||
|
<br>
|
||||||
|
<small>The date is pre-selected. You can only modify the time portion.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="color" class="form-label">Event Color</label>
|
<label for="color" class="form-label">Event Color</label>
|
||||||
<select class="form-control" id="color" name="color">
|
<select class="form-control" id="color" name="color">
|
||||||
@ -59,14 +66,83 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Check if we came from a date click (URL parameter)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const selectedDate = urlParams.get('date');
|
||||||
|
|
||||||
|
if (selectedDate) {
|
||||||
|
// Show the selected date info
|
||||||
|
document.getElementById('selected-date-info').style.display = 'block';
|
||||||
|
document.getElementById('selected-date-display').textContent = new Date(selectedDate).toLocaleDateString();
|
||||||
|
|
||||||
|
// Pre-fill the date portion and restrict date changes
|
||||||
|
const startTimeInput = document.getElementById('start_time');
|
||||||
|
const endTimeInput = document.getElementById('end_time');
|
||||||
|
|
||||||
|
// Set default times (9 AM to 10 AM on the selected date)
|
||||||
|
const startDateTime = new Date(selectedDate + 'T09:00');
|
||||||
|
const endDateTime = new Date(selectedDate + 'T10:00');
|
||||||
|
|
||||||
|
// Format for datetime-local input (YYYY-MM-DDTHH:MM)
|
||||||
|
startTimeInput.value = startDateTime.toISOString().slice(0, 16);
|
||||||
|
endTimeInput.value = endDateTime.toISOString().slice(0, 16);
|
||||||
|
|
||||||
|
// Set minimum and maximum date to the selected date to prevent changing the date
|
||||||
|
const minDate = selectedDate + 'T00:00';
|
||||||
|
const maxDate = selectedDate + 'T23:59';
|
||||||
|
startTimeInput.min = minDate;
|
||||||
|
startTimeInput.max = maxDate;
|
||||||
|
endTimeInput.min = minDate;
|
||||||
|
endTimeInput.max = maxDate;
|
||||||
|
|
||||||
|
// Add event listeners to ensure end time is after start time
|
||||||
|
startTimeInput.addEventListener('change', function () {
|
||||||
|
const startTime = new Date(this.value);
|
||||||
|
const endTime = new Date(endTimeInput.value);
|
||||||
|
|
||||||
|
if (endTime <= startTime) {
|
||||||
|
// Set end time to 1 hour after start time
|
||||||
|
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||||
|
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update end time minimum to be after start time
|
||||||
|
endTimeInput.min = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
endTimeInput.addEventListener('change', function () {
|
||||||
|
const startTime = new Date(startTimeInput.value);
|
||||||
|
const endTime = new Date(this.value);
|
||||||
|
|
||||||
|
if (endTime <= startTime) {
|
||||||
|
// Reset to 1 hour after start time
|
||||||
|
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||||
|
this.value = newEndTime.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No date selected, set default to current time
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||||
|
|
||||||
|
document.getElementById('start_time').value = now.toISOString().slice(0, 16);
|
||||||
|
document.getElementById('end_time').value = oneHourLater.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert datetime-local inputs to RFC3339 format on form submission
|
// Convert datetime-local inputs to RFC3339 format on form submission
|
||||||
document.querySelector('form').addEventListener('submit', function(e) {
|
document.querySelector('form').addEventListener('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const startTime = document.getElementById('start_time').value;
|
const startTime = document.getElementById('start_time').value;
|
||||||
const endTime = document.getElementById('end_time').value;
|
const endTime = document.getElementById('end_time').value;
|
||||||
|
|
||||||
|
// Validate that end time is after start time
|
||||||
|
if (new Date(endTime) <= new Date(startTime)) {
|
||||||
|
alert('End time must be after start time');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to RFC3339 format
|
// Convert to RFC3339 format
|
||||||
const startRFC = new Date(startTime).toISOString();
|
const startRFC = new Date(startTime).toISOString();
|
||||||
const endRFC = new Date(endTime).toISOString();
|
const endRFC = new Date(endTime).toISOString();
|
||||||
|
Loading…
Reference in New Issue
Block a user