...
This commit is contained in:
parent
1fa0b30169
commit
4b637b7e04
80
actix_mvc_app/Cargo.lock
generated
80
actix_mvc_app/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"] }
|
||||
|
56
actix_mvc_app/specs.md
Normal file
56
actix_mvc_app/specs.md
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
|
389
actix_mvc_app/src/controllers/calendar.rs
Normal file
389
actix_mvc_app/src/controllers/calendar.rs
Normal file
@ -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<Tera>,
|
||||
query: web::Query<CalendarQuery>,
|
||||
) -> Result<impl Responder> {
|
||||
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::<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_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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Tera>) -> Result<impl Responder> {
|
||||
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<EventForm>,
|
||||
tmpl: web::Data<Tera>,
|
||||
) -> Result<impl Responder> {
|
||||
// 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<String>,
|
||||
) -> Result<impl Responder> {
|
||||
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<String>,
|
||||
pub date: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<CalendarEvent>,
|
||||
is_current_month: bool,
|
||||
}
|
||||
|
||||
/// Represents a month in the calendar
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CalendarMonth {
|
||||
month: u32,
|
||||
name: String,
|
||||
events: Vec<CalendarEvent>,
|
||||
}
|
@ -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;
|
||||
pub use calendar::CalendarController;
|
@ -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
|
||||
|
94
actix_mvc_app/src/models/calendar.rs
Normal file
94
actix_mvc_app/src/models/calendar.rs
Normal file
@ -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<Utc>,
|
||||
/// End time of the event
|
||||
pub end_time: DateTime<Utc>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl CalendarEvent {
|
||||
/// Creates a new calendar event
|
||||
pub fn new(
|
||||
title: String,
|
||||
description: String,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
color: Option<String>,
|
||||
all_day: bool,
|
||||
user_id: Option<String>,
|
||||
) -> 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<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
|
||||
/// Creates an event from a JSON string
|
||||
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
@ -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};
|
||||
pub use calendar::{CalendarEvent, CalendarViewMode};
|
@ -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))
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
144
actix_mvc_app/src/utils/redis_service.rs
Normal file
144
actix_mvc_app/src/utils/redis_service.rs
Normal file
@ -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<Mutex<Option<Client>>> = 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<Connection, RedisError> {
|
||||
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<Option<CalendarEvent>, RedisError> {
|
||||
let mut conn = get_connection()?;
|
||||
|
||||
// Get the event JSON
|
||||
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id);
|
||||
let json: Option<String> = 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<bool, RedisError> {
|
||||
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<Vec<CalendarEvent>, RedisError> {
|
||||
let mut conn = get_connection()?;
|
||||
|
||||
// Get all event IDs
|
||||
let event_ids: Vec<String> = 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<chrono::Utc>,
|
||||
end: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<Vec<CalendarEvent>, 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)
|
||||
}
|
||||
}
|
@ -32,6 +32,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'calendar' %}active{% endif %}" href="/calendar">Calendar</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if user and user.id %}
|
||||
|
132
actix_mvc_app/src/views/calendar/index.html
Normal file
132
actix_mvc_app/src/views/calendar/index.html
Normal file
@ -0,0 +1,132 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Calendar{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Calendar</h1>
|
||||
|
||||
<p>View Mode: {{ view_mode }}</p>
|
||||
<p>Current Date: {{ current_date }}</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="btn-group">
|
||||
<a href="/calendar?view=day" class="btn btn-outline-primary">Day</a>
|
||||
<a href="/calendar?view=month" class="btn btn-outline-primary">Month</a>
|
||||
<a href="/calendar?view=year" class="btn btn-outline-primary">Year</a>
|
||||
</div>
|
||||
<a href="/calendar/new" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Create New Event
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if view_mode == "month" %}
|
||||
<h2>Month View: {{ month_name }} {{ current_year }}</h2>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sun</th>
|
||||
<th>Mon</th>
|
||||
<th>Tue</th>
|
||||
<th>Wed</th>
|
||||
<th>Thu</th>
|
||||
<th>Fri</th>
|
||||
<th>Sat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for week in range(start=0, end=6) %}
|
||||
<tr>
|
||||
{% for day_idx in range(start=0, end=7) %}
|
||||
<td>
|
||||
{% set idx = week * 7 + day_idx %}
|
||||
{% if idx < calendar_days|length %}
|
||||
{% set day = calendar_days[idx] %}
|
||||
{% if day.day > 0 %}
|
||||
{{ day.day }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% elif view_mode == "year" %}
|
||||
<h2>Year View: {{ current_year }}</h2>
|
||||
|
||||
<div class="row">
|
||||
{% for month in months %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header">{{ month.name }}</div>
|
||||
<div class="card-body">
|
||||
<p>Events: {{ month.events|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif view_mode == "day" %}
|
||||
<h2>Day View: {{ current_date }}</h2>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
All Day Events
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if events is defined and events|length > 0 %}
|
||||
{% for event in events %}
|
||||
{% if event.all_day %}
|
||||
<div class="alert" style="background-color: {{ event.color }}; color: white;">
|
||||
<h5>{{ event.title }}</h5>
|
||||
<p>{{ event.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted">No all-day events</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group">
|
||||
{% for hour in range(start=0, end=24) %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex">
|
||||
<div class="pe-3" style="width: 60px; text-align: right;">
|
||||
<strong>{{ "%02d"|format(value=hour) }}:00</strong>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
{% if events is defined and events|length > 0 %}
|
||||
{% for event in events %}
|
||||
{% if not event.all_day %}
|
||||
{% set start_hour = event.start_time|date(format="%H") %}
|
||||
{% if start_hour == hour|string %}
|
||||
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
|
||||
<h5>{{ event.title }}</h5>
|
||||
<p>{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}</p>
|
||||
<p>{{ event.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Floating Action Button (FAB) for creating new events -->
|
||||
<a href="/calendar/new" class="position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow" style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
|
||||
<i class="bi bi-plus"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
{% endblock %}
|
||||
{% endblock %}
|
98
actix_mvc_app/src/views/calendar/new_event.html
Normal file
98
actix_mvc_app/src/views/calendar/new_event.html
Normal file
@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Calendar Event{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Create New Event</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/calendar/new" 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>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="all_day" name="all_day">
|
||||
<label class="form-check-label" for="all_day">All Day Event</label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label for="start_time" class="form-label">Start Time</label>
|
||||
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="end_time" class="form-label">End Time</label>
|
||||
<input type="datetime-local" class="form-control" id="end_time" name="end_time" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="color" class="form-label">Event Color</label>
|
||||
<select class="form-control" id="color" name="color">
|
||||
<option value="#4285F4">Blue</option>
|
||||
<option value="#EA4335">Red</option>
|
||||
<option value="#34A853">Green</option>
|
||||
<option value="#FBBC05">Yellow</option>
|
||||
<option value="#A142F4">Purple</option>
|
||||
<option value="#24C1E0">Cyan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">Create Event</button>
|
||||
<a href="/calendar" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Convert datetime-local inputs to RFC3339 format on form submission
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const startTime = document.getElementById('start_time').value;
|
||||
const endTime = document.getElementById('end_time').value;
|
||||
|
||||
// Convert to RFC3339 format
|
||||
const startRFC = new Date(startTime).toISOString();
|
||||
const endRFC = new Date(endTime).toISOString();
|
||||
|
||||
// Create hidden inputs for the RFC3339 values
|
||||
const startInput = document.createElement('input');
|
||||
startInput.type = 'hidden';
|
||||
startInput.name = 'start_time';
|
||||
startInput.value = startRFC;
|
||||
|
||||
const endInput = document.createElement('input');
|
||||
endInput.type = 'hidden';
|
||||
endInput.name = 'end_time';
|
||||
endInput.value = endRFC;
|
||||
|
||||
// Remove the original inputs
|
||||
document.getElementById('start_time').removeAttribute('name');
|
||||
document.getElementById('end_time').removeAttribute('name');
|
||||
|
||||
// Add the hidden inputs to the form
|
||||
this.appendChild(startInput);
|
||||
this.appendChild(endInput);
|
||||
|
||||
// Submit the form
|
||||
this.submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user