...
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-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"mio",
|
"mio",
|
||||||
"socket2",
|
"socket2 0.5.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@ -223,7 +223,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"socket2",
|
"socket2 0.5.9",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
@ -258,6 +258,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"redis",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tera",
|
"tera",
|
||||||
@ -652,6 +653,20 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
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]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@ -1941,6 +1956,27 @@ dependencies = [
|
|||||||
"getrandom 0.3.2",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
@ -2119,6 +2155,12 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
@ -2176,6 +2218,16 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
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]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
@ -2325,7 +2377,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2 0.5.9",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2638,6 +2690,22 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -2647,6 +2715,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.0"
|
version = "0.61.0"
|
||||||
|
@ -21,3 +21,4 @@ actix-identity = "0.6.0"
|
|||||||
bcrypt = "0.15.0"
|
bcrypt = "0.15.0"
|
||||||
uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
||||||
lazy_static = "1.4.0"
|
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 home;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
|
pub mod calendar;
|
||||||
|
|
||||||
// Re-export controllers for easier imports
|
// Re-export controllers for easier imports
|
||||||
pub use home::HomeController;
|
pub use home::HomeController;
|
||||||
pub use auth::AuthController;
|
pub use auth::AuthController;
|
||||||
pub use ticket::TicketController;
|
pub use ticket::TicketController;
|
||||||
|
pub use calendar::CalendarController;
|
@ -13,6 +13,7 @@ mod utils;
|
|||||||
|
|
||||||
// Import middleware components
|
// Import middleware components
|
||||||
use app_middleware::{RequestTimer, SecurityHeaders};
|
use app_middleware::{RequestTimer, SecurityHeaders};
|
||||||
|
use utils::redis_service;
|
||||||
|
|
||||||
// Initialize lazy_static for in-memory storage
|
// Initialize lazy_static for in-memory storage
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@ -28,6 +29,15 @@ async fn main() -> io::Result<()> {
|
|||||||
let config = config::get_config();
|
let config = config::get_config();
|
||||||
let bind_address = format!("{}:{}", config.server.host, config.server.port);
|
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);
|
log::info!("Starting server at http://{}", bind_address);
|
||||||
|
|
||||||
// Create and configure the HTTP server
|
// 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
|
// Export models
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
|
pub mod calendar;
|
||||||
|
|
||||||
// Re-export models for easier imports
|
// Re-export models for easier imports
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter};
|
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::home::HomeController;
|
||||||
use crate::controllers::auth::AuthController;
|
use crate::controllers::auth::AuthController;
|
||||||
use crate::controllers::ticket::TicketController;
|
use crate::controllers::ticket::TicketController;
|
||||||
|
use crate::controllers::calendar::CalendarController;
|
||||||
|
|
||||||
/// Configures all application routes
|
/// Configures all application routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
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}", web::get().to(TicketController::show_ticket))
|
||||||
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
|
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
|
||||||
.route("/tickets/{id}/status/{status}", web::get().to(TicketController::update_status))
|
.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 chrono::{DateTime, TimeZone, Utc};
|
||||||
use tera::{self, Function, Result, Value};
|
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
|
/// Registers custom Tera functions
|
||||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||||
tera.register_function("now", NowFunction);
|
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">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
|
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'calendar' %}active{% endif %}" href="/calendar">Calendar</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
{% if user and user.id %}
|
{% 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