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_web::{HttpResponse, Responder, Result, web};
|
||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Tera;
|
||||
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::utils::{RedisCalendarService, render_template};
|
||||
use crate::utils::render_template;
|
||||
use heromodels_core::Model;
|
||||
|
||||
/// Controller for handling calendar-related routes
|
||||
pub struct CalendarController;
|
||||
@ -14,9 +18,11 @@ pub struct CalendarController;
|
||||
impl CalendarController {
|
||||
/// Helper function to get user from session
|
||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
||||
serde_json::from_str(&user_json).ok()
|
||||
})
|
||||
session
|
||||
.get::<String>("user")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|user_json| serde_json::from_str(&user_json).ok())
|
||||
}
|
||||
|
||||
/// Handles the calendar page route
|
||||
@ -29,13 +35,16 @@ impl CalendarController {
|
||||
ctx.insert("active_page", "calendar");
|
||||
|
||||
// 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());
|
||||
|
||||
// Parse the date from the query parameters or use the current date
|
||||
let date = if let Some(date_str) = &query.date {
|
||||
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(),
|
||||
}
|
||||
} else {
|
||||
@ -47,44 +56,109 @@ impl CalendarController {
|
||||
ctx.insert("current_month", &date.month());
|
||||
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) {
|
||||
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
|
||||
let (start_date, end_date) = match view_mode {
|
||||
CalendarViewMode::Year => {
|
||||
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)
|
||||
},
|
||||
}
|
||||
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 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)
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Week => {
|
||||
// Calculate the start of the week (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 end = start + chrono::Duration::days(7);
|
||||
(start, end)
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Day => {
|
||||
let start = Utc.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();
|
||||
let start = Utc
|
||||
.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)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Get events from Redis
|
||||
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
|
||||
Ok(events) => events,
|
||||
// Get events from database
|
||||
let events = match get_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) => {
|
||||
log::error!("Failed to get events from Redis: {}", e);
|
||||
log::error!("Failed to get events from database: {}", e);
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
@ -94,42 +168,47 @@ impl CalendarController {
|
||||
// Generate calendar data based on the view mode
|
||||
match view_mode {
|
||||
CalendarViewMode::Year => {
|
||||
let months = (1..=12).map(|month| {
|
||||
let month_name = match month {
|
||||
1 => "January",
|
||||
2 => "February",
|
||||
3 => "March",
|
||||
4 => "April",
|
||||
5 => "May",
|
||||
6 => "June",
|
||||
7 => "July",
|
||||
8 => "August",
|
||||
9 => "September",
|
||||
10 => "October",
|
||||
11 => "November",
|
||||
12 => "December",
|
||||
_ => "",
|
||||
};
|
||||
let months = (1..=12)
|
||||
.map(|month| {
|
||||
let month_name = match month {
|
||||
1 => "January",
|
||||
2 => "February",
|
||||
3 => "March",
|
||||
4 => "April",
|
||||
5 => "May",
|
||||
6 => "June",
|
||||
7 => "July",
|
||||
8 => "August",
|
||||
9 => "September",
|
||||
10 => "October",
|
||||
11 => "November",
|
||||
12 => "December",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let month_events = events.iter()
|
||||
.filter(|event| {
|
||||
event.start_time.month() == month || event.end_time.month() == month
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let month_events = events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event.start_time.month() == month || event.end_time.month() == month
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
CalendarMonth {
|
||||
month,
|
||||
name: month_name.to_string(),
|
||||
events: month_events,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
CalendarMonth {
|
||||
month,
|
||||
name: month_name.to_string(),
|
||||
events: month_events,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ctx.insert("months", &months);
|
||||
},
|
||||
}
|
||||
CalendarViewMode::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 mut calendar_days = Vec::new();
|
||||
@ -145,13 +224,20 @@ impl CalendarController {
|
||||
|
||||
// Add days for the current month
|
||||
for day in 1..=days_in_month {
|
||||
let day_events = events.iter()
|
||||
let day_events = events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
let day_start = Utc.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();
|
||||
let day_start = Utc
|
||||
.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.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
|
||||
(event.start_time <= day_end && event.end_time >= day_start)
|
||||
|| (event.all_day
|
||||
&& event.start_time.day() <= day
|
||||
&& event.end_time.day() >= day)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@ -175,7 +261,7 @@ impl CalendarController {
|
||||
|
||||
ctx.insert("calendar_days", &calendar_days);
|
||||
ctx.insert("month_name", &Self::month_name(date.month()));
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Week => {
|
||||
// Calculate the start of the week (Sunday)
|
||||
let weekday = date.weekday().num_days_from_sunday();
|
||||
@ -184,13 +270,34 @@ impl CalendarController {
|
||||
let mut week_days = Vec::new();
|
||||
for i in 0..7 {
|
||||
let day_date = week_start + chrono::Duration::days(i);
|
||||
let day_events = events.iter()
|
||||
let day_events = events
|
||||
.iter()
|
||||
.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_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap();
|
||||
let day_start = Utc
|
||||
.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.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day())
|
||||
(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())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@ -203,16 +310,22 @@ impl CalendarController {
|
||||
}
|
||||
|
||||
ctx.insert("week_days", &week_days);
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Day => {
|
||||
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
|
||||
log::info!("Events count: {}", events.len());
|
||||
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)
|
||||
@ -223,9 +336,24 @@ impl CalendarController {
|
||||
let mut ctx = tera::Context::new();
|
||||
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) {
|
||||
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)
|
||||
@ -237,44 +365,91 @@ impl CalendarController {
|
||||
tmpl: web::Data<Tera>,
|
||||
_session: Session,
|
||||
) -> 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
|
||||
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse start time: {}", e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
|
||||
log::error!("Failed to parse start time '{}': {}", form.start_time, e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
|
||||
}
|
||||
};
|
||||
|
||||
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse end time: {}", e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
|
||||
log::error!("Failed to parse end time '{}': {}", form.end_time, e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
|
||||
}
|
||||
};
|
||||
|
||||
// Create the event
|
||||
let event = CalendarEvent::new(
|
||||
form.title.clone(),
|
||||
form.description.clone(),
|
||||
// Get user information from session
|
||||
let user_info = Self::get_user_from_session(&_session);
|
||||
let (user_id, user_name) = if let Some(user) = &user_info {
|
||||
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,
|
||||
end_time,
|
||||
Some(form.color.clone()),
|
||||
None, // location
|
||||
Some(&form.color),
|
||||
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);
|
||||
|
||||
// If user is logged in, add the event to their calendar
|
||||
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(_) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the event to Redis
|
||||
match RedisCalendarService::save_event(&event) {
|
||||
Ok(_) => {
|
||||
// Redirect to the calendar page
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/calendar"))
|
||||
.finish())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to save event to Redis: {}", e);
|
||||
log::error!("Failed to save event to database: {}", e);
|
||||
|
||||
// Show an error message
|
||||
let mut ctx = tera::Context::new();
|
||||
@ -282,13 +457,15 @@ impl CalendarController {
|
||||
ctx.insert("error", "Failed to save event");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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> {
|
||||
let id = path.into_inner();
|
||||
|
||||
// Delete the event from Redis
|
||||
match RedisCalendarService::delete_event(&id) {
|
||||
// Parse the 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(_) => {
|
||||
log::info!("Deleted event with ID: {}", event_id);
|
||||
// Redirect to the calendar page
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/calendar"))
|
||||
.finish())
|
||||
},
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@ -326,7 +513,7 @@ impl CalendarController {
|
||||
} else {
|
||||
28
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => 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 governance;
|
||||
|
@ -13,8 +13,7 @@
|
||||
<p class="text-muted mb-0">Manage your events and schedule</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#newEventModal">
|
||||
<button type="button" class="btn btn-primary" onclick="openEventModal()">
|
||||
<i class="bi bi-plus-circle"></i> Create Event
|
||||
</button>
|
||||
</div>
|
||||
@ -245,6 +244,14 @@
|
||||
</div>
|
||||
<form id="newEventForm" action="/calendar/events" method="post">
|
||||
<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="col-md-8">
|
||||
<div class="mb-3">
|
||||
@ -308,7 +315,7 @@
|
||||
|
||||
<!-- 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"
|
||||
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;">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
@ -429,56 +436,125 @@
|
||||
timeInputs.style.display = 'none';
|
||||
startTime.removeAttribute('required');
|
||||
endTime.removeAttribute('required');
|
||||
// Clear the values to prevent validation issues
|
||||
startTime.value = '';
|
||||
endTime.value = '';
|
||||
} else {
|
||||
timeInputs.style.display = 'block';
|
||||
startTime.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
|
||||
document.getElementById('newEventForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
// Handle form submission (ensure only one listener)
|
||||
const eventForm = document.getElementById('newEventForm');
|
||||
if (!eventForm.hasAttribute('data-listener-added')) {
|
||||
eventForm.setAttribute('data-listener-added', 'true');
|
||||
eventForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const allDay = document.getElementById('allDayEvent').checked;
|
||||
|
||||
if (!allDay) {
|
||||
// Convert datetime-local to RFC3339 format
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
if (startTime) {
|
||||
formData.set('start_time', new Date(startTime).toISOString());
|
||||
// Prevent double submission
|
||||
if (this.hasAttribute('data-submitting')) {
|
||||
return;
|
||||
}
|
||||
if (endTime) {
|
||||
formData.set('end_time', new Date(endTime).toISOString());
|
||||
}
|
||||
} else {
|
||||
// For all-day events, set times to start and end of day
|
||||
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);
|
||||
this.setAttribute('data-submitting', 'true');
|
||||
|
||||
formData.set('start_time', startOfDay.toISOString());
|
||||
formData.set('end_time', endOfDay.toISOString());
|
||||
}
|
||||
const formData = new FormData(this);
|
||||
const allDay = document.getElementById('allDayEvent').checked;
|
||||
|
||||
// Submit the form
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
// Ensure all_day field is always present (checkboxes don't send false values)
|
||||
formData.set('all_day', allDay ? 'true' : 'false');
|
||||
|
||||
if (!allDay) {
|
||||
// Convert datetime-local to RFC3339 format
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
if (startTime) {
|
||||
formData.set('start_time', new Date(startTime).toISOString());
|
||||
}
|
||||
if (endTime) {
|
||||
formData.set('end_time', new Date(endTime).toISOString());
|
||||
}
|
||||
} else {
|
||||
alert('Error creating event. Please try again.');
|
||||
// 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();
|
||||
|
||||
// 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('end_time', endOfDay.toISOString());
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating event. Please try again.');
|
||||
|
||||
// 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, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(formData)
|
||||
}).then(response => {
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response ok:', response.ok);
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
// 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 => {
|
||||
// Reset submitting flag on error
|
||||
eventForm.removeAttribute('data-submitting');
|
||||
console.error('Network error:', error);
|
||||
alert('Network error creating event. Please try again.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete event function
|
||||
function deleteEvent(eventId) {
|
||||
@ -543,8 +619,19 @@
|
||||
endTime.setHours(10, 0, 0, 0);
|
||||
|
||||
// Update the modal form with the selected date
|
||||
document.getElementById('startTime').value = startTime.toISOString().slice(0, 16);
|
||||
document.getElementById('endTime').value = endTime.toISOString().slice(0, 16);
|
||||
const startTimeInput = document.getElementById('startTime');
|
||||
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
|
||||
const modalTitle = document.getElementById('newEventModalLabel');
|
||||
@ -556,12 +643,82 @@
|
||||
});
|
||||
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
|
||||
const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
|
||||
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
|
||||
function initializeCalendar() {
|
||||
// Highlight today's date
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/calendar/new" method="post">
|
||||
<form action="/calendar/events" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Event Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
@ -39,6 +39,13 @@
|
||||
</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">
|
||||
<label for="color" class="form-label">Event Color</label>
|
||||
<select class="form-control" id="color" name="color">
|
||||
@ -59,14 +66,83 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
document.querySelector('form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const startTime = document.getElementById('start_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
|
||||
const startRFC = new Date(startTime).toISOString();
|
||||
const endRFC = new Date(endTime).toISOString();
|
||||
|
Loading…
Reference in New Issue
Block a user