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:
Mahmoud-Emad 2025-05-28 15:48:54 +03:00
parent d815d9d365
commit 58d1cde1ce
5 changed files with 972 additions and 191 deletions

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -1,2 +1,3 @@
pub mod calendar;
pub mod db;
pub mod governance;

View File

@ -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

View File

@ -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();