From 4b637b7e04a69936f4f0951ae3303b70685738a1 Mon Sep 17 00:00:00 2001 From: despiegk Date: Mon, 21 Apr 2025 10:52:19 +0200 Subject: [PATCH] ... --- actix_mvc_app/Cargo.lock | 80 +++- actix_mvc_app/Cargo.toml | 1 + actix_mvc_app/specs.md | 56 +++ actix_mvc_app/src/controllers/calendar.rs | 389 ++++++++++++++++++ actix_mvc_app/src/controllers/mod.rs | 4 +- actix_mvc_app/src/main.rs | 10 + actix_mvc_app/src/models/calendar.rs | 94 +++++ actix_mvc_app/src/models/mod.rs | 4 +- actix_mvc_app/src/routes/mod.rs | 7 + actix_mvc_app/src/utils/mod.rs | 6 + actix_mvc_app/src/utils/redis_service.rs | 144 +++++++ actix_mvc_app/src/views/base.html | 3 + actix_mvc_app/src/views/calendar/index.html | 132 ++++++ .../src/views/calendar/new_event.html | 98 +++++ 14 files changed, 1023 insertions(+), 5 deletions(-) create mode 100644 actix_mvc_app/specs.md create mode 100644 actix_mvc_app/src/controllers/calendar.rs create mode 100644 actix_mvc_app/src/models/calendar.rs create mode 100644 actix_mvc_app/src/utils/redis_service.rs create mode 100644 actix_mvc_app/src/views/calendar/index.html create mode 100644 actix_mvc_app/src/views/calendar/new_event.html diff --git a/actix_mvc_app/Cargo.lock b/actix_mvc_app/Cargo.lock index 926819e..18b2ba5 100644 --- a/actix_mvc_app/Cargo.lock +++ b/actix_mvc_app/Cargo.lock @@ -144,7 +144,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2", + "socket2 0.5.9", "tokio", "tracing", ] @@ -223,7 +223,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.5.9", "time", "tracing", "url", @@ -258,6 +258,7 @@ dependencies = [ "lazy_static", "log", "num_cpus", + "redis", "serde", "serde_json", "tera", @@ -652,6 +653,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "config" version = "0.14.1" @@ -1941,6 +1956,27 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "redis" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.4.10", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.11" @@ -2119,6 +2155,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -2176,6 +2218,16 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.9" @@ -2325,7 +2377,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.9", "windows-sys 0.52.0", ] @@ -2638,6 +2690,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -2647,6 +2715,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.61.0" diff --git a/actix_mvc_app/Cargo.toml b/actix_mvc_app/Cargo.toml index 0de43cf..9e2cce4 100644 --- a/actix_mvc_app/Cargo.toml +++ b/actix_mvc_app/Cargo.toml @@ -21,3 +21,4 @@ actix-identity = "0.6.0" bcrypt = "0.15.0" uuid = { version = "1.6.1", features = ["v4", "serde"] } lazy_static = "1.4.0" +redis = { version = "0.23.0", features = ["tokio-comp"] } diff --git a/actix_mvc_app/specs.md b/actix_mvc_app/specs.md new file mode 100644 index 0000000..9ff8fd5 --- /dev/null +++ b/actix_mvc_app/specs.md @@ -0,0 +1,56 @@ + + +- login +- change profile (email(s), linkedin, websites, telnr's, ...) +- KYC +- wallet + - with EUR/CHF/USD, ability to topup wallet... (stripe, make sure to describe well) + - with real world digital assets (RWDA) + - the RWDA can be transfered to someone else if allowed +- tickets (see own tickets) + - a ticket can have workflow attached to it (as set by rhai script) + - on a workflow step needs to be clear for user if they need to do something + - has type, subject, priority, level, comments (user can add comment) + - if new comment message is sent to inbox as well, if new action needed as well +- inbox + - info we can send to the user, user can reply + - has labels +- actions + - if something to do for user, is part of the workflow +- RWDA'S + - overview (in tiles like blogs) of the RWDA's, has tags user can filter + - if user clicks on one then goes to mini site (like ebook), is shown in app + - on RWDA we see nr of RWDA (marketcap, ... and other core financials, ...) + - use can select RWDA, and buy into it, if not enough cash will be asked to put more cash in +- contracts + - as markdown + - user can sign + - see who signed + +# user flows + +## registration + +- login, user choses secret (done by means of the webassembly component) +- verification level, user can do KYC + + +# rwda + +- name +- description +- link to website +- nr of shares +- share value +- vesting period, lockin period +- symbol + + + +# Dynex + +- meeting with + + + + diff --git a/actix_mvc_app/src/controllers/calendar.rs b/actix_mvc_app/src/controllers/calendar.rs new file mode 100644 index 0000000..e59e09e --- /dev/null +++ b/actix_mvc_app/src/controllers/calendar.rs @@ -0,0 +1,389 @@ +use actix_web::{web, HttpResponse, Responder, Result}; +use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc}; +use serde::{Deserialize, Serialize}; +use tera::Tera; + +use crate::models::{CalendarEvent, CalendarViewMode}; +use crate::utils::RedisCalendarService; + +/// Controller for handling calendar-related routes +pub struct CalendarController; + +impl CalendarController { + /// Handles the calendar page route + pub async fn calendar( + tmpl: web::Data, + query: web::Query, + ) -> Result { + let mut ctx = tera::Context::new(); + 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())); + 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_date(&naive_date).and_hms_opt(0, 0, 0).unwrap(), + Err(_) => Utc::now(), + } + } else { + Utc::now() + }; + + ctx.insert("current_date", &date.format("%Y-%m-%d").to_string()); + ctx.insert("current_year", &date.year()); + ctx.insert("current_month", &date.month()); + ctx.insert("current_day", &date.day()); + + // 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(); + (start, end) + }, + CalendarViewMode::Month => { + 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(); + (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 = Utc.from_utc_date(&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(); + (start, end) + }, + }; + + // Get events from Redis + let events = match RedisCalendarService::get_events_in_range(start_date, end_date) { + Ok(events) => events, + Err(e) => { + log::error!("Failed to get events from Redis: {}", e); + vec![] + } + }; + + ctx.insert("events", &events); + + // 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 month_events = events.iter() + .filter(|event| { + event.start_time.month() == month || event.end_time.month() == month + }) + .cloned() + .collect::>(); + + CalendarMonth { + month, + name: month_name.to_string(), + events: month_events, + } + }).collect::>(); + + 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_weekday = first_day.weekday().num_days_from_sunday(); + + let mut calendar_days = Vec::new(); + + // Add empty days for the start of the month + for _ in 0..first_weekday { + calendar_days.push(CalendarDay { + day: 0, + events: vec![], + is_current_month: false, + }); + } + + // Add days for the current month + for day in 1..=days_in_month { + 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(); + + (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::>(); + + calendar_days.push(CalendarDay { + day, + events: day_events, + is_current_month: true, + }); + } + + // Fill out the rest of the calendar grid (6 rows of 7 days) + let remaining_days = 42 - calendar_days.len(); + for day in 1..=remaining_days { + calendar_days.push(CalendarDay { + day: day as u32, + events: vec![], + is_current_month: false, + }); + } + + 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(); + let week_start = date - chrono::Duration::days(weekday as i64); + + 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() + .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(); + + (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::>(); + + week_days.push(CalendarDay { + day: day_date.day(), + events: day_events, + is_current_month: day_date.month() == date.month(), + }); + } + + 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())); + + // 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())); + }, + } + + let rendered = tmpl.render("calendar/index.html", &ctx) + .map_err(|e| { + eprintln!("Template rendering error: {}", e); + actix_web::error::ErrorInternalServerError("Template rendering error") + })?; + + Ok(HttpResponse::Ok().content_type("text/html").body(rendered)) + } + + /// Handles the new event page route + pub async fn new_event(tmpl: web::Data) -> Result { + let mut ctx = tera::Context::new(); + ctx.insert("active_page", "calendar"); + + let rendered = tmpl.render("calendar/new_event.html", &ctx) + .map_err(|e| { + eprintln!("Template rendering error: {}", e); + actix_web::error::ErrorInternalServerError("Template rendering error") + })?; + + Ok(HttpResponse::Ok().content_type("text/html").body(rendered)) + } + + /// Handles the create event route + pub async fn create_event( + form: web::Form, + tmpl: web::Data, + ) -> Result { + // 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")); + } + }; + + 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")); + } + }; + + // Create the event + let event = CalendarEvent::new( + form.title.clone(), + form.description.clone(), + start_time, + end_time, + Some(form.color.clone()), + form.all_day, + None, // User ID would come from session in a real app + ); + + // 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); + + // Show an error message + let mut ctx = tera::Context::new(); + ctx.insert("active_page", "calendar"); + ctx.insert("error", "Failed to save event"); + + let rendered = tmpl.render("calendar/new_event.html", &ctx) + .map_err(|e| { + eprintln!("Template rendering error: {}", e); + actix_web::error::ErrorInternalServerError("Template rendering error") + })?; + + Ok(HttpResponse::InternalServerError().content_type("text/html").body(rendered)) + } + } + } + + /// Handles the delete event route + pub async fn delete_event( + path: web::Path, + ) -> Result { + let id = path.into_inner(); + + // Delete the event from Redis + match RedisCalendarService::delete_event(&id) { + Ok(_) => { + // Redirect to the calendar page + Ok(HttpResponse::SeeOther() + .append_header(("Location", "/calendar")) + .finish()) + }, + Err(e) => { + log::error!("Failed to delete event from Redis: {}", e); + Ok(HttpResponse::InternalServerError().body("Failed to delete event")) + } + } + } + + /// Returns the last day of the month + fn last_day_of_month(year: i32, month: u32) -> u32 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 { + 29 + } else { + 28 + } + }, + _ => 30, // Default to 30 days + } + } + + /// Returns the name of the month + fn month_name(month: u32) -> &'static str { + 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", + _ => "", + } + } + + /// Returns the name of the day + fn day_name(day: u32) -> &'static str { + match day { + 0 => "Sunday", + 1 => "Monday", + 2 => "Tuesday", + 3 => "Wednesday", + 4 => "Thursday", + 5 => "Friday", + 6 => "Saturday", + _ => "", + } + } +} + +/// Query parameters for the calendar page +#[derive(Debug, Deserialize)] +pub struct CalendarQuery { + pub view: Option, + pub date: Option, +} + +/// Form data for creating an event +#[derive(Debug, Deserialize)] +pub struct EventForm { + pub title: String, + pub description: String, + pub start_time: String, + pub end_time: String, + pub color: String, + pub all_day: bool, +} + +/// Represents a day in the calendar +#[derive(Debug, Serialize)] +struct CalendarDay { + day: u32, + events: Vec, + is_current_month: bool, +} + +/// Represents a month in the calendar +#[derive(Debug, Serialize)] +struct CalendarMonth { + month: u32, + name: String, + events: Vec, +} \ No newline at end of file diff --git a/actix_mvc_app/src/controllers/mod.rs b/actix_mvc_app/src/controllers/mod.rs index 2932600..9152455 100644 --- a/actix_mvc_app/src/controllers/mod.rs +++ b/actix_mvc_app/src/controllers/mod.rs @@ -2,8 +2,10 @@ pub mod home; pub mod auth; pub mod ticket; +pub mod calendar; // Re-export controllers for easier imports pub use home::HomeController; pub use auth::AuthController; -pub use ticket::TicketController; \ No newline at end of file +pub use ticket::TicketController; +pub use calendar::CalendarController; \ No newline at end of file diff --git a/actix_mvc_app/src/main.rs b/actix_mvc_app/src/main.rs index 1dacdb5..375f4c5 100644 --- a/actix_mvc_app/src/main.rs +++ b/actix_mvc_app/src/main.rs @@ -13,6 +13,7 @@ mod utils; // Import middleware components use app_middleware::{RequestTimer, SecurityHeaders}; +use utils::redis_service; // Initialize lazy_static for in-memory storage #[macro_use] @@ -28,6 +29,15 @@ async fn main() -> io::Result<()> { let config = config::get_config(); let bind_address = format!("{}:{}", config.server.host, config.server.port); + // Initialize Redis client + let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); + if let Err(e) = redis_service::init_redis_client(&redis_url) { + log::error!("Failed to initialize Redis client: {}", e); + log::warn!("Calendar functionality will not work properly without Redis"); + } else { + log::info!("Redis client initialized successfully"); + } + log::info!("Starting server at http://{}", bind_address); // Create and configure the HTTP server diff --git a/actix_mvc_app/src/models/calendar.rs b/actix_mvc_app/src/models/calendar.rs new file mode 100644 index 0000000..d62a77e --- /dev/null +++ b/actix_mvc_app/src/models/calendar.rs @@ -0,0 +1,94 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Represents a calendar event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CalendarEvent { + /// Unique identifier for the event + pub id: String, + /// Title of the event + pub title: String, + /// Description of the event + pub description: String, + /// Start time of the event + pub start_time: DateTime, + /// End time of the event + pub end_time: DateTime, + /// Color of the event (hex code) + pub color: String, + /// Whether the event is an all-day event + pub all_day: bool, + /// User ID of the event creator + pub user_id: Option, +} + +impl CalendarEvent { + /// Creates a new calendar event + pub fn new( + title: String, + description: String, + start_time: DateTime, + end_time: DateTime, + color: Option, + all_day: bool, + user_id: Option, + ) -> Self { + Self { + id: Uuid::new_v4().to_string(), + title, + description, + start_time, + end_time, + color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue + all_day, + user_id, + } + } + + /// Converts the event to a JSON string + pub fn to_json(&self) -> Result { + serde_json::to_string(self) + } + + /// Creates an event from a JSON string + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } +} + +/// Represents a view mode for the calendar +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CalendarViewMode { + /// Year view + Year, + /// Month view + Month, + /// Week view + Week, + /// Day view + Day, +} + +impl CalendarViewMode { + /// Converts a string to a view mode + pub fn from_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "year" => Self::Year, + "month" => Self::Month, + "week" => Self::Week, + "day" => Self::Day, + _ => Self::Month, // Default to month view + } + } + + /// Converts the view mode to a string + pub fn to_str(&self) -> &'static str { + match self { + Self::Year => "year", + Self::Month => "month", + Self::Week => "week", + Self::Day => "day", + } + } +} \ No newline at end of file diff --git a/actix_mvc_app/src/models/mod.rs b/actix_mvc_app/src/models/mod.rs index 722c17b..57ca021 100644 --- a/actix_mvc_app/src/models/mod.rs +++ b/actix_mvc_app/src/models/mod.rs @@ -1,7 +1,9 @@ // Export models pub mod user; pub mod ticket; +pub mod calendar; // Re-export models for easier imports pub use user::User; -pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter}; \ No newline at end of file +pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter}; +pub use calendar::{CalendarEvent, CalendarViewMode}; \ No newline at end of file diff --git a/actix_mvc_app/src/routes/mod.rs b/actix_mvc_app/src/routes/mod.rs index 153d462..7bef8da 100644 --- a/actix_mvc_app/src/routes/mod.rs +++ b/actix_mvc_app/src/routes/mod.rs @@ -4,6 +4,7 @@ use actix_web::cookie::Key; use crate::controllers::home::HomeController; use crate::controllers::auth::AuthController; use crate::controllers::ticket::TicketController; +use crate::controllers::calendar::CalendarController; /// Configures all application routes pub fn configure_routes(cfg: &mut web::ServiceConfig) { @@ -42,5 +43,11 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/tickets/{id}", web::get().to(TicketController::show_ticket)) .route("/tickets/{id}/comment", web::post().to(TicketController::add_comment)) .route("/tickets/{id}/status/{status}", web::get().to(TicketController::update_status)) + + // Calendar routes + .route("/calendar", web::get().to(CalendarController::calendar)) + .route("/calendar/new", web::get().to(CalendarController::new_event)) + .route("/calendar/new", web::post().to(CalendarController::create_event)) + .route("/calendar/{id}/delete", web::get().to(CalendarController::delete_event)) ); } \ No newline at end of file diff --git a/actix_mvc_app/src/utils/mod.rs b/actix_mvc_app/src/utils/mod.rs index 872230b..40e0b4c 100644 --- a/actix_mvc_app/src/utils/mod.rs +++ b/actix_mvc_app/src/utils/mod.rs @@ -1,6 +1,12 @@ use chrono::{DateTime, TimeZone, Utc}; use tera::{self, Function, Result, Value}; +// Export modules +pub mod redis_service; + +// Re-export for easier imports +pub use redis_service::RedisCalendarService; + /// Registers custom Tera functions pub fn register_tera_functions(tera: &mut tera::Tera) { tera.register_function("now", NowFunction); diff --git a/actix_mvc_app/src/utils/redis_service.rs b/actix_mvc_app/src/utils/redis_service.rs new file mode 100644 index 0000000..011517c --- /dev/null +++ b/actix_mvc_app/src/utils/redis_service.rs @@ -0,0 +1,144 @@ +use redis::{Client, Commands, Connection, RedisError}; +use std::sync::{Arc, Mutex}; +use lazy_static::lazy_static; +use crate::models::CalendarEvent; + +// Create a lazy static Redis client that can be used throughout the application +lazy_static! { + static ref REDIS_CLIENT: Arc>> = Arc::new(Mutex::new(None)); +} + +/// Initialize the Redis client +pub fn init_redis_client(redis_url: &str) -> Result<(), RedisError> { + let client = redis::Client::open(redis_url)?; + + // Test the connection + let _: Connection = client.get_connection()?; + + // Store the client in the lazy static + let mut client_guard = REDIS_CLIENT.lock().unwrap(); + *client_guard = Some(client); + + Ok(()) +} + +/// Get a Redis connection +pub fn get_connection() -> Result { + let client_guard = REDIS_CLIENT.lock().unwrap(); + + if let Some(client) = &*client_guard { + client.get_connection() + } else { + Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "Redis client not initialized", + ))) + } +} + +/// Redis service for calendar events +pub struct RedisCalendarService; + +impl RedisCalendarService { + /// Key prefix for calendar events + const EVENT_KEY_PREFIX: &'static str = "calendar:event:"; + + /// Key for the set of all event IDs + const ALL_EVENTS_KEY: &'static str = "calendar:all_events"; + + /// Save a calendar event to Redis + pub fn save_event(event: &CalendarEvent) -> Result<(), RedisError> { + let mut conn = get_connection()?; + + // Convert the event to JSON + let json = event.to_json().map_err(|e| { + RedisError::from(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to serialize event: {}", e), + )) + })?; + + // Save the event + let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id); + let _: () = conn.set(event_key, json)?; + + // Add the event ID to the set of all events + let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.id)?; + + Ok(()) + } + + /// Get a calendar event from Redis by ID + pub fn get_event(id: &str) -> Result, RedisError> { + let mut conn = get_connection()?; + + // Get the event JSON + let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id); + let json: Option = conn.get(event_key)?; + + // Parse the JSON + if let Some(json) = json { + let event = CalendarEvent::from_json(&json).map_err(|e| { + RedisError::from(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to deserialize event: {}", e), + )) + })?; + + Ok(Some(event)) + } else { + Ok(None) + } + } + + /// Delete a calendar event from Redis + pub fn delete_event(id: &str) -> Result { + let mut conn = get_connection()?; + + // Delete the event + let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id); + let deleted: i32 = conn.del(event_key)?; + + // Remove the event ID from the set of all events + let _: () = conn.srem(Self::ALL_EVENTS_KEY, id)?; + + Ok(deleted > 0) + } + + /// Get all calendar events from Redis + pub fn get_all_events() -> Result, RedisError> { + let mut conn = get_connection()?; + + // Get all event IDs + let event_ids: Vec = conn.smembers(Self::ALL_EVENTS_KEY)?; + + // Get all events + let mut events = Vec::new(); + for id in event_ids { + if let Some(event) = Self::get_event(&id)? { + events.push(event); + } + } + + Ok(events) + } + + /// Get events for a specific date range + pub fn get_events_in_range( + start: chrono::DateTime, + end: chrono::DateTime, + ) -> Result, RedisError> { + let all_events = Self::get_all_events()?; + + // Filter events that fall within the date range + let filtered_events = all_events + .into_iter() + .filter(|event| { + // Check if the event overlaps with the date range + (event.start_time <= end && event.end_time >= start) + }) + .collect(); + + Ok(filtered_events) + } +} \ No newline at end of file diff --git a/actix_mvc_app/src/views/base.html b/actix_mvc_app/src/views/base.html index 1f1616a..6bcb870 100644 --- a/actix_mvc_app/src/views/base.html +++ b/actix_mvc_app/src/views/base.html @@ -32,6 +32,9 @@ +