WIP: development_backend #4
2
actix_mvc_app/.cargo/config.toml
Normal file
2
actix_mvc_app/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
286
actix_mvc_app/Cargo.lock
generated
286
actix_mvc_app/Cargo.lock
generated
@ -296,6 +296,8 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"heromodels",
|
||||
"heromodels_core",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -309,6 +311,14 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adapter_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
@ -366,6 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.2.15",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.7.35",
|
||||
@ -478,6 +490,12 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.88"
|
||||
@ -547,6 +565,26 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
"unty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
@ -1285,12 +1323,62 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "heromodels"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"adapter_macros",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"ourdb",
|
||||
"rhai",
|
||||
"rhai_autobind_macros",
|
||||
"rhai_client_macros",
|
||||
"rhai_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tst",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heromodels-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heromodels_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
@ -1557,6 +1645,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@ -1756,6 +1853,15 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@ -1824,6 +1930,9 @@ name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@ -1841,6 +1950,16 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ourdb"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
@ -1907,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"thiserror 2.0.12",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@ -2210,6 +2329,75 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"rust_decimal",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_autobind_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rhai",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_macros_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_wrapper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rhai",
|
||||
"rhai_macros_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
@ -2247,6 +2435,16 @@ dependencies = [
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.37.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@ -2421,7 +2619,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
@ -2456,6 +2654,17 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.10"
|
||||
@ -2488,12 +2697,37 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@ -2557,13 +2791,39 @@ dependencies = [
|
||||
"unic-segment",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2723,6 +2983,14 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tst"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ourdb",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
@ -2831,6 +3099,12 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
@ -2888,6 +3162,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
@ -15,6 +15,8 @@ env_logger = "0.11.2"
|
||||
log = "0.4.21"
|
||||
dotenv = "0.15.0"
|
||||
chrono = { version = "0.4.35", features = ["serde"] }
|
||||
heromodels = { path = "../../db/heromodels" }
|
||||
heromodels_core = { path = "../../db/heromodels_core" }
|
||||
config = "0.14.0"
|
||||
num_cpus = "1.16.0"
|
||||
futures = "0.3.30"
|
||||
@ -27,3 +29,8 @@ redis = { version = "0.23.0", features = ["tokio-comp"] }
|
||||
jsonwebtoken = "8.3.0"
|
||||
pulldown-cmark = "0.13.0"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[patch."https://git.ourworld.tf/herocode/db.git"]
|
||||
rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" }
|
||||
rhai_wrapper = { path = "../../rhaj/rhai_wrapper" }
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
use config::{Config, ConfigError, File};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
|
||||
/// Application configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@ -13,6 +13,7 @@ pub struct AppConfig {
|
||||
|
||||
/// Server configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ServerConfig {
|
||||
/// Host address to bind to
|
||||
pub host: String,
|
||||
@ -50,7 +51,8 @@ impl AppConfig {
|
||||
}
|
||||
|
||||
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
|
||||
config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
|
||||
config_builder =
|
||||
config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
|
||||
|
||||
// Build and deserialize the config
|
||||
let config = config_builder.build()?;
|
||||
@ -61,4 +63,4 @@ impl AppConfig {
|
||||
/// Returns the application configuration
|
||||
pub fn get_config() -> AppConfig {
|
||||
AppConfig::new().expect("Failed to load configuration")
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,7 @@ lazy_static! {
|
||||
/// Controller for handling authentication-related routes
|
||||
pub struct AuthController;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AuthController {
|
||||
/// Generate a JWT token for a user
|
||||
fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
|
@ -1,12 +1,17 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use actix_web::{HttpResponse, Responder, Result, web};
|
||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Tera;
|
||||
use serde_json::Value;
|
||||
use tera::Tera;
|
||||
|
||||
use crate::models::{CalendarEvent, CalendarViewMode};
|
||||
use crate::utils::{RedisCalendarService, render_template};
|
||||
use crate::db::calendar::{
|
||||
add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar,
|
||||
};
|
||||
use crate::models::CalendarViewMode;
|
||||
use crate::utils::render_template;
|
||||
use heromodels::models::calendar::Event;
|
||||
use heromodels_core::Model;
|
||||
|
||||
/// Controller for handling calendar-related routes
|
||||
pub struct CalendarController;
|
||||
@ -14,9 +19,11 @@ pub struct CalendarController;
|
||||
impl CalendarController {
|
||||
/// Helper function to get user from session
|
||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
||||
serde_json::from_str(&user_json).ok()
|
||||
})
|
||||
session
|
||||
.get::<String>("user")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|user_json| serde_json::from_str(&user_json).ok())
|
||||
}
|
||||
|
||||
/// Handles the calendar page route
|
||||
@ -27,113 +34,176 @@ impl CalendarController {
|
||||
) -> 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()));
|
||||
let view_mode =
|
||||
CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
|
||||
ctx.insert("view_mode", &view_mode.to_str());
|
||||
|
||||
|
||||
// Parse the date from the query parameters or use the current date
|
||||
let date = if let Some(date_str) = &query.date {
|
||||
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
||||
Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
|
||||
Ok(naive_date) => Utc
|
||||
.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap())
|
||||
.into(),
|
||||
Err(_) => Utc::now(),
|
||||
}
|
||||
} else {
|
||||
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());
|
||||
|
||||
// Add user to context if available
|
||||
|
||||
// Add user to context if available and ensure user has a calendar
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
ctx.insert("user", &user);
|
||||
|
||||
// Get or create user calendar
|
||||
if let (Some(user_id), Some(user_name)) = (
|
||||
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
user.get("full_name").and_then(|v| v.as_str()),
|
||||
) {
|
||||
match get_or_create_user_calendar(user_id, user_name) {
|
||||
Ok(calendar) => {
|
||||
log::info!(
|
||||
"User calendar ready: ID {}, Name: '{}'",
|
||||
calendar.get_id(),
|
||||
calendar.name
|
||||
);
|
||||
ctx.insert("user_calendar", &calendar);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get or create user calendar: {}", e);
|
||||
// Continue without calendar - the app should still work
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get events for the current view
|
||||
let (start_date, end_date) = match view_mode {
|
||||
CalendarViewMode::Year => {
|
||||
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
|
||||
let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap();
|
||||
let end = Utc
|
||||
.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59)
|
||||
.unwrap();
|
||||
(start, end)
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Month => {
|
||||
let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
|
||||
let start = Utc
|
||||
.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
|
||||
.unwrap();
|
||||
let last_day = Self::last_day_of_month(date.year(), date.month());
|
||||
let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap();
|
||||
let end = Utc
|
||||
.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59)
|
||||
.unwrap();
|
||||
(start, end)
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Week => {
|
||||
// Calculate the start of the week (Sunday)
|
||||
let _weekday = date.weekday().num_days_from_sunday();
|
||||
let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
|
||||
let start_date = date
|
||||
.date_naive()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.pred_opt()
|
||||
.unwrap();
|
||||
let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
|
||||
let end = start + chrono::Duration::days(7);
|
||||
(start, end)
|
||||
},
|
||||
}
|
||||
CalendarViewMode::Day => {
|
||||
let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap();
|
||||
let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap();
|
||||
let start = Utc
|
||||
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
|
||||
.unwrap();
|
||||
let end = Utc
|
||||
.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59)
|
||||
.unwrap();
|
||||
(start, end)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Get events from Redis
|
||||
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
|
||||
Ok(events) => events,
|
||||
|
||||
// Get events from database
|
||||
let events = match get_events() {
|
||||
Ok(db_events) => {
|
||||
// Filter events for the date range
|
||||
db_events
|
||||
.into_iter()
|
||||
.filter(|event| {
|
||||
// Event overlaps with the date range
|
||||
event.start_time < end_date && event.end_time > start_date
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get events from Redis: {}", e);
|
||||
log::error!("Failed to get events from database: {}", 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<_>>();
|
||||
|
||||
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_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 {
|
||||
@ -142,27 +212,34 @@ impl CalendarController {
|
||||
is_current_month: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add days for the current month
|
||||
for day in 1..=days_in_month {
|
||||
let day_events = events.iter()
|
||||
let day_events = events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap();
|
||||
let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap();
|
||||
|
||||
(event.start_time <= day_end && event.end_time >= day_start) ||
|
||||
(event.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
|
||||
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 {
|
||||
@ -172,149 +249,250 @@ impl CalendarController {
|
||||
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()
|
||||
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())
|
||||
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()));
|
||||
|
||||
ctx.insert(
|
||||
"day_name",
|
||||
&Self::day_name(date.weekday().num_days_from_sunday()),
|
||||
);
|
||||
|
||||
// Add debug info
|
||||
log::info!("Events count: {}", events.len());
|
||||
log::info!("Current date: {}", date.format("%Y-%m-%d"));
|
||||
log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday()));
|
||||
},
|
||||
log::info!(
|
||||
"Day name: {}",
|
||||
Self::day_name(date.weekday().num_days_from_sunday())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render_template(&tmpl, "calendar/index.html", &ctx)
|
||||
}
|
||||
|
||||
|
||||
/// Handles the new event page route
|
||||
pub async fn new_event(tmpl: web::Data<Tera>, _session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "calendar");
|
||||
|
||||
// Add user to context if available
|
||||
|
||||
// Add user to context if available and ensure user has a calendar
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
ctx.insert("user", &user);
|
||||
|
||||
// Get or create user calendar
|
||||
if let (Some(user_id), Some(user_name)) = (
|
||||
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
user.get("full_name").and_then(|v| v.as_str()),
|
||||
) {
|
||||
match get_or_create_user_calendar(user_id, user_name) {
|
||||
Ok(calendar) => {
|
||||
ctx.insert("user_calendar", &calendar);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get or create user calendar: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render_template(&tmpl, "calendar/new_event.html", &ctx)
|
||||
}
|
||||
|
||||
|
||||
/// Handles the create event route
|
||||
pub async fn create_event(
|
||||
form: web::Form<EventForm>,
|
||||
tmpl: web::Data<Tera>,
|
||||
_session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Log the form data for debugging
|
||||
log::info!(
|
||||
"Creating event with form data: title='{}', start_time='{}', end_time='{}', all_day={}",
|
||||
form.title,
|
||||
form.start_time,
|
||||
form.end_time,
|
||||
form.all_day
|
||||
);
|
||||
|
||||
// Parse the start and end times
|
||||
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse start time: {}", e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
|
||||
log::error!("Failed to parse start time '{}': {}", form.start_time, e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse end time: {}", e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
|
||||
log::error!("Failed to parse end time '{}': {}", form.end_time, e);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
|
||||
}
|
||||
};
|
||||
|
||||
// Create the event
|
||||
let event = CalendarEvent::new(
|
||||
form.title.clone(),
|
||||
form.description.clone(),
|
||||
|
||||
// Get user information from session
|
||||
let user_info = Self::get_user_from_session(&_session);
|
||||
let (user_id, user_name) = if let Some(user) = &user_info {
|
||||
let id = user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32);
|
||||
let name = user
|
||||
.get("full_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown User");
|
||||
log::info!("User from session: id={:?}, name='{}'", id, name);
|
||||
(id, name)
|
||||
} else {
|
||||
log::warn!("No user found in session");
|
||||
(None, "Unknown User")
|
||||
};
|
||||
|
||||
// Create the event in the database
|
||||
match create_new_event(
|
||||
&form.title,
|
||||
Some(&form.description),
|
||||
start_time,
|
||||
end_time,
|
||||
Some(form.color.clone()),
|
||||
None, // location
|
||||
Some(&form.color),
|
||||
form.all_day,
|
||||
None, // User ID would come from session in a real app
|
||||
);
|
||||
|
||||
// Save the event to Redis
|
||||
match RedisCalendarService::save_event(&event) {
|
||||
Ok(_) => {
|
||||
user_id,
|
||||
None, // category
|
||||
None, // reminder_minutes
|
||||
) {
|
||||
Ok((event_id, _saved_event)) => {
|
||||
log::info!("Created event with ID: {}", event_id);
|
||||
|
||||
// If user is logged in, add the event to their calendar
|
||||
if let Some(user_id) = user_id {
|
||||
match get_or_create_user_calendar(user_id, user_name) {
|
||||
Ok(calendar) => match add_event_to_calendar(calendar.get_id(), event_id) {
|
||||
Ok(_) => {
|
||||
log::info!(
|
||||
"Added event {} to calendar {}",
|
||||
event_id,
|
||||
calendar.get_id()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to add event to calendar: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to get user calendar: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to the calendar page
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/calendar"))
|
||||
.finish())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to save event to Redis: {}", e);
|
||||
|
||||
log::error!("Failed to save event to database: {}", e);
|
||||
|
||||
// Show an error message
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "calendar");
|
||||
ctx.insert("error", "Failed to save event");
|
||||
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
if let Some(user) = user_info {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
|
||||
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
|
||||
|
||||
Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
|
||||
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.content_type("text/html")
|
||||
.body(result.into_body()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Handles the delete event route
|
||||
pub async fn delete_event(
|
||||
path: web::Path<String>,
|
||||
_session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
// Delete the event from Redis
|
||||
match RedisCalendarService::delete_event(&id) {
|
||||
|
||||
// Parse the event ID
|
||||
let event_id = match id.parse::<u32>() {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
log::error!("Invalid event ID: {}", id);
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid event ID"));
|
||||
}
|
||||
};
|
||||
|
||||
// Delete the event from database
|
||||
match delete_event(event_id) {
|
||||
Ok(_) => {
|
||||
log::info!("Deleted event with ID: {}", event_id);
|
||||
// Redirect to the calendar page
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/calendar"))
|
||||
.finish())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to delete event from Redis: {}", e);
|
||||
log::error!("Failed to delete event from database: {}", e);
|
||||
Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the last day of the month
|
||||
fn last_day_of_month(year: i32, month: u32) -> u32 {
|
||||
match month {
|
||||
@ -326,11 +504,11 @@ impl CalendarController {
|
||||
} else {
|
||||
28
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => 30, // Default to 30 days
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the name of the month
|
||||
fn month_name(month: u32) -> &'static str {
|
||||
match month {
|
||||
@ -349,7 +527,7 @@ impl CalendarController {
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the name of the day
|
||||
fn day_name(day: u32) -> &'static str {
|
||||
match day {
|
||||
@ -387,7 +565,7 @@ pub struct EventForm {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CalendarDay {
|
||||
day: u32,
|
||||
events: Vec<CalendarEvent>,
|
||||
events: Vec<Event>,
|
||||
is_current_month: bool,
|
||||
}
|
||||
|
||||
@ -396,5 +574,5 @@ struct CalendarDay {
|
||||
struct CalendarMonth {
|
||||
month: u32,
|
||||
name: String,
|
||||
events: Vec<CalendarEvent>,
|
||||
}
|
||||
events: Vec<Event>,
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_web::HttpRequest;
|
||||
use tera::{Context, Tera};
|
||||
use serde::Deserialize;
|
||||
use chrono::Utc;
|
||||
use crate::utils::render_template;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::{HttpResponse, Result, web};
|
||||
use serde::Deserialize;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
// Form structs for company operations
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CompanyRegistrationForm {
|
||||
pub company_name: String,
|
||||
pub company_type: String,
|
||||
@ -20,59 +20,69 @@ impl CompanyController {
|
||||
// Display the company management dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
println!("DEBUG: Starting Company dashboard rendering");
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"company");
|
||||
|
||||
|
||||
// Parse query parameters
|
||||
let query_string = req.query_string();
|
||||
|
||||
|
||||
// Check for success message
|
||||
if let Some(pos) = query_string.find("success=") {
|
||||
let start = pos + 8; // length of "success="
|
||||
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
|
||||
let end = query_string[start..]
|
||||
.find('&')
|
||||
.map_or(query_string.len(), |e| e + start);
|
||||
let success = &query_string[start..end];
|
||||
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
|
||||
context.insert("success", &decoded);
|
||||
}
|
||||
|
||||
|
||||
// Check for entity context
|
||||
if let Some(pos) = query_string.find("entity=") {
|
||||
let start = pos + 7; // length of "entity="
|
||||
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
|
||||
let end = query_string[start..]
|
||||
.find('&')
|
||||
.map_or(query_string.len(), |e| e + start);
|
||||
let entity = &query_string[start..end];
|
||||
context.insert("entity", &entity);
|
||||
|
||||
|
||||
// Also get entity name if present
|
||||
if let Some(pos) = query_string.find("entity_name=") {
|
||||
let start = pos + 12; // length of "entity_name="
|
||||
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
|
||||
let end = query_string[start..]
|
||||
.find('&')
|
||||
.map_or(query_string.len(), |e| e + start);
|
||||
let entity_name = &query_string[start..end];
|
||||
let decoded_name = urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
|
||||
let decoded_name =
|
||||
urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
|
||||
context.insert("entity_name", &decoded_name);
|
||||
println!("DEBUG: Entity context set to {} ({})", entity, decoded_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("DEBUG: Rendering Company dashboard template");
|
||||
let response = render_template(&tmpl, "company/index.html", &context);
|
||||
println!("DEBUG: Finished rendering Company dashboard template");
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
// View company details
|
||||
pub async fn view_company(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
pub async fn view_company(
|
||||
tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let company_id = path.into_inner();
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
println!("DEBUG: Viewing company details for {}", company_id);
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"company");
|
||||
context.insert("company_id", &company_id);
|
||||
|
||||
|
||||
// In a real application, we would fetch company data from a database
|
||||
// For now, we'll use mock data based on the company_id
|
||||
match company_id.as_str() {
|
||||
@ -85,14 +95,11 @@ impl CompanyController {
|
||||
context.insert("plan", &"Startup FZC - $50/month");
|
||||
context.insert("next_billing", &"2025-06-01");
|
||||
context.insert("payment_method", &"Credit Card (****4582)");
|
||||
|
||||
|
||||
// Shareholders data
|
||||
let shareholders = vec![
|
||||
("John Smith", "60%"),
|
||||
("Sarah Johnson", "40%"),
|
||||
];
|
||||
let shareholders = vec![("John Smith", "60%"), ("Sarah Johnson", "40%")];
|
||||
context.insert("shareholders", &shareholders);
|
||||
|
||||
|
||||
// Contracts data
|
||||
let contracts = vec![
|
||||
("Articles of Incorporation", "Signed"),
|
||||
@ -100,7 +107,7 @@ impl CompanyController {
|
||||
("Digital Asset Issuance", "Signed"),
|
||||
];
|
||||
context.insert("contracts", &contracts);
|
||||
},
|
||||
}
|
||||
"company2" => {
|
||||
context.insert("company_name", &"Blockchain Innovations Ltd");
|
||||
context.insert("company_type", &"Growth FZC");
|
||||
@ -110,7 +117,7 @@ impl CompanyController {
|
||||
context.insert("plan", &"Growth FZC - $100/month");
|
||||
context.insert("next_billing", &"2025-06-15");
|
||||
context.insert("payment_method", &"Bank Transfer");
|
||||
|
||||
|
||||
// Shareholders data
|
||||
let shareholders = vec![
|
||||
("Michael Chen", "35%"),
|
||||
@ -118,7 +125,7 @@ impl CompanyController {
|
||||
("David Okonkwo", "30%"),
|
||||
];
|
||||
context.insert("shareholders", &shareholders);
|
||||
|
||||
|
||||
// Contracts data
|
||||
let contracts = vec![
|
||||
("Articles of Incorporation", "Signed"),
|
||||
@ -127,7 +134,7 @@ impl CompanyController {
|
||||
("Physical Asset Holding", "Signed"),
|
||||
];
|
||||
context.insert("contracts", &contracts);
|
||||
},
|
||||
}
|
||||
"company3" => {
|
||||
context.insert("company_name", &"Sustainable Energy Cooperative");
|
||||
context.insert("company_type", &"Cooperative FZC");
|
||||
@ -137,7 +144,7 @@ impl CompanyController {
|
||||
context.insert("plan", &"Cooperative FZC - $200/month");
|
||||
context.insert("next_billing", &"Pending Activation");
|
||||
context.insert("payment_method", &"Pending");
|
||||
|
||||
|
||||
// Shareholders data
|
||||
let shareholders = vec![
|
||||
("Community Energy Group", "40%"),
|
||||
@ -145,7 +152,7 @@ impl CompanyController {
|
||||
("Sustainable Living Collective", "30%"),
|
||||
];
|
||||
context.insert("shareholders", &shareholders);
|
||||
|
||||
|
||||
// Contracts data
|
||||
let contracts = vec![
|
||||
("Articles of Incorporation", "Signed"),
|
||||
@ -153,7 +160,7 @@ impl CompanyController {
|
||||
("Cooperative Governance", "Pending"),
|
||||
];
|
||||
context.insert("contracts", &contracts);
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
// If company_id is not recognized, redirect to company index
|
||||
return Ok(HttpResponse::Found()
|
||||
@ -161,51 +168,56 @@ impl CompanyController {
|
||||
.finish());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("DEBUG: Rendering company view template");
|
||||
let response = render_template(&tmpl, "company/view.html", &context);
|
||||
println!("DEBUG: Finished rendering company view template");
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
// Switch to entity context
|
||||
pub async fn switch_entity(path: web::Path<String>) -> Result<HttpResponse> {
|
||||
let company_id = path.into_inner();
|
||||
|
||||
|
||||
println!("DEBUG: Switching to entity context for {}", company_id);
|
||||
|
||||
|
||||
// Get company name based on ID (in a real app, this would come from a database)
|
||||
let company_name = match company_id.as_str() {
|
||||
"company1" => "Zanzibar Digital Solutions",
|
||||
"company2" => "Blockchain Innovations Ltd",
|
||||
"company3" => "Sustainable Energy Cooperative",
|
||||
_ => "Unknown Company"
|
||||
_ => "Unknown Company",
|
||||
};
|
||||
|
||||
|
||||
// In a real application, we would set a session/cookie for the current entity
|
||||
// Here we'll redirect back to the company page with a success message and entity parameter
|
||||
let success_message = format!("Switched to {} entity context", company_name);
|
||||
let encoded_message = urlencoding::encode(&success_message);
|
||||
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/company?success={}&entity={}&entity_name={}",
|
||||
encoded_message, company_id, urlencoding::encode(company_name))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"/company?success={}&entity={}&entity_name={}",
|
||||
encoded_message,
|
||||
company_id,
|
||||
urlencoding::encode(company_name)
|
||||
),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Process company registration
|
||||
pub async fn register(
|
||||
mut form: actix_multipart::Multipart,
|
||||
) -> Result<HttpResponse> {
|
||||
use actix_web::{http::header};
|
||||
pub async fn register(mut form: actix_multipart::Multipart) -> Result<HttpResponse> {
|
||||
use actix_web::http::header;
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
println!("DEBUG: Processing company registration request");
|
||||
|
||||
|
||||
let mut fields: HashMap<String, String> = HashMap::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
|
||||
// Parse multipart form
|
||||
while let Some(Ok(mut field)) = form.next().await {
|
||||
let mut value = Vec::new();
|
||||
@ -213,33 +225,47 @@ impl CompanyController {
|
||||
let data = chunk.unwrap();
|
||||
value.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
|
||||
// Get field name from content disposition
|
||||
let cd = field.content_disposition();
|
||||
if let Some(name) = cd.get_name() {
|
||||
if name == "company_docs" {
|
||||
files.push(value); // Just collect files in memory for now
|
||||
} else {
|
||||
fields.insert(name.to_string(), String::from_utf8_lossy(&value).to_string());
|
||||
fields.insert(
|
||||
name.to_string(),
|
||||
String::from_utf8_lossy(&value).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract company details
|
||||
let company_name = fields.get("company_name").cloned().unwrap_or_default();
|
||||
let company_type = fields.get("company_type").cloned().unwrap_or_default();
|
||||
let shareholders = fields.get("shareholders").cloned().unwrap_or_default();
|
||||
|
||||
|
||||
// Log received fields (mock DB insert)
|
||||
println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
|
||||
company_name, company_type, shareholders, files.len());
|
||||
|
||||
println!(
|
||||
"[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
|
||||
company_name,
|
||||
company_type,
|
||||
shareholders,
|
||||
files.len()
|
||||
);
|
||||
|
||||
// Create success message
|
||||
let success_message = format!("Successfully registered {} as a {}", company_name, company_type);
|
||||
|
||||
let success_message = format!(
|
||||
"Successfully registered {} as a {}",
|
||||
company_name, company_type
|
||||
);
|
||||
|
||||
// Redirect back to /company with success message
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((header::LOCATION, format!("/company?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
header::LOCATION,
|
||||
format!("/company?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
use actix_web::{web, HttpResponse, Result, Error};
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{Error, HttpResponse, Result, web};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, TocItem};
|
||||
use crate::models::contract::{
|
||||
Contract, ContractRevision, ContractSigner, ContractStatistics, ContractStatus, ContractType,
|
||||
SignerStatus, TocItem,
|
||||
};
|
||||
use crate::utils::render_template;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ContractForm {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
@ -18,6 +21,7 @@ pub struct ContractForm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SignerForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
@ -29,98 +33,99 @@ impl ContractController {
|
||||
// Display the contracts dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
let stats = ContractStatistics::new(&contracts);
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"contracts");
|
||||
|
||||
|
||||
// Add stats
|
||||
context.insert("stats", &serde_json::to_value(stats).unwrap());
|
||||
|
||||
|
||||
// Add recent contracts
|
||||
let recent_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
|
||||
context.insert("recent_contracts", &recent_contracts);
|
||||
|
||||
|
||||
// Add pending signature contracts
|
||||
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::PendingSignatures)
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> =
|
||||
contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::PendingSignatures)
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
context.insert("pending_signature_contracts", &pending_signature_contracts);
|
||||
|
||||
|
||||
// Add draft contracts
|
||||
let draft_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Draft)
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
|
||||
context.insert("draft_contracts", &draft_contracts);
|
||||
|
||||
|
||||
render_template(&tmpl, "contracts/index.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display the list of all contracts
|
||||
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
|
||||
.iter()
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"contracts");
|
||||
|
||||
|
||||
context.insert("contracts", &contracts_data);
|
||||
context.insert("filter", &"all");
|
||||
|
||||
|
||||
render_template(&tmpl, "contracts/contracts.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display the list of user's contracts
|
||||
pub async fn my_contracts(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
|
||||
.iter()
|
||||
.map(|c| Self::contract_to_json(c))
|
||||
.collect();
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"contracts");
|
||||
|
||||
|
||||
context.insert("contracts", &contracts_data);
|
||||
|
||||
|
||||
render_template(&tmpl, "contracts/my_contracts.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display a specific contract
|
||||
pub async fn detail(
|
||||
tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
query: Query<HashMap<String, String>>
|
||||
query: Query<HashMap<String, String>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let contract_id = path.into_inner();
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"contracts");
|
||||
|
||||
|
||||
// Find the contract by ID
|
||||
let contracts = Self::get_mock_contracts();
|
||||
|
||||
|
||||
// For demo purposes, if the ID doesn't match exactly, just show the first contract
|
||||
// In a real app, we would return a 404 if the contract is not found
|
||||
let contract = if let Some(found) = contracts.iter().find(|c| c.id == contract_id) {
|
||||
@ -129,7 +134,7 @@ impl ContractController {
|
||||
// For demo, just use the first contract
|
||||
contracts.first().unwrap()
|
||||
};
|
||||
|
||||
|
||||
// Convert contract to JSON
|
||||
let contract_json = Self::contract_to_json(contract);
|
||||
|
||||
@ -137,10 +142,13 @@ impl ContractController {
|
||||
context.insert("contract", &contract_json);
|
||||
|
||||
// If this contract uses multi-page markdown, load the selected section
|
||||
println!("DEBUG: content_dir = {:?}, toc = {:?}", contract.content_dir, contract.toc);
|
||||
println!(
|
||||
"DEBUG: content_dir = {:?}, toc = {:?}",
|
||||
contract.content_dir, contract.toc
|
||||
);
|
||||
if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) {
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
use std::fs;
|
||||
use pulldown_cmark::{Parser, Options, html};
|
||||
// Helper to flatten toc recursively
|
||||
fn flatten_toc<'a>(items: &'a Vec<TocItem>, out: &mut Vec<&'a TocItem>) {
|
||||
for item in items {
|
||||
@ -154,15 +162,28 @@ impl ContractController {
|
||||
flatten_toc(&toc, &mut flat_toc);
|
||||
let section_param = query.get("section");
|
||||
let selected_file = section_param
|
||||
.and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone()))
|
||||
.unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default());
|
||||
.and_then(|f| {
|
||||
flat_toc
|
||||
.iter()
|
||||
.find(|item| item.file == *f)
|
||||
.map(|item| item.file.clone())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
flat_toc
|
||||
.get(0)
|
||||
.map(|item| item.file.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
context.insert("section", &selected_file);
|
||||
let rel_path = format!("{}/{}", content_dir, selected_file);
|
||||
let abs_path = match std::env::current_dir() {
|
||||
Ok(dir) => dir.join(&rel_path),
|
||||
Err(_) => std::path::PathBuf::from(&rel_path),
|
||||
};
|
||||
println!("DEBUG: Attempting to read markdown file at absolute path: {:?}", abs_path);
|
||||
println!(
|
||||
"DEBUG: Attempting to read markdown file at absolute path: {:?}",
|
||||
abs_path
|
||||
);
|
||||
match fs::read_to_string(&abs_path) {
|
||||
Ok(md) => {
|
||||
println!("DEBUG: Successfully read markdown file");
|
||||
@ -170,52 +191,63 @@ impl ContractController {
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
context.insert("contract_section_content", &html_output);
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Error: Could not read contract section markdown at '{:?}': {}", abs_path, e);
|
||||
let error_msg = format!(
|
||||
"Error: Could not read contract section markdown at '{:?}': {}",
|
||||
abs_path, e
|
||||
);
|
||||
println!("{}", error_msg);
|
||||
context.insert("contract_section_content_error", &error_msg);
|
||||
}
|
||||
}
|
||||
context.insert("toc", &toc);
|
||||
}
|
||||
|
||||
|
||||
// Count signed signers for the template
|
||||
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count();
|
||||
let signed_signers = contract
|
||||
.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Signed)
|
||||
.count();
|
||||
context.insert("signed_signers", &signed_signers);
|
||||
|
||||
|
||||
// Count pending signers for the template
|
||||
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
|
||||
let pending_signers = contract
|
||||
.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Pending)
|
||||
.count();
|
||||
context.insert("pending_signers", &pending_signers);
|
||||
|
||||
|
||||
// For demo purposes, set user_has_signed to false
|
||||
// In a real app, we would check if the current user has already signed
|
||||
context.insert("user_has_signed", &false);
|
||||
|
||||
|
||||
render_template(&tmpl, "contracts/contract_detail.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display the create contract form
|
||||
pub async fn create_form(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"contracts");
|
||||
|
||||
|
||||
// Add contract types for dropdown
|
||||
let contract_types = vec![
|
||||
("Service", "Service Agreement"),
|
||||
("Employment", "Employment Contract"),
|
||||
("NDA", "Non-Disclosure Agreement"),
|
||||
("SLA", "Service Level Agreement"),
|
||||
("Other", "Other")
|
||||
("Other", "Other"),
|
||||
];
|
||||
|
||||
|
||||
context.insert("contract_types", &contract_types);
|
||||
|
||||
|
||||
render_template(&tmpl, "contracts/create_contract.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Process the create contract form
|
||||
pub async fn create(
|
||||
_tmpl: web::Data<Tera>,
|
||||
@ -223,158 +255,334 @@ impl ContractController {
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// In a real application, we would save the contract to the database
|
||||
// For now, we'll just redirect to the contracts list
|
||||
|
||||
Ok(HttpResponse::Found().append_header(("Location", "/contracts")).finish())
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/contracts"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Helper method to convert Contract to a JSON object for templates
|
||||
fn contract_to_json(contract: &Contract) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut map = serde_json::Map::new();
|
||||
|
||||
|
||||
// Basic contract info
|
||||
map.insert("id".to_string(), serde_json::Value::String(contract.id.clone()));
|
||||
map.insert("title".to_string(), serde_json::Value::String(contract.title.clone()));
|
||||
map.insert("description".to_string(), serde_json::Value::String(contract.description.clone()));
|
||||
map.insert("status".to_string(), serde_json::Value::String(contract.status.as_str().to_string()));
|
||||
map.insert("contract_type".to_string(), serde_json::Value::String(contract.contract_type.as_str().to_string()));
|
||||
map.insert("created_by".to_string(), serde_json::Value::String(contract.created_by.clone()));
|
||||
map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()));
|
||||
map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()));
|
||||
|
||||
map.insert(
|
||||
"id".to_string(),
|
||||
serde_json::Value::String(contract.id.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"title".to_string(),
|
||||
serde_json::Value::String(contract.title.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String(contract.description.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"status".to_string(),
|
||||
serde_json::Value::String(contract.status.as_str().to_string()),
|
||||
);
|
||||
map.insert(
|
||||
"contract_type".to_string(),
|
||||
serde_json::Value::String(contract.contract_type.as_str().to_string()),
|
||||
);
|
||||
map.insert(
|
||||
"created_by".to_string(),
|
||||
serde_json::Value::String(contract.created_by.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
map.insert(
|
||||
"updated_at".to_string(),
|
||||
serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
|
||||
// Organization info
|
||||
if let Some(org) = &contract.organization_id {
|
||||
map.insert("organization".to_string(), serde_json::Value::String(org.clone()));
|
||||
map.insert(
|
||||
"organization".to_string(),
|
||||
serde_json::Value::String(org.clone()),
|
||||
);
|
||||
} else {
|
||||
map.insert("organization".to_string(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
|
||||
// Add signers
|
||||
let signers: Vec<serde_json::Value> = contract.signers.iter()
|
||||
let signers: Vec<serde_json::Value> = contract
|
||||
.signers
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let mut signer_map = serde_json::Map::new();
|
||||
signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone()));
|
||||
signer_map.insert("name".to_string(), serde_json::Value::String(s.name.clone()));
|
||||
signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone()));
|
||||
signer_map.insert("status".to_string(), serde_json::Value::String(s.status.as_str().to_string()));
|
||||
|
||||
signer_map.insert(
|
||||
"name".to_string(),
|
||||
serde_json::Value::String(s.name.clone()),
|
||||
);
|
||||
signer_map.insert(
|
||||
"email".to_string(),
|
||||
serde_json::Value::String(s.email.clone()),
|
||||
);
|
||||
signer_map.insert(
|
||||
"status".to_string(),
|
||||
serde_json::Value::String(s.status.as_str().to_string()),
|
||||
);
|
||||
|
||||
if let Some(signed_at) = s.signed_at {
|
||||
signer_map.insert("signed_at".to_string(), serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()));
|
||||
signer_map.insert(
|
||||
"signed_at".to_string(),
|
||||
serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
} else {
|
||||
// For display purposes, add a placeholder date for pending signers
|
||||
if s.status == SignerStatus::Pending {
|
||||
signer_map.insert("signed_at".to_string(), serde_json::Value::String("Pending".to_string()));
|
||||
signer_map.insert(
|
||||
"signed_at".to_string(),
|
||||
serde_json::Value::String("Pending".to_string()),
|
||||
);
|
||||
} else if s.status == SignerStatus::Rejected {
|
||||
signer_map.insert("signed_at".to_string(), serde_json::Value::String("Rejected".to_string()));
|
||||
signer_map.insert(
|
||||
"signed_at".to_string(),
|
||||
serde_json::Value::String("Rejected".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let Some(comments) = &s.comments {
|
||||
signer_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
|
||||
signer_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String(comments.clone()),
|
||||
);
|
||||
} else {
|
||||
signer_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
signer_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
serde_json::Value::Object(signer_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
map.insert("signers".to_string(), serde_json::Value::Array(signers));
|
||||
|
||||
|
||||
// Add pending_signers count for templates
|
||||
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
|
||||
map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers)));
|
||||
|
||||
let pending_signers = contract
|
||||
.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Pending)
|
||||
.count();
|
||||
map.insert(
|
||||
"pending_signers".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(pending_signers)),
|
||||
);
|
||||
|
||||
// Add signed_signers count for templates
|
||||
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count();
|
||||
map.insert("signed_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(signed_signers)));
|
||||
|
||||
let signed_signers = contract
|
||||
.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Signed)
|
||||
.count();
|
||||
map.insert(
|
||||
"signed_signers".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(signed_signers)),
|
||||
);
|
||||
|
||||
// Add revisions
|
||||
let revisions: Vec<serde_json::Value> = contract.revisions.iter()
|
||||
let revisions: Vec<serde_json::Value> = contract
|
||||
.revisions
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut revision_map = serde_json::Map::new();
|
||||
revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(r.version)));
|
||||
revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone()));
|
||||
revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()));
|
||||
revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone()));
|
||||
|
||||
revision_map.insert(
|
||||
"version".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(r.version)),
|
||||
);
|
||||
revision_map.insert(
|
||||
"content".to_string(),
|
||||
serde_json::Value::String(r.content.clone()),
|
||||
);
|
||||
revision_map.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
revision_map.insert(
|
||||
"created_by".to_string(),
|
||||
serde_json::Value::String(r.created_by.clone()),
|
||||
);
|
||||
|
||||
if let Some(comments) = &r.comments {
|
||||
revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
|
||||
revision_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String(comments.clone()),
|
||||
);
|
||||
// Add notes field using comments since ContractRevision doesn't have a notes field
|
||||
revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone()));
|
||||
revision_map.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String(comments.clone()),
|
||||
);
|
||||
} else {
|
||||
revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string()));
|
||||
revision_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
revision_map.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
serde_json::Value::Object(revision_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
map.insert("revisions".to_string(), serde_json::Value::Array(revisions.clone()));
|
||||
|
||||
|
||||
map.insert(
|
||||
"revisions".to_string(),
|
||||
serde_json::Value::Array(revisions.clone()),
|
||||
);
|
||||
|
||||
// Add current_version
|
||||
map.insert("current_version".to_string(), serde_json::Value::Number(serde_json::Number::from(contract.current_version)));
|
||||
|
||||
map.insert(
|
||||
"current_version".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(contract.current_version)),
|
||||
);
|
||||
|
||||
// Add latest_revision as an object
|
||||
if !contract.revisions.is_empty() {
|
||||
// Find the latest revision based on version number
|
||||
if let Some(latest) = contract.revisions.iter().max_by_key(|r| r.version) {
|
||||
let mut latest_revision_map = serde_json::Map::new();
|
||||
latest_revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(latest.version)));
|
||||
latest_revision_map.insert("content".to_string(), serde_json::Value::String(latest.content.clone()));
|
||||
latest_revision_map.insert("created_at".to_string(), serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string()));
|
||||
latest_revision_map.insert("created_by".to_string(), serde_json::Value::String(latest.created_by.clone()));
|
||||
|
||||
latest_revision_map.insert(
|
||||
"version".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(latest.version)),
|
||||
);
|
||||
latest_revision_map.insert(
|
||||
"content".to_string(),
|
||||
serde_json::Value::String(latest.content.clone()),
|
||||
);
|
||||
latest_revision_map.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
latest_revision_map.insert(
|
||||
"created_by".to_string(),
|
||||
serde_json::Value::String(latest.created_by.clone()),
|
||||
);
|
||||
|
||||
if let Some(comments) = &latest.comments {
|
||||
latest_revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
|
||||
latest_revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone()));
|
||||
latest_revision_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String(comments.clone()),
|
||||
);
|
||||
latest_revision_map.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String(comments.clone()),
|
||||
);
|
||||
} else {
|
||||
latest_revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
latest_revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string()));
|
||||
latest_revision_map.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
latest_revision_map.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
map.insert("latest_revision".to_string(), serde_json::Value::Object(latest_revision_map));
|
||||
|
||||
map.insert(
|
||||
"latest_revision".to_string(),
|
||||
serde_json::Value::Object(latest_revision_map),
|
||||
);
|
||||
} else {
|
||||
// Create an empty latest_revision object to avoid template errors
|
||||
let mut empty_revision = serde_json::Map::new();
|
||||
empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
|
||||
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string()));
|
||||
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string()));
|
||||
empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string()));
|
||||
empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string()));
|
||||
|
||||
map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision));
|
||||
empty_revision.insert(
|
||||
"version".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"content".to_string(),
|
||||
serde_json::Value::String("No content available".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String("N/A".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"created_by".to_string(),
|
||||
serde_json::Value::String("N/A".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
|
||||
map.insert(
|
||||
"latest_revision".to_string(),
|
||||
serde_json::Value::Object(empty_revision),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create an empty latest_revision object to avoid template errors
|
||||
let mut empty_revision = serde_json::Map::new();
|
||||
empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
|
||||
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string()));
|
||||
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string()));
|
||||
empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string()));
|
||||
empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string()));
|
||||
|
||||
map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision));
|
||||
empty_revision.insert(
|
||||
"version".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"content".to_string(),
|
||||
serde_json::Value::String("No content available".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String("N/A".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"created_by".to_string(),
|
||||
serde_json::Value::String("N/A".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"comments".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
empty_revision.insert(
|
||||
"notes".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
|
||||
map.insert(
|
||||
"latest_revision".to_string(),
|
||||
serde_json::Value::Object(empty_revision),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Add effective and expiration dates if present
|
||||
if let Some(effective_date) = &contract.effective_date {
|
||||
map.insert("effective_date".to_string(), serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()));
|
||||
map.insert(
|
||||
"effective_date".to_string(),
|
||||
serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if let Some(expiration_date) = &contract.expiration_date {
|
||||
map.insert("expiration_date".to_string(), serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()));
|
||||
map.insert(
|
||||
"expiration_date".to_string(),
|
||||
serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
|
||||
// Generate mock contracts for testing
|
||||
fn get_mock_contracts() -> Vec<Contract> {
|
||||
let mut contracts = Vec::new();
|
||||
|
||||
|
||||
// Mock contract 1 - Signed Service Agreement
|
||||
let mut contract1 = Contract {
|
||||
content_dir: None,
|
||||
@ -394,7 +602,7 @@ impl ContractController {
|
||||
revisions: Vec::new(),
|
||||
current_version: 2,
|
||||
};
|
||||
|
||||
|
||||
// Add signers to contract 1
|
||||
contract1.signers.push(ContractSigner {
|
||||
id: "signer-001".to_string(),
|
||||
@ -404,7 +612,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(5)),
|
||||
comments: Some("Approved as per our discussion.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
contract1.signers.push(ContractSigner {
|
||||
id: "signer-002".to_string(),
|
||||
name: "Nala Okafor".to_string(),
|
||||
@ -413,7 +621,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(6)),
|
||||
comments: Some("Terms look good. Happy to proceed.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Add revisions to contract 1
|
||||
contract1.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
@ -422,7 +630,7 @@ impl ContractController {
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Initial draft of the service agreement.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
contract1.revisions.push(ContractRevision {
|
||||
version: 2,
|
||||
content: "<h1>Digital Hub Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the undersigned client (\"Client\").</p><h2>1. Services</h2><p>Provider agrees to provide Client with cloud hosting and digital infrastructure services as specified in Appendix A.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Client agrees to pay Provider the fees set forth in Appendix B. All fees are due within thirty (30) days of invoice date.</p><h2>4. Confidentiality</h2><p>Each party agrees to maintain the confidentiality of any proprietary information received from the other party during the term of this Agreement.</p><h2>5. Data Protection</h2><p>Provider shall implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk, including encryption of personal data, and shall comply with all applicable data protection laws.</p>".to_string(),
|
||||
@ -430,7 +638,7 @@ impl ContractController {
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Added data protection clause as requested by legal.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Mock contract 2 - Pending Signatures
|
||||
let mut contract2 = Contract {
|
||||
content_dir: None,
|
||||
@ -450,7 +658,7 @@ impl ContractController {
|
||||
revisions: Vec::new(),
|
||||
current_version: 1,
|
||||
};
|
||||
|
||||
|
||||
// Add signers to contract 2
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-003".to_string(),
|
||||
@ -460,7 +668,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(2)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-004".to_string(),
|
||||
name: "Maya Rodriguez".to_string(),
|
||||
@ -469,7 +677,7 @@ impl ContractController {
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-005".to_string(),
|
||||
name: "Jamal Washington".to_string(),
|
||||
@ -478,7 +686,7 @@ impl ContractController {
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
// Add revisions to contract 2
|
||||
contract2.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
@ -487,7 +695,7 @@ impl ContractController {
|
||||
created_by: "Dr. Raj Patel".to_string(),
|
||||
comments: Some("Initial draft of the development agreement.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Mock contract 3 - Draft
|
||||
let mut contract3 = Contract {
|
||||
id: "contract-003".to_string(),
|
||||
@ -554,7 +762,6 @@ impl ContractController {
|
||||
]),
|
||||
};
|
||||
|
||||
|
||||
// Add potential signers to contract 3 (still in draft)
|
||||
contract3.signers.push(ContractSigner {
|
||||
id: "signer-006".to_string(),
|
||||
@ -564,7 +771,7 @@ impl ContractController {
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
contract3.signers.push(ContractSigner {
|
||||
id: "signer-007".to_string(),
|
||||
name: "Ibrahim Al-Farsi".to_string(),
|
||||
@ -573,59 +780,57 @@ impl ContractController {
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
// Add ToC and content directory to contract 3
|
||||
contract3.content_dir = Some("src/content/contract-003".to_string());
|
||||
contract3.toc = Some(vec![
|
||||
TocItem {
|
||||
title: "Digital Asset Tokenization Agreement".to_string(),
|
||||
file: "cover.md".to_string(),
|
||||
children: vec![
|
||||
TocItem {
|
||||
title: "1. Purpose".to_string(),
|
||||
file: "1-purpose.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "2. Tokenization Process".to_string(),
|
||||
file: "2-tokenization-process.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "3. Revenue Sharing".to_string(),
|
||||
file: "3-revenue-sharing.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "4. Governance".to_string(),
|
||||
file: "4-governance.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix A: Properties".to_string(),
|
||||
file: "appendix-a.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix B: Specifications".to_string(),
|
||||
file: "appendix-b.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix C: Revenue Formula".to_string(),
|
||||
file: "appendix-c.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix D: Governance Framework".to_string(),
|
||||
file: "appendix-d.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
],
|
||||
}
|
||||
]);
|
||||
contract3.toc = Some(vec![TocItem {
|
||||
title: "Digital Asset Tokenization Agreement".to_string(),
|
||||
file: "cover.md".to_string(),
|
||||
children: vec![
|
||||
TocItem {
|
||||
title: "1. Purpose".to_string(),
|
||||
file: "1-purpose.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "2. Tokenization Process".to_string(),
|
||||
file: "2-tokenization-process.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "3. Revenue Sharing".to_string(),
|
||||
file: "3-revenue-sharing.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "4. Governance".to_string(),
|
||||
file: "4-governance.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix A: Properties".to_string(),
|
||||
file: "appendix-a.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix B: Specifications".to_string(),
|
||||
file: "appendix-b.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix C: Revenue Formula".to_string(),
|
||||
file: "appendix-c.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
TocItem {
|
||||
title: "Appendix D: Governance Framework".to_string(),
|
||||
file: "appendix-d.md".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
],
|
||||
}]);
|
||||
// No revision content for contract 3, content is in markdown files.
|
||||
|
||||
|
||||
// Mock contract 4 - Rejected
|
||||
let mut contract4 = Contract {
|
||||
content_dir: None,
|
||||
@ -645,7 +850,7 @@ impl ContractController {
|
||||
revisions: Vec::new(),
|
||||
current_version: 1,
|
||||
};
|
||||
|
||||
|
||||
// Add signers to contract 4 with a rejection
|
||||
contract4.signers.push(ContractSigner {
|
||||
id: "signer-008".to_string(),
|
||||
@ -655,7 +860,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(10)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
contract4.signers.push(ContractSigner {
|
||||
id: "signer-009".to_string(),
|
||||
name: "Dr. Amina Diallo".to_string(),
|
||||
@ -664,7 +869,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(8)),
|
||||
comments: Some("Cannot agree to these terms due to privacy concerns. Please revise section 3.2 regarding data retention.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Add revisions to contract 4
|
||||
contract4.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
@ -673,7 +878,7 @@ impl ContractController {
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Initial draft of the data sharing agreement.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Mock contract 5 - Active
|
||||
let mut contract5 = Contract {
|
||||
content_dir: None,
|
||||
@ -693,7 +898,7 @@ impl ContractController {
|
||||
revisions: Vec::new(),
|
||||
current_version: 2,
|
||||
};
|
||||
|
||||
|
||||
// Add signers to contract 5
|
||||
contract5.signers.push(ContractSigner {
|
||||
id: "signer-010".to_string(),
|
||||
@ -703,7 +908,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(47)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
|
||||
contract5.signers.push(ContractSigner {
|
||||
id: "signer-011".to_string(),
|
||||
name: "Li Wei".to_string(),
|
||||
@ -712,7 +917,7 @@ impl ContractController {
|
||||
signed_at: Some(Utc::now() - Duration::days(45)),
|
||||
comments: Some("Approved after legal review.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Add revisions to contract 5
|
||||
contract5.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
@ -721,7 +926,7 @@ impl ContractController {
|
||||
created_by: "Maya Rodriguez".to_string(),
|
||||
comments: Some("Initial draft of the identity verification service agreement.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
contract5.revisions.push(ContractRevision {
|
||||
version: 2,
|
||||
content: "<h1>Digital Identity Verification Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the businesses listed in Appendix A (\"Clients\").</p><h2>1. Services</h2><p>Provider agrees to provide Clients with digital identity verification services as specified in Appendix B.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Clients agree to pay Provider the fees set forth in Appendix C. All fees are due within thirty (30) days of invoice date.</p><h2>4. Service Level Agreement</h2><p>Provider shall maintain a service uptime of at least 99.9% as measured on a monthly basis.</p><h2>5. Compliance</h2><p>Provider shall comply with all applicable laws and regulations regarding identity verification and data protection, including but not limited to the Zanzibar Digital Economy Act.</p>".to_string(),
|
||||
@ -729,14 +934,14 @@ impl ContractController {
|
||||
created_by: "Maya Rodriguez".to_string(),
|
||||
comments: Some("Added compliance clause as requested by legal.".to_string()),
|
||||
});
|
||||
|
||||
|
||||
// Add all contracts to the vector
|
||||
contracts.push(contract1);
|
||||
contracts.push(contract2);
|
||||
contracts.push(contract3);
|
||||
contracts.push(contract4);
|
||||
contracts.push(contract5);
|
||||
|
||||
|
||||
contracts
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use actix_web::HttpRequest;
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use actix_web::{HttpResponse, Result, web};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use tera::{Context, Tera};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::asset::{Asset, AssetType, AssetStatus};
|
||||
use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB};
|
||||
use crate::models::asset::Asset;
|
||||
use crate::models::defi::{
|
||||
DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition,
|
||||
ReceivingPosition,
|
||||
};
|
||||
use crate::utils::render_template;
|
||||
|
||||
// Form structs for DeFi operations
|
||||
@ -26,6 +29,7 @@ pub struct ReceivingForm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LiquidityForm {
|
||||
pub first_token: String,
|
||||
pub first_amount: f64,
|
||||
@ -35,6 +39,7 @@ pub struct LiquidityForm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct StakingForm {
|
||||
pub asset_id: String,
|
||||
pub amount: f64,
|
||||
@ -49,6 +54,7 @@ pub struct SwapForm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CollateralForm {
|
||||
pub asset_id: String,
|
||||
pub amount: f64,
|
||||
@ -63,29 +69,29 @@ impl DefiController {
|
||||
// Display the DeFi dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
println!("DEBUG: Starting DeFi dashboard rendering");
|
||||
|
||||
|
||||
// Get mock assets for the dropdown selectors
|
||||
let assets = Self::get_mock_assets();
|
||||
println!("DEBUG: Generated {} mock assets", assets.len());
|
||||
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"defi");
|
||||
|
||||
|
||||
// Add DeFi stats
|
||||
let defi_stats = Self::get_defi_stats();
|
||||
context.insert("defi_stats", &serde_json::to_value(defi_stats).unwrap());
|
||||
|
||||
|
||||
// Add recent assets for selection in forms
|
||||
let recent_assets: Vec<serde_json::Map<String, serde_json::Value>> = assets
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|a| Self::asset_to_json(a))
|
||||
.collect();
|
||||
|
||||
|
||||
context.insert("recent_assets", &recent_assets);
|
||||
|
||||
|
||||
// Get user's providing positions
|
||||
let db = DEFI_DB.lock().unwrap();
|
||||
let providing_positions = db.get_user_providing_positions("user123");
|
||||
@ -94,7 +100,7 @@ impl DefiController {
|
||||
.map(|p| serde_json::to_value(p).unwrap())
|
||||
.collect();
|
||||
context.insert("providing_positions", &providing_positions_json);
|
||||
|
||||
|
||||
// Get user's receiving positions
|
||||
let receiving_positions = db.get_user_receiving_positions("user123");
|
||||
let receiving_positions_json: Vec<serde_json::Value> = receiving_positions
|
||||
@ -102,27 +108,30 @@ impl DefiController {
|
||||
.map(|p| serde_json::to_value(p).unwrap())
|
||||
.collect();
|
||||
context.insert("receiving_positions", &receiving_positions_json);
|
||||
|
||||
|
||||
// Add success message if present in query params
|
||||
if let Some(success) = req.query_string().strip_prefix("success=") {
|
||||
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
|
||||
context.insert("success_message", &decoded);
|
||||
}
|
||||
|
||||
|
||||
println!("DEBUG: Rendering DeFi dashboard template");
|
||||
let response = render_template(&tmpl, "defi/index.html", &context);
|
||||
println!("DEBUG: Finished rendering DeFi dashboard template");
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
// Process providing request
|
||||
pub async fn create_providing(_tmpl: web::Data<Tera>, form: web::Form<ProvidingForm>) -> Result<HttpResponse> {
|
||||
pub async fn create_providing(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<ProvidingForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing providing request: {:?}", form);
|
||||
|
||||
|
||||
// Get the asset obligationails (in a real app, this would come from a database)
|
||||
let assets = Self::get_mock_assets();
|
||||
let asset = assets.iter().find(|a| a.id == form.asset_id);
|
||||
|
||||
|
||||
if let Some(asset) = asset {
|
||||
// Calculate profit share and return amount
|
||||
let profit_share = match form.duration {
|
||||
@ -133,9 +142,10 @@ impl DefiController {
|
||||
365 => 12.0,
|
||||
_ => 4.2, // Default to 30 days rate
|
||||
};
|
||||
|
||||
let return_amount = form.amount + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
|
||||
|
||||
|
||||
let return_amount = form.amount
|
||||
+ (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
|
||||
|
||||
// Create a new providing position
|
||||
let providing_position = ProvidingPosition {
|
||||
base: DefiPosition {
|
||||
@ -156,17 +166,23 @@ impl DefiController {
|
||||
profit_share_earned: profit_share,
|
||||
return_amount,
|
||||
};
|
||||
|
||||
|
||||
// Add the position to the database
|
||||
{
|
||||
let mut db = DEFI_DB.lock().unwrap();
|
||||
db.add_providing_position(providing_position);
|
||||
}
|
||||
|
||||
|
||||
// Redirect with success message
|
||||
let success_message = format!("Successfully provided {} {} for {} days", form.amount, asset.name, form.duration);
|
||||
let success_message = format!(
|
||||
"Successfully provided {} {} for {} days",
|
||||
form.amount, asset.name, form.duration
|
||||
);
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
} else {
|
||||
// Asset not found, redirect with error
|
||||
@ -175,15 +191,18 @@ impl DefiController {
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process receiving request
|
||||
pub async fn create_receiving(_tmpl: web::Data<Tera>, form: web::Form<ReceivingForm>) -> Result<HttpResponse> {
|
||||
pub async fn create_receiving(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<ReceivingForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing receiving request: {:?}", form);
|
||||
|
||||
|
||||
// Get the asset obligationails (in a real app, this would come from a database)
|
||||
let assets = Self::get_mock_assets();
|
||||
let collateral_asset = assets.iter().find(|a| a.id == form.collateral_asset_id);
|
||||
|
||||
|
||||
if let Some(collateral_asset) = collateral_asset {
|
||||
// Calculate profit share rate based on duration
|
||||
let profit_share_rate = match form.duration {
|
||||
@ -194,15 +213,17 @@ impl DefiController {
|
||||
365 => 10.0,
|
||||
_ => 5.0, // Default to 30 days rate
|
||||
};
|
||||
|
||||
|
||||
// Calculate profit share and total to repay
|
||||
let profit_share = form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
|
||||
let profit_share =
|
||||
form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
|
||||
let total_to_repay = form.amount + profit_share;
|
||||
|
||||
|
||||
// Calculate collateral value and ratio
|
||||
let collateral_value = form.collateral_amount * collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
|
||||
let collateral_value = form.collateral_amount
|
||||
* collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
|
||||
let collateral_ratio = (collateral_value / form.amount) * 100.0;
|
||||
|
||||
|
||||
// Create a new receiving position
|
||||
let receiving_position = ReceivingPosition {
|
||||
base: DefiPosition {
|
||||
@ -230,18 +251,23 @@ impl DefiController {
|
||||
total_to_repay,
|
||||
collateral_ratio,
|
||||
};
|
||||
|
||||
|
||||
// Add the position to the database
|
||||
{
|
||||
let mut db = DEFI_DB.lock().unwrap();
|
||||
db.add_receiving_position(receiving_position);
|
||||
}
|
||||
|
||||
|
||||
// Redirect with success message
|
||||
let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral",
|
||||
form.amount, form.collateral_amount, collateral_asset.name);
|
||||
let success_message = format!(
|
||||
"Successfully borrowed {} ZDFZ using {} {} as collateral",
|
||||
form.amount, form.collateral_amount, collateral_asset.name
|
||||
);
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
} else {
|
||||
// Asset not found, redirect with error
|
||||
@ -250,116 +276,202 @@ impl DefiController {
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process liquidity provision
|
||||
pub async fn add_liquidity(_tmpl: web::Data<Tera>, form: web::Form<LiquidityForm>) -> Result<HttpResponse> {
|
||||
pub async fn add_liquidity(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<LiquidityForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing liquidity provision: {:?}", form);
|
||||
|
||||
|
||||
// In a real application, this would add liquidity to a pool in the database
|
||||
// For now, we'll just redirect back to the DeFi dashboard with a success message
|
||||
|
||||
let success_message = format!("Successfully added liquidity: {} {} and {} {}",
|
||||
form.first_amount, form.first_token, form.second_amount, form.second_token);
|
||||
|
||||
|
||||
let success_message = format!(
|
||||
"Successfully added liquidity: {} {} and {} {}",
|
||||
form.first_amount, form.first_token, form.second_amount, form.second_token
|
||||
);
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Process staking request
|
||||
pub async fn create_staking(_tmpl: web::Data<Tera>, form: web::Form<StakingForm>) -> Result<HttpResponse> {
|
||||
pub async fn create_staking(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<StakingForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing staking request: {:?}", form);
|
||||
|
||||
|
||||
// In a real application, this would create a staking position in the database
|
||||
// For now, we'll just redirect back to the DeFi dashboard with a success message
|
||||
|
||||
|
||||
let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id);
|
||||
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Process token swap
|
||||
pub async fn swap_tokens(_tmpl: web::Data<Tera>, form: web::Form<SwapForm>) -> Result<HttpResponse> {
|
||||
pub async fn swap_tokens(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<SwapForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing token swap: {:?}", form);
|
||||
|
||||
|
||||
// In a real application, this would perform a token swap in the database
|
||||
// For now, we'll just redirect back to the DeFi dashboard with a success message
|
||||
|
||||
let success_message = format!("Successfully swapped {} {} to {}",
|
||||
form.from_amount, form.from_token, form.to_token);
|
||||
|
||||
|
||||
let success_message = format!(
|
||||
"Successfully swapped {} {} to {}",
|
||||
form.from_amount, form.from_token, form.to_token
|
||||
);
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Process collateral position creation
|
||||
pub async fn create_collateral(_tmpl: web::Data<Tera>, form: web::Form<CollateralForm>) -> Result<HttpResponse> {
|
||||
pub async fn create_collateral(
|
||||
_tmpl: web::Data<Tera>,
|
||||
form: web::Form<CollateralForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing collateral creation: {:?}", form);
|
||||
|
||||
|
||||
// In a real application, this would create a collateral position in the database
|
||||
// For now, we'll just redirect back to the DeFi dashboard with a success message
|
||||
|
||||
|
||||
let purpose_str = match form.purpose.as_str() {
|
||||
"funds" => "secure a funds",
|
||||
"synthetic" => "generate synthetic assets",
|
||||
"leverage" => "leverage trading",
|
||||
_ => "collateralization",
|
||||
};
|
||||
|
||||
let success_message = format!("Successfully collateralized {} {} for {}",
|
||||
form.amount, form.asset_id, purpose_str);
|
||||
|
||||
|
||||
let success_message = format!(
|
||||
"Successfully collateralized {} {} for {}",
|
||||
form.amount, form.asset_id, purpose_str
|
||||
);
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("/defi?success={}", urlencoding::encode(&success_message)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Helper method to get DeFi statistics
|
||||
fn get_defi_stats() -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut stats = serde_json::Map::new();
|
||||
|
||||
|
||||
// Handle Option<Number> by unwrapping with expect
|
||||
stats.insert("total_value_locked".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(1250000.0).expect("Valid float")));
|
||||
stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")));
|
||||
stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")));
|
||||
stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12)));
|
||||
stats.insert("active_stakers".to_string(), serde_json::Value::Number(serde_json::Number::from(156)));
|
||||
stats.insert("total_swap_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")));
|
||||
|
||||
stats.insert(
|
||||
"total_value_locked".to_string(),
|
||||
serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(1250000.0).expect("Valid float"),
|
||||
),
|
||||
);
|
||||
stats.insert(
|
||||
"providing_volume".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")),
|
||||
);
|
||||
stats.insert(
|
||||
"receiving_volume".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")),
|
||||
);
|
||||
stats.insert(
|
||||
"liquidity_pools_count".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(12)),
|
||||
);
|
||||
stats.insert(
|
||||
"active_stakers".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(156)),
|
||||
);
|
||||
stats.insert(
|
||||
"total_swap_volume".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")),
|
||||
);
|
||||
|
||||
stats
|
||||
}
|
||||
|
||||
|
||||
// Helper method to convert Asset to a JSON object for templates
|
||||
fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut map = serde_json::Map::new();
|
||||
|
||||
map.insert("id".to_string(), serde_json::Value::String(asset.id.clone()));
|
||||
map.insert("name".to_string(), serde_json::Value::String(asset.name.clone()));
|
||||
map.insert("description".to_string(), serde_json::Value::String(asset.description.clone()));
|
||||
map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string()));
|
||||
map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string()));
|
||||
|
||||
|
||||
map.insert(
|
||||
"id".to_string(),
|
||||
serde_json::Value::String(asset.id.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"name".to_string(),
|
||||
serde_json::Value::String(asset.name.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String(asset.description.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"asset_type".to_string(),
|
||||
serde_json::Value::String(asset.asset_type.as_str().to_string()),
|
||||
);
|
||||
map.insert(
|
||||
"status".to_string(),
|
||||
serde_json::Value::String(asset.status.as_str().to_string()),
|
||||
);
|
||||
|
||||
// Add current valuation
|
||||
if let Some(latest) = asset.latest_valuation() {
|
||||
if let Some(num) = serde_json::Number::from_f64(latest.value) {
|
||||
map.insert("current_valuation".to_string(), serde_json::Value::Number(num));
|
||||
map.insert(
|
||||
"current_valuation".to_string(),
|
||||
serde_json::Value::Number(num),
|
||||
);
|
||||
} else {
|
||||
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
|
||||
map.insert(
|
||||
"current_valuation".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
}
|
||||
map.insert("valuation_currency".to_string(), serde_json::Value::String(latest.currency.clone()));
|
||||
map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()));
|
||||
map.insert(
|
||||
"valuation_currency".to_string(),
|
||||
serde_json::Value::String(latest.currency.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"valuation_date".to_string(),
|
||||
serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()),
|
||||
);
|
||||
} else {
|
||||
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
|
||||
map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string()));
|
||||
map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".to_string()));
|
||||
map.insert(
|
||||
"current_valuation".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
map.insert(
|
||||
"valuation_currency".to_string(),
|
||||
serde_json::Value::String("USD".to_string()),
|
||||
);
|
||||
map.insert(
|
||||
"valuation_date".to_string(),
|
||||
serde_json::Value::String("N/A".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
|
||||
// Generate mock assets for testing
|
||||
fn get_mock_assets() -> Vec<Asset> {
|
||||
// Reuse the asset controller's mock data function
|
||||
|
@ -609,6 +609,7 @@ impl FlowController {
|
||||
|
||||
/// Form for creating a new flow
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct FlowForm {
|
||||
/// Flow name
|
||||
pub name: String,
|
||||
@ -620,6 +621,7 @@ pub struct FlowForm {
|
||||
|
||||
/// Form for marking a step as stuck
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct StuckForm {
|
||||
/// Reason for being stuck
|
||||
pub reason: String,
|
||||
@ -627,6 +629,7 @@ pub struct StuckForm {
|
||||
|
||||
/// Form for adding a log to a step
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LogForm {
|
||||
/// Log message
|
||||
pub message: String,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -96,6 +96,7 @@ impl HomeController {
|
||||
|
||||
/// Represents the data submitted in the contact form
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ContactForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
|
@ -1,12 +1,11 @@
|
||||
use actix_web::{web, HttpResponse, Result, http};
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use actix_web::{HttpResponse, Result, http, web};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
use crate::models::asset::{Asset, AssetType, AssetStatus};
|
||||
use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
|
||||
use crate::controllers::asset::AssetController;
|
||||
use crate::models::asset::{Asset, AssetStatus, AssetType};
|
||||
use crate::models::marketplace::{Listing, ListingStatus, ListingType, MarketplaceStatistics};
|
||||
use crate::utils::render_template;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -22,6 +21,7 @@ pub struct ListingForm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct BidForm {
|
||||
pub amount: f64,
|
||||
pub currency: String,
|
||||
@ -38,30 +38,33 @@ impl MarketplaceController {
|
||||
// Display the marketplace dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let listings = Self::get_mock_listings();
|
||||
let stats = MarketplaceStatistics::new(&listings);
|
||||
|
||||
|
||||
// Get featured listings (up to 4)
|
||||
let featured_listings: Vec<&Listing> = listings.iter()
|
||||
let featured_listings: Vec<&Listing> = listings
|
||||
.iter()
|
||||
.filter(|l| l.featured && l.status == ListingStatus::Active)
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
|
||||
// Get recent listings (up to 8)
|
||||
let mut recent_listings: Vec<&Listing> = listings.iter()
|
||||
let mut recent_listings: Vec<&Listing> = listings
|
||||
.iter()
|
||||
.filter(|l| l.status == ListingStatus::Active)
|
||||
.collect();
|
||||
|
||||
|
||||
// Sort by created_at (newest first)
|
||||
recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
|
||||
|
||||
|
||||
// Get recent sales (up to 5)
|
||||
let mut recent_sales: Vec<&Listing> = listings.iter()
|
||||
let mut recent_sales: Vec<&Listing> = listings
|
||||
.iter()
|
||||
.filter(|l| l.status == ListingStatus::Sold)
|
||||
.collect();
|
||||
|
||||
|
||||
// Sort by sold_at (newest first)
|
||||
recent_sales.sort_by(|a, b| {
|
||||
let a_sold = a.sold_at.unwrap_or(a.created_at);
|
||||
@ -69,88 +72,101 @@ impl MarketplaceController {
|
||||
b_sold.cmp(&a_sold)
|
||||
});
|
||||
let recent_sales = recent_sales.into_iter().take(5).collect::<Vec<_>>();
|
||||
|
||||
|
||||
// Add data to context
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("stats", &stats);
|
||||
context.insert("featured_listings", &featured_listings);
|
||||
context.insert("recent_listings", &recent_listings);
|
||||
context.insert("recent_sales", &recent_sales);
|
||||
|
||||
|
||||
render_template(&tmpl, "marketplace/index.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display all marketplace listings
|
||||
pub async fn list_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let listings = Self::get_mock_listings();
|
||||
|
||||
|
||||
// Filter active listings
|
||||
let active_listings: Vec<&Listing> = listings.iter()
|
||||
let active_listings: Vec<&Listing> = listings
|
||||
.iter()
|
||||
.filter(|l| l.status == ListingStatus::Active)
|
||||
.collect();
|
||||
|
||||
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("listings", &active_listings);
|
||||
context.insert("listing_types", &[
|
||||
ListingType::FixedPrice.as_str(),
|
||||
ListingType::Auction.as_str(),
|
||||
ListingType::Exchange.as_str(),
|
||||
]);
|
||||
context.insert("asset_types", &[
|
||||
AssetType::Token.as_str(),
|
||||
AssetType::Artwork.as_str(),
|
||||
AssetType::RealEstate.as_str(),
|
||||
AssetType::IntellectualProperty.as_str(),
|
||||
AssetType::Commodity.as_str(),
|
||||
AssetType::Share.as_str(),
|
||||
AssetType::Bond.as_str(),
|
||||
AssetType::Other.as_str(),
|
||||
]);
|
||||
|
||||
context.insert(
|
||||
"listing_types",
|
||||
&[
|
||||
ListingType::FixedPrice.as_str(),
|
||||
ListingType::Auction.as_str(),
|
||||
ListingType::Exchange.as_str(),
|
||||
],
|
||||
);
|
||||
context.insert(
|
||||
"asset_types",
|
||||
&[
|
||||
AssetType::Token.as_str(),
|
||||
AssetType::Artwork.as_str(),
|
||||
AssetType::RealEstate.as_str(),
|
||||
AssetType::IntellectualProperty.as_str(),
|
||||
AssetType::Commodity.as_str(),
|
||||
AssetType::Share.as_str(),
|
||||
AssetType::Bond.as_str(),
|
||||
AssetType::Other.as_str(),
|
||||
],
|
||||
);
|
||||
|
||||
render_template(&tmpl, "marketplace/listings.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display my listings
|
||||
pub async fn my_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let listings = Self::get_mock_listings();
|
||||
|
||||
|
||||
// Filter by current user (mock user ID)
|
||||
let user_id = "user-123";
|
||||
let my_listings: Vec<&Listing> = listings.iter()
|
||||
.filter(|l| l.seller_id == user_id)
|
||||
.collect();
|
||||
|
||||
let my_listings: Vec<&Listing> =
|
||||
listings.iter().filter(|l| l.seller_id == user_id).collect();
|
||||
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("listings", &my_listings);
|
||||
|
||||
|
||||
render_template(&tmpl, "marketplace/my_listings.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Display listing details
|
||||
pub async fn listing_detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
pub async fn listing_detail(
|
||||
tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let listing_id = path.into_inner();
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
let listings = Self::get_mock_listings();
|
||||
|
||||
|
||||
// Find the listing
|
||||
let listing = listings.iter().find(|l| l.id == listing_id);
|
||||
|
||||
|
||||
if let Some(listing) = listing {
|
||||
// Get similar listings (same asset type, active)
|
||||
let similar_listings: Vec<&Listing> = listings.iter()
|
||||
.filter(|l| l.asset_type == listing.asset_type &&
|
||||
l.status == ListingStatus::Active &&
|
||||
l.id != listing.id)
|
||||
let similar_listings: Vec<&Listing> = listings
|
||||
.iter()
|
||||
.filter(|l| {
|
||||
l.asset_type == listing.asset_type
|
||||
&& l.status == ListingStatus::Active
|
||||
&& l.id != listing.id
|
||||
})
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
|
||||
// Get highest bid amount and minimum bid for auction listings
|
||||
let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction {
|
||||
let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction
|
||||
{
|
||||
if let Some(bid) = listing.highest_bid() {
|
||||
(Some(bid.amount), bid.amount + 1.0)
|
||||
} else {
|
||||
@ -159,74 +175,79 @@ impl MarketplaceController {
|
||||
} else {
|
||||
(None, 0.0)
|
||||
};
|
||||
|
||||
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("listing", listing);
|
||||
context.insert("similar_listings", &similar_listings);
|
||||
context.insert("highest_bid_amount", &highest_bid_amount);
|
||||
context.insert("minimum_bid", &minimum_bid);
|
||||
|
||||
|
||||
// Add current user info for bid/purchase forms
|
||||
let user_id = "user-123";
|
||||
let user_name = "Alice Hostly";
|
||||
context.insert("user_id", &user_id);
|
||||
context.insert("user_name", &user_name);
|
||||
|
||||
|
||||
render_template(&tmpl, "marketplace/listing_detail.html", &context)
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().finish())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Display create listing form
|
||||
pub async fn create_listing_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
|
||||
// Get user's assets for selection
|
||||
let assets = AssetController::get_mock_assets();
|
||||
let user_id = "user-123"; // Mock user ID
|
||||
|
||||
let user_assets: Vec<&Asset> = assets.iter()
|
||||
|
||||
let user_assets: Vec<&Asset> = assets
|
||||
.iter()
|
||||
.filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
|
||||
.collect();
|
||||
|
||||
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("assets", &user_assets);
|
||||
context.insert("listing_types", &[
|
||||
ListingType::FixedPrice.as_str(),
|
||||
ListingType::Auction.as_str(),
|
||||
ListingType::Exchange.as_str(),
|
||||
]);
|
||||
|
||||
context.insert(
|
||||
"listing_types",
|
||||
&[
|
||||
ListingType::FixedPrice.as_str(),
|
||||
ListingType::Auction.as_str(),
|
||||
ListingType::Exchange.as_str(),
|
||||
],
|
||||
);
|
||||
|
||||
render_template(&tmpl, "marketplace/create_listing.html", &context)
|
||||
}
|
||||
|
||||
|
||||
// Create a new listing
|
||||
pub async fn create_listing(
|
||||
tmpl: web::Data<Tera>,
|
||||
form: web::Form<ListingForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
|
||||
|
||||
// Get the asset details
|
||||
let assets = AssetController::get_mock_assets();
|
||||
let asset = assets.iter().find(|a| a.id == form.asset_id);
|
||||
|
||||
|
||||
if let Some(asset) = asset {
|
||||
// Process tags
|
||||
let tags = match form.tags {
|
||||
Some(tags_str) => tags_str.split(',')
|
||||
Some(tags_str) => tags_str
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
|
||||
// Calculate expiration date if provided
|
||||
let expires_at = form.duration_days.map(|days| {
|
||||
Utc::now() + Duration::days(days as i64)
|
||||
});
|
||||
|
||||
let expires_at = form
|
||||
.duration_days
|
||||
.map(|days| Utc::now() + Duration::days(days as i64));
|
||||
|
||||
// Parse listing type
|
||||
let listing_type = match form.listing_type.as_str() {
|
||||
"Fixed Price" => ListingType::FixedPrice,
|
||||
@ -234,11 +255,11 @@ impl MarketplaceController {
|
||||
"Exchange" => ListingType::Exchange,
|
||||
_ => ListingType::FixedPrice,
|
||||
};
|
||||
|
||||
|
||||
// Mock user data
|
||||
let user_id = "user-123";
|
||||
let user_name = "Alice Hostly";
|
||||
|
||||
|
||||
// Create the listing
|
||||
let _listing = Listing::new(
|
||||
form.title,
|
||||
@ -255,9 +276,9 @@ impl MarketplaceController {
|
||||
tags,
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
// In a real application, we would save the listing to a database here
|
||||
|
||||
|
||||
// Redirect to the marketplace
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.insert_header((http::header::LOCATION, "/marketplace"))
|
||||
@ -267,94 +288,101 @@ impl MarketplaceController {
|
||||
let mut context = Context::new();
|
||||
context.insert("active_page", &"marketplace");
|
||||
context.insert("error", &"Asset not found");
|
||||
|
||||
|
||||
render_template(&tmpl, "marketplace/create_listing.html", &context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Submit a bid on an auction listing
|
||||
#[allow(dead_code)]
|
||||
pub async fn submit_bid(
|
||||
tmpl: web::Data<Tera>,
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
form: web::Form<BidForm>,
|
||||
_form: web::Form<BidForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
let listing_id = path.into_inner();
|
||||
let form = form.into_inner();
|
||||
|
||||
let _form = _form.into_inner();
|
||||
|
||||
// In a real application, we would:
|
||||
// 1. Find the listing in the database
|
||||
// 2. Validate the bid
|
||||
// 3. Create the bid
|
||||
// 4. Save it to the database
|
||||
|
||||
|
||||
// For now, we'll just redirect back to the listing
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
|
||||
.insert_header((
|
||||
http::header::LOCATION,
|
||||
format!("/marketplace/{}", listing_id),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Purchase a fixed-price listing
|
||||
pub async fn purchase_listing(
|
||||
tmpl: web::Data<Tera>,
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
form: web::Form<PurchaseForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
let listing_id = path.into_inner();
|
||||
let form = form.into_inner();
|
||||
|
||||
|
||||
if !form.agree_to_terms {
|
||||
// User must agree to terms
|
||||
return Ok(HttpResponse::SeeOther()
|
||||
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
|
||||
.insert_header((
|
||||
http::header::LOCATION,
|
||||
format!("/marketplace/{}", listing_id),
|
||||
))
|
||||
.finish());
|
||||
}
|
||||
|
||||
|
||||
// In a real application, we would:
|
||||
// 1. Find the listing in the database
|
||||
// 2. Validate the purchase
|
||||
// 3. Process the transaction
|
||||
// 4. Update the listing status
|
||||
// 5. Transfer the asset
|
||||
|
||||
|
||||
// For now, we'll just redirect to the marketplace
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.insert_header((http::header::LOCATION, "/marketplace"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Cancel a listing
|
||||
pub async fn cancel_listing(
|
||||
tmpl: web::Data<Tera>,
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let _listing_id = path.into_inner();
|
||||
|
||||
|
||||
// In a real application, we would:
|
||||
// 1. Find the listing in the database
|
||||
// 2. Validate that the current user is the seller
|
||||
// 3. Update the listing status
|
||||
|
||||
|
||||
// For now, we'll just redirect to my listings
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.insert_header((http::header::LOCATION, "/marketplace/my"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
|
||||
// Generate mock listings for development
|
||||
pub fn get_mock_listings() -> Vec<Listing> {
|
||||
let assets = AssetController::get_mock_assets();
|
||||
let mut listings = Vec::new();
|
||||
|
||||
|
||||
// Mock user data
|
||||
let user_ids = vec!["user-123", "user-456", "user-789"];
|
||||
let user_names = vec!["Alice Hostly", "Ethan Cloudman", "Priya Servera"];
|
||||
|
||||
|
||||
// Create some fixed price listings
|
||||
for i in 0..6 {
|
||||
let asset_index = i % assets.len();
|
||||
let asset = &assets[asset_index];
|
||||
let user_index = i % user_ids.len();
|
||||
|
||||
|
||||
let price = match asset.asset_type {
|
||||
AssetType::Token => 50.0 + (i as f64 * 10.0),
|
||||
AssetType::Artwork => 500.0 + (i as f64 * 100.0),
|
||||
@ -365,10 +393,13 @@ impl MarketplaceController {
|
||||
AssetType::Bond => 1500.0 + (i as f64 * 300.0),
|
||||
AssetType::Other => 800.0 + (i as f64 * 150.0),
|
||||
};
|
||||
|
||||
|
||||
let mut listing = Listing::new(
|
||||
format!("{} for Sale", asset.name),
|
||||
format!("This is a great opportunity to own {}. {}", asset.name, asset.description),
|
||||
format!(
|
||||
"This is a great opportunity to own {}. {}",
|
||||
asset.name, asset.description
|
||||
),
|
||||
asset.id.clone(),
|
||||
asset.name.clone(),
|
||||
asset.asset_type.clone(),
|
||||
@ -381,21 +412,21 @@ impl MarketplaceController {
|
||||
vec!["digital".to_string(), "asset".to_string()],
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
// Make some listings featured
|
||||
if i % 5 == 0 {
|
||||
listing.set_featured(true);
|
||||
}
|
||||
|
||||
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
|
||||
// Create some auction listings
|
||||
for i in 0..4 {
|
||||
let asset_index = (i + 6) % assets.len();
|
||||
let asset = &assets[asset_index];
|
||||
let user_index = i % user_ids.len();
|
||||
|
||||
|
||||
let starting_price = match asset.asset_type {
|
||||
AssetType::Token => 40.0 + (i as f64 * 5.0),
|
||||
AssetType::Artwork => 400.0 + (i as f64 * 50.0),
|
||||
@ -406,7 +437,7 @@ impl MarketplaceController {
|
||||
AssetType::Bond => 1200.0 + (i as f64 * 250.0),
|
||||
AssetType::Other => 600.0 + (i as f64 * 120.0),
|
||||
};
|
||||
|
||||
|
||||
let mut listing = Listing::new(
|
||||
format!("Auction: {}", asset.name),
|
||||
format!("Bid on this amazing {}. {}", asset.name, asset.description),
|
||||
@ -422,12 +453,13 @@ impl MarketplaceController {
|
||||
vec!["auction".to_string(), "bidding".to_string()],
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
// Add some bids to the auctions
|
||||
let num_bids = 2 + (i % 3);
|
||||
for j in 0..num_bids {
|
||||
let bidder_index = (j + 1) % user_ids.len();
|
||||
if bidder_index != user_index { // Ensure seller isn't bidding
|
||||
if bidder_index != user_index {
|
||||
// Ensure seller isn't bidding
|
||||
let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
|
||||
let _ = listing.add_bid(
|
||||
user_ids[bidder_index].to_string(),
|
||||
@ -437,21 +469,21 @@ impl MarketplaceController {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make some listings featured
|
||||
if i % 3 == 0 {
|
||||
listing.set_featured(true);
|
||||
}
|
||||
|
||||
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
|
||||
// Create some exchange listings
|
||||
for i in 0..3 {
|
||||
let asset_index = (i + 10) % assets.len();
|
||||
let asset = &assets[asset_index];
|
||||
let user_index = i % user_ids.len();
|
||||
|
||||
|
||||
let value = match asset.asset_type {
|
||||
AssetType::Token => 60.0 + (i as f64 * 15.0),
|
||||
AssetType::Artwork => 600.0 + (i as f64 * 150.0),
|
||||
@ -462,33 +494,36 @@ impl MarketplaceController {
|
||||
AssetType::Bond => 1800.0 + (i as f64 * 350.0),
|
||||
AssetType::Other => 1000.0 + (i as f64 * 200.0),
|
||||
};
|
||||
|
||||
|
||||
let listing = Listing::new(
|
||||
format!("Trade: {}", asset.name),
|
||||
format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name),
|
||||
format!(
|
||||
"Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.",
|
||||
asset.name
|
||||
),
|
||||
asset.id.clone(),
|
||||
asset.name.clone(),
|
||||
asset.asset_type.clone(),
|
||||
user_ids[user_index].to_string(),
|
||||
user_names[user_index].to_string(),
|
||||
value, // Estimated value for exchange
|
||||
value, // Estimated value for exchange
|
||||
"USD".to_string(),
|
||||
ListingType::Exchange,
|
||||
Some(Utc::now() + Duration::days(60)),
|
||||
vec!["exchange".to_string(), "trade".to_string()],
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
|
||||
// Create some sold listings
|
||||
for i in 0..5 {
|
||||
let asset_index = (i + 13) % assets.len();
|
||||
let asset = &assets[asset_index];
|
||||
let seller_index = i % user_ids.len();
|
||||
let buyer_index = (i + 1) % user_ids.len();
|
||||
|
||||
|
||||
let price = match asset.asset_type {
|
||||
AssetType::Token => 55.0 + (i as f64 * 12.0),
|
||||
AssetType::Artwork => 550.0 + (i as f64 * 120.0),
|
||||
@ -499,9 +534,9 @@ impl MarketplaceController {
|
||||
AssetType::Bond => 1650.0 + (i as f64 * 330.0),
|
||||
AssetType::Other => 900.0 + (i as f64 * 180.0),
|
||||
};
|
||||
|
||||
let sale_price = price * 0.95; // Slight discount on sale
|
||||
|
||||
|
||||
let sale_price = price * 0.95; // Slight discount on sale
|
||||
|
||||
let mut listing = Listing::new(
|
||||
format!("{} - SOLD", asset.name),
|
||||
format!("This {} was sold recently.", asset.name),
|
||||
@ -517,27 +552,27 @@ impl MarketplaceController {
|
||||
vec!["sold".to_string()],
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
// Mark as sold
|
||||
let _ = listing.mark_as_sold(
|
||||
user_ids[buyer_index].to_string(),
|
||||
user_names[buyer_index].to_string(),
|
||||
sale_price,
|
||||
);
|
||||
|
||||
|
||||
// Set sold date to be sometime in the past
|
||||
let days_ago = i as i64 + 1;
|
||||
listing.sold_at = Some(Utc::now() - Duration::days(days_ago));
|
||||
|
||||
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
|
||||
// Create a few cancelled listings
|
||||
for i in 0..2 {
|
||||
let asset_index = (i + 18) % assets.len();
|
||||
let asset = &assets[asset_index];
|
||||
let user_index = i % user_ids.len();
|
||||
|
||||
|
||||
let price = match asset.asset_type {
|
||||
AssetType::Token => 45.0 + (i as f64 * 8.0),
|
||||
AssetType::Artwork => 450.0 + (i as f64 * 80.0),
|
||||
@ -548,7 +583,7 @@ impl MarketplaceController {
|
||||
AssetType::Bond => 1350.0 + (i as f64 * 270.0),
|
||||
AssetType::Other => 750.0 + (i as f64 * 150.0),
|
||||
};
|
||||
|
||||
|
||||
let mut listing = Listing::new(
|
||||
format!("{} - Cancelled", asset.name),
|
||||
format!("This listing for {} was cancelled.", asset.name),
|
||||
@ -564,13 +599,13 @@ impl MarketplaceController {
|
||||
vec!["cancelled".to_string()],
|
||||
asset.image_url.clone(),
|
||||
);
|
||||
|
||||
|
||||
// Cancel the listing
|
||||
let _ = listing.cancel();
|
||||
|
||||
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
|
||||
listings
|
||||
}
|
||||
}
|
||||
|
360
actix_mvc_app/src/db/calendar.rs
Normal file
360
actix_mvc_app/src/db/calendar.rs
Normal file
@ -0,0 +1,360 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels::{
|
||||
db::{Collection, Db},
|
||||
models::calendar::{AttendanceStatus, Attendee, Calendar, Event},
|
||||
};
|
||||
|
||||
use super::db::get_db;
|
||||
|
||||
/// Creates a new calendar and saves it to the database. Returns the saved calendar and its ID.
|
||||
pub fn create_new_calendar(
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
owner_id: Option<u32>,
|
||||
is_public: bool,
|
||||
color: Option<&str>,
|
||||
) -> Result<(u32, Calendar), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
|
||||
// Create a new calendar (with auto-generated ID)
|
||||
let mut calendar = Calendar::new(None, name);
|
||||
|
||||
if let Some(desc) = description {
|
||||
calendar = calendar.description(desc);
|
||||
}
|
||||
if let Some(owner) = owner_id {
|
||||
calendar = calendar.owner_id(owner);
|
||||
}
|
||||
if let Some(col) = color {
|
||||
calendar = calendar.color(col);
|
||||
}
|
||||
|
||||
calendar = calendar.is_public(is_public);
|
||||
|
||||
// Save the calendar to the database
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.expect("can open calendar collection");
|
||||
let (calendar_id, saved_calendar) = collection.set(&calendar).expect("can save calendar");
|
||||
|
||||
Ok((calendar_id, saved_calendar))
|
||||
}
|
||||
|
||||
/// Creates a new event and saves it to the database. Returns the saved event and its ID.
|
||||
pub fn create_new_event(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
location: Option<&str>,
|
||||
color: Option<&str>,
|
||||
all_day: bool,
|
||||
created_by: Option<u32>,
|
||||
category: Option<&str>,
|
||||
reminder_minutes: Option<i32>,
|
||||
) -> Result<(u32, Event), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
|
||||
// Create a new event (with auto-generated ID)
|
||||
let mut event = Event::new(title, start_time, end_time);
|
||||
|
||||
if let Some(desc) = description {
|
||||
event = event.description(desc);
|
||||
}
|
||||
if let Some(loc) = location {
|
||||
event = event.location(loc);
|
||||
}
|
||||
if let Some(col) = color {
|
||||
event = event.color(col);
|
||||
}
|
||||
if let Some(user_id) = created_by {
|
||||
event = event.created_by(user_id);
|
||||
}
|
||||
if let Some(cat) = category {
|
||||
event = event.category(cat);
|
||||
}
|
||||
if let Some(reminder) = reminder_minutes {
|
||||
event = event.reminder_minutes(reminder);
|
||||
}
|
||||
|
||||
event = event.all_day(all_day);
|
||||
|
||||
// Save the event to the database
|
||||
let collection = db.collection::<Event>().expect("can open event collection");
|
||||
let (event_id, saved_event) = collection.set(&event).expect("can save event");
|
||||
|
||||
Ok((event_id, saved_event))
|
||||
}
|
||||
|
||||
/// Loads all calendars from the database and returns them as a Vec<Calendar>.
|
||||
pub fn get_calendars() -> Result<Vec<Calendar>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.expect("can open calendar collection");
|
||||
|
||||
// Try to load all calendars, but handle deserialization errors gracefully
|
||||
let calendars = match collection.get_all() {
|
||||
Ok(calendars) => calendars,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading calendars: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
Ok(calendars)
|
||||
}
|
||||
|
||||
/// Loads all events from the database and returns them as a Vec<Event>.
|
||||
pub fn get_events() -> Result<Vec<Event>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db.collection::<Event>().expect("can open event collection");
|
||||
|
||||
// Try to load all events, but handle deserialization errors gracefully
|
||||
let events = match collection.get_all() {
|
||||
Ok(events) => events,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading events: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Fetches a single calendar by its ID from the database.
|
||||
pub fn get_calendar_by_id(calendar_id: u32) -> Result<Option<Calendar>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
match collection.get_by_id(calendar_id) {
|
||||
Ok(calendar) => Ok(calendar),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching calendar by id {}: {:?}", calendar_id, e);
|
||||
Err(format!("Failed to fetch calendar: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches a single event by its ID from the database.
|
||||
pub fn get_event_by_id(event_id: u32) -> Result<Option<Event>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Event>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
match collection.get_by_id(event_id) {
|
||||
Ok(event) => Ok(event),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching event by id {}: {:?}", event_id, e);
|
||||
Err(format!("Failed to fetch event: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new attendee and saves it to the database. Returns the saved attendee and its ID.
|
||||
pub fn create_new_attendee(
|
||||
contact_id: u32,
|
||||
status: AttendanceStatus,
|
||||
) -> Result<(u32, Attendee), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
|
||||
// Create a new attendee (with auto-generated ID)
|
||||
let attendee = Attendee::new(contact_id).status(status);
|
||||
|
||||
// Save the attendee to the database
|
||||
let collection = db
|
||||
.collection::<Attendee>()
|
||||
.expect("can open attendee collection");
|
||||
let (attendee_id, saved_attendee) = collection.set(&attendee).expect("can save attendee");
|
||||
|
||||
Ok((attendee_id, saved_attendee))
|
||||
}
|
||||
|
||||
/// Fetches a single attendee by its ID from the database.
|
||||
pub fn get_attendee_by_id(attendee_id: u32) -> Result<Option<Attendee>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Attendee>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
match collection.get_by_id(attendee_id) {
|
||||
Ok(attendee) => Ok(attendee),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching attendee by id {}: {:?}", attendee_id, e);
|
||||
Err(format!("Failed to fetch attendee: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates attendee status in the database and returns the updated attendee.
|
||||
pub fn update_attendee_status(
|
||||
attendee_id: u32,
|
||||
status: AttendanceStatus,
|
||||
) -> Result<Attendee, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Attendee>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut attendee) = collection
|
||||
.get_by_id(attendee_id)
|
||||
.map_err(|e| format!("Failed to fetch attendee: {:?}", e))?
|
||||
{
|
||||
attendee = attendee.status(status);
|
||||
let (_, updated_attendee) = collection
|
||||
.set(&attendee)
|
||||
.map_err(|e| format!("Failed to update attendee: {:?}", e))?;
|
||||
Ok(updated_attendee)
|
||||
} else {
|
||||
Err("Attendee not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add attendee to event
|
||||
pub fn add_attendee_to_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Event>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut event) = collection
|
||||
.get_by_id(event_id)
|
||||
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
|
||||
{
|
||||
event = event.add_attendee(attendee_id);
|
||||
let (_, updated_event) = collection
|
||||
.set(&event)
|
||||
.map_err(|e| format!("Failed to update event: {:?}", e))?;
|
||||
Ok(updated_event)
|
||||
} else {
|
||||
Err("Event not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove attendee from event
|
||||
pub fn remove_attendee_from_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Event>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut event) = collection
|
||||
.get_by_id(event_id)
|
||||
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
|
||||
{
|
||||
event = event.remove_attendee(attendee_id);
|
||||
let (_, updated_event) = collection
|
||||
.set(&event)
|
||||
.map_err(|e| format!("Failed to update event: {:?}", e))?;
|
||||
Ok(updated_event)
|
||||
} else {
|
||||
Err("Event not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add event to calendar
|
||||
pub fn add_event_to_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut calendar) = collection
|
||||
.get_by_id(calendar_id)
|
||||
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
|
||||
{
|
||||
calendar = calendar.add_event(event_id as i64);
|
||||
let (_, updated_calendar) = collection
|
||||
.set(&calendar)
|
||||
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
|
||||
Ok(updated_calendar)
|
||||
} else {
|
||||
Err("Calendar not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove event from calendar
|
||||
pub fn remove_event_from_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut calendar) = collection
|
||||
.get_by_id(calendar_id)
|
||||
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
|
||||
{
|
||||
calendar = calendar.remove_event(event_id as i64);
|
||||
let (_, updated_calendar) = collection
|
||||
.set(&calendar)
|
||||
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
|
||||
Ok(updated_calendar)
|
||||
} else {
|
||||
Err("Calendar not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a calendar from the database.
|
||||
pub fn delete_calendar(calendar_id: u32) -> Result<(), String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
collection
|
||||
.delete_by_id(calendar_id)
|
||||
.map_err(|e| format!("Failed to delete calendar: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes an event from the database.
|
||||
pub fn delete_event(event_id: u32) -> Result<(), String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Event>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
collection
|
||||
.delete_by_id(event_id)
|
||||
.map_err(|e| format!("Failed to delete event: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets or creates a calendar for a user. If the user already has a calendar, returns it.
|
||||
/// If not, creates a new calendar for the user and returns it.
|
||||
pub fn get_or_create_user_calendar(user_id: u32, user_name: &str) -> Result<Calendar, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Calendar>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
// Try to find existing calendar for this user
|
||||
let calendars = match collection.get_all() {
|
||||
Ok(calendars) => calendars,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading calendars: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
|
||||
// Look for a calendar owned by this user
|
||||
for calendar in calendars {
|
||||
if let Some(owner_id) = calendar.owner_id {
|
||||
if owner_id == user_id {
|
||||
return Ok(calendar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No calendar found for this user, create a new one
|
||||
let calendar_name = format!("{}'s Calendar", user_name);
|
||||
let (_, new_calendar) = create_new_calendar(
|
||||
&calendar_name,
|
||||
Some("Personal calendar"),
|
||||
Some(user_id),
|
||||
false, // Private calendar
|
||||
Some("#4285F4"), // Default blue color
|
||||
)?;
|
||||
|
||||
Ok(new_calendar)
|
||||
}
|
17
actix_mvc_app/src/db/db.rs
Normal file
17
actix_mvc_app/src/db/db.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use heromodels::db::hero::OurDB;
|
||||
|
||||
/// The path to the database file. Change this as needed for your environment.
|
||||
pub const DB_PATH: &str = "/tmp/freezone_db";
|
||||
|
||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
|
||||
pub fn get_db() -> Result<OurDB, String> {
|
||||
let db_path = PathBuf::from(DB_PATH);
|
||||
if let Some(parent) = db_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
// Temporarily reset the database to fix the serialization issue
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
|
||||
Ok(db)
|
||||
}
|
257
actix_mvc_app/src/db/governance.rs
Normal file
257
actix_mvc_app/src/db/governance.rs
Normal file
@ -0,0 +1,257 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::{
|
||||
db::{Collection, Db},
|
||||
models::governance::{Activity, ActivityType, Proposal, ProposalStatus},
|
||||
};
|
||||
|
||||
use super::db::get_db;
|
||||
|
||||
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
|
||||
pub fn create_new_proposal(
|
||||
creator_id: &str,
|
||||
creator_name: &str,
|
||||
title: &str,
|
||||
description: &str,
|
||||
status: ProposalStatus,
|
||||
voting_start_date: Option<chrono::DateTime<Utc>>,
|
||||
voting_end_date: Option<chrono::DateTime<Utc>>,
|
||||
) -> Result<(u32, Proposal), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
|
||||
let created_at = Utc::now();
|
||||
let updated_at = created_at;
|
||||
|
||||
// Create a new proposal (with auto-generated ID)
|
||||
let proposal = Proposal::new(
|
||||
None,
|
||||
creator_id,
|
||||
creator_name,
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
created_at,
|
||||
updated_at,
|
||||
voting_start_date.unwrap_or_else(Utc::now),
|
||||
voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
|
||||
);
|
||||
// Save the proposal to the database
|
||||
let collection = db
|
||||
.collection::<Proposal>()
|
||||
.expect("can open proposal collection");
|
||||
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
|
||||
|
||||
Ok((proposal_id, saved_proposal))
|
||||
}
|
||||
|
||||
/// Loads all proposals from the database and returns them as a Vec<Proposal>.
|
||||
pub fn get_proposals() -> Result<Vec<Proposal>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Proposal>()
|
||||
.expect("can open proposal collection");
|
||||
|
||||
// Try to load all proposals, but handle deserialization errors gracefully
|
||||
let proposals = match collection.get_all() {
|
||||
Ok(props) => props,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading proposals: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
Ok(proposals)
|
||||
}
|
||||
|
||||
/// Fetches a single proposal by its ID from the database.
|
||||
pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Proposal>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
match collection.get_by_id(proposal_id) {
|
||||
Ok(proposal) => Ok(Some(proposal.expect("proposal not found"))),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching proposal by id {}: {:?}", proposal_id, e);
|
||||
Err(format!("Failed to fetch proposal: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits a vote on a proposal and returns the updated proposal
|
||||
pub fn submit_vote_on_proposal(
|
||||
proposal_id: u32,
|
||||
user_id: i32,
|
||||
vote_type: &str,
|
||||
shares_count: u32, // Default to 1 if not specified
|
||||
comment: Option<String>,
|
||||
) -> Result<Proposal, String> {
|
||||
// Get the proposal from the database
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Proposal>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
// Get the proposal
|
||||
let mut proposal = collection
|
||||
.get_by_id(proposal_id)
|
||||
.map_err(|e| format!("Failed to fetch proposal: {:?}", e))?
|
||||
.ok_or_else(|| format!("Proposal not found with ID: {}", proposal_id))?;
|
||||
|
||||
// Ensure the proposal has vote options
|
||||
// Check if the proposal already has options
|
||||
if proposal.options.is_empty() {
|
||||
// Add standard vote options if they don't exist
|
||||
proposal = proposal.add_option(1, "Approve", Some("Approve the proposal"));
|
||||
proposal = proposal.add_option(2, "Reject", Some("Reject the proposal"));
|
||||
proposal = proposal.add_option(3, "Abstain", Some("Abstain from voting"));
|
||||
}
|
||||
|
||||
// Map vote_type to option_id
|
||||
let option_id = match vote_type {
|
||||
"Yes" => 1, // Approve
|
||||
"No" => 2, // Reject
|
||||
"Abstain" => 3, // Abstain
|
||||
_ => return Err(format!("Invalid vote type: {}", vote_type)),
|
||||
};
|
||||
|
||||
// Since we're having issues with the cast_vote method, let's implement a workaround
|
||||
// that directly updates the vote count for the selected option
|
||||
|
||||
// Check if the proposal is active
|
||||
if proposal.status != ProposalStatus::Active {
|
||||
return Err(format!(
|
||||
"Cannot vote on a proposal with status: {:?}",
|
||||
proposal.status
|
||||
));
|
||||
}
|
||||
|
||||
// Check if voting period is valid
|
||||
let now = Utc::now();
|
||||
if now > proposal.vote_end_date {
|
||||
return Err("Voting period has ended".to_string());
|
||||
}
|
||||
|
||||
if now < proposal.vote_start_date {
|
||||
return Err("Voting period has not started yet".to_string());
|
||||
}
|
||||
|
||||
// Find the option and increment its count
|
||||
let mut option_found = false;
|
||||
for option in &mut proposal.options {
|
||||
if option.id == option_id {
|
||||
option.count += shares_count as i64;
|
||||
option_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !option_found {
|
||||
return Err(format!("Option with ID {} not found", option_id));
|
||||
}
|
||||
|
||||
// Record the vote in the proposal's ballots
|
||||
// We'll create a simple ballot with an auto-generated ID
|
||||
let ballot_id = proposal.ballots.len() as u32 + 1;
|
||||
|
||||
// Create a new ballot and add it to the proposal's ballots
|
||||
use heromodels::models::governance::Ballot;
|
||||
|
||||
// Use the Ballot::new constructor which handles the BaseModelData creation
|
||||
let mut ballot = Ballot::new(
|
||||
Some(ballot_id),
|
||||
user_id as u32,
|
||||
option_id,
|
||||
shares_count as i64,
|
||||
);
|
||||
|
||||
// Set the comment if provided
|
||||
ballot.comment = comment;
|
||||
|
||||
// Store the local time (EEST = UTC+3) as the vote timestamp
|
||||
// This ensures the displayed time matches the user's local time
|
||||
let utc_now = Utc::now();
|
||||
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||
let local_time = utc_now.with_timezone(&local_offset);
|
||||
|
||||
// Store the local time as a timestamp (this is what will be displayed)
|
||||
ballot.base_data.created_at = local_time.timestamp();
|
||||
|
||||
// Add the ballot to the proposal's ballots
|
||||
proposal.ballots.push(ballot);
|
||||
|
||||
// Update the proposal's updated_at timestamp
|
||||
proposal.updated_at = Utc::now();
|
||||
|
||||
// Save the updated proposal
|
||||
let (_, updated_proposal) = collection
|
||||
.set(&proposal)
|
||||
.map_err(|e| format!("Failed to save vote: {:?}", e))?;
|
||||
|
||||
Ok(updated_proposal)
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
/// Creates a new governance activity and saves it to the database using OurDB
|
||||
pub fn create_activity(
|
||||
proposal_id: u32,
|
||||
proposal_title: &str,
|
||||
creator_name: &str,
|
||||
activity_type: &ActivityType,
|
||||
) -> Result<(u32, Activity), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
let mut activity = Activity::default();
|
||||
|
||||
match activity_type {
|
||||
ActivityType::ProposalCreated => {
|
||||
activity = Activity::proposal_created(proposal_id, proposal_title, creator_name);
|
||||
}
|
||||
ActivityType::VoteCast => {
|
||||
activity = Activity::vote_cast(proposal_id, proposal_title, creator_name);
|
||||
}
|
||||
ActivityType::VotingStarted => {
|
||||
activity = Activity::voting_started(proposal_id, proposal_title);
|
||||
}
|
||||
ActivityType::VotingEnded => {
|
||||
activity = Activity::voting_ended(proposal_id, proposal_title);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the proposal to the database
|
||||
let collection = db
|
||||
.collection::<Activity>()
|
||||
.expect("can open activity collection");
|
||||
|
||||
let (proposal_id, saved_proposal) = collection.set(&activity).expect("can save proposal");
|
||||
Ok((proposal_id, saved_proposal))
|
||||
}
|
||||
|
||||
pub fn get_recent_activities() -> Result<Vec<Activity>, String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
let collection = db
|
||||
.collection::<Activity>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
let mut db_activities = collection
|
||||
.get_all()
|
||||
.map_err(|e| format!("DB fetch error: {:?}", e))?;
|
||||
|
||||
// Sort by created_at descending
|
||||
db_activities.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
// Take the top 10 most recent
|
||||
let recent_activities = db_activities.into_iter().take(10).collect();
|
||||
|
||||
Ok(recent_activities)
|
||||
}
|
||||
|
||||
pub fn get_all_activities() -> Result<Vec<Activity>, String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
let collection = db
|
||||
.collection::<Activity>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
let db_activities = collection
|
||||
.get_all()
|
||||
.map_err(|e| format!("DB fetch error: {:?}", e))?;
|
||||
|
||||
Ok(db_activities)
|
||||
}
|
4
actix_mvc_app/src/db/mod.rs
Normal file
4
actix_mvc_app/src/db/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod calendar;
|
||||
pub mod contracts;
|
||||
pub mod db;
|
||||
pub mod governance;
|
@ -1,22 +1,23 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use actix_web::middleware::Logger;
|
||||
use tera::Tera;
|
||||
use std::io;
|
||||
use std::env;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use lazy_static::lazy_static;
|
||||
use std::env;
|
||||
use std::io;
|
||||
use tera::Tera;
|
||||
|
||||
mod config;
|
||||
mod controllers;
|
||||
mod db;
|
||||
mod middleware;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod utils;
|
||||
|
||||
// Import middleware components
|
||||
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
|
||||
use utils::redis_service;
|
||||
use middleware::{JwtAuth, RequestTimer, SecurityHeaders};
|
||||
use models::initialize_mock_data;
|
||||
use utils::redis_service;
|
||||
|
||||
// Initialize lazy_static for in-memory storage
|
||||
extern crate lazy_static;
|
||||
@ -29,13 +30,13 @@ lazy_static! {
|
||||
// Create a key that's at least 64 bytes long
|
||||
"my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string()
|
||||
});
|
||||
|
||||
|
||||
// Ensure the key is at least 64 bytes
|
||||
let mut key_bytes = secret.as_bytes().to_vec();
|
||||
while key_bytes.len() < 64 {
|
||||
key_bytes.extend_from_slice(b"0123456789abcdef");
|
||||
}
|
||||
|
||||
|
||||
actix_web::cookie::Key::from(&key_bytes[0..64])
|
||||
};
|
||||
}
|
||||
@ -45,14 +46,14 @@ async fn main() -> io::Result<()> {
|
||||
// Initialize environment
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||
|
||||
|
||||
// Load configuration
|
||||
let config = config::get_config();
|
||||
|
||||
|
||||
// Check for port override from command line arguments
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut port = config.server.port;
|
||||
|
||||
|
||||
for i in 1..args.len() {
|
||||
if args[i] == "--port" && i + 1 < args.len() {
|
||||
if let Ok(p) = args[i + 1].parse::<u16>() {
|
||||
@ -61,24 +62,28 @@ async fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let bind_address = format!("{}:{}", config.server.host, port);
|
||||
|
||||
|
||||
// Initialize Redis client
|
||||
let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
// Initialize mock data for DeFi operations
|
||||
initialize_mock_data();
|
||||
log::info!("DeFi mock data initialized successfully");
|
||||
|
||||
|
||||
// Governance activity tracker is now ready to record real user activities
|
||||
log::info!("Governance activity tracker initialized and ready");
|
||||
|
||||
log::info!("Starting server at http://{}", bind_address);
|
||||
|
||||
|
||||
// Create and configure the HTTP server
|
||||
HttpServer::new(move || {
|
||||
// Initialize Tera templates
|
||||
@ -89,10 +94,10 @@ async fn main() -> io::Result<()> {
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Register custom Tera functions
|
||||
utils::register_tera_functions(&mut tera);
|
||||
|
||||
|
||||
App::new()
|
||||
// Enable logger middleware
|
||||
.wrap(Logger::default())
|
||||
|
@ -112,6 +112,7 @@ pub struct Asset {
|
||||
pub external_url: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Asset {
|
||||
/// Creates a new asset
|
||||
pub fn new(
|
||||
|
@ -1,61 +1,4 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
// No imports needed for this module currently
|
||||
|
||||
/// Represents a view mode for the calendar
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -91,4 +34,4 @@ impl CalendarViewMode {
|
||||
Self::Day => "day",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ pub struct ContractSigner {
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ContractSigner {
|
||||
/// Creates a new contract signer
|
||||
pub fn new(name: String, email: String) -> Self {
|
||||
@ -123,6 +124,7 @@ pub struct ContractRevision {
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ContractRevision {
|
||||
/// Creates a new contract revision
|
||||
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
|
||||
@ -166,6 +168,7 @@ pub struct Contract {
|
||||
pub toc: Option<Vec<TocItem>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Contract {
|
||||
/// Creates a new contract
|
||||
pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {
|
||||
|
@ -14,6 +14,7 @@ pub enum DefiPositionStatus {
|
||||
Cancelled
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DefiPositionStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -35,6 +36,7 @@ pub enum DefiPositionType {
|
||||
Collateral,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DefiPositionType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -95,6 +97,7 @@ pub struct DefiDatabase {
|
||||
receiving_positions: HashMap<String, ReceivingPosition>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DefiDatabase {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -110,6 +110,7 @@ pub struct FlowStep {
|
||||
pub logs: Vec<FlowLog>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FlowStep {
|
||||
/// Creates a new flow step
|
||||
pub fn new(name: String, description: String, order: u32) -> Self {
|
||||
@ -189,6 +190,7 @@ pub struct FlowLog {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FlowLog {
|
||||
/// Creates a new flow log
|
||||
pub fn new(message: String) -> Self {
|
||||
@ -231,6 +233,7 @@ pub struct Flow {
|
||||
pub current_step: Option<FlowStep>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Flow {
|
||||
/// Creates a new flow
|
||||
pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self {
|
||||
|
@ -1,248 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Represents the status of a governance proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ProposalStatus {
|
||||
/// Proposal is in draft status, not yet open for voting
|
||||
Draft,
|
||||
/// Proposal is active and open for voting
|
||||
Active,
|
||||
/// Proposal has been approved by the community
|
||||
Approved,
|
||||
/// Proposal has been rejected by the community
|
||||
Rejected,
|
||||
/// Proposal has been cancelled by the creator
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProposalStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProposalStatus::Draft => write!(f, "Draft"),
|
||||
ProposalStatus::Active => write!(f, "Active"),
|
||||
ProposalStatus::Approved => write!(f, "Approved"),
|
||||
ProposalStatus::Rejected => write!(f, "Rejected"),
|
||||
ProposalStatus::Cancelled => write!(f, "Cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a vote on a governance proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum VoteType {
|
||||
/// Vote in favor of the proposal
|
||||
Yes,
|
||||
/// Vote against the proposal
|
||||
No,
|
||||
/// Abstain from voting on the proposal
|
||||
Abstain,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VoteType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VoteType::Yes => write!(f, "Yes"),
|
||||
VoteType::No => write!(f, "No"),
|
||||
VoteType::Abstain => write!(f, "Abstain"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a governance proposal in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Proposal {
|
||||
/// Unique identifier for the proposal
|
||||
pub id: String,
|
||||
/// User ID of the proposal creator
|
||||
pub creator_id: i32,
|
||||
/// Name of the proposal creator
|
||||
pub creator_name: String,
|
||||
/// Title of the proposal
|
||||
pub title: String,
|
||||
/// Detailed description of the proposal
|
||||
pub description: String,
|
||||
/// Current status of the proposal
|
||||
pub status: ProposalStatus,
|
||||
/// Date and time when the proposal was created
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Date and time when the proposal was last updated
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// Date and time when voting starts
|
||||
pub voting_starts_at: Option<DateTime<Utc>>,
|
||||
/// Date and time when voting ends
|
||||
pub voting_ends_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Proposal {
|
||||
/// Creates a new proposal
|
||||
pub fn new(creator_id: i32, creator_name: String, title: String, description: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
creator_id,
|
||||
creator_name,
|
||||
title,
|
||||
description,
|
||||
status: ProposalStatus::Draft,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
voting_starts_at: None,
|
||||
voting_ends_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the proposal status
|
||||
pub fn update_status(&mut self, status: ProposalStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Sets the voting period for the proposal
|
||||
pub fn set_voting_period(&mut self, starts_at: DateTime<Utc>, ends_at: DateTime<Utc>) {
|
||||
self.voting_starts_at = Some(starts_at);
|
||||
self.voting_ends_at = Some(ends_at);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Activates the proposal for voting
|
||||
pub fn activate(&mut self) {
|
||||
self.status = ProposalStatus::Active;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Cancels the proposal
|
||||
pub fn cancel(&mut self) {
|
||||
self.status = ProposalStatus::Cancelled;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a vote cast on a proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Vote {
|
||||
/// Unique identifier for the vote
|
||||
pub id: String,
|
||||
/// ID of the proposal being voted on
|
||||
pub proposal_id: String,
|
||||
/// User ID of the voter
|
||||
pub voter_id: i32,
|
||||
/// Name of the voter
|
||||
pub voter_name: String,
|
||||
/// Type of vote cast
|
||||
pub vote_type: VoteType,
|
||||
/// Optional comment explaining the vote
|
||||
pub comment: Option<String>,
|
||||
/// Date and time when the vote was cast
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Date and time when the vote was last updated
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Vote {
|
||||
/// Creates a new vote
|
||||
pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option<String>) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
proposal_id,
|
||||
voter_id,
|
||||
voter_name,
|
||||
vote_type,
|
||||
comment,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the vote type
|
||||
pub fn update_vote(&mut self, vote_type: VoteType, comment: Option<String>) {
|
||||
self.vote_type = vote_type;
|
||||
self.comment = comment;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a filter for searching proposals
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProposalFilter {
|
||||
/// Filter by proposal status
|
||||
pub status: Option<String>,
|
||||
/// Filter by creator ID
|
||||
pub creator_id: Option<i32>,
|
||||
/// Search term for title and description
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ProposalFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: None,
|
||||
creator_id: None,
|
||||
search: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the voting results for a proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VotingResults {
|
||||
/// Proposal ID
|
||||
pub proposal_id: String,
|
||||
/// Number of yes votes
|
||||
pub yes_count: usize,
|
||||
/// Number of no votes
|
||||
pub no_count: usize,
|
||||
/// Number of abstain votes
|
||||
pub abstain_count: usize,
|
||||
/// Total number of votes
|
||||
pub total_votes: usize,
|
||||
}
|
||||
|
||||
impl VotingResults {
|
||||
/// Creates a new empty voting results object
|
||||
pub fn new(proposal_id: String) -> Self {
|
||||
Self {
|
||||
proposal_id,
|
||||
yes_count: 0,
|
||||
no_count: 0,
|
||||
abstain_count: 0,
|
||||
total_votes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a vote to the results
|
||||
pub fn add_vote(&mut self, vote_type: &VoteType) {
|
||||
match vote_type {
|
||||
VoteType::Yes => self.yes_count += 1,
|
||||
VoteType::No => self.no_count += 1,
|
||||
VoteType::Abstain => self.abstain_count += 1,
|
||||
}
|
||||
self.total_votes += 1;
|
||||
}
|
||||
|
||||
/// Calculates the percentage of yes votes
|
||||
pub fn yes_percentage(&self) -> f64 {
|
||||
if self.total_votes == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
(self.yes_count as f64 / self.total_votes as f64) * 100.0
|
||||
}
|
||||
|
||||
/// Calculates the percentage of no votes
|
||||
pub fn no_percentage(&self) -> f64 {
|
||||
if self.total_votes == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
(self.no_count as f64 / self.total_votes as f64) * 100.0
|
||||
}
|
||||
|
||||
/// Calculates the percentage of abstain votes
|
||||
pub fn abstain_percentage(&self) -> f64 {
|
||||
if self.total_votes == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
(self.abstain_count as f64 / self.total_votes as f64) * 100.0
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use crate::models::asset::AssetType;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use crate::models::asset::{Asset, AssetType};
|
||||
|
||||
/// Status of a marketplace listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@ -12,6 +12,7 @@ pub enum ListingStatus {
|
||||
Expired,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ListingStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -63,6 +64,7 @@ pub enum BidStatus {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BidStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -103,6 +105,7 @@ pub struct Listing {
|
||||
pub image_url: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Listing {
|
||||
/// Creates a new listing
|
||||
pub fn new(
|
||||
@ -150,7 +153,13 @@ impl Listing {
|
||||
}
|
||||
|
||||
/// Adds a bid to the listing
|
||||
pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> {
|
||||
pub fn add_bid(
|
||||
&mut self,
|
||||
bidder_id: String,
|
||||
bidder_name: String,
|
||||
amount: f64,
|
||||
currency: String,
|
||||
) -> Result<(), String> {
|
||||
if self.status != ListingStatus::Active {
|
||||
return Err("Listing is not active".to_string());
|
||||
}
|
||||
@ -160,7 +169,10 @@ impl Listing {
|
||||
}
|
||||
|
||||
if currency != self.currency {
|
||||
return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency));
|
||||
return Err(format!(
|
||||
"Currency mismatch: expected {}, got {}",
|
||||
self.currency, currency
|
||||
));
|
||||
}
|
||||
|
||||
// Check if bid amount is higher than current highest bid or starting price
|
||||
@ -193,13 +205,19 @@ impl Listing {
|
||||
|
||||
/// Gets the highest bid on the listing
|
||||
pub fn highest_bid(&self) -> Option<&Bid> {
|
||||
self.bids.iter()
|
||||
self.bids
|
||||
.iter()
|
||||
.filter(|b| b.status == BidStatus::Active)
|
||||
.max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
|
||||
}
|
||||
|
||||
/// Marks the listing as sold
|
||||
pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> {
|
||||
pub fn mark_as_sold(
|
||||
&mut self,
|
||||
buyer_id: String,
|
||||
buyer_name: String,
|
||||
sale_price: f64,
|
||||
) -> Result<(), String> {
|
||||
if self.status != ListingStatus::Active {
|
||||
return Err("Listing is not active".to_string());
|
||||
}
|
||||
@ -257,11 +275,13 @@ impl MarketplaceStatistics {
|
||||
let mut listings_by_type = std::collections::HashMap::new();
|
||||
let mut sales_by_asset_type = std::collections::HashMap::new();
|
||||
|
||||
let active_listings = listings.iter()
|
||||
let active_listings = listings
|
||||
.iter()
|
||||
.filter(|l| l.status == ListingStatus::Active)
|
||||
.count();
|
||||
|
||||
let sold_listings = listings.iter()
|
||||
let sold_listings = listings
|
||||
.iter()
|
||||
.filter(|l| l.status == ListingStatus::Sold)
|
||||
.count();
|
||||
|
||||
|
@ -1,17 +1,16 @@
|
||||
// Export models
|
||||
pub mod user;
|
||||
pub mod ticket;
|
||||
pub mod calendar;
|
||||
pub mod governance;
|
||||
pub mod flow;
|
||||
pub mod contract;
|
||||
pub mod asset;
|
||||
pub mod marketplace;
|
||||
pub mod calendar;
|
||||
pub mod contract;
|
||||
pub mod defi;
|
||||
pub mod flow;
|
||||
|
||||
pub mod marketplace;
|
||||
pub mod ticket;
|
||||
pub mod user;
|
||||
|
||||
// Re-export models for easier imports
|
||||
pub use calendar::CalendarViewMode;
|
||||
pub use defi::initialize_mock_data;
|
||||
pub use ticket::{Ticket, TicketComment, TicketPriority, TicketStatus};
|
||||
pub use user::User;
|
||||
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
|
||||
pub use calendar::{CalendarEvent, CalendarViewMode};
|
||||
pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
|
||||
pub use defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB, initialize_mock_data};
|
||||
|
@ -76,6 +76,7 @@ pub struct Ticket {
|
||||
pub assigned_to: Option<i32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Ticket {
|
||||
/// Creates a new ticket
|
||||
pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self {
|
||||
|
@ -4,6 +4,7 @@ use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
|
||||
/// Represents a user in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct User {
|
||||
/// Unique identifier for the user
|
||||
pub id: Option<i32>,
|
||||
@ -31,6 +32,7 @@ pub enum UserRole {
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl User {
|
||||
/// Creates a new user with default values
|
||||
pub fn new(name: String, email: String) -> Self {
|
||||
@ -125,6 +127,7 @@ impl User {
|
||||
|
||||
/// Represents user login credentials
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LoginCredentials {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
@ -132,6 +135,7 @@ pub struct LoginCredentials {
|
||||
|
||||
/// Represents user registration data
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct RegistrationData {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
|
@ -1,28 +1,26 @@
|
||||
use actix_web::web;
|
||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
||||
use crate::controllers::home::HomeController;
|
||||
use crate::controllers::auth::AuthController;
|
||||
use crate::controllers::ticket::TicketController;
|
||||
use crate::controllers::calendar::CalendarController;
|
||||
use crate::controllers::governance::GovernanceController;
|
||||
use crate::controllers::flow::FlowController;
|
||||
use crate::controllers::contract::ContractController;
|
||||
use crate::controllers::asset::AssetController;
|
||||
use crate::controllers::marketplace::MarketplaceController;
|
||||
use crate::controllers::defi::DefiController;
|
||||
use crate::controllers::company::CompanyController;
|
||||
use crate::middleware::JwtAuth;
|
||||
use crate::SESSION_KEY;
|
||||
use crate::controllers::asset::AssetController;
|
||||
use crate::controllers::auth::AuthController;
|
||||
use crate::controllers::calendar::CalendarController;
|
||||
use crate::controllers::company::CompanyController;
|
||||
use crate::controllers::contract::ContractController;
|
||||
use crate::controllers::defi::DefiController;
|
||||
use crate::controllers::flow::FlowController;
|
||||
use crate::controllers::governance::GovernanceController;
|
||||
use crate::controllers::home::HomeController;
|
||||
use crate::controllers::marketplace::MarketplaceController;
|
||||
use crate::controllers::ticket::TicketController;
|
||||
use crate::middleware::JwtAuth;
|
||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
||||
use actix_web::web;
|
||||
|
||||
/// Configures all application routes
|
||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
// Configure session middleware with the consistent key
|
||||
let session_middleware = SessionMiddleware::builder(
|
||||
CookieSessionStore::default(),
|
||||
SESSION_KEY.clone()
|
||||
)
|
||||
.cookie_secure(false) // Set to true in production with HTTPS
|
||||
.build();
|
||||
let session_middleware =
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
|
||||
.cookie_secure(false) // Set to true in production with HTTPS
|
||||
.build();
|
||||
|
||||
// Public routes that don't require authentication
|
||||
cfg.service(
|
||||
@ -33,56 +31,98 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("/about", web::get().to(HomeController::about))
|
||||
.route("/contact", web::get().to(HomeController::contact))
|
||||
.route("/contact", web::post().to(HomeController::submit_contact))
|
||||
|
||||
// Auth routes
|
||||
.route("/login", web::get().to(AuthController::login_page))
|
||||
.route("/login", web::post().to(AuthController::login))
|
||||
.route("/register", web::get().to(AuthController::register_page))
|
||||
.route("/register", web::post().to(AuthController::register))
|
||||
.route("/logout", web::get().to(AuthController::logout))
|
||||
|
||||
// Protected routes that require authentication
|
||||
// These routes will be protected by the JwtAuth middleware in the main.rs file
|
||||
.route("/editor", web::get().to(HomeController::editor))
|
||||
|
||||
// Ticket routes
|
||||
.route("/tickets", web::get().to(TicketController::list_tickets))
|
||||
.route("/tickets/new", web::get().to(TicketController::new_ticket))
|
||||
.route("/tickets", web::post().to(TicketController::create_ticket))
|
||||
.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::post().to(TicketController::update_status))
|
||||
.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::post().to(TicketController::update_status),
|
||||
)
|
||||
.route("/my-tickets", web::get().to(TicketController::my_tickets))
|
||||
|
||||
// Calendar routes
|
||||
.route("/calendar", web::get().to(CalendarController::calendar))
|
||||
.route("/calendar/events/new", web::get().to(CalendarController::new_event))
|
||||
.route("/calendar/events", web::post().to(CalendarController::create_event))
|
||||
.route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
|
||||
|
||||
.route(
|
||||
"/calendar/events/new",
|
||||
web::get().to(CalendarController::new_event),
|
||||
)
|
||||
.route(
|
||||
"/calendar/events",
|
||||
web::post().to(CalendarController::create_event),
|
||||
)
|
||||
.route(
|
||||
"/calendar/events/{id}/delete",
|
||||
web::post().to(CalendarController::delete_event),
|
||||
)
|
||||
// Governance routes
|
||||
.route("/governance", web::get().to(GovernanceController::index))
|
||||
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
|
||||
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
|
||||
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
|
||||
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
|
||||
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
|
||||
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
|
||||
|
||||
.route(
|
||||
"/governance/proposals",
|
||||
web::get().to(GovernanceController::proposals),
|
||||
)
|
||||
.route(
|
||||
"/governance/proposals/{id}",
|
||||
web::get().to(GovernanceController::proposal_detail),
|
||||
)
|
||||
.route(
|
||||
"/governance/proposals/{id}/vote",
|
||||
web::post().to(GovernanceController::submit_vote),
|
||||
)
|
||||
.route(
|
||||
"/governance/create",
|
||||
web::get().to(GovernanceController::create_proposal_form),
|
||||
)
|
||||
.route(
|
||||
"/governance/create",
|
||||
web::post().to(GovernanceController::submit_proposal),
|
||||
)
|
||||
.route(
|
||||
"/governance/my-votes",
|
||||
web::get().to(GovernanceController::my_votes),
|
||||
)
|
||||
.route(
|
||||
"/governance/activities",
|
||||
web::get().to(GovernanceController::all_activities),
|
||||
)
|
||||
// Flow routes
|
||||
.service(
|
||||
web::scope("/flows")
|
||||
.route("", web::get().to(FlowController::index))
|
||||
.route("/list", web::get().to(FlowController::list_flows))
|
||||
.route("/{id}", web::get().to(FlowController::flow_detail))
|
||||
.route("/{id}/advance", web::post().to(FlowController::advance_flow_step))
|
||||
.route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck))
|
||||
.route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step))
|
||||
.route(
|
||||
"/{id}/advance",
|
||||
web::post().to(FlowController::advance_flow_step),
|
||||
)
|
||||
.route(
|
||||
"/{id}/stuck",
|
||||
web::post().to(FlowController::mark_flow_step_stuck),
|
||||
)
|
||||
.route(
|
||||
"/{id}/step/{step_id}/log",
|
||||
web::post().to(FlowController::add_log_to_flow_step),
|
||||
)
|
||||
.route("/create", web::get().to(FlowController::create_flow_form))
|
||||
.route("/create", web::post().to(FlowController::create_flow))
|
||||
.route("/my-flows", web::get().to(FlowController::my_flows))
|
||||
.route("/my-flows", web::get().to(FlowController::my_flows)),
|
||||
)
|
||||
|
||||
// Contract routes
|
||||
.service(
|
||||
web::scope("/contracts")
|
||||
@ -91,9 +131,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("/my", web::get().to(ContractController::my_contracts))
|
||||
.route("/{id}", web::get().to(ContractController::detail))
|
||||
.route("/create", web::get().to(ContractController::create_form))
|
||||
.route("/create", web::post().to(ContractController::create))
|
||||
.route("/create", web::post().to(ContractController::create)),
|
||||
)
|
||||
|
||||
// Asset routes
|
||||
.service(
|
||||
web::scope("/assets")
|
||||
@ -104,35 +143,72 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("/create", web::post().to(AssetController::create))
|
||||
.route("/test", web::get().to(AssetController::test))
|
||||
.route("/{id}", web::get().to(AssetController::detail))
|
||||
.route("/{id}/valuation", web::post().to(AssetController::add_valuation))
|
||||
.route("/{id}/transaction", web::post().to(AssetController::add_transaction))
|
||||
.route("/{id}/status/{status}", web::post().to(AssetController::update_status))
|
||||
.route(
|
||||
"/{id}/valuation",
|
||||
web::post().to(AssetController::add_valuation),
|
||||
)
|
||||
.route(
|
||||
"/{id}/transaction",
|
||||
web::post().to(AssetController::add_transaction),
|
||||
)
|
||||
.route(
|
||||
"/{id}/status/{status}",
|
||||
web::post().to(AssetController::update_status),
|
||||
),
|
||||
)
|
||||
|
||||
// Marketplace routes
|
||||
.service(
|
||||
web::scope("/marketplace")
|
||||
.route("", web::get().to(MarketplaceController::index))
|
||||
.route("/listings", web::get().to(MarketplaceController::list_listings))
|
||||
.route(
|
||||
"/listings",
|
||||
web::get().to(MarketplaceController::list_listings),
|
||||
)
|
||||
.route("/my", web::get().to(MarketplaceController::my_listings))
|
||||
.route("/create", web::get().to(MarketplaceController::create_listing_form))
|
||||
.route("/create", web::post().to(MarketplaceController::create_listing))
|
||||
.route("/{id}", web::get().to(MarketplaceController::listing_detail))
|
||||
.route("/{id}/bid", web::post().to(MarketplaceController::submit_bid))
|
||||
.route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing))
|
||||
.route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing))
|
||||
.route(
|
||||
"/create",
|
||||
web::get().to(MarketplaceController::create_listing_form),
|
||||
)
|
||||
.route(
|
||||
"/create",
|
||||
web::post().to(MarketplaceController::create_listing),
|
||||
)
|
||||
.route(
|
||||
"/{id}",
|
||||
web::get().to(MarketplaceController::listing_detail),
|
||||
)
|
||||
.route(
|
||||
"/{id}/bid",
|
||||
web::post().to(MarketplaceController::submit_bid),
|
||||
)
|
||||
.route(
|
||||
"/{id}/purchase",
|
||||
web::post().to(MarketplaceController::purchase_listing),
|
||||
)
|
||||
.route(
|
||||
"/{id}/cancel",
|
||||
web::post().to(MarketplaceController::cancel_listing),
|
||||
),
|
||||
)
|
||||
|
||||
// DeFi routes
|
||||
.service(
|
||||
web::scope("/defi")
|
||||
.route("", web::get().to(DefiController::index))
|
||||
.route("/providing", web::post().to(DefiController::create_providing))
|
||||
.route("/receiving", web::post().to(DefiController::create_receiving))
|
||||
.route(
|
||||
"/providing",
|
||||
web::post().to(DefiController::create_providing),
|
||||
)
|
||||
.route(
|
||||
"/receiving",
|
||||
web::post().to(DefiController::create_receiving),
|
||||
)
|
||||
.route("/liquidity", web::post().to(DefiController::add_liquidity))
|
||||
.route("/staking", web::post().to(DefiController::create_staking))
|
||||
.route("/swap", web::post().to(DefiController::swap_tokens))
|
||||
.route("/collateral", web::post().to(DefiController::create_collateral))
|
||||
.route(
|
||||
"/collateral",
|
||||
web::post().to(DefiController::create_collateral),
|
||||
),
|
||||
)
|
||||
// Company routes
|
||||
.service(
|
||||
@ -140,13 +216,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("", web::get().to(CompanyController::index))
|
||||
.route("/register", web::post().to(CompanyController::register))
|
||||
.route("/view/{id}", web::get().to(CompanyController::view_company))
|
||||
.route("/switch/{id}", web::get().to(CompanyController::switch_entity))
|
||||
)
|
||||
.route(
|
||||
"/switch/{id}",
|
||||
web::get().to(CompanyController::switch_entity),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// Keep the /protected scope for any future routes that should be under that path
|
||||
cfg.service(
|
||||
web::scope("/protected")
|
||||
.wrap(JwtAuth) // Apply JWT authentication middleware
|
||||
web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
use actix_web::{error, Error, HttpResponse};
|
||||
use actix_web::{Error, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use tera::{self, Context, Function, Tera, Value};
|
||||
use std::error::Error as StdError;
|
||||
use tera::{self, Context, Function, Tera, Value};
|
||||
|
||||
// Export modules
|
||||
pub mod redis_service;
|
||||
|
||||
// Re-export for easier imports
|
||||
pub use redis_service::RedisCalendarService;
|
||||
// pub use redis_service::RedisCalendarService; // Currently unused
|
||||
|
||||
/// Error type for template rendering
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TemplateError {
|
||||
pub message: String,
|
||||
pub details: String,
|
||||
@ -25,10 +26,16 @@ impl std::fmt::Display for TemplateError {
|
||||
|
||||
impl std::error::Error for TemplateError {}
|
||||
|
||||
/// Registers custom Tera functions
|
||||
/// Registers custom Tera functions and filters
|
||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||
tera.register_function("now", NowFunction);
|
||||
tera.register_function("format_date", FormatDateFunction);
|
||||
tera.register_function("local_time", LocalTimeFunction);
|
||||
|
||||
// Register custom filters
|
||||
tera.register_filter("format_hour", format_hour_filter);
|
||||
tera.register_filter("extract_hour", extract_hour_filter);
|
||||
tera.register_filter("format_time", format_time_filter);
|
||||
}
|
||||
|
||||
/// Tera function to get the current date/time
|
||||
@ -46,7 +53,7 @@ impl Function for NowFunction {
|
||||
};
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
|
||||
// Special case for just getting the year
|
||||
if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return Ok(Value::String(now.format("%Y").to_string()));
|
||||
@ -68,14 +75,10 @@ impl Function for FormatDateFunction {
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"The 'timestamp' argument must be a valid timestamp",
|
||||
))
|
||||
));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"The 'timestamp' argument is required",
|
||||
))
|
||||
}
|
||||
None => return Err(tera::Error::msg("The 'timestamp' argument is required")),
|
||||
};
|
||||
|
||||
let format = match args.get("format") {
|
||||
@ -89,23 +92,130 @@ impl Function for FormatDateFunction {
|
||||
// Convert timestamp to DateTime using the non-deprecated method
|
||||
let datetime = match DateTime::from_timestamp(timestamp, 0) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"Failed to convert timestamp to datetime",
|
||||
))
|
||||
}
|
||||
None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")),
|
||||
};
|
||||
|
||||
|
||||
Ok(Value::String(datetime.format(format).to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera function to convert UTC datetime to local time
|
||||
#[derive(Clone)]
|
||||
pub struct LocalTimeFunction;
|
||||
|
||||
impl Function for LocalTimeFunction {
|
||||
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
|
||||
let datetime_value = match args.get("datetime") {
|
||||
Some(val) => val,
|
||||
None => return Err(tera::Error::msg("The 'datetime' argument is required")),
|
||||
};
|
||||
|
||||
let format = match args.get("format") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
None => "%Y-%m-%d %H:%M",
|
||||
},
|
||||
None => "%Y-%m-%d %H:%M",
|
||||
};
|
||||
|
||||
// The datetime comes from Rust as a serialized DateTime<Utc>
|
||||
// We need to handle it properly
|
||||
let utc_datetime = if let Some(dt_str) = datetime_value.as_str() {
|
||||
// Try to parse as RFC3339 first
|
||||
match DateTime::parse_from_rfc3339(dt_str) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_) => {
|
||||
// Try to parse as our standard format
|
||||
match DateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_) => return Err(tera::Error::msg("Invalid datetime string format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(tera::Error::msg("Datetime must be a string"));
|
||||
};
|
||||
|
||||
// Convert UTC to local time (EEST = UTC+3)
|
||||
// In a real application, you'd want to get the user's timezone from their profile
|
||||
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||
let local_datetime = utc_datetime.with_timezone(&local_offset);
|
||||
|
||||
Ok(Value::String(local_datetime.format(format).to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera filter to format hour with zero padding
|
||||
pub fn format_hour_filter(
|
||||
value: &Value,
|
||||
_args: &std::collections::HashMap<String, Value>,
|
||||
) -> tera::Result<Value> {
|
||||
match value.as_i64() {
|
||||
Some(hour) => Ok(Value::String(format!("{:02}", hour))),
|
||||
None => Err(tera::Error::msg("Value must be a number")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera filter to extract hour from datetime string
|
||||
pub fn extract_hour_filter(
|
||||
value: &Value,
|
||||
_args: &std::collections::HashMap<String, Value>,
|
||||
) -> tera::Result<Value> {
|
||||
match value.as_str() {
|
||||
Some(datetime_str) => {
|
||||
// Try to parse as RFC3339 first
|
||||
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
|
||||
Ok(Value::String(dt.format("%H").to_string()))
|
||||
} else {
|
||||
// Try to parse as our standard format
|
||||
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
|
||||
Ok(dt) => Ok(Value::String(dt.format("%H").to_string())),
|
||||
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(tera::Error::msg("Value must be a string")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera filter to format time from datetime string
|
||||
pub fn format_time_filter(
|
||||
value: &Value,
|
||||
args: &std::collections::HashMap<String, Value>,
|
||||
) -> tera::Result<Value> {
|
||||
let format = match args.get("format") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
None => "%H:%M",
|
||||
},
|
||||
None => "%H:%M",
|
||||
};
|
||||
|
||||
match value.as_str() {
|
||||
Some(datetime_str) => {
|
||||
// Try to parse as RFC3339 first
|
||||
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
|
||||
Ok(Value::String(dt.format(format).to_string()))
|
||||
} else {
|
||||
// Try to parse as our standard format
|
||||
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
|
||||
Ok(dt) => Ok(Value::String(dt.format(format).to_string())),
|
||||
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(tera::Error::msg("Value must be a string")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a date for display
|
||||
#[allow(dead_code)]
|
||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
||||
date.format(format).to_string()
|
||||
}
|
||||
|
||||
/// Truncates a string to a maximum length and adds an ellipsis if truncated
|
||||
#[allow(dead_code)]
|
||||
pub fn truncate_string(s: &str, max_length: usize) -> String {
|
||||
if s.len() <= max_length {
|
||||
s.to_string()
|
||||
@ -124,38 +234,41 @@ pub fn render_template(
|
||||
ctx: &Context,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
println!("DEBUG: Attempting to render template: {}", template_name);
|
||||
|
||||
|
||||
// Print all context keys for debugging
|
||||
let mut keys = Vec::new();
|
||||
for (key, _) in ctx.clone().into_json().as_object().unwrap().iter() {
|
||||
keys.push(key.clone());
|
||||
}
|
||||
println!("DEBUG: Context keys: {:?}", keys);
|
||||
|
||||
|
||||
match tmpl.render(template_name, ctx) {
|
||||
Ok(content) => {
|
||||
println!("DEBUG: Successfully rendered template: {}", template_name);
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(content))
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// Log the error with more details
|
||||
println!("DEBUG: Template rendering error for {}: {}", template_name, e);
|
||||
println!(
|
||||
"DEBUG: Template rendering error for {}: {}",
|
||||
template_name, e
|
||||
);
|
||||
println!("DEBUG: Error details: {:?}", e);
|
||||
|
||||
|
||||
// Print the error cause chain for better debugging
|
||||
let mut current_error: Option<&dyn StdError> = Some(&e);
|
||||
let mut error_chain = Vec::new();
|
||||
|
||||
|
||||
while let Some(error) = current_error {
|
||||
error_chain.push(format!("{}", error));
|
||||
current_error = error.source();
|
||||
}
|
||||
|
||||
|
||||
println!("DEBUG: Error chain: {:?}", error_chain);
|
||||
|
||||
|
||||
// Log the error
|
||||
log::error!("Template rendering error: {}", e);
|
||||
|
||||
|
||||
// Create a simple error response with more detailed information
|
||||
let error_html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
@ -187,9 +300,9 @@ pub fn render_template(
|
||||
e,
|
||||
error_chain.join("\n")
|
||||
);
|
||||
|
||||
|
||||
println!("DEBUG: Returning simple error page");
|
||||
|
||||
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.content_type("text/html")
|
||||
.body(error_html))
|
||||
@ -207,4 +320,4 @@ mod tests {
|
||||
assert_eq!(truncate_string("Hello, world!", 5), "Hello...");
|
||||
assert_eq!(truncate_string("", 5), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use heromodels::models::Event as CalendarEvent;
|
||||
use lazy_static::lazy_static;
|
||||
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! {
|
||||
@ -11,21 +11,21 @@ lazy_static! {
|
||||
/// 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 {
|
||||
@ -42,14 +42,14 @@ 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(
|
||||
@ -57,25 +57,25 @@ impl RedisCalendarService {
|
||||
format!("Failed to serialize event: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
|
||||
// Save the event
|
||||
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id);
|
||||
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.base_data.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)?;
|
||||
|
||||
let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.base_data.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| {
|
||||
@ -84,34 +84,34 @@ impl RedisCalendarService {
|
||||
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 {
|
||||
@ -119,23 +119,23 @@ impl RedisCalendarService {
|
||||
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| event.start_time <= end && event.end_time >= start)
|
||||
.collect();
|
||||
|
||||
|
||||
Ok(filtered_events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,29 +5,29 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1>Create New Event</h1>
|
||||
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/calendar/new" method="post">
|
||||
|
||||
<form action="/calendar/events" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Event Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</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>
|
||||
@ -38,7 +38,14 @@
|
||||
<input type="datetime-local" class="form-control" id="end_time" name="end_time" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Show selected date info when coming from calendar date click -->
|
||||
<div id="selected-date-info" class="alert alert-info" style="display: none;">
|
||||
<strong>Selected Date:</strong> <span id="selected-date-display"></span>
|
||||
<br>
|
||||
<small>The date is pre-selected. You can only modify the time portion.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="color" class="form-label">Event Color</label>
|
||||
<select class="form-control" id="color" name="color">
|
||||
@ -50,7 +57,7 @@
|
||||
<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>
|
||||
@ -59,37 +66,106 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check if we came from a date click (URL parameter)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const selectedDate = urlParams.get('date');
|
||||
|
||||
if (selectedDate) {
|
||||
// Show the selected date info
|
||||
document.getElementById('selected-date-info').style.display = 'block';
|
||||
document.getElementById('selected-date-display').textContent = new Date(selectedDate).toLocaleDateString();
|
||||
|
||||
// Pre-fill the date portion and restrict date changes
|
||||
const startTimeInput = document.getElementById('start_time');
|
||||
const endTimeInput = document.getElementById('end_time');
|
||||
|
||||
// Set default times (9 AM to 10 AM on the selected date)
|
||||
const startDateTime = new Date(selectedDate + 'T09:00');
|
||||
const endDateTime = new Date(selectedDate + 'T10:00');
|
||||
|
||||
// Format for datetime-local input (YYYY-MM-DDTHH:MM)
|
||||
startTimeInput.value = startDateTime.toISOString().slice(0, 16);
|
||||
endTimeInput.value = endDateTime.toISOString().slice(0, 16);
|
||||
|
||||
// Set minimum and maximum date to the selected date to prevent changing the date
|
||||
const minDate = selectedDate + 'T00:00';
|
||||
const maxDate = selectedDate + 'T23:59';
|
||||
startTimeInput.min = minDate;
|
||||
startTimeInput.max = maxDate;
|
||||
endTimeInput.min = minDate;
|
||||
endTimeInput.max = maxDate;
|
||||
|
||||
// Add event listeners to ensure end time is after start time
|
||||
startTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(this.value);
|
||||
const endTime = new Date(endTimeInput.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Set end time to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Update end time minimum to be after start time
|
||||
endTimeInput.min = this.value;
|
||||
});
|
||||
|
||||
endTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(startTimeInput.value);
|
||||
const endTime = new Date(this.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Reset to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
this.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No date selected, set default to current time
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
|
||||
document.getElementById('start_time').value = now.toISOString().slice(0, 16);
|
||||
document.getElementById('end_time').value = oneHourLater.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Convert datetime-local inputs to RFC3339 format on form submission
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
document.querySelector('form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const startTime = document.getElementById('start_time').value;
|
||||
const endTime = document.getElementById('end_time').value;
|
||||
|
||||
|
||||
// Validate that end time is after start time
|
||||
if (new Date(endTime) <= new Date(startTime)) {
|
||||
alert('End time must be after start time');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to RFC3339 format
|
||||
const startRFC = new Date(startTime).toISOString();
|
||||
const endRFC = new Date(endTime).toISOString();
|
||||
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
18
actix_mvc_app/src/views/governance/_header.html
Normal file
18
actix_mvc_app/src/views/governance/_header.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!-- Governance Page Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">{{ page_title }}</h1>
|
||||
<p class="text-muted mb-0">{{ page_description }}</p>
|
||||
</div>
|
||||
{% if show_create_button %}
|
||||
<div>
|
||||
<a href="/governance/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!-- Governance Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'dashboard' %}active{% endif %}" href="/governance">
|
||||
<i class="bi bi-house"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'proposals' %}active{% endif %}" href="/governance/proposals">
|
||||
<i class="bi bi-file-text"></i> All Proposals
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'create' %}active{% endif %}" href="/governance/create">
|
||||
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'my-votes' %}active{% endif %}" href="/governance/my-votes">
|
||||
<i class="bi bi-check-circle"></i> My Votes
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'activities' %}active{% endif %}" href="/governance/activities">
|
||||
<i class="bi bi-activity"></i> All Activities
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
@ -0,0 +1,118 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}All Governance Activities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Activities List -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-activity"></i> Governance Activity History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if activities %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">Type</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Proposal</th>
|
||||
<th width="150">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for activity in activities %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="{{ activity.icon }}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ activity.user }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ activity.action }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ activity.proposal_id }}"
|
||||
class="text-decoration-none">
|
||||
{{ activity.proposal_title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ activity.created_at | date(format="%Y-%m-%d %H:%M") }}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-activity display-1 text-muted"></i>
|
||||
<h4 class="mt-3">No Activities Yet</h4>
|
||||
<p class="text-muted">
|
||||
Governance activities will appear here as users create proposals and cast votes.
|
||||
</p>
|
||||
<a href="/governance/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First Proposal
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Statistics -->
|
||||
{% if activities %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ activities | length }}</h5>
|
||||
<p class="card-text text-muted">Total Activities</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-activity text-primary"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Activity Timeline</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-people text-success"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Community Engagement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -4,69 +4,74 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="display-5 mb-4">Create Governance Proposal</h1>
|
||||
<p class="lead">Submit a new proposal for the community to vote on.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
|
||||
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
|
||||
clearly state the problem, solution, and implementation details. The community will review and vote
|
||||
on your proposal, so be thorough and thoughtful in your submission.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
|
||||
class="bi bi-file-earmark-text"></i> Proposal Templates</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proposal Form -->
|
||||
<!-- Proposal Form and Guidelines in Flex Layout -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card">
|
||||
<!-- Proposal Form Column -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">New Proposal</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/governance/create" method="post">
|
||||
<form action="/governance/create" method="post" id="proposalForm" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required
|
||||
placeholder="Enter a clear, concise title for your proposal">
|
||||
<input type="text" class="form-control" id="title" name="title" required minlength="5"
|
||||
maxlength="100" placeholder="Enter a clear, concise title for your proposal">
|
||||
<div class="invalid-feedback">Please provide a title (5-100 characters).</div>
|
||||
<div class="form-text">Make it descriptive and specific</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6" required
|
||||
placeholder="Provide a detailed description of your proposal..."></textarea>
|
||||
<textarea class="form-control" id="description" name="description" rows="8" required
|
||||
minlength="50" maxlength="5000"
|
||||
placeholder="Provide a detailed description of your proposal..."></textarea>
|
||||
<div class="invalid-feedback">Please provide a detailed description (at least 50
|
||||
characters).</div>
|
||||
<div class="form-text">Explain the purpose, benefits, and implementation details</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="voting_start_date" class="form-label">Voting Start Date</label>
|
||||
<input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
|
||||
<div class="invalid-feedback" id="start_date_feedback">Please select a valid start date.
|
||||
</div>
|
||||
<div class="form-text">When should voting begin?</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="voting_end_date" class="form-label">Voting End Date</label>
|
||||
<input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
|
||||
<div class="invalid-feedback" id="end_date_feedback">End date must be after start date.
|
||||
</div>
|
||||
<div class="form-text">When should voting end?</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="draft" name="draft" value="true">
|
||||
@ -75,7 +80,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Submit Proposal</button>
|
||||
<a href="/governance" class="btn btn-outline-secondary">Cancel</a>
|
||||
@ -84,12 +89,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Guidelines Card -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card bg-light">
|
||||
|
||||
<!-- Guidelines Column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card bg-light h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Proposal Guidelines</h5>
|
||||
</div>
|
||||
@ -116,4 +119,111 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('proposalForm');
|
||||
const startDateInput = document.getElementById('voting_start_date');
|
||||
const endDateInput = document.getElementById('voting_end_date');
|
||||
const startDateFeedback = document.getElementById('start_date_feedback');
|
||||
const endDateFeedback = document.getElementById('end_date_feedback');
|
||||
|
||||
// Set default dates
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const nextWeek = new Date(today);
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
|
||||
// Format dates for input fields
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Set default values
|
||||
startDateInput.value = formatDate(tomorrow);
|
||||
endDateInput.value = formatDate(nextWeek);
|
||||
|
||||
// Validate dates when they change
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
const currentDate = new Date();
|
||||
currentDate.setHours(0, 0, 0, 0); // Reset time to start of day
|
||||
|
||||
let startValid = true;
|
||||
let endValid = true;
|
||||
|
||||
// Validate start date is not in the past
|
||||
if (startDate < currentDate) {
|
||||
startDateInput.classList.add('is-invalid');
|
||||
startDateFeedback.textContent = 'Start date cannot be in the past.';
|
||||
startValid = false;
|
||||
} else {
|
||||
startDateInput.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate end date is after start date
|
||||
if (endDate < startDate) {
|
||||
endDateInput.classList.add('is-invalid');
|
||||
endDateFeedback.textContent = 'End date must be after start date.';
|
||||
endValid = false;
|
||||
} else {
|
||||
endDateInput.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
return startValid && endValid;
|
||||
}
|
||||
|
||||
// Validate on input
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
|
||||
// Form submission validation
|
||||
form.addEventListener('submit', function (event) {
|
||||
let formValid = true;
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
formValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Check minlength if specified
|
||||
if (field.minLength && field.value.length < field.minLength) {
|
||||
field.classList.add('is-invalid');
|
||||
formValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Validate dates
|
||||
const datesValid = validateDates();
|
||||
formValid = formValid && datesValid;
|
||||
|
||||
// If form is not valid, prevent submission
|
||||
if (!formValid) {
|
||||
event.preventDefault();
|
||||
// Scroll to the first invalid element
|
||||
const firstInvalid = form.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
firstInvalid.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initial validation
|
||||
validateDates();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
@ -3,170 +3,192 @@
|
||||
{% block title %}Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row mb-2">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Governance</h5>
|
||||
<p>The governance system allows token holders to participate in decision-making processes by voting on
|
||||
proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction
|
||||
of our decentralized ecosystem.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i>
|
||||
Read Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row mb-2">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Governance</h5>
|
||||
<p>The governance system allows token holders to participate in decision-making processes by voting on proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction of our decentralized ecosystem.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
|
||||
<!-- Dashboard Main Content -->
|
||||
<div class="row mb-3">
|
||||
<!-- Voting Pane for Nearest Deadline Proposal -->
|
||||
<div class="col-lg-8 mb-4 mb-lg-0">
|
||||
{% if nearest_proposal is defined %}
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Urgent: Voting Closes Soon</h5>
|
||||
<div>
|
||||
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.vote_end_date |
|
||||
date(format="%Y-%m-%d") }}</span>
|
||||
<a href="/governance/proposals/{{ nearest_proposal.base_data.id }}"
|
||||
class="btn btn-sm btn-outline-primary">View Full Proposal</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{ nearest_proposal.title }}</h4>
|
||||
<h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>{{ nearest_proposal.description }}</p>
|
||||
</div>
|
||||
|
||||
{% set yes_percent = 0 %}
|
||||
{% set no_percent = 0 %}
|
||||
{% set abstain_percent = 0 %}
|
||||
{% set total_votes = 0 %}
|
||||
|
||||
{% if nearest_proposal_results is defined %}
|
||||
{% if nearest_proposal_results.total_votes > 0 %}
|
||||
{% set yes_percent = (nearest_proposal_results.yes_count * 100 / nearest_proposal_results.total_votes) |
|
||||
int %}
|
||||
{% set no_percent = (nearest_proposal_results.no_count * 100 / nearest_proposal_results.total_votes) |
|
||||
int %}
|
||||
{% set abstain_percent = (nearest_proposal_results.abstain_count * 100 /
|
||||
nearest_proposal_results.total_votes) |
|
||||
int %}
|
||||
{% endif %}
|
||||
{% set total_votes = nearest_proposal_results.total_votes %}
|
||||
{% endif %}
|
||||
|
||||
<div class="progress mb-3" style="height: 25px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
|
||||
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100">{{ yes_percent }}% Yes
|
||||
</div>
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
|
||||
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100">{{ no_percent }}% No
|
||||
</div>
|
||||
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"
|
||||
aria-valuenow="{{ abstain_percent }}" aria-valuemin="0" aria-valuemax="100">{{ abstain_percent
|
||||
}}% Abstain
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between text-muted small mb-4">
|
||||
<span>{{ total_votes }} votes cast</span>
|
||||
<span>Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Cast Your Vote</h5>
|
||||
<form action="/governance/proposals/{{ nearest_proposal.base_data.id }}/vote" method="post">
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" name="comment"
|
||||
placeholder="Optional comment on your vote" aria-label="Vote comment">
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" name="vote_type" value="Yes" class="btn btn-success">Vote Yes</button>
|
||||
<button type="submit" name="vote_type" value="No" class="btn btn-danger">Vote No</button>
|
||||
<button type="submit" name="vote_type" value="Abstain"
|
||||
class="btn btn-secondary">Abstain</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Main Content -->
|
||||
<div class="row mb-3">
|
||||
<!-- Voting Pane for Nearest Deadline Proposal -->
|
||||
<div class="col-lg-8 mb-4 mb-lg-0">
|
||||
{% if nearest_proposal is defined %}
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Urgent: Voting Closes Soon</h5>
|
||||
<div>
|
||||
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
|
||||
<a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{ nearest_proposal.title }}</h4>
|
||||
<h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>{{ nearest_proposal.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="progress mb-3" style="height: 25px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">65% Yes</div>
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between text-muted small mb-4">
|
||||
<span>26 votes cast</span>
|
||||
<span>Quorum: 75% reached</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Cast Your Vote</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment">
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" name="vote" value="yes" class="btn btn-success">Vote Yes</button>
|
||||
<button type="submit" name="vote" value="no" class="btn btn-danger">Vote No</button>
|
||||
<button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
|
||||
<h5>No active proposals requiring votes</h5>
|
||||
<p class="text-muted">When new proposals are created, they will appear here for voting.</p>
|
||||
<a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
|
||||
<h5>No active proposals requiring votes</h5>
|
||||
<p class="text-muted">When new proposals are created, they will appear here for voting.</p>
|
||||
<a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity Timeline -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Activity</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for activity in recent_activity %}
|
||||
<div class="list-group-item border-start-0 border-end-0 py-3">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="bi {{ activity.icon }} fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ activity.user }}</strong>
|
||||
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p>
|
||||
{% if activity.type == "comment" and activity.comment is defined %}
|
||||
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity Timeline -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Activity</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for activity in recent_activity %}
|
||||
<div class="list-group-item border-start-0 border-end-0 py-3">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="bi {{ activity.icon }} fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ activity.user }}</strong>
|
||||
<small class="text-muted">{{ activity.created_at | date(format="%H:%M") }}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ activity.action }} on <a
|
||||
href="/governance/proposals/{{ activity.proposal_id }}">{{
|
||||
activity.proposal_title }}</a></p>
|
||||
{% if activity.type == "comment" and activity.comment is defined %}
|
||||
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="/governance/activities" class="btn btn-sm btn-outline-info">View All Activities</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Proposals Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Active Proposals (Ending Soon)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% set count = 0 %}
|
||||
{% for proposal in proposals %}
|
||||
{% if count < 3 %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ proposal.title }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
|
||||
<p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted text-center">
|
||||
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recent Proposals Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Active Proposals (Ending Soon)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% set count = 0 %}
|
||||
{% for proposal in proposals %}
|
||||
{% if count < 3 %} <div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ proposal.title }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
|
||||
<p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span
|
||||
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
<a href="/governance/proposals/{{ proposal.base_data.id }}"
|
||||
class="btn btn-sm btn-outline-primary">View Details</a>
|
||||
</div>
|
||||
{% set count = count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted text-center">
|
||||
<span>Voting ends: {{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set count = count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -3,133 +3,121 @@
|
||||
{% block title %}My Votes - Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- My Votes List -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">My Voting History</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if votes | length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proposal</th>
|
||||
<th>My Vote</th>
|
||||
<th>Status</th>
|
||||
<th>Voted On</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
|
||||
<tr>
|
||||
<td>{{ proposal.title }}</td>
|
||||
<td>
|
||||
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ vote.vote_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View Proposal</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
|
||||
<h5>You haven't voted on any proposals yet</h5>
|
||||
<p class="text-muted">When you vote on proposals, they will appear here.</p>
|
||||
<a href="/governance/proposals" class="btn btn-primary mt-3">Browse Proposals</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Voting Stats -->
|
||||
{% if votes | length > 0 %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-success h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Yes Votes</h5>
|
||||
<p class="display-4">
|
||||
{% set yes_count = 0 %}
|
||||
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
|
||||
{% if vote.vote_type == 'Yes' %}
|
||||
{% set yes_count = yes_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ yes_count }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-danger h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">No Votes</h5>
|
||||
<p class="display-4">
|
||||
{% set no_count = 0 %}
|
||||
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
|
||||
{% if vote.vote_type == 'No' %}
|
||||
{% set no_count = no_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ no_count }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-secondary h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Abstain Votes</h5>
|
||||
<p class="display-4">
|
||||
{% set abstain_count = 0 %}
|
||||
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
|
||||
{% if vote.vote_type == 'Abstain' %}
|
||||
{% set abstain_count = abstain_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ abstain_count }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Info Alert -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Votes</h5>
|
||||
<p>Voting is a fundamental right of all token holders in our governance system. Each vote carries weight
|
||||
proportional to your token holdings, ensuring fair representation. The voting statistics below show the
|
||||
community's collective decision-making across all proposals.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/voting-guide" class="btn btn-sm btn-outline-primary"><i
|
||||
class="bi bi-check2-square"></i> Voting Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Voting Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-success h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Yes Votes</h5>
|
||||
<p class="display-4">
|
||||
{{ total_yes_votes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-danger h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">No Votes</h5>
|
||||
<p class="display-4">
|
||||
{{ total_no_votes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-white bg-secondary h-100">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Abstain Votes</h5>
|
||||
<p class="display-4">
|
||||
{{ total_abstain_votes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- My Votes List -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">My Voting History</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if votes | length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proposal</th>
|
||||
<th>My Vote</th>
|
||||
<th>Status</th>
|
||||
<th>Voted On</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
|
||||
<tr>
|
||||
<td>{{ proposal.title }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ vote.vote_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ proposal.base_data.id }}"
|
||||
class="btn btn-sm btn-primary">View Proposal</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
|
||||
<h5>You haven't voted on any proposals yet</h5>
|
||||
<p class="text-muted">When you vote on proposals, they will appear here.</p>
|
||||
<a href="/governance/proposals" class="btn btn-primary mt-3">Browse Proposals</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -2,8 +2,45 @@
|
||||
|
||||
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comment-text:hover {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.progress {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
@ -30,160 +67,549 @@
|
||||
|
||||
<!-- Proposal Details -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">{{ proposal.title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span
|
||||
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
|
||||
<i
|
||||
class="bi {% if proposal.status == 'Active' %}bi-check-circle{% elif proposal.status == 'Approved' %}bi-trophy{% elif proposal.status == 'Rejected' %}bi-x-circle{% elif proposal.status == 'Draft' %}bi-pencil{% else %}bi-exclamation-circle{% endif %} me-1"></i>
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
<small class="text-muted">Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }}</small>
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>Created by {{ proposal.creator_name
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h5><i class="bi bi-file-text me-2"></i>Description</h5>
|
||||
<div class="p-3 bg-light rounded mb-4">{{ proposal.description }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
|
||||
{% if proposal.vote_start_date and proposal.vote_end_date %}
|
||||
<div>
|
||||
<div class="text-muted mb-1">Start Date</div>
|
||||
<div class="fw-bold">{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<i class="bi bi-arrow-right fs-4 text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted mb-1">End Date</div>
|
||||
<div class="fw-bold">{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center w-100">Not set</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Description</h5>
|
||||
<p class="mb-4">{{ proposal.description }}</p>
|
||||
|
||||
<h5>Voting Period</h5>
|
||||
<p>
|
||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
|
||||
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||
{% else %}
|
||||
Not set
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Voting Results</h5>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4 shadow-sm h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Voting Results Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="border-bottom pb-2 mb-3">Results</h6>
|
||||
|
||||
{% set yes_percent = 0 %}
|
||||
{% set no_percent = 0 %}
|
||||
{% set abstain_percent = 0 %}
|
||||
|
||||
{% if results.total_votes > 0 %}
|
||||
{% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
|
||||
{% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
|
||||
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
|
||||
{% endif %}
|
||||
|
||||
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div>
|
||||
</div>
|
||||
|
||||
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div>
|
||||
</div>
|
||||
|
||||
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote Form -->
|
||||
{% if proposal.status == "Active" and user and user.id %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Cast Your Vote</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/governance/proposals/{{ proposal.id }}/vote" method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Vote Type</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked>
|
||||
<label class="form-check-label" for="voteYes">
|
||||
Yes - I support this proposal
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
|
||||
<label class="form-check-label" for="voteNo">
|
||||
No - I oppose this proposal
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain" value="Abstain">
|
||||
<label class="form-check-label" for="voteAbstain">
|
||||
Abstain - I choose not to vote
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="comment" class="form-label">Comment (Optional)</label>
|
||||
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="Explain your vote..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not user or not user.id %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p>You must be logged in to vote.</p>
|
||||
<a href="/login" class="btn btn-primary">Login to Vote</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Votes List -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Votes ({{ votes | length }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if votes | length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Voter</th>
|
||||
<th>Vote</th>
|
||||
<th>Comment</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vote in votes %}
|
||||
<tr>
|
||||
<td>{{ vote.voter_name }}</td>
|
||||
<td>
|
||||
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||
{{ vote.vote_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
|
||||
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if results.total_votes > 0 %}
|
||||
{% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
|
||||
{% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
|
||||
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Yes votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-success"><i class="bi bi-check-circle-fill me-1"></i> Yes</span>
|
||||
<span class="badge bg-success rounded-pill">{{ results.yes_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
|
||||
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="{{ yes_percent }}% of votes"></div>
|
||||
</div>
|
||||
|
||||
<!-- No votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-danger"><i class="bi bi-x-circle-fill me-1"></i> No</span>
|
||||
<span class="badge bg-danger rounded-pill">{{ results.no_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
|
||||
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100"
|
||||
title="{{ no_percent }}% of votes"></div>
|
||||
</div>
|
||||
|
||||
<!-- Abstain votes -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold text-secondary"><i class="bi bi-dash-circle-fill me-1"></i>
|
||||
Abstain</span>
|
||||
<span class="badge bg-secondary rounded-pill">{{ results.abstain_count }}</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height: 12px;">
|
||||
<div class="progress-bar bg-secondary" role="progressbar"
|
||||
style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
|
||||
aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
|
||||
<div class="text-center">
|
||||
<h4 class="mb-0">{{ results.total_votes }}</h4>
|
||||
<small class="text-muted">Total Votes</small>
|
||||
</div>
|
||||
|
||||
{% if proposal.status == "Active" %}
|
||||
<div class="text-center">
|
||||
<div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
|
||||
<svg width="60" height="60">
|
||||
<circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
|
||||
</circle>
|
||||
<circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
|
||||
stroke-dasharray="157"
|
||||
stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
|
||||
transform="rotate(-90 30 30)"></circle>
|
||||
</svg>
|
||||
<div
|
||||
class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
|
||||
{{ yes_percent }}%</div>
|
||||
</div>
|
||||
<small class="text-muted">Approval Rate</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote Form Section -->
|
||||
{% if proposal.status == "Active" and user and user.id %}
|
||||
<div class="mt-auto">
|
||||
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
|
||||
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post"
|
||||
id="voteForm">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteYes"
|
||||
value="Yes" required>
|
||||
<label class="form-check-label text-success" for="voteYes"><i
|
||||
class="bi bi-check-circle-fill me-1"></i>Yes</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteNo"
|
||||
value="No">
|
||||
<label class="form-check-label text-danger" for="voteNo"><i
|
||||
class="bi bi-x-circle-fill me-1"></i>No</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
|
||||
value="Abstain">
|
||||
<label class="form-check-label text-secondary" for="voteAbstain"><i
|
||||
class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control" id="comment" name="comment" rows="2"
|
||||
placeholder="Add your thoughts about this proposal (optional)..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-send me-2"></i>Submit
|
||||
Vote</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif proposal.status != "Active" %}
|
||||
<div class="mt-auto text-center p-3 bg-light rounded">
|
||||
<i class="bi bi-info-circle fs-4 text-muted"></i>
|
||||
<p class="mb-0 mt-2">Voting is {{ proposal.status | lower }} for this proposal</p>
|
||||
</div>
|
||||
{% elif not user or not user.id %}
|
||||
<div class="mt-auto text-center p-3 bg-light rounded">
|
||||
<i class="bi bi-person-lock fs-4 text-muted"></i>
|
||||
<p class="mb-0 mt-2">You must be logged in to vote</p>
|
||||
<a href="/login" class="btn btn-primary btn-sm mt-2">Login to Vote</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No votes have been cast yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Votes List -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Voter</th>
|
||||
<th>Vote</th>
|
||||
<th>Comment</th>
|
||||
<th class="text-end pe-3">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="votesTableBody">
|
||||
{% if votes | length == 0 %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4">
|
||||
<div class="py-3">
|
||||
<i class="bi bi-inbox fs-1 text-muted"></i>
|
||||
<p class="mt-2 mb-0">No votes have been cast yet</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for vote in votes %}
|
||||
<tr class="vote-row" data-vote-type="{{ vote.vote_type | lower }}">
|
||||
<td class="ps-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2 bg-primary text-white">
|
||||
U
|
||||
</div>
|
||||
<span>{{ vote.voter_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %} rounded-pill px-3 py-2">
|
||||
{% if vote.vote_type == 'Yes' %}
|
||||
<i class="bi bi-check-circle-fill me-1"></i>
|
||||
{% elif vote.vote_type == 'No' %}
|
||||
<i class="bi bi-x-circle-fill me-1"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-dash-circle-fill me-1"></i>
|
||||
{% endif %}
|
||||
{{ vote.vote_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if vote.comment %}
|
||||
<div class="comment-text">{{ vote.comment }}</div>
|
||||
{% else %}
|
||||
<span class="text-muted fst-italic">No comment provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="d-flex flex-column align-items-end">
|
||||
<span>{{ vote.created_at | date(format="%Y-%m-%d") }}</span>
|
||||
<small class="text-muted">{{ vote.created_at | date(format="%H:%M")
|
||||
}}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% if votes | length > 10 %}
|
||||
<div class="d-flex justify-content-between align-items-center p-3 border-top">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="me-2 text-muted small">Rows per page:</label>
|
||||
<select id="rowsPerPage" class="form-select form-select-sm" style="width: auto;">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<nav aria-label="Votes pagination">
|
||||
<ul class="pagination pagination-sm mb-0" id="paginationControls">
|
||||
<li class="page-item disabled" id="prevPage">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||
<li class="page-item" id="nextPage">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="text-muted small" id="paginationInfo">
|
||||
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span
|
||||
id="totalRows">{{ votes | length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Remove query parameters from URL without refreshing the page
|
||||
if (window.location.search.includes('vote_success=true')) {
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
|
||||
// Auto-hide the success alert after 5 seconds
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
setTimeout(function () {
|
||||
successAlert.classList.remove('show');
|
||||
setTimeout(function () {
|
||||
successAlert.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination functionality
|
||||
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
||||
const paginationControls = document.getElementById('paginationControls');
|
||||
const votesTableBody = document.getElementById('votesTableBody');
|
||||
const startRowElement = document.getElementById('startRow');
|
||||
const endRowElement = document.getElementById('endRow');
|
||||
const totalRowsElement = document.getElementById('totalRows');
|
||||
const prevPageBtn = document.getElementById('prevPage');
|
||||
const nextPageBtn = document.getElementById('nextPage');
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
|
||||
|
||||
// Function to update pagination display
|
||||
function updatePagination() {
|
||||
if (!paginationControls) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = filteredRows.length;
|
||||
|
||||
// Calculate total pages
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Update pagination controls
|
||||
if (paginationControls) {
|
||||
// Clear existing page links (except prev/next)
|
||||
const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
|
||||
pageLinks.forEach(link => link.remove());
|
||||
|
||||
// Add new page links
|
||||
const maxVisiblePages = 5;
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// Adjust if we're near the end
|
||||
if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
// Insert page links before the next button
|
||||
const nextPageElement = document.getElementById('nextPage');
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.className = 'page-link';
|
||||
a.href = '#';
|
||||
a.textContent = i;
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
currentPage = i;
|
||||
updatePagination();
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
paginationControls.insertBefore(li, nextPageElement);
|
||||
}
|
||||
|
||||
// Update prev/next buttons
|
||||
prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||
nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||
}
|
||||
|
||||
// Show current page
|
||||
showCurrentPage();
|
||||
}
|
||||
|
||||
// Function to show current page
|
||||
function showCurrentPage() {
|
||||
if (!votesTableBody) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide all rows first
|
||||
voteRows.forEach(row => row.style.display = 'none');
|
||||
|
||||
// Calculate pagination
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Show only rows for current page
|
||||
const start = (currentPage - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
filteredRows.slice(start, end).forEach(row => row.style.display = '');
|
||||
|
||||
// Update pagination info
|
||||
if (startRowElement && endRowElement && totalRowsElement) {
|
||||
startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
|
||||
endRowElement.textContent = Math.min(end, totalRows);
|
||||
totalRowsElement.textContent = totalRows;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for pagination
|
||||
if (prevPageBtn) {
|
||||
prevPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextPageBtn) {
|
||||
nextPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (rowsPerPageSelect) {
|
||||
rowsPerPageSelect.addEventListener('change', function () {
|
||||
rowsPerPage = parseInt(this.value);
|
||||
currentPage = 1; // Reset to first page
|
||||
updatePagination();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize pagination (but don't interfere with filtering)
|
||||
if (paginationControls) {
|
||||
// Only initialize pagination if there are many votes
|
||||
// The filtering will handle showing/hiding rows
|
||||
console.log('Pagination controls available but not interfering with filtering');
|
||||
}
|
||||
|
||||
// Initialize tooltips for all elements with title attributes
|
||||
const tooltipElements = document.querySelectorAll('[title]');
|
||||
if (tooltipElements.length > 0) {
|
||||
[].slice.call(tooltipElements).map(function (el) {
|
||||
return new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Add debugging for vote form
|
||||
const voteForm = document.getElementById('voteForm');
|
||||
if (voteForm) {
|
||||
console.log('Vote form found:', voteForm);
|
||||
voteForm.addEventListener('submit', function (e) {
|
||||
console.log('Vote form submitted');
|
||||
const formData = new FormData(voteForm);
|
||||
console.log('Form data:', Object.fromEntries(formData));
|
||||
});
|
||||
} else {
|
||||
console.log('Vote form not found');
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log('Filter buttons found:', filterButtons.length);
|
||||
console.log('Vote rows found:', voteRows.length);
|
||||
console.log('Search input found:', searchInput ? 'Yes' : 'No');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -3,128 +3,140 @@
|
||||
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Success message if present -->
|
||||
{% if success %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Success message if present -->
|
||||
{% if success %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
|
||||
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filter Controls -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form action="/governance/proposals" method="get" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Rejected">Rejected</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
|
||||
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal
|
||||
includes a detailed description, implementation plan, and voting period. Browse the list below to see all
|
||||
active and past proposals.</p>
|
||||
<div class="mt-2">
|
||||
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i
|
||||
class="bi bi-file-text"></i> Proposal Guidelines</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proposals List -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Proposals</h5>
|
||||
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Creator</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Voting Period</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in proposals %}
|
||||
<tr>
|
||||
<td>{{ proposal.title }}</td>
|
||||
<td>{{ proposal.creator_name }}</td>
|
||||
<td>
|
||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||
{% else %}
|
||||
Not set
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Filter Controls -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form action="/governance/proposals" method="get" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
|
||||
Statuses</option>
|
||||
<option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
|
||||
<option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
|
||||
<option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
|
||||
</option>
|
||||
<option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
|
||||
</option>
|
||||
<option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="Search by title or description"
|
||||
value="{% if search_filter %}{{ search_filter }}{% endif %}">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proposals List -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">All Proposals</h5>
|
||||
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if proposals and proposals|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Creator</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Voting Period</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for proposal in proposals %}
|
||||
<tr>
|
||||
<td>{{ proposal.title }}</td>
|
||||
<td>{{ proposal.creator_name }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
||||
{{ proposal.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
{% if proposal.vote_start_date and proposal.vote_end_date %}
|
||||
{{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
|
||||
proposal.vote_end_date | date(format="%Y-%m-%d") }}
|
||||
{% else %}
|
||||
Not set
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ proposal.base_data.id }}"
|
||||
class="btn btn-sm btn-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center py-5">
|
||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||
<h5>No proposals found</h5>
|
||||
{% if status_filter or search_filter %}
|
||||
<p>No proposals match your current filter criteria. Try adjusting your filters or <a
|
||||
href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
|
||||
{% else %}
|
||||
<p>There are no proposals in the system yet.</p>
|
||||
{% endif %}
|
||||
<a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user