use axum::{ extract::{Path, Query, State}, response::Html, routing::get, Router, }; use chrono::{DateTime, Datelike, Duration, NaiveDate, Utc}; use diesel::prelude::*; use serde::Deserialize; use std::sync::Arc; use uuid::Uuid; use crate::bot::get_default_bot; use crate::core::shared::schema::{calendar_events, calendars}; use crate::shared::state::AppState; #[derive(Debug, Deserialize, Default)] pub struct EventsQuery { pub calendar_id: Option, pub start: Option>, pub end: Option>, pub view: Option, } pub async fn events_list( State(state): State>, Query(query): Query, ) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let now = Utc::now(); let start = query.start.unwrap_or(now); let end = query.end.unwrap_or(now + Duration::days(30)); let mut db_query = calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .filter(calendar_events::start_time.ge(start)) .filter(calendar_events::start_time.le(end)) .into_boxed(); if let Some(calendar_id) = query.calendar_id { db_query = db_query.filter(calendar_events::calendar_id.eq(calendar_id)); } db_query = db_query.order(calendar_events::start_time.asc()); db_query .select(( calendar_events::id, calendar_events::title, calendar_events::description, calendar_events::location, calendar_events::start_time, calendar_events::end_time, calendar_events::all_day, calendar_events::color, calendar_events::status, )) .load::<( Uuid, String, Option, Option, DateTime, DateTime, bool, Option, String, )>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(events) if !events.is_empty() => { let items: String = events .iter() .map(|(id, title, desc, location, start, end, all_day, color, status)| { let event_color = color.clone().unwrap_or_else(|| "#3b82f6".to_string()); let location_text = location.clone().unwrap_or_default(); let time_str = if *all_day { "All day".to_string() } else { format!("{} - {}", start.format("%H:%M"), end.format("%H:%M")) }; let date_str = start.format("%b %d").to_string(); format!( r##"
{}
{} {} {}
"##, id, event_color, id, date_str, title, time_str, if location_text.is_empty() { String::new() } else { format!(r##"{}"##, location_text) } ) }) .collect(); Html(format!(r##"
{}
"##, items)) } _ => Html( r##"

No events found

"## .to_string(), ), } } pub async fn event_detail( State(state): State>, Path(id): Path, ) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; calendar_events::table .find(id) .select(( calendar_events::id, calendar_events::title, calendar_events::description, calendar_events::location, calendar_events::start_time, calendar_events::end_time, calendar_events::all_day, calendar_events::color, calendar_events::status, calendar_events::attendees, )) .first::<( Uuid, String, Option, Option, DateTime, DateTime, bool, Option, String, serde_json::Value, )>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some((id, title, desc, location, start, end, all_day, color, status, attendees)) => { let description = desc.unwrap_or_else(|| "No description".to_string()); let location_text = location.unwrap_or_else(|| "No location".to_string()); let event_color = color.unwrap_or_else(|| "#3b82f6".to_string()); let time_str = if all_day { format!("{} (All day)", start.format("%B %d, %Y")) } else { format!( "{} - {}", start.format("%B %d, %Y %H:%M"), end.format("%H:%M") ) }; let attendees_list: Vec = serde_json::from_value(attendees).unwrap_or_default(); let attendees_html = if attendees_list.is_empty() { "

No attendees

".to_string() } else { attendees_list .iter() .map(|a| format!(r##"{}"##, a)) .collect::>() .join("") }; Html(format!( r##"

{}

{}
{}
{}

Description

{}

Attendees

{}
"##, event_color, title, status, status, time_str, location_text, description, attendees_html, id, id )) } None => Html( r##"

Event not found

"## .to_string(), ), } } pub async fn calendars_sidebar(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; calendars::table .filter(calendars::bot_id.eq(bot_id)) .order(calendars::is_primary.desc()) .select(( calendars::id, calendars::name, calendars::color, calendars::is_visible, calendars::is_primary, )) .load::<(Uuid, String, Option, bool, bool)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(cals) if !cals.is_empty() => { let items: String = cals .iter() .map(|(id, name, color, visible, primary)| { let cal_color = color.clone().unwrap_or_else(|| "#3b82f6".to_string()); let checked = if *visible { "checked" } else { "" }; let primary_badge = if *primary { r##"Primary"## } else { "" }; format!( r##"
{} {}
"##, id, checked, id, !visible, cal_color, name, primary_badge ) }) .collect(); Html(format!( r##"
{}
"##, items )) } _ => Html( r##"

No calendars yet

"## .to_string(), ), } } pub async fn upcoming_events(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let now = Utc::now(); let end = now + Duration::days(7); calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .filter(calendar_events::start_time.ge(now)) .filter(calendar_events::start_time.le(end)) .order(calendar_events::start_time.asc()) .limit(5) .select(( calendar_events::id, calendar_events::title, calendar_events::start_time, calendar_events::color, )) .load::<(Uuid, String, DateTime, Option)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(events) if !events.is_empty() => { let items: String = events .iter() .map(|(id, title, start, color)| { let event_color = color.clone().unwrap_or_else(|| "#3b82f6".to_string()); let time_str = start.format("%b %d, %H:%M").to_string(); format!( r##"
{} {}
"##, id, event_color, title, time_str ) }) .collect(); Html(format!(r##"
{}
"##, items)) } _ => Html( r##"
No upcoming events Create your first event
"## .to_string(), ), } } pub async fn events_count(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(format!("{}", result.unwrap_or(0))) } pub async fn today_events_count(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let today = Utc::now().date_naive(); let today_start = today.and_hms_opt(0, 0, 0)?; let today_end = today.and_hms_opt(23, 59, 59)?; calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .filter(calendar_events::start_time.ge(today_start)) .filter(calendar_events::start_time.le(today_end)) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(format!("{}", result.unwrap_or(0))) } #[derive(Debug, Deserialize, Default)] pub struct MonthQuery { pub year: Option, pub month: Option, } pub async fn month_view( State(state): State>, Query(query): Query, ) -> Html { let pool = state.pool.clone(); let now = Utc::now(); let year = query.year.unwrap_or(now.year()); let month = query.month.unwrap_or(now.month()); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let first_day = NaiveDate::from_ymd_opt(year, month, 1)?; let last_day = if month == 12 { NaiveDate::from_ymd_opt(year + 1, 1, 1)? } else { NaiveDate::from_ymd_opt(year, month + 1, 1)? } .pred_opt()?; let start = first_day.and_hms_opt(0, 0, 0)?; let end = last_day.and_hms_opt(23, 59, 59)?; let events = calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .filter(calendar_events::start_time.ge(start)) .filter(calendar_events::start_time.le(end)) .select(( calendar_events::id, calendar_events::title, calendar_events::start_time, calendar_events::color, )) .load::<(Uuid, String, DateTime, Option)>(&mut conn) .ok()?; Some((first_day, last_day, events)) }) .await .ok() .flatten(); match result { Some((first_day, last_day, events)) => { let month_name = first_day.format("%B %Y").to_string(); let start_weekday = first_day.weekday().num_days_from_sunday(); let mut days_html = String::new(); for _ in 0..start_weekday { days_html.push_str(r#"
"#); } let mut current = first_day; while current <= last_day { let day_num = current.day(); let day_events: Vec<_> = events .iter() .filter(|(_, _, start, _)| start.date_naive() == current) .collect(); let events_dots: String = day_events .iter() .take(3) .map(|(_, _, _, color)| { let c = color.clone().unwrap_or_else(|| "#3b82f6".to_string()); format!(r##""##, c) }) .collect(); let is_today = current == Utc::now().date_naive(); let today_class = if is_today { "today" } else { "" }; days_html.push_str(&format!( r##"
{}
{}
"##, today_class, current, current, day_num, events_dots )); current = current.succ_opt().unwrap_or(current); } let prev_month = if month == 1 { 12 } else { month - 1 }; let prev_year = if month == 1 { year - 1 } else { year }; let next_month = if month == 12 { 1 } else { month + 1 }; let next_year = if month == 12 { year + 1 } else { year }; Html(format!( r##"

{}

Sun
Mon
Tue
Wed
Thu
Fri
Sat
{}
"##, prev_year, prev_month, month_name, next_year, next_month, days_html )) } None => Html( r##"

Could not load calendar

"## .to_string(), ), } } #[derive(Debug, Deserialize)] pub struct DayQuery { pub date: NaiveDate, } pub async fn day_events( State(state): State>, Query(query): Query, ) -> Html { let pool = state.pool.clone(); let date = query.date; let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let start = date.and_hms_opt(0, 0, 0)?; let end = date.and_hms_opt(23, 59, 59)?; calendar_events::table .filter(calendar_events::bot_id.eq(bot_id)) .filter(calendar_events::start_time.ge(start)) .filter(calendar_events::start_time.le(end)) .order(calendar_events::start_time.asc()) .select(( calendar_events::id, calendar_events::title, calendar_events::start_time, calendar_events::end_time, calendar_events::color, calendar_events::all_day, )) .load::<(Uuid, String, DateTime, DateTime, Option, bool)>(&mut conn) .ok() }) .await .ok() .flatten(); let date_str = date.format("%A, %B %d, %Y").to_string(); match result { Some(events) if !events.is_empty() => { let items: String = events .iter() .map(|(id, title, start, end, color, all_day)| { let event_color = color.clone().unwrap_or_else(|| "#3b82f6".to_string()); let time_str = if *all_day { "All day".to_string() } else { format!("{} - {}", start.format("%H:%M"), end.format("%H:%M")) }; format!( r##"
{} {}
"##, event_color, id, time_str, title ) }) .collect(); Html(format!( r##"

{}

{}
"##, date_str, items )) } _ => Html(format!( r##"

{}

No events on this day

"##, date_str )), } } pub async fn new_event_form() -> Html { let now = Utc::now(); let date = now.format("%Y-%m-%d").to_string(); let time = now.format("%H:00").to_string(); let end_time = (now + Duration::hours(1)).format("%H:00").to_string(); Html(format!( r##"
"##, date, time, end_time )) } pub async fn new_calendar_form() -> Html { Html( r##"
"## .to_string(), ) } pub fn configure_calendar_ui_routes() -> Router> { Router::new() .route("/api/ui/calendar/events", get(events_list)) .route("/api/ui/calendar/events/count", get(events_count)) .route("/api/ui/calendar/events/today", get(today_events_count)) .route("/api/ui/calendar/events/:id", get(event_detail)) .route("/api/ui/calendar/calendars", get(calendars_sidebar)) .route("/api/ui/calendar/upcoming", get(upcoming_events)) .route("/api/ui/calendar/month", get(month_view)) .route("/api/ui/calendar/day", get(day_events)) .route("/api/ui/calendar/new-event", get(new_event_form)) .route("/api/ui/calendar/new-calendar", get(new_calendar_form)) }